Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/common/request-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function makePlaneRequest<T>(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";
}

Expand All @@ -21,7 +21,7 @@ export async function makePlaneRequest<T>(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;
}

Expand Down
104 changes: 99 additions & 5 deletions src/tools/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,109 @@ 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 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;
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: IssueWithDetails[];
};
Comment on lines +26 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Consider more flexible typing for nullable fields.

The IssuesResponse type definition hardcodes several fields as null (lines 8, 9, 18). If the Plane API can return non-null values for grouped_by, sub_grouped_by, or extra_stats in different contexts (e.g., when grouping is applied), the current typing will cause runtime type mismatches.

Consider using union types for these fields if they can have values:

 type IssuesResponse = {
-  grouped_by: null;
-  sub_grouped_by: null;
+  grouped_by: string | null;
+  sub_grouped_by: string | 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;
+  extra_stats: Record<string, any> | null;
   results: Issue[];
 };

Please verify the Plane API documentation to confirm whether these fields can have non-null values.


🌐 Web query:

What are the possible values for grouped_by, sub_grouped_by, and extra_stats fields in Plane API issues endpoint response?

💡 Result:

Do you mean Plane (plane.so) API? I can look up the official docs and list the exact permitted values — want me to fetch that now?



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for API response examples, fixtures, or references to grouped_by, sub_grouped_by, extra_stats
rg -nC5 "grouped_by|sub_grouped_by|extra_stats" --type=ts --type=js --type=json

Length of output: 2088



---

🏁 Script executed:

```shell
#!/bin/bash
# Check how IssuesResponse is used in the codebase
echo "=== Searching for IssuesResponse usage ==="
rg -nC3 "IssuesResponse" --type=ts --type=js

echo -e "\n=== Checking for makePlaneRequest calls to issues endpoint ==="
rg -nC5 "makePlaneRequest.*issues|/issues" --type=ts --type=js

echo -e "\n=== Looking for test files or mock data ==="
fd -e "test\|spec\|mock\|fixture" --type f

echo -e "\n=== Searching for any Plane API version info or documentation references ==="
rg -nC2 "plane\.so|developers\.plane|API.*version" --type=ts --type=js --type=md

Length of output: 9275



---

🏁 Script executed:

```shell
#!/bin/bash
# Check Plane source code for API response schemas
# Try to find issues API response definition
rg -nC5 "grouped_by|sub_grouped_by" --type=py --type=json -g "*.py" -g "*.json"

# Also check if there are any import statements or API schema definitions
echo -e "\n=== Checking for schema or serializer files ==="
fd -e "schema" -e "serializer" -e "response" | head -20

Length of output: 48



---

🌐 Web query:

```web
Plane API list issues endpoint response schema grouped_by sub_grouped_by

💡 Result:

The List Issues response includes top-level keys grouped_by and sub_grouped_by. Type: string | null — they contain the field name used to group results (or null when not grouped). Example (from Plane dummy response): "grouped_by": null, "sub_grouped_by": null. [1][2]

Sources:

  • Plane API — List issues (response/pagination description). [1]
  • Example/dummy JSON showing grouped_by and sub_grouped_by. [2]

Update field types to match Plane API schema.

The IssuesResponse type (lines 8, 9, 18) has types that are too narrow and don't match the documented Plane API response schema:

  • grouped_by and sub_grouped_by should be string | null per Plane API docs—they contain the grouping field name when grouping is applied, or null when not grouped
  • extra_stats should be Record<string, any> | null per API documentation (shown as {} in responses, not null)

The same issue exists in src/tools/projects.ts with the identical ProjectsResponse type definition.

Update both files:

 type IssuesResponse = {
-  grouped_by: null;
-  sub_grouped_by: null;
+  grouped_by: string | null;
+  sub_grouped_by: string | 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;
+  extra_stats: Record<string, any> | null;
   results: Issue[];
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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[];
};
type IssuesResponse = {
grouped_by: string | null;
sub_grouped_by: string | 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: Record<string, any> | null;
results: Issue[];
};
🤖 Prompt for AI Agents
In src/tools/issues.ts around lines 7 to 20, update the IssuesResponse type so
that grouped_by and sub_grouped_by are typed as string | null (they hold the
grouping field name or null) and change extra_stats to Record<string, any> |
null (API returns an object or null); apply the same changes to the identical
ProjectsResponse type in src/tools/projects.ts so both response types match the
Plane API schema.


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<IssuesResponse>(
"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) => {
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: [
{
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(
Expand Down
2 changes: 1 addition & 1 deletion src/tools/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down