postgresai 0.16.0-rc.4 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +154 -0
  2. package/dist/bin/postgres-ai.js +2911 -255
  3. package/package.json +12 -3
  4. package/schemas/A002.schema.json +63 -0
  5. package/schemas/A003.schema.json +73 -0
  6. package/schemas/A004.schema.json +81 -0
  7. package/schemas/A007.schema.json +71 -0
  8. package/schemas/A013.schema.json +61 -0
  9. package/schemas/D001.schema.json +71 -0
  10. package/schemas/D004.schema.json +136 -0
  11. package/schemas/F001.schema.json +73 -0
  12. package/schemas/F002.schema.json +108 -0
  13. package/schemas/F003.schema.json +138 -0
  14. package/schemas/F004.schema.json +125 -0
  15. package/schemas/F005.schema.json +131 -0
  16. package/schemas/F009.schema.json +155 -0
  17. package/schemas/G001.schema.json +135 -0
  18. package/schemas/G003.schema.json +90 -0
  19. package/schemas/H001.schema.json +141 -0
  20. package/schemas/H002.schema.json +129 -0
  21. package/schemas/H004.schema.json +128 -0
  22. package/schemas/I001.schema.json +149 -0
  23. package/schemas/K001.schema.json +161 -0
  24. package/schemas/K003.schema.json +163 -0
  25. package/schemas/K004.schema.json +110 -0
  26. package/schemas/K005.schema.json +110 -0
  27. package/schemas/K006.schema.json +110 -0
  28. package/schemas/K007.schema.json +110 -0
  29. package/schemas/K008.schema.json +110 -0
  30. package/schemas/M001.schema.json +119 -0
  31. package/schemas/M002.schema.json +110 -0
  32. package/schemas/M003.schema.json +128 -0
  33. package/schemas/N001.schema.json +161 -0
  34. package/schemas/query.schema.json +62 -0
  35. package/CHANGELOG.md +0 -11
  36. package/bin/postgres-ai.ts +0 -5578
  37. package/bun.lock +0 -258
  38. package/bunfig.toml +0 -20
  39. package/lib/aas-onboard.ts +0 -251
  40. package/lib/auth-server.ts +0 -285
  41. package/lib/checkup-api.ts +0 -526
  42. package/lib/checkup-dictionary.ts +0 -103
  43. package/lib/checkup-summary.ts +0 -338
  44. package/lib/checkup.ts +0 -2261
  45. package/lib/config.ts +0 -171
  46. package/lib/init.ts +0 -1152
  47. package/lib/instances.ts +0 -245
  48. package/lib/issues.ts +0 -1060
  49. package/lib/mcp-server.ts +0 -667
  50. package/lib/metrics-loader.ts +0 -134
  51. package/lib/pkce.ts +0 -79
  52. package/lib/reports.ts +0 -373
  53. package/lib/storage.ts +0 -367
  54. package/lib/supabase.ts +0 -826
  55. package/lib/util.ts +0 -134
  56. package/packages/postgres-ai/README.md +0 -26
  57. package/packages/postgres-ai/bin/postgres-ai.js +0 -27
  58. package/packages/postgres-ai/package.json +0 -27
  59. package/scripts/embed-checkup-dictionary.ts +0 -115
  60. package/scripts/embed-metrics.ts +0 -160
  61. package/scripts/generate-release-notes.ts +0 -668
  62. package/test/PERMISSION_CHECK_TEST_SUMMARY.md +0 -139
  63. package/test/aas-onboard.test.ts +0 -301
  64. package/test/auth.test.ts +0 -287
  65. package/test/checkup.integration.test.ts +0 -413
  66. package/test/checkup.test.ts +0 -3626
  67. package/test/compose-cmd.test.ts +0 -120
  68. package/test/config-consistency.test.ts +0 -352
  69. package/test/init.integration.test.ts +0 -438
  70. package/test/init.test.ts +0 -1816
  71. package/test/issues.cli.test.ts +0 -1162
  72. package/test/issues.test.ts +0 -456
  73. package/test/mcp-server.test.ts +0 -2530
  74. package/test/monitoring.test.ts +0 -746
  75. package/test/permission-check-sql.test.ts +0 -116
  76. package/test/reports.cli.test.ts +0 -793
  77. package/test/reports.test.ts +0 -977
  78. package/test/schema-validation.test.ts +0 -231
  79. package/test/storage.test.ts +0 -935
  80. package/test/supabase.test.ts +0 -709
  81. package/test/targets-add-config.test.ts +0 -28
  82. package/test/test-utils.ts +0 -190
  83. package/test/upgrade.test.ts +0 -1056
  84. package/test/util.test.ts +0 -44
  85. package/tsconfig.json +0 -20
