From 02145b507a472500f58501a99990351a2fc52fa6 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Fri, 24 Oct 2025 01:03:11 +0800 Subject: [PATCH 1/2] feat: Add list_project_issues tool to fetch all issues in a project --- src/common/request-helper.ts | 4 +-- src/tools/issues.ts | 67 +++++++++++++++++++++++++++++++++--- src/tools/modules.ts | 2 +- 3 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/common/request-helper.ts b/src/common/request-helper.ts index b6ddc21..3e2f488 100644 --- a/src/common/request-helper.ts +++ b/src/common/request-helper.ts @@ -9,7 +9,7 @@ export async function makePlaneRequest(method: string, path: string, body: an }; // Only add Content-Type for non-GET requests - if (method.toUpperCase() !== 'GET') { + if (method.toUpperCase() !== "GET") { headers["Content-Type"] = "application/json"; } @@ -21,7 +21,7 @@ export async function makePlaneRequest(method: string, path: string, body: an }; // Only include body for non-GET requests - if (method.toUpperCase() !== 'GET' && body !== null) { + if (method.toUpperCase() !== "GET" && body !== null) { config.data = body; } diff --git a/src/tools/issues.ts b/src/tools/issues.ts index 6c413a0..73465c1 100644 --- a/src/tools/issues.ts +++ b/src/tools/issues.ts @@ -2,15 +2,72 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { makePlaneRequest } from "../common/request-helper.js"; -import { Issue as IssueSchema } from "../schemas.js"; +import { type Issue, Issue as IssueSchema } from "../schemas.js"; + +type IssuesResponse = { + grouped_by: null; + sub_grouped_by: null; + total_count: number; + next_cursor: string; + prev_cursor: string; + next_page_results: boolean; + prev_page_results: boolean; + count: number; + total_pages: number; + total_results: number; + extra_stats: null; + results: Issue[]; +}; + +export const registerIssueTools = (server: McpServer): void => { + server.tool( + "list_project_issues", + "Get all issues for a specific project. This requests project_id as uuid parameter. If you have a readable identifier for project, you can use the get_projects tool to get the project_id from it", + { + project_id: z.string().describe("The uuid identifier of the project to get issues for"), + }, + async ({ project_id }) => { + const issuesResponse: IssuesResponse = await makePlaneRequest( + "GET", + `workspaces/${process.env.PLANE_WORKSPACE_SLUG}/projects/${project_id}/issues/` + ); + + // Return only essential fields to reduce token usage and improve LLM processing + const simplifiedIssues = issuesResponse.results.map((issue) => ({ + id: issue.id, + name: issue.name, + sequence_id: issue.sequence_id, + state: issue.state, + priority: issue.priority, + created_at: issue.created_at, + updated_at: issue.updated_at, + })); + + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + total_count: issuesResponse.total_count, + count: issuesResponse.count, + results: simplifiedIssues, + }, + null, + 2 + ), + }, + ], + }; + } + ); -export const registerIssueTools = (server: McpServer) => { server.tool( "get_issue_using_readable_identifier", - "Get all issues for a specific project. When issue identifier is provided something like FIRST-123, ABC-123, etc. For FIRST-123, project_identifier is FIRST and issue_identifier is 123", + "Get a specific issue using its readable identifier. When issue identifier is provided something like FIRST-123, ABC-123, etc. For FIRST-123, project_identifier is FIRST and issue_identifier is 123", { - project_identifier: z.string().describe("The readable identifier of the project to get issues for"), - issue_identifier: z.string().describe("The identifier of the issue to get"), + project_identifier: z.string().describe("The readable identifier of the project (e.g., 'FIRST' for FIRST-123)"), + issue_identifier: z.string().describe("The issue number (e.g., '123' for FIRST-123)"), }, async ({ project_identifier, issue_identifier }) => { const issue = await makePlaneRequest( diff --git a/src/tools/modules.ts b/src/tools/modules.ts index 7436be3..53ea228 100644 --- a/src/tools/modules.ts +++ b/src/tools/modules.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { makePlaneRequest } from "../common/request-helper.js"; import { Module as ModuleSchema } from "../schemas.js"; -export const registerModuleTools = (server: McpServer) => { +export const registerModuleTools = (server: McpServer): void => { server.tool( "list_modules", "Get all modules for a specific project", From 82d747c5247aa553ab55f4a810f43fc9fc61eab2 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Mon, 27 Oct 2025 13:36:40 +0800 Subject: [PATCH 2/2] feat: Enhance issue response structure with detailed state and priority information --- src/tools/issues.ts | 57 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/src/tools/issues.ts b/src/tools/issues.ts index 73465c1..c5d4f21 100644 --- a/src/tools/issues.ts +++ b/src/tools/issues.ts @@ -4,6 +4,25 @@ import { z } from "zod"; import { makePlaneRequest } from "../common/request-helper.js"; import { type Issue, Issue as IssueSchema } from "../schemas.js"; +type IssueStateSummary = { + id: string; + name?: string; + color?: string; + group?: string; +}; + +type IssuePrioritySummary = { + id?: string; + label?: string; + name?: string; + key?: string; +}; + +type IssueWithDetails = Issue & { + state_detail?: IssueStateSummary | null; + priority_detail?: IssuePrioritySummary | null; +}; + type IssuesResponse = { grouped_by: null; sub_grouped_by: null; @@ -16,7 +35,7 @@ type IssuesResponse = { total_pages: number; total_results: number; extra_stats: null; - results: Issue[]; + results: IssueWithDetails[]; }; export const registerIssueTools = (server: McpServer): void => { @@ -33,15 +52,33 @@ export const registerIssueTools = (server: McpServer): void => { ); // Return only essential fields to reduce token usage and improve LLM processing - const simplifiedIssues = issuesResponse.results.map((issue) => ({ - id: issue.id, - name: issue.name, - sequence_id: issue.sequence_id, - state: issue.state, - priority: issue.priority, - created_at: issue.created_at, - updated_at: issue.updated_at, - })); + const simplifiedIssues = issuesResponse.results.map((issue) => { + const stateDetail = issue.state_detail ?? null; + const priorityDetail = + issue.priority_detail ?? + (typeof issue.priority === "object" && issue.priority !== null + ? (issue.priority as IssuePrioritySummary) + : null); + + return { + id: issue.id, + name: issue.name, + sequence_id: issue.sequence_id, + state: { + id: issue.state ?? stateDetail?.id ?? null, + name: stateDetail?.name ?? null, + color: stateDetail?.color ?? null, + group: stateDetail?.group ?? null, + }, + priority: { + id: typeof issue.priority === "string" ? issue.priority : (priorityDetail?.id ?? null), + label: priorityDetail?.label ?? priorityDetail?.name ?? null, + key: priorityDetail?.key ?? null, + }, + created_at: issue.created_at, + updated_at: issue.updated_at, + }; + }); return { content: [