Skip to main content
Glama
index.ts102 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import dotenv from "dotenv"; // Load environment variables dotenv.config(); // Retrieve the Plane API key from environment variables const PLANE_API_KEY = process.env.PLANE_API_KEY; const PLANE_WORKSPACE_SLUG = process.env.PLANE_WORKSPACE_SLUG; if (!PLANE_API_KEY) { console.error("Error: PLANE_API_KEY environment variable is required"); process.exit(1); } if (!PLANE_WORKSPACE_SLUG) { console.error("Error: PLANE_WORKSPACE_SLUG environment variable is required"); process.exit(1); } // Define tools const LIST_PROJECTS_TOOL: Tool = { name: "list-projects", description: "List all projects in the workspace with optional pagination", inputSchema: { type: "object", properties: { per_page: { type: "number", description: "Number of items per page (default: 100, max: 100)", minimum: 1, maximum: 100, }, cursor: { type: "string", description: "Pagination cursor in format 'value:offset:is_prev' (optional)", }, }, required: [], }, }; const GET_PROJECT_TOOL: Tool = { name: "get-project", description: "Get detailed information about a specific project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to retrieve", }, }, required: ["project_id"], }, }; const CREATE_ISSUE_TOOL: Tool = { name: "create-issue", description: "Create a new issue in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project where the issue should be created", }, name: { type: "string", description: "Title of the issue", }, description_html: { type: "string", description: "HTML description of the issue (required by Plane API)", }, priority: { type: "string", description: "Priority of the issue (urgent, high, medium, low, none)", enum: ["urgent", "high", "medium", "low", "none"], }, state_id: { type: "string", description: "ID of the state for this issue (optional)", }, assignees: { type: "array", items: { type: "string", }, description: "Array of user IDs to assign to this issue (optional)", }, labels: { type: "array", items: { type: "string", }, description: "Array of label IDs to assign to this issue (optional)", }, }, required: ["project_id", "name"], }, }; const LIST_ISSUES_TOOL: Tool = { name: "list-issues", description: "List issues from a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get issues from", }, state_id: { type: "string", description: "Filter by state ID (optional)", }, priority: { type: "string", description: "Filter by priority (optional)", enum: ["urgent", "high", "medium", "low", "none"], }, assignee_id: { type: "string", description: "Filter by assignee ID (optional)", }, per_page: { type: "number", description: "Number of items per page (default: 100, max: 100)", minimum: 1, maximum: 100, }, cursor: { type: "string", description: "Pagination cursor in format 'value:offset:is_prev' (optional)", }, }, required: ["project_id"], }, }; const GET_ISSUE_TOOL: Tool = { name: "get-issue", description: "Get detailed information about a specific issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to retrieve", }, }, required: ["project_id", "issue_id"], }, }; const UPDATE_ISSUE_TOOL: Tool = { name: "update-issue", description: "Update an existing issue in a project, delete just update the issue title with 'delete' or 'remove'", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to update", }, name: { type: "string", description: "Updated title of the issue (optional)", }, description_html: { type: "string", description: "HTML description of the issue (required by Plane API)", }, priority: { type: "string", description: "Updated priority of the issue (optional)", enum: ["urgent", "high", "medium", "low", "none"], }, state_id: { type: "string", description: "Updated state ID of the issue (optional)", }, assignees: { type: "array", items: { type: "string", }, description: "Updated array of user IDs to assign to this issue (optional)", }, labels: { type: "array", items: { type: "string", }, description: "Updated array of label IDs to assign to this issue (optional)", }, }, required: ["project_id", "issue_id"], }, }; // State Management Tools const LIST_STATES_TOOL: Tool = { name: "list-states", description: "List all states in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get states from", }, }, required: ["project_id"], }, }; const GET_STATE_TOOL: Tool = { name: "get-state", description: "Get detailed information about a specific state", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the state", }, state_id: { type: "string", description: "ID of the state to retrieve", }, }, required: ["project_id", "state_id"], }, }; const CREATE_STATE_TOOL: Tool = { name: "create-state", description: "Create a new state in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project where the state should be created", }, name: { type: "string", description: "Name of the state", }, description: { type: "string", description: "Description of the state (optional)", }, color: { type: "string", description: "Color code for the state (optional, e.g., #ff0000)", }, group: { type: "string", description: "State group (unstarted, started, completed, cancelled)", enum: ["unstarted", "started", "completed", "cancelled"], }, }, required: ["project_id", "name", "group"], }, }; const UPDATE_STATE_TOOL: Tool = { name: "update-state", description: "Update an existing state in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the state", }, state_id: { type: "string", description: "ID of the state to update", }, name: { type: "string", description: "Updated name of the state (optional)", }, description: { type: "string", description: "Updated description of the state (optional)", }, color: { type: "string", description: "Updated color code for the state (optional)", }, group: { type: "string", description: "Updated state group (optional)", enum: ["unstarted", "started", "completed", "cancelled"], }, }, required: ["project_id", "state_id"], }, }; const DELETE_STATE_TOOL: Tool = { name: "delete-state", description: "Delete a state from a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the state", }, state_id: { type: "string", description: "ID of the state to delete", }, }, required: ["project_id", "state_id"], }, }; // Module (Sprint) Management Tools const LIST_MODULES_TOOL: Tool = { name: "list-modules", description: "List all modules (sprints) in a project with optional pagination", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get modules from", }, per_page: { type: "number", description: "Number of items per page (default: 100, max: 100)", minimum: 1, maximum: 100, }, cursor: { type: "string", description: "Pagination cursor in format 'value:offset:is_prev' (optional)", }, }, required: ["project_id"], }, }; const GET_MODULE_TOOL: Tool = { name: "get-module", description: "Get detailed information about a specific module", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the module", }, module_id: { type: "string", description: "ID of the module to retrieve", }, }, required: ["project_id", "module_id"], }, }; const CREATE_MODULE_TOOL: Tool = { name: "create-module", description: "Create a new module (sprint) in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project where the module should be created", }, name: { type: "string", description: "Name of the module", }, description: { type: "string", description: "Description of the module (optional)", }, start_date: { type: "string", description: "Start date of the module (YYYY-MM-DD format, optional)", }, target_date: { type: "string", description: "Target end date of the module (YYYY-MM-DD format, optional)", }, status: { type: "string", description: "Status of the module", enum: ["planned", "in-progress", "paused", "completed", "cancelled"], }, lead: { type: "string", description: "User ID of the module lead (optional)", }, members: { type: "array", items: { type: "string", }, description: "Array of user IDs to assign as module members (optional)", }, }, required: ["project_id", "name"], }, }; const UPDATE_MODULE_TOOL: Tool = { name: "update-module", description: "Update an existing module in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the module", }, module_id: { type: "string", description: "ID of the module to update", }, name: { type: "string", description: "Updated name of the module (optional)", }, description: { type: "string", description: "Updated description of the module (optional)", }, start_date: { type: "string", description: "Updated start date of the module (YYYY-MM-DD format, optional)", }, target_date: { type: "string", description: "Updated target end date of the module (YYYY-MM-DD format, optional)", }, status: { type: "string", description: "Updated status of the module (optional)", enum: ["planned", "in-progress", "paused", "completed", "cancelled"], }, lead: { type: "string", description: "Updated user ID of the module lead (optional)", }, members: { type: "array", items: { type: "string", }, description: "Updated array of user IDs for module members (optional)", }, }, required: ["project_id", "module_id"], }, }; const DELETE_MODULE_TOOL: Tool = { name: "delete-module", description: "Delete a module from a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the module", }, module_id: { type: "string", description: "ID of the module to delete", }, }, required: ["project_id", "module_id"], }, }; const LIST_MODULE_ISSUES_TOOL: Tool = { name: "list-module-issues", description: "List all issues in a specific module", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the module", }, module_id: { type: "string", description: "ID of the module to get issues from", }, }, required: ["project_id", "module_id"], }, }; const ADD_ISSUES_TO_MODULE_TOOL: Tool = { name: "add-issues-to-module", description: "Add issues to a module", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the module", }, module_id: { type: "string", description: "ID of the module to add issues to", }, issues: { type: "array", items: { type: "string", }, description: "Array of issue IDs to add to the module", }, }, required: ["project_id", "module_id", "issues"], }, }; const REMOVE_ISSUE_FROM_MODULE_TOOL: Tool = { name: "remove-issue-from-module", description: "Remove an issue from a module", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the module", }, module_id: { type: "string", description: "ID of the module to remove issue from", }, issue_id: { type: "string", description: "ID of the issue to remove from the module", }, }, required: ["project_id", "module_id", "issue_id"], }, }; // Cycle Management Tools const LIST_CYCLES_TOOL: Tool = { name: "list-cycles", description: "List all cycles in a project with optional pagination", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get cycles from", }, per_page: { type: "number", description: "Number of items per page (default: 100, max: 100)", minimum: 1, maximum: 100, }, cursor: { type: "string", description: "Pagination cursor in format 'value:offset:is_prev' (optional)", }, }, required: ["project_id"], }, }; const GET_CYCLE_TOOL: Tool = { name: "get-cycle", description: "Get detailed information about a specific cycle", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the cycle", }, cycle_id: { type: "string", description: "ID of the cycle to retrieve", }, }, required: ["project_id", "cycle_id"], }, }; const CREATE_CYCLE_TOOL: Tool = { name: "create-cycle", description: "Create a new cycle in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project where the cycle should be created", }, name: { type: "string", description: "Name of the cycle", }, description: { type: "string", description: "Description of the cycle (optional)", }, start_date: { type: "string", description: "Start date of the cycle (YYYY-MM-DD format, optional)", }, end_date: { type: "string", description: "End date of the cycle (YYYY-MM-DD format, optional)", }, owned_by: { type: "string", description: "ID of the user who will own this cycle", }, }, required: ["project_id", "name", "owned_by"], }, }; const UPDATE_CYCLE_TOOL: Tool = { name: "update-cycle", description: "Update an existing cycle in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the cycle", }, cycle_id: { type: "string", description: "ID of the cycle to update", }, name: { type: "string", description: "Updated name of the cycle (optional)", }, description: { type: "string", description: "Updated description of the cycle (optional)", }, start_date: { type: "string", description: "Updated start date of the cycle (YYYY-MM-DD format, optional)", }, end_date: { type: "string", description: "Updated end date of the cycle (YYYY-MM-DD format, optional)", }, }, required: ["project_id", "cycle_id"], }, }; const DELETE_CYCLE_TOOL: Tool = { name: "delete-cycle", description: "Delete a cycle from a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the cycle", }, cycle_id: { type: "string", description: "ID of the cycle to delete", }, }, required: ["project_id", "cycle_id"], }, }; const LIST_CYCLE_ISSUES_TOOL: Tool = { name: "list-cycle-issues", description: "List all issues in a specific cycle", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the cycle", }, cycle_id: { type: "string", description: "ID of the cycle to get issues from", }, }, required: ["project_id", "cycle_id"], }, }; const ADD_ISSUES_TO_CYCLE_TOOL: Tool = { name: "add-issues-to-cycle", description: "Add issues to a cycle", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the cycle", }, cycle_id: { type: "string", description: "ID of the cycle to add issues to", }, issues: { type: "array", items: { type: "string", }, description: "Array of issue IDs to add to the cycle", }, }, required: ["project_id", "cycle_id", "issues"], }, }; const REMOVE_ISSUE_FROM_CYCLE_TOOL: Tool = { name: "remove-issue-from-cycle", description: "Remove an issue from a cycle", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the cycle", }, cycle_id: { type: "string", description: "ID of the cycle to remove issue from", }, issue_id: { type: "string", description: "ID of the issue to remove from the cycle", }, }, required: ["project_id", "cycle_id", "issue_id"], }, }; // Label Management Tools const LIST_LABELS_TOOL: Tool = { name: "list-labels", description: "List all labels in a project with optional pagination", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get labels from", }, per_page: { type: "number", description: "Number of items per page (default: 100, max: 100)", minimum: 1, maximum: 100, }, cursor: { type: "string", description: "Pagination cursor in format 'value:offset:is_prev' (optional)", }, }, required: ["project_id"], }, }; const GET_LABEL_TOOL: Tool = { name: "get-label", description: "Get detailed information about a specific label", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the label", }, label_id: { type: "string", description: "ID of the label to retrieve", }, }, required: ["project_id", "label_id"], }, }; const CREATE_LABEL_TOOL: Tool = { name: "create-label", description: "Create a new label in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project where the label should be created", }, name: { type: "string", description: "Name of the label", }, description: { type: "string", description: "Description of the label (optional)", }, color: { type: "string", description: "Color code for the label (optional, e.g., #ff0000)", }, }, required: ["project_id", "name"], }, }; const UPDATE_LABEL_TOOL: Tool = { name: "update-label", description: "Update an existing label in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the label", }, label_id: { type: "string", description: "ID of the label to update", }, name: { type: "string", description: "Updated name of the label (optional)", }, description: { type: "string", description: "Updated description of the label (optional)", }, color: { type: "string", description: "Updated color code for the label (optional)", }, }, required: ["project_id", "label_id"], }, }; const DELETE_LABEL_TOOL: Tool = { name: "delete-label", description: "Delete a label from a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the label", }, label_id: { type: "string", description: "ID of the label to delete", }, }, required: ["project_id", "label_id"], }, }; // Members and Workspace Management Tools const LIST_WORKSPACE_MEMBERS_TOOL: Tool = { name: "list-workspace-members", description: "List all members in the workspace", inputSchema: { type: "object", properties: {}, required: [], }, }; const LIST_PROJECT_MEMBERS_TOOL: Tool = { name: "list-project-members", description: "List all members in a specific project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get members from", }, }, required: ["project_id"], }, }; // Issue Comments Tools const LIST_ISSUE_COMMENTS_TOOL: Tool = { name: "list-issue-comments", description: "List all comments on a specific issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to get comments from", }, }, required: ["project_id", "issue_id"], }, }; const GET_ISSUE_COMMENT_TOOL: Tool = { name: "get-issue-comment", description: "Get detailed information about a specific comment", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue containing the comment", }, comment_id: { type: "string", description: "ID of the comment to retrieve", }, }, required: ["project_id", "issue_id", "comment_id"], }, }; const CREATE_ISSUE_COMMENT_TOOL: Tool = { name: "create-issue-comment", description: "Create a new comment on an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to comment on", }, comment: { type: "string", description: "The comment text (will be converted to HTML)", }, access: { type: "string", description: "Comment visibility: INTERNAL (default) or EXTERNAL", enum: ["INTERNAL", "EXTERNAL"], }, }, required: ["project_id", "issue_id", "comment"], }, }; const UPDATE_ISSUE_COMMENT_TOOL: Tool = { name: "update-issue-comment", description: "Update an existing comment on an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue containing the comment", }, comment_id: { type: "string", description: "ID of the comment to update", }, comment: { type: "string", description: "The updated comment text (will be converted to HTML)", }, access: { type: "string", description: "Comment visibility: INTERNAL (default) or EXTERNAL", enum: ["INTERNAL", "EXTERNAL"], }, }, required: ["project_id", "issue_id", "comment_id", "comment"], }, }; const DELETE_ISSUE_COMMENT_TOOL: Tool = { name: "delete-issue-comment", description: "Delete a comment from an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue containing the comment", }, comment_id: { type: "string", description: "ID of the comment to delete", }, }, required: ["project_id", "issue_id", "comment_id"], }, }; // Issue Links Tools const LIST_ISSUE_LINKS_TOOL: Tool = { name: "list-issue-links", description: "List all external links attached to an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to get links from", }, }, required: ["project_id", "issue_id"], }, }; const GET_ISSUE_LINK_TOOL: Tool = { name: "get-issue-link", description: "Get detailed information about a specific issue link", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue containing the link", }, link_id: { type: "string", description: "ID of the link to retrieve", }, }, required: ["project_id", "issue_id", "link_id"], }, }; const CREATE_ISSUE_LINK_TOOL: Tool = { name: "create-issue-link", description: "Create a new external link for an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to add link to", }, title: { type: "string", description: "Title/name for the link", }, url: { type: "string", description: "The URL to link to", }, }, required: ["project_id", "issue_id", "title", "url"], }, }; const UPDATE_ISSUE_LINK_TOOL: Tool = { name: "update-issue-link", description: "Update an existing issue link", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue containing the link", }, link_id: { type: "string", description: "ID of the link to update", }, title: { type: "string", description: "Updated title/name for the link (optional)", }, url: { type: "string", description: "Updated URL (optional)", }, }, required: ["project_id", "issue_id", "link_id"], }, }; const DELETE_ISSUE_LINK_TOOL: Tool = { name: "delete-issue-link", description: "Delete an external link from an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue containing the link", }, link_id: { type: "string", description: "ID of the link to delete", }, }, required: ["project_id", "issue_id", "link_id"], }, }; // Issue Attachments Tools const LIST_ISSUE_ATTACHMENTS_TOOL: Tool = { name: "list-issue-attachments", description: "List all file attachments for an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to get attachments from", }, }, required: ["project_id", "issue_id"], }, }; const GET_ISSUE_ATTACHMENT_UPLOAD_URL_TOOL: Tool = { name: "get-issue-attachment-upload-url", description: "Get a pre-signed URL for uploading file attachments to an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to attach file to", }, file_name: { type: "string", description: "Name of the file to upload", }, file_size: { type: "number", description: "Size of the file in bytes", }, content_type: { type: "string", description: "MIME type of the file (e.g., 'image/png', 'application/pdf')", }, }, required: ["project_id", "issue_id", "file_name", "file_size", "content_type"], }, }; // Issue Activities Tool const LIST_ISSUE_ACTIVITIES_TOOL: Tool = { name: "list-issue-activities", description: "List all activities/history for an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to get activities from", }, }, required: ["project_id", "issue_id"], }, }; const GET_ISSUE_ACTIVITY_TOOL: Tool = { name: "get-issue-activity", description: "Get detailed information about a specific issue activity", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue containing the activity", }, activity_id: { type: "string", description: "ID of the activity to retrieve", }, }, required: ["project_id", "issue_id", "activity_id"], }, }; // Worklogs Tools const LIST_ISSUE_WORKLOGS_TOOL: Tool = { name: "list-issue-worklogs", description: "List all time logs for a specific issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to get worklogs from", }, }, required: ["project_id", "issue_id"], }, }; const GET_PROJECT_TOTAL_WORKLOGS_TOOL: Tool = { name: "get-project-total-worklogs", description: "Get total time logged across all issues in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get total worklogs from", }, }, required: ["project_id"], }, }; const CREATE_ISSUE_WORKLOG_TOOL: Tool = { name: "create-issue-worklog", description: "Create a new time log entry for an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue to log time for", }, duration: { type: "number", description: "Time logged in hours (e.g., 2.5 for 2 hours 30 minutes)", }, description: { type: "string", description: "Description of the work done (optional)", }, }, required: ["project_id", "issue_id", "duration"], }, }; const UPDATE_ISSUE_WORKLOG_TOOL: Tool = { name: "update-issue-worklog", description: "Update an existing worklog entry", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue containing the worklog", }, worklog_id: { type: "string", description: "ID of the worklog to update", }, duration: { type: "number", description: "Updated time logged in hours (optional)", }, description: { type: "string", description: "Updated description of the work done (optional)", }, }, required: ["project_id", "issue_id", "worklog_id"], }, }; const DELETE_ISSUE_WORKLOG_TOOL: Tool = { name: "delete-issue-worklog", description: "Delete a worklog entry from an issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue", }, issue_id: { type: "string", description: "ID of the issue containing the worklog", }, worklog_id: { type: "string", description: "ID of the worklog to delete", }, }, required: ["project_id", "issue_id", "worklog_id"], }, }; // Issue Types Tools const LIST_ISSUE_TYPES_TOOL: Tool = { name: "list-issue-types", description: "List all custom issue types in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get issue types from", }, }, required: ["project_id"], }, }; const GET_ISSUE_TYPE_TOOL: Tool = { name: "get-issue-type", description: "Get detailed information about a specific issue type", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue type", }, type_id: { type: "string", description: "ID of the issue type to retrieve", }, }, required: ["project_id", "type_id"], }, }; const CREATE_ISSUE_TYPE_TOOL: Tool = { name: "create-issue-type", description: "Create a new custom issue type in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to create issue type in", }, name: { type: "string", description: "Name of the issue type", }, description: { type: "string", description: "Description of the issue type (optional)", }, }, required: ["project_id", "name"], }, }; const UPDATE_ISSUE_TYPE_TOOL: Tool = { name: "update-issue-type", description: "Update an existing issue type", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue type", }, type_id: { type: "string", description: "ID of the issue type to update", }, name: { type: "string", description: "Updated name of the issue type (optional)", }, description: { type: "string", description: "Updated description (optional)", }, }, required: ["project_id", "type_id"], }, }; const DELETE_ISSUE_TYPE_TOOL: Tool = { name: "delete-issue-type", description: "Delete an issue type from a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the issue type", }, type_id: { type: "string", description: "ID of the issue type to delete", }, }, required: ["project_id", "type_id"], }, }; // Intake Issues Tools const LIST_INTAKE_ISSUES_TOOL: Tool = { name: "list-intake-issues", description: "List all intake/inbox issues in a project", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to get intake issues from", }, }, required: ["project_id"], }, }; const GET_INTAKE_ISSUE_TOOL: Tool = { name: "get-intake-issue", description: "Get detailed information about a specific intake issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the intake issue", }, issue_id: { type: "string", description: "ID of the intake issue to retrieve", }, }, required: ["project_id", "issue_id"], }, }; const CREATE_INTAKE_ISSUE_TOOL: Tool = { name: "create-intake-issue", description: "Create a new intake/inbox issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project to create intake issue in", }, name: { type: "string", description: "Title of the intake issue", }, description_html: { type: "string", description: "HTML description of the issue (optional)", }, priority: { type: "string", description: "Priority of the issue (urgent, high, medium, low, none)", enum: ["urgent", "high", "medium", "low", "none"], }, }, required: ["project_id", "name"], }, }; const UPDATE_INTAKE_ISSUE_TOOL: Tool = { name: "update-intake-issue", description: "Update an existing intake issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the intake issue", }, issue_id: { type: "string", description: "ID of the intake issue to update", }, name: { type: "string", description: "Updated title (optional)", }, description_html: { type: "string", description: "Updated HTML description (optional)", }, priority: { type: "string", description: "Updated priority (optional)", enum: ["urgent", "high", "medium", "low", "none"], }, }, required: ["project_id", "issue_id"], }, }; const DELETE_INTAKE_ISSUE_TOOL: Tool = { name: "delete-intake-issue", description: "Delete an intake issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project containing the intake issue", }, issue_id: { type: "string", description: "ID of the intake issue to delete", }, }, required: ["project_id", "issue_id"], }, }; // Issue Property Management Tools const LIST_ISSUE_PROPERTIES_TOOL: Tool = { name: "list-issue-properties", description: "List all custom properties for a specific issue type", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, type_id: { type: "string", description: "ID of the issue type to get properties for", }, }, required: ["project_id", "type_id"], }, }; const GET_ISSUE_PROPERTY_TOOL: Tool = { name: "get-issue-property", description: "Get details of a specific issue property", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, type_id: { type: "string", description: "ID of the issue type", }, property_id: { type: "string", description: "ID of the property to retrieve", }, }, required: ["project_id", "type_id", "property_id"], }, }; const CREATE_ISSUE_PROPERTY_TOOL: Tool = { name: "create-issue-property", description: "Create a new custom property for an issue type", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, type_id: { type: "string", description: "ID of the issue type", }, name: { type: "string", description: "Internal name for the property", }, display_name: { type: "string", description: "Display name for the property", }, description: { type: "string", description: "Description of the property", }, property_type: { type: "string", description: "Type of property (TEXT, NUMBER, DATE, OPTION, etc.)", enum: ["TEXT", "NUMBER", "DATE", "OPTION", "MULTI_OPTION", "BOOLEAN"], }, is_required: { type: "boolean", description: "Whether this property is required", }, is_multi: { type: "boolean", description: "Whether multiple values are allowed (for OPTION type)", }, }, required: ["project_id", "type_id", "name", "display_name", "property_type"], }, }; const UPDATE_ISSUE_PROPERTY_TOOL: Tool = { name: "update-issue-property", description: "Update an existing issue property", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, type_id: { type: "string", description: "ID of the issue type", }, property_id: { type: "string", description: "ID of the property to update", }, name: { type: "string", description: "Internal name for the property", }, display_name: { type: "string", description: "Display name for the property", }, description: { type: "string", description: "Description of the property", }, is_required: { type: "boolean", description: "Whether this property is required", }, is_multi: { type: "boolean", description: "Whether multiple values are allowed", }, }, required: ["project_id", "type_id", "property_id"], }, }; const DELETE_ISSUE_PROPERTY_TOOL: Tool = { name: "delete-issue-property", description: "Delete an issue property", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, type_id: { type: "string", description: "ID of the issue type", }, property_id: { type: "string", description: "ID of the property to delete", }, }, required: ["project_id", "type_id", "property_id"], }, }; const LIST_ISSUE_PROPERTY_OPTIONS_TOOL: Tool = { name: "list-issue-property-options", description: "List all options for a dropdown/select issue property", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, property_id: { type: "string", description: "ID of the property to get options for", }, }, required: ["project_id", "property_id"], }, }; const CREATE_ISSUE_PROPERTY_OPTION_TOOL: Tool = { name: "create-issue-property-option", description: "Create a new option for a dropdown/select issue property", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, property_id: { type: "string", description: "ID of the property to add option to", }, name: { type: "string", description: "Name of the option", }, description: { type: "string", description: "Description of the option", }, sort_order: { type: "number", description: "Sort order for the option", }, is_default: { type: "boolean", description: "Whether this is the default option", }, }, required: ["project_id", "property_id", "name"], }, }; const UPDATE_ISSUE_PROPERTY_OPTION_TOOL: Tool = { name: "update-issue-property-option", description: "Update an option for a dropdown/select issue property", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, property_id: { type: "string", description: "ID of the property", }, option_id: { type: "string", description: "ID of the option to update", }, name: { type: "string", description: "Name of the option", }, description: { type: "string", description: "Description of the option", }, sort_order: { type: "number", description: "Sort order for the option", }, is_default: { type: "boolean", description: "Whether this is the default option", }, }, required: ["project_id", "property_id", "option_id"], }, }; // Sub-issues & Relations Tools const LIST_SUB_ISSUES_TOOL: Tool = { name: "list-sub-issues", description: "List all sub-issues of a parent issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, parent_issue_id: { type: "string", description: "ID of the parent issue to get sub-issues for", }, }, required: ["project_id", "parent_issue_id"], }, }; const CREATE_SUB_ISSUE_TOOL: Tool = { name: "create-sub-issue", description: "Create a new sub-issue under a parent issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, parent_issue_id: { type: "string", description: "ID of the parent issue", }, name: { type: "string", description: "Title of the sub-issue", }, description_html: { type: "string", description: "HTML description of the sub-issue", }, priority: { type: "string", description: "Priority of the sub-issue (urgent, high, medium, low, none)", enum: ["urgent", "high", "medium", "low", "none"], }, state_id: { type: "string", description: "ID of the state for this sub-issue (optional)", }, assignees: { type: "array", items: { type: "string", }, description: "Array of user IDs to assign to this sub-issue (optional)", }, }, required: ["project_id", "parent_issue_id", "name"], }, }; const CONVERT_TO_SUB_ISSUE_TOOL: Tool = { name: "convert-to-sub-issue", description: "Convert an existing issue to a sub-issue of another issue", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, issue_id: { type: "string", description: "ID of the issue to convert to sub-issue", }, parent_issue_id: { type: "string", description: "ID of the parent issue", }, }, required: ["project_id", "issue_id", "parent_issue_id"], }, }; const CONVERT_TO_ISSUE_TOOL: Tool = { name: "convert-to-issue", description: "Convert a sub-issue back to a regular issue (remove parent relationship)", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, issue_id: { type: "string", description: "ID of the sub-issue to convert to regular issue", }, }, required: ["project_id", "issue_id"], }, }; // Issue Transfer Operations Tool const TRANSFER_ISSUES_TOOL: Tool = { name: "transfer-issues", description: "Transfer issues from one cycle to another", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ID of the project", }, cycle_id: { type: "string", description: "ID of the source cycle to transfer issues from", }, new_cycle_id: { type: "string", description: "ID of the target cycle to transfer issues to", }, }, required: ["project_id", "cycle_id", "new_cycle_id"], }, }; /** * Calls the Plane API with appropriate headers and error handling * @param endpoint - API endpoint to call (without base URL) * @param method - HTTP method (GET, POST, PATCH, DELETE) * @param body - Optional request body for POST/PATCH requests * @returns Response data from the API */ async function callPlaneAPI( endpoint: string, method: string, body?: any ): Promise<any> { const baseUrl = `https://api.plane.so/api/v1/workspaces/${PLANE_WORKSPACE_SLUG}`; const url = `${baseUrl}${endpoint}`; // Debug logging for cycle and module operations if (endpoint.includes('/cycles/') && method === 'POST') { console.error(`DEBUG: Cycle creation URL: ${url}`); console.error(`DEBUG: Cycle creation body:`, JSON.stringify(body, null, 2)); } if (endpoint.includes('/modules/')) { console.error(`DEBUG: Module operation URL: ${url}`); console.error(`DEBUG: Module operation method: ${method}`); if (body) console.error(`DEBUG: Module operation body:`, JSON.stringify(body, null, 2)); } const options: RequestInit = { method, headers: { "Content-Type": "application/json", "X-API-Key": PLANE_API_KEY as string, }, }; if (body && (method === "POST" || method === "PATCH")) { options.body = JSON.stringify(body); } try { const response = await fetch(url, options); if (!response.ok) { let errorText; try { errorText = await response.text(); } catch (parseError) { errorText = "Unable to parse error response"; } throw new Error( `Plane API error: ${response.status} ${response.statusText}\n${errorText}` ); } // For DELETE requests that return 204 No Content if (response.status === 204) { return { success: true }; } return await response.json(); } catch (error) { throw new Error( `Error calling Plane API: ${ error instanceof Error ? error.message : String(error) }` ); } } // Initialize the server with tool metadata and capabilities const server = new Server( { name: "plane-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Register handler for listing available tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // Project Management LIST_PROJECTS_TOOL, GET_PROJECT_TOOL, // Issue Management CREATE_ISSUE_TOOL, LIST_ISSUES_TOOL, GET_ISSUE_TOOL, UPDATE_ISSUE_TOOL, // State Management LIST_STATES_TOOL, GET_STATE_TOOL, CREATE_STATE_TOOL, UPDATE_STATE_TOOL, DELETE_STATE_TOOL, // Module (Sprint) Management LIST_MODULES_TOOL, GET_MODULE_TOOL, CREATE_MODULE_TOOL, UPDATE_MODULE_TOOL, DELETE_MODULE_TOOL, LIST_MODULE_ISSUES_TOOL, ADD_ISSUES_TO_MODULE_TOOL, REMOVE_ISSUE_FROM_MODULE_TOOL, // Cycle Management LIST_CYCLES_TOOL, GET_CYCLE_TOOL, CREATE_CYCLE_TOOL, UPDATE_CYCLE_TOOL, DELETE_CYCLE_TOOL, LIST_CYCLE_ISSUES_TOOL, ADD_ISSUES_TO_CYCLE_TOOL, REMOVE_ISSUE_FROM_CYCLE_TOOL, // Label Management LIST_LABELS_TOOL, GET_LABEL_TOOL, CREATE_LABEL_TOOL, UPDATE_LABEL_TOOL, DELETE_LABEL_TOOL, // Members and Workspace LIST_WORKSPACE_MEMBERS_TOOL, LIST_PROJECT_MEMBERS_TOOL, // Issue Comments LIST_ISSUE_COMMENTS_TOOL, GET_ISSUE_COMMENT_TOOL, CREATE_ISSUE_COMMENT_TOOL, UPDATE_ISSUE_COMMENT_TOOL, DELETE_ISSUE_COMMENT_TOOL, // Issue Links LIST_ISSUE_LINKS_TOOL, GET_ISSUE_LINK_TOOL, CREATE_ISSUE_LINK_TOOL, UPDATE_ISSUE_LINK_TOOL, DELETE_ISSUE_LINK_TOOL, // Issue Attachments LIST_ISSUE_ATTACHMENTS_TOOL, GET_ISSUE_ATTACHMENT_UPLOAD_URL_TOOL, // Issue Activities LIST_ISSUE_ACTIVITIES_TOOL, GET_ISSUE_ACTIVITY_TOOL, // Worklogs (Time Tracking) LIST_ISSUE_WORKLOGS_TOOL, GET_PROJECT_TOTAL_WORKLOGS_TOOL, CREATE_ISSUE_WORKLOG_TOOL, UPDATE_ISSUE_WORKLOG_TOOL, DELETE_ISSUE_WORKLOG_TOOL, // Issue Types LIST_ISSUE_TYPES_TOOL, GET_ISSUE_TYPE_TOOL, CREATE_ISSUE_TYPE_TOOL, UPDATE_ISSUE_TYPE_TOOL, DELETE_ISSUE_TYPE_TOOL, // Intake Issues LIST_INTAKE_ISSUES_TOOL, GET_INTAKE_ISSUE_TOOL, CREATE_INTAKE_ISSUE_TOOL, UPDATE_INTAKE_ISSUE_TOOL, DELETE_INTAKE_ISSUE_TOOL, // Issue Property Management LIST_ISSUE_PROPERTIES_TOOL, GET_ISSUE_PROPERTY_TOOL, CREATE_ISSUE_PROPERTY_TOOL, UPDATE_ISSUE_PROPERTY_TOOL, DELETE_ISSUE_PROPERTY_TOOL, LIST_ISSUE_PROPERTY_OPTIONS_TOOL, CREATE_ISSUE_PROPERTY_OPTION_TOOL, UPDATE_ISSUE_PROPERTY_OPTION_TOOL, // Sub-issues & Relations LIST_SUB_ISSUES_TOOL, CREATE_SUB_ISSUE_TOOL, CONVERT_TO_SUB_ISSUE_TOOL, CONVERT_TO_ISSUE_TOOL, // Issue Transfer Operations TRANSFER_ISSUES_TOOL, ], })); // Register handler for calling tools server.setRequestHandler(CallToolRequestSchema, async (request) => { try { let { name, arguments: args = {} } = request.params; // Normalize tool name to handle both hyphen and underscore formats const normalizedName = name.replace(/_/g, "-"); name = normalizedName; switch (name) { case "list-projects": { // Build query string for pagination parameters const queryParams = args || {}; const queryString = Object.entries(queryParams) .filter(([_, value]) => value !== undefined) .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`) .join("&"); const endpoint = `/projects/${queryString ? `?${queryString}` : ""}`; const projects = await callPlaneAPI(endpoint, "GET"); return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }], isError: false, }; } case "get-project": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id } = args; const project = await callPlaneAPI(`/projects/${project_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(project, null, 2) }], isError: false, }; } case "create-issue": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id, ...issueData } = args; // Ensure assignees is properly formatted as an array if (issueData.assignees) { // Special case: detect if entire issue is nested in assignees if ( typeof issueData.assignees === "object" && !Array.isArray(issueData.assignees) && (issueData.assignees as Record<string, any>).project_id && (issueData.assignees as Record<string, any>).name ) { // Issue is nested inside assignees, remove it completely delete issueData.assignees; } else if (!Array.isArray(issueData.assignees)) { if (typeof issueData.assignees === "string") { // Convert single string to array issueData.assignees = [issueData.assignees]; } else if (typeof issueData.assignees === "object") { // Convert object to array of values issueData.assignees = Object.values(issueData.assignees); } else { // Remove invalid assignees delete issueData.assignees; } } } const issue = await callPlaneAPI( `/projects/${project_id}/issues/`, "POST", issueData ); return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], isError: false, }; } case "list-issues": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id, ...queryParams } = args; // Build query string from other parameters const queryString = Object.entries(queryParams) .filter(([_, value]) => value !== undefined) .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`) .join("&"); const endpoint = `/projects/${project_id}/issues/${ queryString ? `?${queryString}` : "" }`; const issues = await callPlaneAPI(endpoint, "GET"); return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }], isError: false, }; } case "get-issue": { if ( !args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" ) { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id } = args; const issue = await callPlaneAPI( `/projects/${project_id}/issues/${issue_id}/`, "GET" ); return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }], isError: false, }; } case "update-issue": { if ( !args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" ) { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id, ...updateData } = args; // Ensure assignees is properly formatted as an array if (updateData.assignees) { // Special case: detect if entire issue is nested in assignees if ( typeof updateData.assignees === "object" && !Array.isArray(updateData.assignees) && (updateData.assignees as Record<string, any>).project_id && (updateData.assignees as Record<string, any>).name ) { // Issue is nested inside assignees, remove it completely delete updateData.assignees; } else if (!Array.isArray(updateData.assignees)) { if (typeof updateData.assignees === "string") { // Convert single string to array updateData.assignees = [updateData.assignees]; } else if (typeof updateData.assignees === "object") { // Convert object to array of values updateData.assignees = Object.values(updateData.assignees); } else { // Remove invalid assignees delete updateData.assignees; } } } const updatedIssue = await callPlaneAPI( `/projects/${project_id}/issues/${issue_id}/`, "PATCH", updateData ); return { content: [ { type: "text", text: JSON.stringify(updatedIssue, null, 2) }, ], isError: false, }; } // State Management Cases case "list-states": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id } = args; const states = await callPlaneAPI(`/projects/${project_id}/states/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(states, null, 2) }], isError: false, }; } case "get-state": { if (!args || typeof args.project_id !== "string" || typeof args.state_id !== "string") { throw new Error("Project ID and State ID are required"); } const { project_id, state_id } = args; const state = await callPlaneAPI(`/projects/${project_id}/states/${state_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(state, null, 2) }], isError: false, }; } case "create-state": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id, ...stateData } = args; const state = await callPlaneAPI(`/projects/${project_id}/states/`, "POST", stateData); return { content: [{ type: "text", text: JSON.stringify(state, null, 2) }], isError: false, }; } case "update-state": { if (!args || typeof args.project_id !== "string" || typeof args.state_id !== "string") { throw new Error("Project ID and State ID are required"); } const { project_id, state_id, ...updateData } = args; const updatedState = await callPlaneAPI(`/projects/${project_id}/states/${state_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedState, null, 2) }], isError: false, }; } case "delete-state": { if (!args || typeof args.project_id !== "string" || typeof args.state_id !== "string") { throw new Error("Project ID and State ID are required"); } const { project_id, state_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/states/${state_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } // Module Management Cases case "list-modules": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id } = args; const modules = await callPlaneAPI(`/projects/${project_id}/modules/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(modules, null, 2) }], isError: false, }; } case "get-module": { if (!args || typeof args.project_id !== "string" || typeof args.module_id !== "string") { throw new Error("Project ID and Module ID are required"); } const { project_id, module_id } = args; const module = await callPlaneAPI(`/projects/${project_id}/modules/${module_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(module, null, 2) }], isError: false, }; } case "create-module": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id, ...moduleData } = args; // Ensure members is properly formatted as an array if (moduleData.members && !Array.isArray(moduleData.members)) { if (typeof moduleData.members === "string") { moduleData.members = [moduleData.members]; } else if (typeof moduleData.members === "object") { moduleData.members = Object.values(moduleData.members); } else { delete moduleData.members; } } const module = await callPlaneAPI(`/projects/${project_id}/modules/`, "POST", moduleData); return { content: [{ type: "text", text: JSON.stringify(module, null, 2) }], isError: false, }; } case "update-module": { if (!args || typeof args.project_id !== "string" || typeof args.module_id !== "string") { throw new Error("Project ID and Module ID are required"); } const { project_id, module_id, ...updateData } = args; // Ensure members is properly formatted as an array if (updateData.members && !Array.isArray(updateData.members)) { if (typeof updateData.members === "string") { updateData.members = [updateData.members]; } else if (typeof updateData.members === "object") { updateData.members = Object.values(updateData.members); } else { delete updateData.members; } } const updatedModule = await callPlaneAPI(`/projects/${project_id}/modules/${module_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedModule, null, 2) }], isError: false, }; } case "delete-module": { if (!args || typeof args.project_id !== "string" || typeof args.module_id !== "string") { throw new Error("Project ID and Module ID are required"); } const { project_id, module_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/modules/${module_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } case "list-module-issues": { if (!args || typeof args.project_id !== "string" || typeof args.module_id !== "string") { throw new Error("Project ID and Module ID are required"); } const { project_id, module_id } = args; const issues = await callPlaneAPI(`/projects/${project_id}/modules/${module_id}/module-issues/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }], isError: false, }; } case "add-issues-to-module": { if (!args || typeof args.project_id !== "string" || typeof args.module_id !== "string" || !Array.isArray(args.issues)) { throw new Error("Project ID, Module ID, and issues array are required"); } const { project_id, module_id, issues } = args; const result = await callPlaneAPI(`/projects/${project_id}/modules/${module_id}/module-issues/`, "POST", { issues }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } case "remove-issue-from-module": { if (!args || typeof args.project_id !== "string" || typeof args.module_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID, Module ID, and Issue ID are required"); } const { project_id, module_id, issue_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/modules/${module_id}/module-issues/${issue_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } // Cycle Management Cases case "list-cycles": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id } = args; const cycles = await callPlaneAPI(`/projects/${project_id}/cycles/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(cycles, null, 2) }], isError: false, }; } case "get-cycle": { if (!args || typeof args.project_id !== "string" || typeof args.cycle_id !== "string") { throw new Error("Project ID and Cycle ID are required"); } const { project_id, cycle_id } = args; const cycle = await callPlaneAPI(`/projects/${project_id}/cycles/${cycle_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(cycle, null, 2) }], isError: false, }; } case "create-cycle": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id, ...cycleData } = args; // Include project_id in request body as some cycle APIs require it const requestBody = { ...cycleData, project: project_id }; const cycle = await callPlaneAPI(`/projects/${project_id}/cycles/`, "POST", requestBody); return { content: [{ type: "text", text: JSON.stringify(cycle, null, 2) }], isError: false, }; } case "update-cycle": { if (!args || typeof args.project_id !== "string" || typeof args.cycle_id !== "string") { throw new Error("Project ID and Cycle ID are required"); } const { project_id, cycle_id, ...updateData } = args; const updatedCycle = await callPlaneAPI(`/projects/${project_id}/cycles/${cycle_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedCycle, null, 2) }], isError: false, }; } case "delete-cycle": { if (!args || typeof args.project_id !== "string" || typeof args.cycle_id !== "string") { throw new Error("Project ID and Cycle ID are required"); } const { project_id, cycle_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/cycles/${cycle_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } case "list-cycle-issues": { if (!args || typeof args.project_id !== "string" || typeof args.cycle_id !== "string") { throw new Error("Project ID and Cycle ID are required"); } const { project_id, cycle_id } = args; const issues = await callPlaneAPI(`/projects/${project_id}/cycles/${cycle_id}/cycle-issues/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }], isError: false, }; } case "add-issues-to-cycle": { if (!args || typeof args.project_id !== "string" || typeof args.cycle_id !== "string" || !Array.isArray(args.issues)) { throw new Error("Project ID, Cycle ID, and issues array are required"); } const { project_id, cycle_id, issues } = args; const result = await callPlaneAPI(`/projects/${project_id}/cycles/${cycle_id}/cycle-issues/`, "POST", { issues }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } case "remove-issue-from-cycle": { if (!args || typeof args.project_id !== "string" || typeof args.cycle_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID, Cycle ID, and Issue ID are required"); } const { project_id, cycle_id, issue_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/cycles/${cycle_id}/cycle-issues/${issue_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } // Label Management Cases case "list-labels": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id } = args; const labels = await callPlaneAPI(`/projects/${project_id}/labels/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(labels, null, 2) }], isError: false, }; } case "get-label": { if (!args || typeof args.project_id !== "string" || typeof args.label_id !== "string") { throw new Error("Project ID and Label ID are required"); } const { project_id, label_id } = args; const label = await callPlaneAPI(`/projects/${project_id}/labels/${label_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(label, null, 2) }], isError: false, }; } case "create-label": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id, ...labelData } = args; const label = await callPlaneAPI(`/projects/${project_id}/labels/`, "POST", labelData); return { content: [{ type: "text", text: JSON.stringify(label, null, 2) }], isError: false, }; } case "update-label": { if (!args || typeof args.project_id !== "string" || typeof args.label_id !== "string") { throw new Error("Project ID and Label ID are required"); } const { project_id, label_id, ...updateData } = args; const updatedLabel = await callPlaneAPI(`/projects/${project_id}/labels/${label_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedLabel, null, 2) }], isError: false, }; } case "delete-label": { if (!args || typeof args.project_id !== "string" || typeof args.label_id !== "string") { throw new Error("Project ID and Label ID are required"); } const { project_id, label_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/labels/${label_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } // Members and Workspace Management Cases case "list-workspace-members": { const members = await callPlaneAPI("/members/", "GET"); return { content: [{ type: "text", text: JSON.stringify(members, null, 2) }], isError: false, }; } case "list-project-members": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id } = args; const members = await callPlaneAPI(`/projects/${project_id}/members/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(members, null, 2) }], isError: false, }; } // Issue Comments Cases case "list-issue-comments": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id } = args; const comments = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/comments/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(comments, null, 2) }], isError: false, }; } case "get-issue-comment": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.comment_id !== "string") { throw new Error("Project ID, Issue ID, and Comment ID are required"); } const { project_id, issue_id, comment_id } = args; const comment = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/comments/${comment_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(comment, null, 2) }], isError: false, }; } case "create-issue-comment": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.comment !== "string") { throw new Error("Project ID, Issue ID, and comment text are required"); } const { project_id, issue_id, comment } = args; // Convert plain text to HTML format as required by Plane API const comment_html = `<p>${comment.replace(/\n/g, '</p><p>')}</p>`; const newComment = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/comments/`, "POST", { comment_html, access: args.access || "INTERNAL" }); return { content: [{ type: "text", text: JSON.stringify(newComment, null, 2) }], isError: false, }; } case "update-issue-comment": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.comment_id !== "string" || typeof args.comment !== "string") { throw new Error("Project ID, Issue ID, Comment ID, and comment text are required"); } const { project_id, issue_id, comment_id, comment } = args; // Convert plain text to HTML format as required by Plane API const comment_html = `<p>${comment.replace(/\n/g, '</p><p>')}</p>`; const updatedComment = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/comments/${comment_id}/`, "PATCH", { comment_html, access: args.access || "INTERNAL" }); return { content: [{ type: "text", text: JSON.stringify(updatedComment, null, 2) }], isError: false, }; } case "delete-issue-comment": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.comment_id !== "string") { throw new Error("Project ID, Issue ID, and Comment ID are required"); } const { project_id, issue_id, comment_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/comments/${comment_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } // Issue Links Cases case "list-issue-links": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id } = args; const links = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/links/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(links, null, 2) }], isError: false, }; } case "get-issue-link": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.link_id !== "string") { throw new Error("Project ID, Issue ID, and Link ID are required"); } const { project_id, issue_id, link_id } = args; const link = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/links/${link_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(link, null, 2) }], isError: false, }; } case "create-issue-link": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.title !== "string" || typeof args.url !== "string") { throw new Error("Project ID, Issue ID, title, and URL are required"); } const { project_id, issue_id, title, url } = args; const link = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/links/`, "POST", { title, url }); return { content: [{ type: "text", text: JSON.stringify(link, null, 2) }], isError: false, }; } case "update-issue-link": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.link_id !== "string") { throw new Error("Project ID, Issue ID, and Link ID are required"); } const { project_id, issue_id, link_id, ...updateData } = args; const updatedLink = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/links/${link_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedLink, null, 2) }], isError: false, }; } case "delete-issue-link": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.link_id !== "string") { throw new Error("Project ID, Issue ID, and Link ID are required"); } const { project_id, issue_id, link_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/links/${link_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } // Issue Attachments Cases case "list-issue-attachments": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id } = args; const attachments = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/issue-attachments/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(attachments, null, 2) }], isError: false, }; } case "get-issue-attachment-upload-url": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.file_name !== "string" || typeof args.file_size !== "number" || typeof args.content_type !== "string") { throw new Error("Project ID, Issue ID, file_name, file_size, and content_type are required"); } const { project_id, issue_id, file_name, file_size, content_type } = args; const uploadData = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/issue-attachments/`, "POST", { attributes: { name: file_name, size: file_size, type: content_type } }); return { content: [{ type: "text", text: JSON.stringify(uploadData, null, 2) }], isError: false, }; } // Issue Activities Cases case "list-issue-activities": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id } = args; const activities = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/activities/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(activities, null, 2) }], isError: false, }; } case "get-issue-activity": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.activity_id !== "string") { throw new Error("Project ID, Issue ID, and Activity ID are required"); } const { project_id, issue_id, activity_id } = args; const activity = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/activities/${activity_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(activity, null, 2) }], isError: false, }; } // Worklogs Cases case "list-issue-worklogs": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id } = args; const worklogs = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/worklogs/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(worklogs, null, 2) }], isError: false, }; } case "get-project-total-worklogs": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id } = args; const totalWorklogs = await callPlaneAPI(`/projects/${project_id}/total-worklogs/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(totalWorklogs, null, 2) }], isError: false, }; } case "create-issue-worklog": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.duration !== "number") { throw new Error("Project ID, Issue ID, and duration are required"); } const { project_id, issue_id, duration, description } = args; const worklogData: any = { duration }; if (description) worklogData.description = description; const worklog = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/worklogs/`, "POST", worklogData); return { content: [{ type: "text", text: JSON.stringify(worklog, null, 2) }], isError: false, }; } case "update-issue-worklog": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.worklog_id !== "string") { throw new Error("Project ID, Issue ID, and Worklog ID are required"); } const { project_id, issue_id, worklog_id, ...updateData } = args; const updatedWorklog = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/worklogs/${worklog_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedWorklog, null, 2) }], isError: false, }; } case "delete-issue-worklog": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.worklog_id !== "string") { throw new Error("Project ID, Issue ID, and Worklog ID are required"); } const { project_id, issue_id, worklog_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/worklogs/${worklog_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } // Issue Types Cases case "list-issue-types": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id } = args; const issueTypes = await callPlaneAPI(`/projects/${project_id}/issue-types/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(issueTypes, null, 2) }], isError: false, }; } case "get-issue-type": { if (!args || typeof args.project_id !== "string" || typeof args.type_id !== "string") { throw new Error("Project ID and Type ID are required"); } const { project_id, type_id } = args; const issueType = await callPlaneAPI(`/projects/${project_id}/issue-types/${type_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(issueType, null, 2) }], isError: false, }; } case "create-issue-type": { if (!args || typeof args.project_id !== "string" || typeof args.name !== "string") { throw new Error("Project ID and name are required"); } const { project_id, ...typeData } = args; const issueType = await callPlaneAPI(`/projects/${project_id}/issue-types/`, "POST", typeData); return { content: [{ type: "text", text: JSON.stringify(issueType, null, 2) }], isError: false, }; } case "update-issue-type": { if (!args || typeof args.project_id !== "string" || typeof args.type_id !== "string") { throw new Error("Project ID and Type ID are required"); } const { project_id, type_id, ...updateData } = args; const updatedIssueType = await callPlaneAPI(`/projects/${project_id}/issue-types/${type_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedIssueType, null, 2) }], isError: false, }; } case "delete-issue-type": { if (!args || typeof args.project_id !== "string" || typeof args.type_id !== "string") { throw new Error("Project ID and Type ID are required"); } const { project_id, type_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/issue-types/${type_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } // Intake Issues Cases case "list-intake-issues": { if (!args || typeof args.project_id !== "string") { throw new Error("Project ID is required"); } const { project_id } = args; const intakeIssues = await callPlaneAPI(`/projects/${project_id}/intake-issues/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(intakeIssues, null, 2) }], isError: false, }; } case "get-intake-issue": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id } = args; const intakeIssue = await callPlaneAPI(`/projects/${project_id}/intake-issues/${issue_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(intakeIssue, null, 2) }], isError: false, }; } case "create-intake-issue": { if (!args || typeof args.project_id !== "string" || typeof args.name !== "string") { throw new Error("Project ID and name are required"); } const { project_id, ...issueData } = args; const intakeIssue = await callPlaneAPI(`/projects/${project_id}/intake-issues/`, "POST", issueData); return { content: [{ type: "text", text: JSON.stringify(intakeIssue, null, 2) }], isError: false, }; } case "update-intake-issue": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id, ...updateData } = args; const updatedIntakeIssue = await callPlaneAPI(`/projects/${project_id}/intake-issues/${issue_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedIntakeIssue, null, 2) }], isError: false, }; } case "delete-intake-issue": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/intake-issues/${issue_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } // Issue Property Management Cases case "list-issue-properties": { if (!args || typeof args.project_id !== "string" || typeof args.type_id !== "string") { throw new Error("Project ID and Type ID are required"); } const { project_id, type_id } = args; const properties = await callPlaneAPI(`/projects/${project_id}/issue-types/${type_id}/issue-properties/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(properties, null, 2) }], isError: false, }; } case "get-issue-property": { if (!args || typeof args.project_id !== "string" || typeof args.type_id !== "string" || typeof args.property_id !== "string") { throw new Error("Project ID, Type ID, and Property ID are required"); } const { project_id, type_id, property_id } = args; const property = await callPlaneAPI(`/projects/${project_id}/issue-types/${type_id}/issue-properties/${property_id}/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(property, null, 2) }], isError: false, }; } case "create-issue-property": { if (!args || typeof args.project_id !== "string" || typeof args.type_id !== "string") { throw new Error("Project ID and Type ID are required"); } const { project_id, type_id, ...propertyData } = args; const property = await callPlaneAPI(`/projects/${project_id}/issue-types/${type_id}/issue-properties/`, "POST", propertyData); return { content: [{ type: "text", text: JSON.stringify(property, null, 2) }], isError: false, }; } case "update-issue-property": { if (!args || typeof args.project_id !== "string" || typeof args.type_id !== "string" || typeof args.property_id !== "string") { throw new Error("Project ID, Type ID, and Property ID are required"); } const { project_id, type_id, property_id, ...updateData } = args; const updatedProperty = await callPlaneAPI(`/projects/${project_id}/issue-types/${type_id}/issue-properties/${property_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedProperty, null, 2) }], isError: false, }; } case "delete-issue-property": { if (!args || typeof args.project_id !== "string" || typeof args.type_id !== "string" || typeof args.property_id !== "string") { throw new Error("Project ID, Type ID, and Property ID are required"); } const { project_id, type_id, property_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/issue-types/${type_id}/issue-properties/${property_id}/`, "DELETE"); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } case "list-issue-property-options": { if (!args || typeof args.project_id !== "string" || typeof args.property_id !== "string") { throw new Error("Project ID and Property ID are required"); } const { project_id, property_id } = args; const options = await callPlaneAPI(`/projects/${project_id}/issue-properties/${property_id}/options/`, "GET"); return { content: [{ type: "text", text: JSON.stringify(options, null, 2) }], isError: false, }; } case "create-issue-property-option": { if (!args || typeof args.project_id !== "string" || typeof args.property_id !== "string") { throw new Error("Project ID and Property ID are required"); } const { project_id, property_id, ...optionData } = args; const option = await callPlaneAPI(`/projects/${project_id}/issue-properties/${property_id}/options/`, "POST", optionData); return { content: [{ type: "text", text: JSON.stringify(option, null, 2) }], isError: false, }; } case "update-issue-property-option": { if (!args || typeof args.project_id !== "string" || typeof args.property_id !== "string" || typeof args.option_id !== "string") { throw new Error("Project ID, Property ID, and Option ID are required"); } const { project_id, property_id, option_id, ...updateData } = args; const updatedOption = await callPlaneAPI(`/projects/${project_id}/issue-properties/${property_id}/options/${option_id}/`, "PATCH", updateData); return { content: [{ type: "text", text: JSON.stringify(updatedOption, null, 2) }], isError: false, }; } // Sub-issues & Relations Cases case "list-sub-issues": { if (!args || typeof args.project_id !== "string" || typeof args.parent_issue_id !== "string") { throw new Error("Project ID and Parent Issue ID are required"); } const { project_id, parent_issue_id } = args; const issues = await callPlaneAPI(`/projects/${project_id}/issues/?parent=${parent_issue_id}`, "GET"); return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }], isError: false, }; } case "create-sub-issue": { if (!args || typeof args.project_id !== "string" || typeof args.parent_issue_id !== "string") { throw new Error("Project ID and Parent Issue ID are required"); } const { project_id, parent_issue_id, ...issueData } = args; const subIssueData = { ...issueData, parent: parent_issue_id, }; const subIssue = await callPlaneAPI(`/projects/${project_id}/issues/`, "POST", subIssueData); return { content: [{ type: "text", text: JSON.stringify(subIssue, null, 2) }], isError: false, }; } case "convert-to-sub-issue": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string" || typeof args.parent_issue_id !== "string") { throw new Error("Project ID, Issue ID, and Parent Issue ID are required"); } const { project_id, issue_id, parent_issue_id } = args; const updatedIssue = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/`, "PATCH", { parent: parent_issue_id }); return { content: [{ type: "text", text: JSON.stringify(updatedIssue, null, 2) }], isError: false, }; } case "convert-to-issue": { if (!args || typeof args.project_id !== "string" || typeof args.issue_id !== "string") { throw new Error("Project ID and Issue ID are required"); } const { project_id, issue_id } = args; const updatedIssue = await callPlaneAPI(`/projects/${project_id}/issues/${issue_id}/`, "PATCH", { parent: null }); return { content: [{ type: "text", text: JSON.stringify(updatedIssue, null, 2) }], isError: false, }; } // Issue Transfer Operations Cases case "transfer-issues": { if (!args || typeof args.project_id !== "string" || typeof args.cycle_id !== "string" || typeof args.new_cycle_id !== "string") { throw new Error("Project ID, Cycle ID, and New Cycle ID are required"); } const { project_id, cycle_id, new_cycle_id } = args; const result = await callPlaneAPI(`/projects/${project_id}/cycles/${cycle_id}/transfer-issues/`, "POST", { new_cycle_id }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false, }; } default: return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } } catch (error) { return { content: [ { type: "text", text: `Error: ${ error instanceof Error ? error.message : String(error) }`, }, ], isError: true, }; } }); /** * Initializes and runs the MCP server using stdio for communication */ async function runServer() { try { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Plane MCP Server running on stdio"); } catch (error) { console.error("Fatal error running server:", error); process.exit(1); } } // Start the server runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/disrex-group/plane-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server