package/lib/mcp-server.ts DELETED
@@ -1,667 +0,0 @@
1
- import pkg from "../package.json";
2
- import * as config from "./config";
3
- import {
4
- fetchIssues,
5
- fetchIssueComments,
6
- createIssueComment,
7
- fetchIssue,
8
- createIssue,
9
- updateIssue,
10
- updateIssueComment,
11
- fetchActionItem,
12
- fetchActionItems,
13
- createActionItem,
14
- updateActionItem,
15
- type ConfigChange,
16
- } from "./issues";
17
- import { fetchReports, fetchAllReports, fetchReportFiles, fetchReportFileData, parseFlexibleDate } from "./reports";
18
- import { uploadFile, downloadFile, buildMarkdownLink, uploadAttachments, appendAttachmentsToContent } from "./storage";
19
- import { resolveBaseUrls } from "./util";
20
-
21
- // MCP SDK imports - Bun handles these directly
22
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
23
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
25
-
26
- export interface RootOptsLike {
27
- apiKey?: string;
28
- apiBaseUrl?: string;
29
- }
30
-
31
- // Interpret escape sequences (e.g., \n -> newline). Input comes from JSON, but
32
- // we still normalize common escapes for consistency.
33
- export const interpretEscapes = (str: string): string =>
34
- (str || "")
35
- .replace(/\\n/g, "\n")
36
- .replace(/\\t/g, "\t")
37
- .replace(/\\r/g, "\r")
38
- .replace(/\\"/g, '"')
39
- .replace(/\\'/g, "'");
40
-
41
- export interface McpToolRequest {
42
- params: {
43
- name: string;
44
- arguments?: Record<string, unknown>;
45
- };
46
- }
47
-
48
- export interface McpToolResponse {
49
- content: Array<{ type: string; text: string }>;
50
- isError?: boolean;
51
- }
52
-
53
- /** Handle MCP tool calls - exported for testing */
54
- export async function handleToolCall(
55
- req: McpToolRequest,
56
- rootOpts?: RootOptsLike,
57
- extra?: { debug?: boolean }
58
- ): Promise<McpToolResponse> {
59
- const toolName = req.params.name;
60
- const args = (req.params.arguments as Record<string, unknown>) || {};
61
-
62
- const cfg = config.readConfig();
63
- const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
64
- const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
65
-
66
- const debug = Boolean(args.debug ?? extra?.debug);
67
-
68
- if (!apiKey) {
69
- return {
70
- content: [
71
- {
72
- type: "text",
73
- text: "API key is required. Run 'pgai auth' or set PGAI_API_KEY.",
74
- },
75
- ],
76
- isError: true,
77
- };
78
- }
79
-
80
- try {
81
- if (toolName === "list_issues") {
82
- const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
83
- const statusArg = args.status ? String(args.status) : undefined;
84
- let status: "open" | "closed" | undefined;
85
- if (statusArg === "open") status = "open";
86
- else if (statusArg === "closed") status = "closed";
87
- const limit = args.limit !== undefined ? Number(args.limit) : undefined;
88
- const offset = args.offset !== undefined ? Number(args.offset) : undefined;
89
- const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, status, limit, offset, debug });
90
- return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
91
- }
92
-
93
- if (toolName === "view_issue") {
94
- const issueId = String(args.issue_id || "").trim();
95
- if (!issueId) {
96
- return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
97
- }
98
- const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
99
- if (!issue) {
100
- return { content: [{ type: "text", text: "Issue not found" }], isError: true };
101
- }
102
- const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
103
- const combined = { issue, comments };
104
- return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
105
- }
106
-
107
- if (toolName === "post_issue_comment") {
108
- const issueId = String(args.issue_id || "").trim();
109
- const rawContent = String(args.content || "");
110
- const parentCommentId = args.parent_comment_id ? String(args.parent_comment_id) : undefined;
111
- const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
112
- if (!issueId) {
113
- return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
114
- }
115
- if (!rawContent && attachments.length === 0) {
116
- return { content: [{ type: "text", text: "content or attachments is required" }], isError: true };
117
- }
118
- let content = interpretEscapes(rawContent);
119
- if (attachments.length > 0) {
120
- const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
121
- const uploaded = await uploadAttachments({ apiKey, storageBaseUrl, attachmentPaths: attachments, debug });
122
- content = appendAttachmentsToContent(content, uploaded);
123
- }
124
- const result = await createIssueComment({ apiKey, apiBaseUrl, issueId, content, parentCommentId, debug });
125
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
126
- }
127
-
128
- if (toolName === "create_issue") {
129
- const rawTitle = String(args.title || "").trim();
130
- if (!rawTitle) {
131
- return { content: [{ type: "text", text: "title is required" }], isError: true };
132
- }
133
- const title = interpretEscapes(rawTitle);
134
- const rawDescription = args.description ? String(args.description) : undefined;
135
- let description = rawDescription ? interpretEscapes(rawDescription) : undefined;
136
- const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
137
- const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
138
- const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
139
- // Get orgId from args or fall back to config
140
- const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId;
141
- // Note: orgId=0 is technically valid (though unlikely), so don't use falsy check
142
- if (orgId === undefined || orgId === null || Number.isNaN(orgId)) {
143
- return { content: [{ type: "text", text: "org_id is required. Either provide it as a parameter or run 'pgai auth' to set it in config." }], isError: true };
144
- }
145
- if (attachments.length > 0) {
146
- const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
147
- const uploaded = await uploadAttachments({ apiKey, storageBaseUrl, attachmentPaths: attachments, debug });
148
- description = appendAttachmentsToContent(description ?? "", uploaded);
149
- }
150
- const result = await createIssue({ apiKey, apiBaseUrl, title, orgId, description, projectId, labels, debug });
151
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
152
- }
153
-
154
- if (toolName === "update_issue") {
155
- const issueId = String(args.issue_id || "").trim();
156
- if (!issueId) {
157
- return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
158
- }
159
- const rawTitle = args.title !== undefined ? String(args.title) : undefined;
160
- const title = rawTitle !== undefined ? interpretEscapes(rawTitle) : undefined;
161
- const rawDescription = args.description !== undefined ? String(args.description) : undefined;
162
- let description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
163
- const status = args.status !== undefined ? Number(args.status) : undefined;
164
- const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
165
- const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
166
- // Validate that at least one update field is provided (attachments alone counts)
167
- if (title === undefined && description === undefined && status === undefined && labels === undefined && attachments.length === 0) {
168
- return { content: [{ type: "text", text: "At least one field to update is required (title, description, status, labels, or attachments)" }], isError: true };
169
- }
170
- // Validate status value if provided (check for NaN and valid values)
171
- if (status !== undefined && (Number.isNaN(status) || (status !== 0 && status !== 1))) {
172
- return { content: [{ type: "text", text: "status must be 0 (open) or 1 (closed)" }], isError: true };
173
- }
174
- if (attachments.length > 0) {
175
- // If the caller did not supply a new description, fetch the existing one
176
- // and append to it so "add a screenshot to issue X" is one round-trip
177
- // for the agent. Same race-window tradeoff as the CLI flag.
178
- if (description === undefined) {
179
- const existing = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
180
- if (!existing) {
181
- return { content: [{ type: "text", text: `Issue not found: ${issueId}` }], isError: true };
182
- }
183
- description = (existing as { description?: string | null }).description ?? "";
184
- }
185
- const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
186
- const uploaded = await uploadAttachments({ apiKey, storageBaseUrl, attachmentPaths: attachments, debug });
187
- description = appendAttachmentsToContent(description, uploaded);
188
- }
189
- const result = await updateIssue({ apiKey, apiBaseUrl, issueId, title, description, status, labels, debug });
190
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
191
- }
192
-
193
- if (toolName === "update_issue_comment") {
194
- const commentId = String(args.comment_id || "").trim();
195
- const rawContent = String(args.content || "");
196
- const attachments = Array.isArray(args.attachments) ? args.attachments.map(String).filter((p) => p.length > 0) : [];
197
- if (!commentId) {
198
- return { content: [{ type: "text", text: "comment_id is required" }], isError: true };
199
- }
200
- if (!rawContent.trim() && attachments.length === 0) {
201
- return { content: [{ type: "text", text: "content or attachments is required" }], isError: true };
202
- }
203
- let content = interpretEscapes(rawContent);
204
- if (attachments.length > 0) {
205
- const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
206
- const uploaded = await uploadAttachments({ apiKey, storageBaseUrl, attachmentPaths: attachments, debug });
207
- content = appendAttachmentsToContent(content, uploaded);
208
- }
209
- const result = await updateIssueComment({ apiKey, apiBaseUrl, commentId, content, debug });
210
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
211
- }
212
-
213
- if (toolName === "upload_file") {
214
- const filePath = String(args.path || "").trim();
215
- if (!filePath) {
216
- return { content: [{ type: "text", text: "path is required" }], isError: true };
217
- }
218
- const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
219
- const result = await uploadFile({ apiKey, storageBaseUrl, filePath, debug });
220
- const markdown = buildMarkdownLink(result.url, storageBaseUrl, result.metadata.originalName);
221
- return {
222
- content: [
223
- {
224
- type: "text",
225
- text: JSON.stringify(
226
- {
227
- success: result.success,
228
- url: result.url,
229
- markdown,
230
- metadata: result.metadata,
231
- requestId: result.requestId,
232
- },
233
- null,
234
- 2
235
- ),
236
- },
237
- ],
238
- };
239
- }
240
-
241
- if (toolName === "download_file") {
242
- const fileUrl = String(args.url || "").trim();
243
- const outputPath = args.output_path !== undefined ? String(args.output_path) : undefined;
244
- if (!fileUrl) {
245
- return { content: [{ type: "text", text: "url is required" }], isError: true };
246
- }
247
- const { storageBaseUrl } = resolveBaseUrls(rootOpts, cfg);
248
- const result = await downloadFile({ apiKey, storageBaseUrl, fileUrl, outputPath, debug });
249
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
250
- }
251
-
252
- // Action Items Tools
253
- if (toolName === "view_action_item") {
254
- // Support both single ID and array of IDs
255
- let actionItemIds: string[];
256
- if (Array.isArray(args.action_item_ids)) {
257
- actionItemIds = args.action_item_ids.map((id: unknown) => String(id).trim()).filter((id: string) => id);
258
- } else if (args.action_item_id) {
259
- actionItemIds = [String(args.action_item_id).trim()];
260
- } else {
261
- actionItemIds = [];
262
- }
263
- if (actionItemIds.length === 0) {
264
- return { content: [{ type: "text", text: "action_item_id or action_item_ids is required" }], isError: true };
265
- }
266
- const actionItems = await fetchActionItem({ apiKey, apiBaseUrl, actionItemIds, debug });
267
- if (actionItems.length === 0) {
268
- return { content: [{ type: "text", text: "Action item(s) not found" }], isError: true };
269
- }
270
- return { content: [{ type: "text", text: JSON.stringify(actionItems, null, 2) }] };
271
- }
272
-
273
- if (toolName === "list_action_items") {
274
- const issueId = String(args.issue_id || "").trim();
275
- if (!issueId) {
276
- return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
277
- }
278
- const actionItems = await fetchActionItems({ apiKey, apiBaseUrl, issueId, debug });
279
- return { content: [{ type: "text", text: JSON.stringify(actionItems, null, 2) }] };
280
- }
281
-
282
- if (toolName === "create_action_item") {
283
- const issueId = String(args.issue_id || "").trim();
284
- const rawTitle = String(args.title || "").trim();
285
- if (!issueId) {
286
- return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
287
- }
288
- if (!rawTitle) {
289
- return { content: [{ type: "text", text: "title is required" }], isError: true };
290
- }
291
- const title = interpretEscapes(rawTitle);
292
- const rawDescription = args.description ? String(args.description) : undefined;
293
- const description = rawDescription ? interpretEscapes(rawDescription) : undefined;
294
- const sqlAction = args.sql_action !== undefined ? String(args.sql_action) : undefined;
295
- const configs = Array.isArray(args.configs) ? args.configs as ConfigChange[] : undefined;
296
- const result = await createActionItem({ apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug });
297
- return { content: [{ type: "text", text: JSON.stringify({ id: result }, null, 2) }] };
298
- }
299
-
300
- if (toolName === "update_action_item") {
301
- const actionItemId = String(args.action_item_id || "").trim();
302
- if (!actionItemId) {
303
- return { content: [{ type: "text", text: "action_item_id is required" }], isError: true };
304
- }
305
- const rawTitle = args.title !== undefined ? String(args.title) : undefined;
306
- const title = rawTitle !== undefined ? interpretEscapes(rawTitle) : undefined;
307
- const rawDescription = args.description !== undefined ? String(args.description) : undefined;
308
- const description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
309
- const isDone = args.is_done !== undefined ? Boolean(args.is_done) : undefined;
310
- const status = args.status !== undefined ? String(args.status) : undefined;
311
- const statusReason = args.status_reason !== undefined ? String(args.status_reason) : undefined;
312
-
313
- // Validate that at least one update field is provided
314
- if (title === undefined && description === undefined &&
315
- isDone === undefined && status === undefined && statusReason === undefined) {
316
- return { content: [{ type: "text", text: "At least one field to update is required (title, description, is_done, status, or status_reason)" }], isError: true };
317
- }
318
-
319
- // Validate status value if provided
320
- if (status !== undefined && !["waiting_for_approval", "approved", "rejected"].includes(status)) {
321
- return { content: [{ type: "text", text: "status must be 'waiting_for_approval', 'approved', or 'rejected'" }], isError: true };
322
- }
323
-
324
- await updateActionItem({ apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, debug });
325
- return { content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }] };
326
- }
327
-
328
- // Reports Tools
329
- if (toolName === "list_reports") {
330
- const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
331
- const status = args.status ? String(args.status) : undefined;
332
- const limit = args.limit !== undefined ? Number(args.limit) : undefined;
333
- const beforeDate = args.before_date ? parseFlexibleDate(String(args.before_date)) : undefined;
334
- const all = args.all === true;
335
- let reports;
336
- if (all) {
337
- reports = await fetchAllReports({ apiKey, apiBaseUrl, projectId, status, limit, debug });
338
- } else {
339
- reports = await fetchReports({ apiKey, apiBaseUrl, projectId, status, limit, beforeDate, debug });
340
- }
341
- return { content: [{ type: "text", text: JSON.stringify(reports, null, 2) }] };
342
- }
343
-
344
- if (toolName === "list_report_files") {
345
- const reportId = args.report_id !== undefined ? Number(args.report_id) : undefined;
346
- if (reportId !== undefined && isNaN(reportId)) {
347
- return { content: [{ type: "text", text: "report_id must be a number" }], isError: true };
348
- }
349
- const type = args.type ? String(args.type) as "json" | "md" : undefined;
350
- const checkId = args.check_id ? String(args.check_id) : undefined;
351
- if (reportId === undefined && !checkId) {
352
- return { content: [{ type: "text", text: "Either report_id or check_id is required" }], isError: true };
353
- }
354
- const files = await fetchReportFiles({ apiKey, apiBaseUrl, reportId, type, checkId, debug });
355
- return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
356
- }
357
-
358
- if (toolName === "get_report_data") {
359
- const reportId = args.report_id !== undefined ? Number(args.report_id) : undefined;
360
- if (reportId !== undefined && isNaN(reportId)) {
361
- return { content: [{ type: "text", text: "report_id must be a number" }], isError: true };
362
- }
363
- const type = args.type ? String(args.type) as "json" | "md" : undefined;
364
- const checkId = args.check_id ? String(args.check_id) : undefined;
365
- if (reportId === undefined && !checkId) {
366
- return { content: [{ type: "text", text: "Either report_id or check_id is required" }], isError: true };
367
- }
368
- const files = await fetchReportFileData({ apiKey, apiBaseUrl, reportId, type, checkId, debug });
369
- return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
370
- }
371
-
372
- throw new Error(`Unknown tool: ${toolName}`);
373
- } catch (err) {
374
- const message = err instanceof Error ? err.message : String(err);
375
- return { content: [{ type: "text", text: message }], isError: true };
376
- }
377
- }
378
-
379
- export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?: boolean }): Promise<void> {
380
- const server = new Server(
381
- {
382
- name: "postgresai-mcp",
383
- version: pkg.version,
384
- title: "PostgresAI MCP Server",
385
- },
386
- { capabilities: { tools: {} } }
387
- );
388
-
389
- server.setRequestHandler(ListToolsRequestSchema, async () => {
390
- return {
391
- tools: [
392
- {
393
- name: "list_issues",
394
- description: "List issues from PostgresAI API (same as CLI 'issues list')",
395
- inputSchema: {
396
- type: "object",
397
- properties: {
398
- org_id: { type: "number", description: "Organization ID (optional, falls back to config)" },
399
- status: { type: "string", description: "Filter by status: 'open', 'closed', or omit for all" },
400
- limit: { type: "number", description: "Max number of issues to return (default: 20)" },
401
- offset: { type: "number", description: "Number of issues to skip (default: 0)" },
402
- debug: { type: "boolean", description: "Enable verbose debug logs" },
403
- },
404
- additionalProperties: false,
405
- },
406
- },
407
- {
408
- name: "view_issue",
409
- description: "View a specific issue with its comments",
410
- inputSchema: {
411
- type: "object",
412
- properties: {
413
- issue_id: { type: "string", description: "Issue ID (UUID)" },
414
- debug: { type: "boolean", description: "Enable verbose debug logs" },
415
- },
416
- required: ["issue_id"],
417
- additionalProperties: false,
418
- },
419
- },
420
- {
421
- name: "post_issue_comment",
422
- description: "Post a new comment to an issue (optionally as a reply). Local files passed via 'attachments' are uploaded to PostgresAI storage and the resulting markdown links are appended to the comment body (image extensions render inline).",
423
- inputSchema: {
424
- type: "object",
425
- properties: {
426
- issue_id: { type: "string", description: "Issue ID (UUID)" },
427
- content: { type: "string", description: "Comment text (supports \\n as newline)" },
428
- parent_comment_id: { type: "string", description: "Parent comment ID (UUID) for replies" },
429
- attachments: {
430
- type: "array",
431
- items: { type: "string" },
432
- description: "Local file paths to upload and append as markdown links (images render inline). Either 'content' or 'attachments' must be non-empty.",
433
- },
434
- debug: { type: "boolean", description: "Enable verbose debug logs" },
435
- },
436
- required: ["issue_id"],
437
- additionalProperties: false,
438
- },
439
- },
440
- {
441
- name: "create_issue",
442
- description: "Create a new issue in PostgresAI. Local files passed via 'attachments' are uploaded to PostgresAI storage and the resulting markdown links are appended to the issue description.",
443
- inputSchema: {
444
- type: "object",
445
- properties: {
446
- title: { type: "string", description: "Issue title (required)" },
447
- description: { type: "string", description: "Issue description (supports \\n as newline)" },
448
- org_id: { type: "number", description: "Organization ID (uses config value if not provided)" },
449
- project_id: { type: "number", description: "Project ID to associate the issue with" },
450
- labels: {
451
- type: "array",
452
- items: { type: "string" },
453
- description: "Labels to apply to the issue",
454
- },
455
- attachments: {
456
- type: "array",
457
- items: { type: "string" },
458
- description: "Local file paths to upload and append as markdown links to the description (images render inline)",
459
- },
460
- debug: { type: "boolean", description: "Enable verbose debug logs" },
461
- },
462
- required: ["title"],
463
- additionalProperties: false,
464
- },
465
- },
466
- {
467
- name: "update_issue",
468
- description: "Update an existing issue (title, description, status, labels). Use status=1 to close, status=0 to reopen. Local files passed via 'attachments' are uploaded and appended to 'description'; if 'description' is omitted, the existing description is fetched first and appended to.",
469
- inputSchema: {
470
- type: "object",
471
- properties: {
472
- issue_id: { type: "string", description: "Issue ID (UUID)" },
473
- title: { type: "string", description: "New title (supports \\n as newline)" },
474
- description: { type: "string", description: "New description (supports \\n as newline)" },
475
- status: { type: "number", description: "Status: 0=open, 1=closed" },
476
- labels: {
477
- type: "array",
478
- items: { type: "string" },
479
- description: "Labels to set on the issue",
480
- },
481
- attachments: {
482
- type: "array",
483
- items: { type: "string" },
484
- description: "Local file paths to upload and append as markdown links (images render inline). When provided without 'description', the existing description is fetched and appended to.",
485
- },
486
- debug: { type: "boolean", description: "Enable verbose debug logs" },
487
- },
488
- required: ["issue_id"],
489
- additionalProperties: false,
490
- },
491
- },
492
- {
493
- name: "update_issue_comment",
494
- description: "Update an existing issue comment. Local files passed via 'attachments' are uploaded and appended to 'content' as markdown links.",
495
- inputSchema: {
496
- type: "object",
497
- properties: {
498
- comment_id: { type: "string", description: "Comment ID (UUID)" },
499
- content: { type: "string", description: "New comment text (supports \\n as newline). Either 'content' or 'attachments' must be non-empty." },
500
- attachments: {
501
- type: "array",
502
- items: { type: "string" },
503
- description: "Local file paths to upload and append as markdown links (images render inline)",
504
- },
505
- debug: { type: "boolean", description: "Enable verbose debug logs" },
506
- },
507
- required: ["comment_id"],
508
- additionalProperties: false,
509
- },
510
- },
511
- {
512
- name: "upload_file",
513
- description: "Upload a local file to PostgresAI storage. Returns the storage URL and a ready-to-paste markdown link (image extensions get the inline `![](url)` form, others get `[](url)`). For posting attachments alongside an issue or comment, prefer the 'attachments' parameter on the issue/comment tools — this tool is for ad-hoc uploads or when you need the URL out of band.",
514
- inputSchema: {
515
- type: "object",
516
- properties: {
517
- path: { type: "string", description: "Local file path to upload (absolute or relative to CWD)" },
518
- debug: { type: "boolean", description: "Enable verbose debug logs" },
519
- },
520
- required: ["path"],
521
- additionalProperties: false,
522
- },
523
- },
524
- {
525
- name: "download_file",
526
- description: "Download a file from PostgresAI storage by URL (full URL under the storage base, or a relative path like /files/123/foo.png).",
527
- inputSchema: {
528
- type: "object",
529
- properties: {
530
- url: { type: "string", description: "Full URL (must be under the configured storage base) or relative storage path (e.g. /files/123/foo.png)" },
531
- output_path: { type: "string", description: "Local destination path (default: derive filename from URL, save in CWD). When omitted, the path-traversal guard restricts the destination to CWD." },
532
- debug: { type: "boolean", description: "Enable verbose debug logs" },
533
- },
534
- required: ["url"],
535
- additionalProperties: false,
536
- },
537
- },
538
- // Action Items Tools
539
- {
540
- name: "view_action_item",
541
- description: "View action item(s) with all details. Supports single ID or multiple IDs.",
542
- inputSchema: {
543
- type: "object",
544
- properties: {
545
- action_item_id: { type: "string", description: "Single action item ID (UUID)" },
546
- action_item_ids: { type: "array", items: { type: "string" }, description: "Multiple action item IDs (UUIDs)" },
547
- debug: { type: "boolean", description: "Enable verbose debug logs" },
548
- },
549
- additionalProperties: false,
550
- },
551
- },
552
- {
553
- name: "list_action_items",
554
- description: "List action items for an issue",
555
- inputSchema: {
556
- type: "object",
557
- properties: {
558
- issue_id: { type: "string", description: "Issue ID (UUID)" },
559
- debug: { type: "boolean", description: "Enable verbose debug logs" },
560
- },
561
- required: ["issue_id"],
562
- additionalProperties: false,
563
- },
564
- },
565
- {
566
- name: "create_action_item",
567
- description: "Create a new action item for an issue",
568
- inputSchema: {
569
- type: "object",
570
- properties: {
571
- issue_id: { type: "string", description: "Issue ID (UUID)" },
572
- title: { type: "string", description: "Action item title" },
573
- description: { type: "string", description: "Detailed description" },
574
- sql_action: { type: "string", description: "SQL command to execute, e.g. 'DROP INDEX CONCURRENTLY idx_unused;'" },
575
- configs: {
576
- type: "array",
577
- items: {
578
- type: "object",
579
- properties: {
580
- parameter: { type: "string" },
581
- value: { type: "string" },
582
- },
583
- required: ["parameter", "value"],
584
- },
585
- description: "Configuration parameter changes",
586
- },
587
- debug: { type: "boolean", description: "Enable verbose debug logs" },
588
- },
589
- required: ["issue_id", "title"],
590
- additionalProperties: false,
591
- },
592
- },
593
- {
594
- name: "update_action_item",
595
- description: "Update an action item: mark as done/not done, approve/reject, or edit title/description",
596
- inputSchema: {
597
- type: "object",
598
- properties: {
599
- action_item_id: { type: "string", description: "Action item ID (UUID)" },
600
- title: { type: "string", description: "New title" },
601
- description: { type: "string", description: "New description" },
602
- is_done: { type: "boolean", description: "Mark as done (true) or not done (false)" },
603
- status: { type: "string", description: "Approval status: 'waiting_for_approval', 'approved', or 'rejected'" },
604
- status_reason: { type: "string", description: "Reason for approval/rejection" },
605
- debug: { type: "boolean", description: "Enable verbose debug logs" },
606
- },
607
- required: ["action_item_id"],
608
- additionalProperties: false,
609
- },
610
- },
611
- // Reports Tools
612
- {
613
- name: "list_reports",
614
- description: "List checkup reports. Returns report metadata: id, project, status, timestamps. Use get_report_data to fetch actual report content. Supports date-based filtering with before_date.",
615
- inputSchema: {
616
- type: "object",
617
- properties: {
618
- project_id: { type: "number", description: "Filter by project ID" },
619
- status: { type: "string", description: "Filter by status (e.g., 'completed')" },
620
- limit: { type: "number", description: "Max number of reports to return (default: 20)" },
621
- before_date: { type: "string", description: "Show reports created before this date (YYYY-MM-DD, DD.MM.YYYY, YYYY-MM-DD HH:mm, etc.)" },
622
- all: { type: "boolean", description: "Fetch all reports (paginated automatically)" },
623
- debug: { type: "boolean", description: "Enable verbose debug logs" },
624
- },
625
- additionalProperties: false,
626
- },
627
- },
628
- {
629
- name: "list_report_files",
630
- description: "List files in a checkup report (metadata only, no content). Each report contains json (raw data) and md (markdown analysis) files per check. Either report_id or check_id must be provided.",
631
- inputSchema: {
632
- type: "object",
633
- properties: {
634
- report_id: { type: "number", description: "Checkup report ID (optional if check_id is provided)" },
635
- type: { type: "string", description: "Filter by file type: 'json' or 'md'" },
636
- check_id: { type: "string", description: "Filter by check ID (e.g., 'H002', 'F004')" },
637
- debug: { type: "boolean", description: "Enable verbose debug logs" },
638
- },
639
- additionalProperties: false,
640
- },
641
- },
642
- {
643
- name: "get_report_data",
644
- description: "Get checkup report file content. Returns files with a 'data' field containing the actual content: markdown analysis or JSON raw data. Use type='md' for human-readable analysis with recommendations, type='json' for raw check data. Either report_id or check_id must be provided.",
645
- inputSchema: {
646
- type: "object",
647
- properties: {
648
- report_id: { type: "number", description: "Checkup report ID (optional if check_id is provided)" },
649
- type: { type: "string", description: "Filter by file type: 'json' for raw data, 'md' for markdown analysis" },
650
- check_id: { type: "string", description: "Filter by check ID (e.g., 'H002', 'F004')" },
651
- debug: { type: "boolean", description: "Enable verbose debug logs" },
652
- },
653
- additionalProperties: false,
654
- },
655
- },
656
- ],
657
- };
658
- });
659
-
660
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
661
- server.setRequestHandler(CallToolRequestSchema, async (req: any): Promise<any> => {
662
- return handleToolCall(req, rootOpts, extra);
663
- });
664
-
665
- const transport = new StdioServerTransport();
666
- await server.connect(transport);
667
- }