Skip to content

Commit 137990e

Browse files
committed
Simplify issues cli and mcp
1 parent 37e660e commit 137990e

File tree

4 files changed

+134
-17
lines changed

4 files changed

+134
-17
lines changed

cli/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,14 @@ Cursor configuration example (Settings → MCP):
117117

118118
Tools exposed:
119119
- list_issues: returns the same JSON as `pgai issues list`.
120-
- list_issue_comments: list comments for an issue (args: { issue_id, debug? })
120+
- view_issue: view a single issue with its comments (args: { issue_id, debug? })
121121
- post_issue_comment: post a comment (args: { issue_id, content, parent_comment_id?, debug? })
122122

123123
### Issues management (`issues` group)
124124

125125
```bash
126-
pgai issues list # List issues
127-
pgai issues comments <issueId> # List comments for an issue
126+
pgai issues list # List issues (shows: id, title, status, created_at)
127+
pgai issues view <issueId> # View issue details and comments
128128
pgai issues post_comment <issueId> <content> # Post a comment to an issue
129129
# Options:
130130
# --parent <uuid> Parent comment ID (for replies)
@@ -145,7 +145,7 @@ pgai issues list --json | jq '.[] | {id, title}'
145145
- Rely on auto-detection: when stdout is not a TTY (e.g., piped or redirected), output is JSON automatically:
146146

147147
```bash
148-
pgai issues comments <issueId> > comments.json
148+
pgai issues view <issueId> > issue.json
149149
```
150150

151151
#### Grafana management

cli/bin/postgres-ai.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as readline from "readline";
1313
import * as http from "https";
1414
import { URL } from "url";
1515
import { startMcpServer } from "../lib/mcp-server";
16-
import { fetchIssues, fetchIssueComments, createIssueComment } from "../lib/issues";
16+
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
1717
import { resolveBaseUrls } from "../lib/util";
1818

1919
const execPromise = promisify(exec);
@@ -1225,7 +1225,15 @@ issues
12251225
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
12261226

12271227
const result = await fetchIssues({ apiKey, apiBaseUrl, debug: !!opts.debug });
1228-
printResult(result, opts.json);
1228+
const trimmed = Array.isArray(result)
1229+
? (result as any[]).map((r) => ({
1230+
id: (r as any).id,
1231+
title: (r as any).title,
1232+
status: (r as any).status,
1233+
created_at: (r as any).created_at,
1234+
}))
1235+
: result;
1236+
printResult(trimmed, opts.json);
12291237
} catch (err) {
12301238
const message = err instanceof Error ? err.message : String(err);
12311239
console.error(message);
@@ -1234,8 +1242,8 @@ issues
12341242
});
12351243

12361244
issues
1237-
.command("comments <issueId>")
1238-
.description("list comments for an issue")
1245+
.command("view <issueId>")
1246+
.description("view issue details and comments")
12391247
.option("--debug", "enable debug output")
12401248
.option("--json", "output raw JSON")
12411249
.action(async (issueId: string, opts: { debug?: boolean; json?: boolean }) => {
@@ -1251,8 +1259,16 @@ issues
12511259

12521260
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
12531261

1254-
const result = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
1255-
printResult(result, opts.json);
1262+
const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
1263+
if (!issue) {
1264+
console.error("Issue not found");
1265+
process.exitCode = 1;
1266+
return;
1267+
}
1268+
1269+
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
1270+
const combined = { issue, comments };
1271+
printResult(combined, opts.json);
12561272
} catch (err) {
12571273
const message = err instanceof Error ? err.message : String(err);
12581274
console.error(message);

cli/lib/issues.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,94 @@ export async function fetchIssueComments(params: FetchIssueCommentsParams): Prom
163163
});
164164
}
165165

166+
export interface FetchIssueParams {
167+
apiKey: string;
168+
apiBaseUrl: string;
169+
issueId: string;
170+
debug?: boolean;
171+
}
172+
173+
export async function fetchIssue(params: FetchIssueParams): Promise<unknown> {
174+
const { apiKey, apiBaseUrl, issueId, debug } = params;
175+
if (!apiKey) {
176+
throw new Error("API key is required");
177+
}
178+
if (!issueId) {
179+
throw new Error("issueId is required");
180+
}
181+
182+
const base = normalizeBaseUrl(apiBaseUrl);
183+
const url = new URL(`${base}/issues`);
184+
url.searchParams.set("id", `eq.${issueId}`);
185+
url.searchParams.set("limit", "1");
186+
187+
const headers: Record<string, string> = {
188+
"access-token": apiKey,
189+
"Prefer": "return=representation",
190+
"Content-Type": "application/json",
191+
};
192+
193+
if (debug) {
194+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
195+
// eslint-disable-next-line no-console
196+
console.log(`Debug: Resolved API base URL: ${base}`);
197+
// eslint-disable-next-line no-console
198+
console.log(`Debug: GET URL: ${url.toString()}`);
199+
// eslint-disable-next-line no-console
200+
console.log(`Debug: Auth scheme: access-token`);
201+
// eslint-disable-next-line no-console
202+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
203+
}
204+
205+
return new Promise((resolve, reject) => {
206+
const req = https.request(
207+
url,
208+
{
209+
method: "GET",
210+
headers,
211+
},
212+
(res) => {
213+
let data = "";
214+
res.on("data", (chunk) => (data += chunk));
215+
res.on("end", () => {
216+
if (debug) {
217+
// eslint-disable-next-line no-console
218+
console.log(`Debug: Response status: ${res.statusCode}`);
219+
// eslint-disable-next-line no-console
220+
console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
221+
}
222+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
223+
try {
224+
const parsed = JSON.parse(data);
225+
if (Array.isArray(parsed)) {
226+
resolve(parsed[0] ?? null);
227+
} else {
228+
resolve(parsed);
229+
}
230+
} catch {
231+
resolve(data);
232+
}
233+
} else {
234+
let errMsg = `Failed to fetch issue: HTTP ${res.statusCode}`;
235+
if (data) {
236+
try {
237+
const errObj = JSON.parse(data);
238+
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
239+
} catch {
240+
errMsg += `\n${data}`;
241+
}
242+
}
243+
reject(new Error(errMsg));
244+
}
245+
});
246+
}
247+
);
248+
249+
req.on("error", (err: Error) => reject(err));
250+
req.end();
251+
});
252+
}
253+
166254
export interface CreateIssueCommentParams {
167255
apiKey: string;
168256
apiBaseUrl: string;

cli/lib/mcp-server.ts

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

66
// MCP SDK imports
@@ -54,8 +54,8 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
5454
},
5555
},
5656
{
57-
name: "list_issue_comments",
58-
description: "List comments for a specific issue (issue_id UUID)",
57+
name: "view_issue",
58+
description: "View a specific issue with its comments",
5959
inputSchema: {
6060
type: "object",
6161
properties: {
@@ -110,16 +110,29 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
110110
try {
111111
if (toolName === "list_issues") {
112112
const result = await fetchIssues({ apiKey, apiBaseUrl, debug });
113-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
113+
const trimmed = Array.isArray(result)
114+
? (result as any[]).map((r) => ({
115+
id: (r as any).id,
116+
title: (r as any).title,
117+
status: (r as any).status,
118+
created_at: (r as any).created_at,
119+
}))
120+
: result;
121+
return { content: [{ type: "text", text: JSON.stringify(trimmed, null, 2) }] };
114122
}
115123

116-
if (toolName === "list_issue_comments") {
124+
if (toolName === "view_issue") {
117125
const issueId = String(args.issue_id || "").trim();
118126
if (!issueId) {
119127
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
120128
}
121-
const result = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
122-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
129+
const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
130+
if (!issue) {
131+
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
132+
}
133+
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
134+
const combined = { issue, comments };
135+
return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
123136
}
124137

125138
if (toolName === "post_issue_comment") {

0 commit comments

Comments
 (0)