Skip to content

Commit 7ae3863

Browse files
committed
feat(mcp): add create_issue tool for issue creation via MCP
Add new create_issue tool to the MCP server that allows AI agents to create issues directly in PostgresAI. Supports title, description, project_id, and labels parameters.
1 parent f509de2 commit 7ae3863

2 files changed

Lines changed: 120 additions & 1 deletion

File tree

cli/lib/issues.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,91 @@ export async function fetchIssue(params: FetchIssueParams): Promise<IssueDetail
223223
}
224224
}
225225

226+
export interface CreateIssueParams {
227+
apiKey: string;
228+
apiBaseUrl: string;
229+
title: string;
230+
description?: string;
231+
projectId?: number;
232+
labels?: string[];
233+
debug?: boolean;
234+
}
235+
236+
export interface CreatedIssue {
237+
id: string;
238+
title: string;
239+
description: string | null;
240+
created_at: string;
241+
status: number;
242+
project_id: number | null;
243+
labels: string[] | null;
244+
}
245+
246+
export async function createIssue(params: CreateIssueParams): Promise<CreatedIssue> {
247+
const { apiKey, apiBaseUrl, title, description, projectId, labels, debug } = params;
248+
if (!apiKey) {
249+
throw new Error("API key is required");
250+
}
251+
if (!title) {
252+
throw new Error("title is required");
253+
}
254+
255+
const base = normalizeBaseUrl(apiBaseUrl);
256+
const url = new URL(`${base}/rpc/issue_create`);
257+
258+
const bodyObj: Record<string, unknown> = {
259+
title: title,
260+
};
261+
if (description) {
262+
bodyObj.description = description;
263+
}
264+
if (projectId !== undefined) {
265+
bodyObj.project_id = projectId;
266+
}
267+
if (labels && labels.length > 0) {
268+
bodyObj.labels = labels;
269+
}
270+
const body = JSON.stringify(bodyObj);
271+
272+
const headers: Record<string, string> = {
273+
"access-token": apiKey,
274+
"Prefer": "return=representation",
275+
"Content-Type": "application/json",
276+
};
277+
278+
if (debug) {
279+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
280+
console.log(`Debug: Resolved API base URL: ${base}`);
281+
console.log(`Debug: POST URL: ${url.toString()}`);
282+
console.log(`Debug: Auth scheme: access-token`);
283+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
284+
console.log(`Debug: Request body: ${body}`);
285+
}
286+
287+
const response = await fetch(url.toString(), {
288+
method: "POST",
289+
headers,
290+
body,
291+
});
292+
293+
if (debug) {
294+
console.log(`Debug: Response status: ${response.status}`);
295+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
296+
}
297+
298+
const data = await response.text();
299+
300+
if (response.ok) {
301+
try {
302+
return JSON.parse(data) as CreatedIssue;
303+
} catch {
304+
throw new Error(`Failed to parse create issue response: ${data}`);
305+
}
306+
} else {
307+
throw new Error(formatHttpError("Failed to create issue", response.status, data));
308+
}
309+
}
310+
226311
export interface CreateIssueCommentParams {
227312
apiKey: string;
228313
apiBaseUrl: string;

cli/lib/mcp-server.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pkg from "../package.json";
22
import * as config from "./config";
3-
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "./issues";
3+
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue, createIssue } from "./issues";
44
import { resolveBaseUrls } from "./util";
55

66
// MCP SDK imports - Bun handles these directly
@@ -71,6 +71,26 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
7171
additionalProperties: false,
7272
},
7373
},
74+
{
75+
name: "create_issue",
76+
description: "Create a new issue in PostgresAI",
77+
inputSchema: {
78+
type: "object",
79+
properties: {
80+
title: { type: "string", description: "Issue title (required)" },
81+
description: { type: "string", description: "Issue description (supports \\n as newline)" },
82+
project_id: { type: "number", description: "Project ID to associate the issue with" },
83+
labels: {
84+
type: "array",
85+
items: { type: "string" },
86+
description: "Labels to apply to the issue",
87+
},
88+
debug: { type: "boolean", description: "Enable verbose debug logs" },
89+
},
90+
required: ["title"],
91+
additionalProperties: false,
92+
},
93+
},
7494
],
7595
};
7696
});
@@ -133,6 +153,20 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
133153
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
134154
}
135155

156+
if (toolName === "create_issue") {
157+
const rawTitle = String(args.title || "").trim();
158+
if (!rawTitle) {
159+
return { content: [{ type: "text", text: "title is required" }], isError: true };
160+
}
161+
const title = interpretEscapes(rawTitle);
162+
const rawDescription = args.description ? String(args.description) : undefined;
163+
const description = rawDescription ? interpretEscapes(rawDescription) : undefined;
164+
const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
165+
const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
166+
const result = await createIssue({ apiKey, apiBaseUrl, title, description, projectId, labels, debug });
167+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
168+
}
169+
136170
throw new Error(`Unknown tool: ${toolName}`);
137171
} catch (err) {
138172
const message = err instanceof Error ? err.message : String(err);

0 commit comments

Comments
 (0)