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.
- package/README.md +154 -0
- package/dist/bin/postgres-ai.js +2911 -255
- package/package.json +12 -3
- package/schemas/A002.schema.json +63 -0
- package/schemas/A003.schema.json +73 -0
- package/schemas/A004.schema.json +81 -0
- package/schemas/A007.schema.json +71 -0
- package/schemas/A013.schema.json +61 -0
- package/schemas/D001.schema.json +71 -0
- package/schemas/D004.schema.json +136 -0
- package/schemas/F001.schema.json +73 -0
- package/schemas/F002.schema.json +108 -0
- package/schemas/F003.schema.json +138 -0
- package/schemas/F004.schema.json +125 -0
- package/schemas/F005.schema.json +131 -0
- package/schemas/F009.schema.json +155 -0
- package/schemas/G001.schema.json +135 -0
- package/schemas/G003.schema.json +90 -0
- package/schemas/H001.schema.json +141 -0
- package/schemas/H002.schema.json +129 -0
- package/schemas/H004.schema.json +128 -0
- package/schemas/I001.schema.json +149 -0
- package/schemas/K001.schema.json +161 -0
- package/schemas/K003.schema.json +163 -0
- package/schemas/K004.schema.json +110 -0
- package/schemas/K005.schema.json +110 -0
- package/schemas/K006.schema.json +110 -0
- package/schemas/K007.schema.json +110 -0
- package/schemas/K008.schema.json +110 -0
- package/schemas/M001.schema.json +119 -0
- package/schemas/M002.schema.json +110 -0
- package/schemas/M003.schema.json +128 -0
- package/schemas/N001.schema.json +161 -0
- package/schemas/query.schema.json +62 -0
- package/CHANGELOG.md +0 -11
- package/bin/postgres-ai.ts +0 -5578
- package/bun.lock +0 -258
- package/bunfig.toml +0 -20
- package/lib/aas-onboard.ts +0 -251
- package/lib/auth-server.ts +0 -285
- package/lib/checkup-api.ts +0 -526
- package/lib/checkup-dictionary.ts +0 -103
- package/lib/checkup-summary.ts +0 -338
- package/lib/checkup.ts +0 -2261
- package/lib/config.ts +0 -171
- package/lib/init.ts +0 -1152
- package/lib/instances.ts +0 -245
- package/lib/issues.ts +0 -1060
- package/lib/mcp-server.ts +0 -667
- package/lib/metrics-loader.ts +0 -134
- package/lib/pkce.ts +0 -79
- package/lib/reports.ts +0 -373
- package/lib/storage.ts +0 -367
- package/lib/supabase.ts +0 -826
- package/lib/util.ts +0 -134
- package/packages/postgres-ai/README.md +0 -26
- package/packages/postgres-ai/bin/postgres-ai.js +0 -27
- package/packages/postgres-ai/package.json +0 -27
- package/scripts/embed-checkup-dictionary.ts +0 -115
- package/scripts/embed-metrics.ts +0 -160
- package/scripts/generate-release-notes.ts +0 -668
- package/test/PERMISSION_CHECK_TEST_SUMMARY.md +0 -139
- package/test/aas-onboard.test.ts +0 -301
- package/test/auth.test.ts +0 -287
- package/test/checkup.integration.test.ts +0 -413
- package/test/checkup.test.ts +0 -3626
- package/test/compose-cmd.test.ts +0 -120
- package/test/config-consistency.test.ts +0 -352
- package/test/init.integration.test.ts +0 -438
- package/test/init.test.ts +0 -1816
- package/test/issues.cli.test.ts +0 -1162
- package/test/issues.test.ts +0 -456
- package/test/mcp-server.test.ts +0 -2530
- package/test/monitoring.test.ts +0 -746
- package/test/permission-check-sql.test.ts +0 -116
- package/test/reports.cli.test.ts +0 -793
- package/test/reports.test.ts +0 -977
- package/test/schema-validation.test.ts +0 -231
- package/test/storage.test.ts +0 -935
- package/test/supabase.test.ts +0 -709
- package/test/targets-add-config.test.ts +0 -28
- package/test/test-utils.ts +0 -190
- package/test/upgrade.test.ts +0 -1056
- package/test/util.test.ts +0 -44
- 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 `` 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
|
-
}
|