Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions plane_mcp/tools/cycles.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Cycle-related tools for Plane MCP Server."""

import json
from typing import Any

from fastmcp import FastMCP
Expand Down Expand Up @@ -207,6 +208,16 @@ def add_work_items_to_cycle(
cycle_id: UUID of the cycle
issue_ids: List of work item IDs to add to the cycle
"""
# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(issue_ids, str):
try:
issue_ids = json.loads(issue_ids)
except json.JSONDecodeError as e:
raise ValueError(
f"issue_ids must be a JSON array string or a list, got: {issue_ids!r}"
) from e
if not isinstance(issue_ids, list) or any(not isinstance(i, str) for i in issue_ids):
raise ValueError("issue_ids must be a list[str] or a JSON array string of strings")
client, workspace_slug = get_plane_client_context()
client.cycles.add_work_items(
workspace_slug=workspace_slug,
Expand Down
53 changes: 52 additions & 1 deletion plane_mcp/tools/epics.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Epic-related tools for Plane MCP Server."""
import json
from typing import get_args

from fastmcp import FastMCP
Expand All @@ -18,7 +19,9 @@
def register_epic_tools(mcp: FastMCP) -> None:
"""Register all epic-related tools with the MCP server."""

def _get_epic_work_item_type(client: PlaneClient, workspace_slug: str, project_id: str) -> WorkItemType | None:
def _get_epic_work_item_type(
client: PlaneClient, workspace_slug: str, project_id: str
) -> WorkItemType | None:
"""Helper function to get the work item type ID for epics."""
response = client.work_item_types.list(
workspace_slug=workspace_slug,
Expand Down Expand Up @@ -122,6 +125,30 @@ def create_epic(
if epic_type is None:
raise ValueError("No work item type with is_epic=True found in the project")

# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(assignees, str):
try:
assignees = json.loads(assignees)
except json.JSONDecodeError as e:
raise ValueError(
f"assignees must be a JSON array string or a list, got: {assignees!r}"
) from e
if isinstance(labels, str):
try:
labels = json.loads(labels)
except json.JSONDecodeError as e:
raise ValueError(
f"labels must be a JSON array string or a list, got: {labels!r}"
) from e
if assignees is not None and (
not isinstance(assignees, list) or any(not isinstance(i, str) for i in assignees)
):
raise ValueError("assignees must be a list[str] or a JSON array string of strings")
if labels is not None and (
not isinstance(labels, list) or any(not isinstance(i, str) for i in labels)
):
raise ValueError("labels must be a list[str] or a JSON array string of strings")

data = CreateWorkItem(
name=name,
assignees=assignees,
Expand Down Expand Up @@ -205,6 +232,30 @@ def update_epic(
raise ValueError(f"Invalid priority '{priority}'. Must be one of: {valid_priorities}")
validated_priority: PriorityEnum | None = priority # type: ignore[assignment]

# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(assignees, str):
try:
assignees = json.loads(assignees)
except json.JSONDecodeError as e:
raise ValueError(
f"assignees must be a JSON array string or a list, got: {assignees!r}"
) from e
if isinstance(labels, str):
try:
labels = json.loads(labels)
except json.JSONDecodeError as e:
raise ValueError(
f"labels must be a JSON array string or a list, got: {labels!r}"
) from e
if assignees is not None and (
not isinstance(assignees, list) or any(not isinstance(i, str) for i in assignees)
):
raise ValueError("assignees must be a list[str] or a JSON array string of strings")
if labels is not None and (
not isinstance(labels, list) or any(not isinstance(i, str) for i in labels)
):
raise ValueError("labels must be a list[str] or a JSON array string of strings")

data = UpdateWorkItem(
name=name,
assignees=assignees,
Expand Down
21 changes: 21 additions & 0 deletions plane_mcp/tools/milestones.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Milestone-related tools for Plane MCP Server."""

import json
from typing import Any

from fastmcp import FastMCP
Expand Down Expand Up @@ -157,6 +158,16 @@ def add_work_items_to_milestone(
milestone_id: UUID of the milestone
issue_ids: List of work item IDs to add to the milestone
"""
# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(issue_ids, str):
try:
issue_ids = json.loads(issue_ids)
except json.JSONDecodeError as e:
raise ValueError(
f"issue_ids must be a JSON array string or a list, got: {issue_ids!r}"
) from e
if not isinstance(issue_ids, list) or any(not isinstance(i, str) for i in issue_ids):
raise ValueError("issue_ids must be a list[str] or a JSON array string of strings")
client, workspace_slug = get_plane_client_context()
client.milestones.add_work_items(
workspace_slug=workspace_slug,
Expand All @@ -179,6 +190,16 @@ def remove_work_items_from_milestone(
milestone_id: UUID of the milestone
issue_ids: List of work item IDs to remove from the milestone
"""
# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(issue_ids, str):
try:
issue_ids = json.loads(issue_ids)
except json.JSONDecodeError as e:
raise ValueError(
f"issue_ids must be a JSON array string or a list, got: {issue_ids!r}"
) from e
if not isinstance(issue_ids, list) or any(not isinstance(i, str) for i in issue_ids):
raise ValueError("issue_ids must be a list[str] or a JSON array string of strings")
client, workspace_slug = get_plane_client_context()
client.milestones.remove_work_items(
workspace_slug=workspace_slug,
Expand Down
37 changes: 37 additions & 0 deletions plane_mcp/tools/modules.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Module-related tools for Plane MCP Server."""

import json
from typing import Any, get_args

from fastmcp import FastMCP
Expand Down Expand Up @@ -81,6 +82,19 @@ def create_module(
status if status in get_args(ModuleStatusEnum) else None # type: ignore[assignment]
)

# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(members, str):
try:
members = json.loads(members)
except json.JSONDecodeError as e:
raise ValueError(
f"members must be a JSON array string or a list, got: {members!r}"
) from e
if members is not None and (
not isinstance(members, list) or any(not isinstance(i, str) for i in members)
):
raise ValueError("members must be a list[str] or a JSON array string of strings")

data = CreateModule(
name=name,
description=description,
Expand Down Expand Up @@ -156,6 +170,19 @@ def update_module(
status if status in get_args(ModuleStatusEnum) else None # type: ignore[assignment]
)

# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(members, str):
try:
members = json.loads(members)
except json.JSONDecodeError as e:
raise ValueError(
f"members must be a JSON array string or a list, got: {members!r}"
) from e
if members is not None and (
not isinstance(members, list) or any(not isinstance(i, str) for i in members)
):
raise ValueError("members must be a list[str] or a JSON array string of strings")

data = UpdateModule(
name=name,
description=description,
Expand Down Expand Up @@ -224,6 +251,16 @@ def add_work_items_to_module(
module_id: UUID of the module
issue_ids: List of work item IDs to add to the module
"""
# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(issue_ids, str):
try:
issue_ids = json.loads(issue_ids)
except json.JSONDecodeError as e:
raise ValueError(
f"issue_ids must be a JSON array string or a list, got: {issue_ids!r}"
) from e
if not isinstance(issue_ids, list) or any(not isinstance(i, str) for i in issue_ids):
raise ValueError("issue_ids must be a list[str] or a JSON array string of strings")
client, workspace_slug = get_plane_client_context()
client.modules.add_work_items(
workspace_slug=workspace_slug,
Expand Down
41 changes: 37 additions & 4 deletions plane_mcp/tools/work_item_properties.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Work item property-related tools for Plane MCP Server."""

import json
from typing import Any

from fastmcp import FastMCP
Expand Down Expand Up @@ -74,14 +75,16 @@ def create_work_item_property(
project_id: UUID of the project
type_id: UUID of the work item type
display_name: Display name for the property
property_type: Type of property (TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE)
property_type: Type of property
(TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE)
relation_type: Relation type (ISSUE, USER) - required for RELATION properties
description: Property description
is_required: Whether the property is required
default_value: Default value(s) for the property
settings: Settings dictionary - required for TEXT and DATETIME properties
For TEXT: {"display_format": "single-line"|"multi-line"|"readonly"}
For DATETIME: {"display_format": "MMM dd, yyyy"|"dd/MM/yyyy"|"MM/dd/yyyy"|"yyyy/MM/dd"}
For DATETIME: {"display_format": "MMM dd, yyyy"|"dd/MM/yyyy"|"MM/dd/yyyy"|
"yyyy/MM/dd"}
is_active: Whether the property is active
is_multi: Whether the property supports multiple values
validation_rules: Validation rules dictionary
Expand Down Expand Up @@ -115,6 +118,20 @@ def create_work_item_property(
if options:
processed_options = [CreateWorkItemPropertyOption(**opt) for opt in options]

# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(default_value, str):
try:
default_value = json.loads(default_value)
except json.JSONDecodeError as e:
raise ValueError(
f"default_value must be a JSON array string or a list, got: {default_value!r}"
) from e
if default_value is not None and (
not isinstance(default_value, list)
or any(not isinstance(i, str) for i in default_value)
):
raise ValueError("default_value must be a list[str] or a JSON array string of strings")

data = CreateWorkItemProperty(
display_name=display_name,
property_type=validated_property_type,
Expand Down Expand Up @@ -188,14 +205,16 @@ def update_work_item_property(
type_id: UUID of the work item type
work_item_property_id: UUID of the property
display_name: Display name for the property
property_type: Type of property (TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE)
property_type: Type of property
(TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE)
relation_type: Relation type (ISSUE, USER) - required when updating to RELATION
description: Property description
is_required: Whether the property is required
default_value: Default value(s) for the property
settings: Settings dictionary - required when updating to TEXT or DATETIME
For TEXT: {"display_format": "single-line"|"multi-line"|"readonly"}
For DATETIME: {"display_format": "MMM dd, yyyy"|"dd/MM/yyyy"|"MM/dd/yyyy"|"yyyy/MM/dd"}
For DATETIME: {"display_format": "MMM dd, yyyy"|"dd/MM/yyyy"|"MM/dd/yyyy"|
"yyyy/MM/dd"}
is_active: Whether the property is active
is_multi: Whether the property supports multiple values
validation_rules: Validation rules dictionary
Expand Down Expand Up @@ -225,6 +244,20 @@ def update_work_item_property(
elif property_type == "DATETIME":
processed_settings = DateAttributeSettings(**settings)

# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(default_value, str):
try:
default_value = json.loads(default_value)
except json.JSONDecodeError as e:
raise ValueError(
f"default_value must be a JSON array string or a list, got: {default_value!r}"
) from e
if default_value is not None and (
not isinstance(default_value, list)
or any(not isinstance(i, str) for i in default_value)
):
raise ValueError("default_value must be a list[str] or a JSON array string of strings")

data = UpdateWorkItemProperty(
display_name=display_name,
property_type=validated_property_type,
Expand Down
12 changes: 12 additions & 0 deletions plane_mcp/tools/work_item_relations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Work item relation-related tools for Plane MCP Server."""

import json
from typing import get_args

from fastmcp import FastMCP
Expand Down Expand Up @@ -73,6 +74,17 @@ def create_work_item_relation(
)
validated_relation_type: WorkItemRelationTypeEnum = relation_type # type: ignore[assignment]

# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(issues, str):
try:
issues = json.loads(issues)
except json.JSONDecodeError as e:
raise ValueError(
f"issues must be a JSON array string or a list, got: {issues!r}"
) from e
if not isinstance(issues, list) or any(not isinstance(i, str) for i in issues):
raise ValueError("issues must be a list[str] or a JSON array string of strings")

data = CreateWorkItemRelation(
relation_type=validated_relation_type,
issues=issues,
Expand Down
27 changes: 27 additions & 0 deletions plane_mcp/tools/work_item_types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Work item type-related tools for Plane MCP Server."""

import json
from typing import Any

from fastmcp import FastMCP
Expand Down Expand Up @@ -64,6 +65,19 @@ def create_work_item_type(
"""
client, workspace_slug = get_plane_client_context()

# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(project_ids, str):
try:
project_ids = json.loads(project_ids)
except json.JSONDecodeError as e:
raise ValueError(
f"project_ids must be a JSON array string or a list, got: {project_ids!r}"
) from e
if project_ids is not None and (
not isinstance(project_ids, list) or any(not isinstance(i, str) for i in project_ids)
):
raise ValueError("project_ids must be a list[str] or a JSON array string of strings")

data = CreateWorkItemType(
name=name,
description=description,
Expand Down Expand Up @@ -131,6 +145,19 @@ def update_work_item_type(
"""
client, workspace_slug = get_plane_client_context()

# Some MCP clients serialize list parameters as JSON strings; handle both cases
if isinstance(project_ids, str):
try:
project_ids = json.loads(project_ids)
except json.JSONDecodeError as e:
raise ValueError(
f"project_ids must be a JSON array string or a list, got: {project_ids!r}"
) from e
if project_ids is not None and (
not isinstance(project_ids, list) or any(not isinstance(i, str) for i in project_ids)
):
raise ValueError("project_ids must be a list[str] or a JSON array string of strings")

data = UpdateWorkItemType(
name=name,
description=description,
Expand Down
Loading