postgresai 0.14.0-dev.8 → 0.14.0-dev.81
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 +161 -61
- package/bin/postgres-ai.ts +2596 -428
- package/bun.lock +258 -0
- package/bunfig.toml +20 -0
- package/dist/bin/postgres-ai.js +31277 -1575
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.extensions.sql +8 -0
- package/dist/sql/03.permissions.sql +38 -0
- package/dist/sql/04.optional_rds.sql +6 -0
- package/dist/sql/05.optional_self_managed.sql +8 -0
- package/dist/sql/06.helpers.sql +439 -0
- package/dist/sql/sql/01.role.sql +16 -0
- package/dist/sql/sql/02.extensions.sql +8 -0
- package/dist/sql/sql/03.permissions.sql +38 -0
- package/dist/sql/sql/04.optional_rds.sql +6 -0
- package/dist/sql/sql/05.optional_self_managed.sql +8 -0
- package/dist/sql/sql/06.helpers.sql +439 -0
- package/dist/sql/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/sql/uninit/03.role.sql +27 -0
- package/dist/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/uninit/03.role.sql +27 -0
- package/lib/auth-server.ts +124 -106
- package/lib/checkup-api.ts +386 -0
- package/lib/checkup-dictionary.ts +113 -0
- package/lib/checkup.ts +1512 -0
- package/lib/config.ts +6 -3
- package/lib/init.ts +655 -189
- package/lib/issues.ts +848 -193
- package/lib/mcp-server.ts +391 -91
- package/lib/metrics-loader.ts +127 -0
- package/lib/supabase.ts +824 -0
- package/lib/util.ts +61 -0
- package/package.json +22 -10
- package/packages/postgres-ai/README.md +26 -0
- package/packages/postgres-ai/bin/postgres-ai.js +27 -0
- package/packages/postgres-ai/package.json +27 -0
- package/scripts/embed-checkup-dictionary.ts +106 -0
- package/scripts/embed-metrics.ts +154 -0
- package/sql/01.role.sql +16 -0
- package/sql/02.extensions.sql +8 -0
- package/sql/03.permissions.sql +38 -0
- package/sql/04.optional_rds.sql +6 -0
- package/sql/05.optional_self_managed.sql +8 -0
- package/sql/06.helpers.sql +439 -0
- package/sql/uninit/01.helpers.sql +5 -0
- package/sql/uninit/02.permissions.sql +30 -0
- package/sql/uninit/03.role.sql +27 -0
- package/test/auth.test.ts +258 -0
- package/test/checkup.integration.test.ts +321 -0
- package/test/checkup.test.ts +1116 -0
- package/test/config-consistency.test.ts +36 -0
- package/test/init.integration.test.ts +508 -0
- package/test/init.test.ts +916 -0
- package/test/issues.cli.test.ts +538 -0
- package/test/issues.test.ts +456 -0
- package/test/mcp-server.test.ts +1527 -0
- package/test/schema-validation.test.ts +81 -0
- package/test/supabase.test.ts +568 -0
- package/test/test-utils.ts +128 -0
- package/tsconfig.json +12 -20
- package/dist/bin/postgres-ai.d.ts +0 -3
- package/dist/bin/postgres-ai.d.ts.map +0 -1
- package/dist/bin/postgres-ai.js.map +0 -1
- package/dist/lib/auth-server.d.ts +0 -31
- package/dist/lib/auth-server.d.ts.map +0 -1
- package/dist/lib/auth-server.js +0 -263
- package/dist/lib/auth-server.js.map +0 -1
- package/dist/lib/config.d.ts +0 -45
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js +0 -181
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/init.d.ts +0 -64
- package/dist/lib/init.d.ts.map +0 -1
- package/dist/lib/init.js +0 -399
- package/dist/lib/init.js.map +0 -1
- package/dist/lib/issues.d.ts +0 -75
- package/dist/lib/issues.d.ts.map +0 -1
- package/dist/lib/issues.js +0 -336
- package/dist/lib/issues.js.map +0 -1
- package/dist/lib/mcp-server.d.ts +0 -9
- package/dist/lib/mcp-server.d.ts.map +0 -1
- package/dist/lib/mcp-server.js +0 -168
- package/dist/lib/mcp-server.js.map +0 -1
- package/dist/lib/pkce.d.ts +0 -32
- package/dist/lib/pkce.d.ts.map +0 -1
- package/dist/lib/pkce.js +0 -101
- package/dist/lib/pkce.js.map +0 -1
- package/dist/lib/util.d.ts +0 -27
- package/dist/lib/util.d.ts.map +0 -1
- package/dist/lib/util.js +0 -46
- package/dist/lib/util.js.map +0 -1
- package/dist/package.json +0 -46
- package/test/init.integration.test.cjs +0 -269
- package/test/init.test.cjs +0 -76
package/lib/mcp-server.ts
CHANGED
|
@@ -1,44 +1,272 @@
|
|
|
1
|
-
import
|
|
1
|
+
import pkg from "../package.json";
|
|
2
2
|
import * as config from "./config";
|
|
3
|
-
import {
|
|
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";
|
|
4
17
|
import { resolveBaseUrls } from "./util";
|
|
5
18
|
|
|
6
|
-
// MCP SDK imports
|
|
7
|
-
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
8
|
-
import
|
|
9
|
-
|
|
19
|
+
// MCP SDK imports - Bun handles these directly
|
|
20
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
10
23
|
|
|
11
|
-
interface RootOptsLike {
|
|
24
|
+
export interface RootOptsLike {
|
|
12
25
|
apiKey?: string;
|
|
13
26
|
apiBaseUrl?: string;
|
|
14
27
|
}
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
// Interpret escape sequences (e.g., \n -> newline). Input comes from JSON, but
|
|
30
|
+
// we still normalize common escapes for consistency.
|
|
31
|
+
export const interpretEscapes = (str: string): string =>
|
|
32
|
+
(str || "")
|
|
33
|
+
.replace(/\\n/g, "\n")
|
|
34
|
+
.replace(/\\t/g, "\t")
|
|
35
|
+
.replace(/\\r/g, "\r")
|
|
36
|
+
.replace(/\\"/g, '"')
|
|
37
|
+
.replace(/\\'/g, "'");
|
|
38
|
+
|
|
39
|
+
export interface McpToolRequest {
|
|
40
|
+
params: {
|
|
41
|
+
name: string;
|
|
42
|
+
arguments?: Record<string, unknown>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface McpToolResponse {
|
|
47
|
+
content: Array<{ type: string; text: string }>;
|
|
48
|
+
isError?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Handle MCP tool calls - exported for testing */
|
|
52
|
+
export async function handleToolCall(
|
|
53
|
+
req: McpToolRequest,
|
|
54
|
+
rootOpts?: RootOptsLike,
|
|
55
|
+
extra?: { debug?: boolean }
|
|
56
|
+
): Promise<McpToolResponse> {
|
|
57
|
+
const toolName = req.params.name;
|
|
58
|
+
const args = (req.params.arguments as Record<string, unknown>) || {};
|
|
59
|
+
|
|
60
|
+
const cfg = config.readConfig();
|
|
61
|
+
const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
|
|
62
|
+
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
63
|
+
|
|
64
|
+
const debug = Boolean(args.debug ?? extra?.debug);
|
|
65
|
+
|
|
66
|
+
if (!apiKey) {
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: "API key is required. Run 'pgai auth' or set PGAI_API_KEY.",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
isError: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
26
77
|
|
|
78
|
+
try {
|
|
79
|
+
if (toolName === "list_issues") {
|
|
80
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId ?? undefined;
|
|
81
|
+
const statusArg = args.status ? String(args.status) : undefined;
|
|
82
|
+
let status: "open" | "closed" | undefined;
|
|
83
|
+
if (statusArg === "open") status = "open";
|
|
84
|
+
else if (statusArg === "closed") status = "closed";
|
|
85
|
+
const limit = args.limit !== undefined ? Number(args.limit) : undefined;
|
|
86
|
+
const offset = args.offset !== undefined ? Number(args.offset) : undefined;
|
|
87
|
+
const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, status, limit, offset, debug });
|
|
88
|
+
return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (toolName === "view_issue") {
|
|
92
|
+
const issueId = String(args.issue_id || "").trim();
|
|
93
|
+
if (!issueId) {
|
|
94
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
95
|
+
}
|
|
96
|
+
const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
|
|
97
|
+
if (!issue) {
|
|
98
|
+
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
|
|
99
|
+
}
|
|
100
|
+
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
|
|
101
|
+
const combined = { issue, comments };
|
|
102
|
+
return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (toolName === "post_issue_comment") {
|
|
106
|
+
const issueId = String(args.issue_id || "").trim();
|
|
107
|
+
const rawContent = String(args.content || "");
|
|
108
|
+
const parentCommentId = args.parent_comment_id ? String(args.parent_comment_id) : undefined;
|
|
109
|
+
if (!issueId) {
|
|
110
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
111
|
+
}
|
|
112
|
+
if (!rawContent) {
|
|
113
|
+
return { content: [{ type: "text", text: "content is required" }], isError: true };
|
|
114
|
+
}
|
|
115
|
+
const content = interpretEscapes(rawContent);
|
|
116
|
+
const result = await createIssueComment({ apiKey, apiBaseUrl, issueId, content, parentCommentId, debug });
|
|
117
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (toolName === "create_issue") {
|
|
121
|
+
const rawTitle = String(args.title || "").trim();
|
|
122
|
+
if (!rawTitle) {
|
|
123
|
+
return { content: [{ type: "text", text: "title is required" }], isError: true };
|
|
124
|
+
}
|
|
125
|
+
const title = interpretEscapes(rawTitle);
|
|
126
|
+
const rawDescription = args.description ? String(args.description) : undefined;
|
|
127
|
+
const description = rawDescription ? interpretEscapes(rawDescription) : undefined;
|
|
128
|
+
const projectId = args.project_id !== undefined ? Number(args.project_id) : undefined;
|
|
129
|
+
const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
|
|
130
|
+
// Get orgId from args or fall back to config
|
|
131
|
+
const orgId = args.org_id !== undefined ? Number(args.org_id) : cfg.orgId;
|
|
132
|
+
// Note: orgId=0 is technically valid (though unlikely), so don't use falsy check
|
|
133
|
+
if (orgId === undefined || orgId === null || Number.isNaN(orgId)) {
|
|
134
|
+
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 };
|
|
135
|
+
}
|
|
136
|
+
const result = await createIssue({ apiKey, apiBaseUrl, title, orgId, description, projectId, labels, debug });
|
|
137
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (toolName === "update_issue") {
|
|
141
|
+
const issueId = String(args.issue_id || "").trim();
|
|
142
|
+
if (!issueId) {
|
|
143
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
144
|
+
}
|
|
145
|
+
const rawTitle = args.title !== undefined ? String(args.title) : undefined;
|
|
146
|
+
const title = rawTitle !== undefined ? interpretEscapes(rawTitle) : undefined;
|
|
147
|
+
const rawDescription = args.description !== undefined ? String(args.description) : undefined;
|
|
148
|
+
const description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
|
|
149
|
+
const status = args.status !== undefined ? Number(args.status) : undefined;
|
|
150
|
+
const labels = Array.isArray(args.labels) ? args.labels.map(String) : undefined;
|
|
151
|
+
// Validate that at least one update field is provided
|
|
152
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined) {
|
|
153
|
+
return { content: [{ type: "text", text: "At least one field to update is required (title, description, status, or labels)" }], isError: true };
|
|
154
|
+
}
|
|
155
|
+
// Validate status value if provided (check for NaN and valid values)
|
|
156
|
+
if (status !== undefined && (Number.isNaN(status) || (status !== 0 && status !== 1))) {
|
|
157
|
+
return { content: [{ type: "text", text: "status must be 0 (open) or 1 (closed)" }], isError: true };
|
|
158
|
+
}
|
|
159
|
+
const result = await updateIssue({ apiKey, apiBaseUrl, issueId, title, description, status, labels, debug });
|
|
160
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (toolName === "update_issue_comment") {
|
|
164
|
+
const commentId = String(args.comment_id || "").trim();
|
|
165
|
+
const rawContent = String(args.content || "");
|
|
166
|
+
if (!commentId) {
|
|
167
|
+
return { content: [{ type: "text", text: "comment_id is required" }], isError: true };
|
|
168
|
+
}
|
|
169
|
+
if (!rawContent.trim()) {
|
|
170
|
+
return { content: [{ type: "text", text: "content is required" }], isError: true };
|
|
171
|
+
}
|
|
172
|
+
const content = interpretEscapes(rawContent);
|
|
173
|
+
const result = await updateIssueComment({ apiKey, apiBaseUrl, commentId, content, debug });
|
|
174
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Action Items Tools
|
|
178
|
+
if (toolName === "view_action_item") {
|
|
179
|
+
// Support both single ID and array of IDs
|
|
180
|
+
let actionItemIds: string[];
|
|
181
|
+
if (Array.isArray(args.action_item_ids)) {
|
|
182
|
+
actionItemIds = args.action_item_ids.map((id: unknown) => String(id).trim()).filter((id: string) => id);
|
|
183
|
+
} else if (args.action_item_id) {
|
|
184
|
+
actionItemIds = [String(args.action_item_id).trim()];
|
|
185
|
+
} else {
|
|
186
|
+
actionItemIds = [];
|
|
187
|
+
}
|
|
188
|
+
if (actionItemIds.length === 0) {
|
|
189
|
+
return { content: [{ type: "text", text: "action_item_id or action_item_ids is required" }], isError: true };
|
|
190
|
+
}
|
|
191
|
+
const actionItems = await fetchActionItem({ apiKey, apiBaseUrl, actionItemIds, debug });
|
|
192
|
+
if (actionItems.length === 0) {
|
|
193
|
+
return { content: [{ type: "text", text: "Action item(s) not found" }], isError: true };
|
|
194
|
+
}
|
|
195
|
+
return { content: [{ type: "text", text: JSON.stringify(actionItems, null, 2) }] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (toolName === "list_action_items") {
|
|
199
|
+
const issueId = String(args.issue_id || "").trim();
|
|
200
|
+
if (!issueId) {
|
|
201
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
202
|
+
}
|
|
203
|
+
const actionItems = await fetchActionItems({ apiKey, apiBaseUrl, issueId, debug });
|
|
204
|
+
return { content: [{ type: "text", text: JSON.stringify(actionItems, null, 2) }] };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (toolName === "create_action_item") {
|
|
208
|
+
const issueId = String(args.issue_id || "").trim();
|
|
209
|
+
const rawTitle = String(args.title || "").trim();
|
|
210
|
+
if (!issueId) {
|
|
211
|
+
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
212
|
+
}
|
|
213
|
+
if (!rawTitle) {
|
|
214
|
+
return { content: [{ type: "text", text: "title is required" }], isError: true };
|
|
215
|
+
}
|
|
216
|
+
const title = interpretEscapes(rawTitle);
|
|
217
|
+
const rawDescription = args.description ? String(args.description) : undefined;
|
|
218
|
+
const description = rawDescription ? interpretEscapes(rawDescription) : undefined;
|
|
219
|
+
const sqlAction = args.sql_action !== undefined ? String(args.sql_action) : undefined;
|
|
220
|
+
const configs = Array.isArray(args.configs) ? args.configs as ConfigChange[] : undefined;
|
|
221
|
+
const result = await createActionItem({ apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug });
|
|
222
|
+
return { content: [{ type: "text", text: JSON.stringify({ id: result }, null, 2) }] };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (toolName === "update_action_item") {
|
|
226
|
+
const actionItemId = String(args.action_item_id || "").trim();
|
|
227
|
+
if (!actionItemId) {
|
|
228
|
+
return { content: [{ type: "text", text: "action_item_id is required" }], isError: true };
|
|
229
|
+
}
|
|
230
|
+
const rawTitle = args.title !== undefined ? String(args.title) : undefined;
|
|
231
|
+
const title = rawTitle !== undefined ? interpretEscapes(rawTitle) : undefined;
|
|
232
|
+
const rawDescription = args.description !== undefined ? String(args.description) : undefined;
|
|
233
|
+
const description = rawDescription !== undefined ? interpretEscapes(rawDescription) : undefined;
|
|
234
|
+
const isDone = args.is_done !== undefined ? Boolean(args.is_done) : undefined;
|
|
235
|
+
const status = args.status !== undefined ? String(args.status) : undefined;
|
|
236
|
+
const statusReason = args.status_reason !== undefined ? String(args.status_reason) : undefined;
|
|
237
|
+
|
|
238
|
+
// Validate that at least one update field is provided
|
|
239
|
+
if (title === undefined && description === undefined &&
|
|
240
|
+
isDone === undefined && status === undefined && statusReason === undefined) {
|
|
241
|
+
return { content: [{ type: "text", text: "At least one field to update is required (title, description, is_done, status, or status_reason)" }], isError: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Validate status value if provided
|
|
245
|
+
if (status !== undefined && !["waiting_for_approval", "approved", "rejected"].includes(status)) {
|
|
246
|
+
return { content: [{ type: "text", text: "status must be 'waiting_for_approval', 'approved', or 'rejected'" }], isError: true };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await updateActionItem({ apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, debug });
|
|
250
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }] };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
256
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?: boolean }): Promise<void> {
|
|
27
261
|
const server = new Server(
|
|
28
|
-
{
|
|
262
|
+
{
|
|
263
|
+
name: "postgresai-mcp",
|
|
264
|
+
version: pkg.version,
|
|
265
|
+
title: "PostgresAI MCP Server",
|
|
266
|
+
},
|
|
29
267
|
{ capabilities: { tools: {} } }
|
|
30
268
|
);
|
|
31
269
|
|
|
32
|
-
// Interpret escape sequences (e.g., \n -> newline). Input comes from JSON, but
|
|
33
|
-
// we still normalize common escapes for consistency.
|
|
34
|
-
const interpretEscapes = (str: string): string =>
|
|
35
|
-
(str || "")
|
|
36
|
-
.replace(/\\n/g, "\n")
|
|
37
|
-
.replace(/\\t/g, "\t")
|
|
38
|
-
.replace(/\\r/g, "\r")
|
|
39
|
-
.replace(/\\"/g, '"')
|
|
40
|
-
.replace(/\\'/g, "'");
|
|
41
|
-
|
|
42
270
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
43
271
|
return {
|
|
44
272
|
tools: [
|
|
@@ -48,6 +276,10 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
48
276
|
inputSchema: {
|
|
49
277
|
type: "object",
|
|
50
278
|
properties: {
|
|
279
|
+
org_id: { type: "number", description: "Organization ID (optional, falls back to config)" },
|
|
280
|
+
status: { type: "string", description: "Filter by status: 'open', 'closed', or omit for all" },
|
|
281
|
+
limit: { type: "number", description: "Max number of issues to return (default: 20)" },
|
|
282
|
+
offset: { type: "number", description: "Number of issues to skip (default: 0)" },
|
|
51
283
|
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
52
284
|
},
|
|
53
285
|
additionalProperties: false,
|
|
@@ -81,76 +313,144 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
81
313
|
additionalProperties: false,
|
|
82
314
|
},
|
|
83
315
|
},
|
|
316
|
+
{
|
|
317
|
+
name: "create_issue",
|
|
318
|
+
description: "Create a new issue in PostgresAI",
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: {
|
|
322
|
+
title: { type: "string", description: "Issue title (required)" },
|
|
323
|
+
description: { type: "string", description: "Issue description (supports \\n as newline)" },
|
|
324
|
+
org_id: { type: "number", description: "Organization ID (uses config value if not provided)" },
|
|
325
|
+
project_id: { type: "number", description: "Project ID to associate the issue with" },
|
|
326
|
+
labels: {
|
|
327
|
+
type: "array",
|
|
328
|
+
items: { type: "string" },
|
|
329
|
+
description: "Labels to apply to the issue",
|
|
330
|
+
},
|
|
331
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
332
|
+
},
|
|
333
|
+
required: ["title"],
|
|
334
|
+
additionalProperties: false,
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "update_issue",
|
|
339
|
+
description: "Update an existing issue (title, description, status, labels). Use status=1 to close, status=0 to reopen.",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: "object",
|
|
342
|
+
properties: {
|
|
343
|
+
issue_id: { type: "string", description: "Issue ID (UUID)" },
|
|
344
|
+
title: { type: "string", description: "New title (supports \\n as newline)" },
|
|
345
|
+
description: { type: "string", description: "New description (supports \\n as newline)" },
|
|
346
|
+
status: { type: "number", description: "Status: 0=open, 1=closed" },
|
|
347
|
+
labels: {
|
|
348
|
+
type: "array",
|
|
349
|
+
items: { type: "string" },
|
|
350
|
+
description: "Labels to set on the issue",
|
|
351
|
+
},
|
|
352
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
353
|
+
},
|
|
354
|
+
required: ["issue_id"],
|
|
355
|
+
additionalProperties: false,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: "update_issue_comment",
|
|
360
|
+
description: "Update an existing issue comment",
|
|
361
|
+
inputSchema: {
|
|
362
|
+
type: "object",
|
|
363
|
+
properties: {
|
|
364
|
+
comment_id: { type: "string", description: "Comment ID (UUID)" },
|
|
365
|
+
content: { type: "string", description: "New comment text (supports \\n as newline)" },
|
|
366
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
367
|
+
},
|
|
368
|
+
required: ["comment_id", "content"],
|
|
369
|
+
additionalProperties: false,
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
// Action Items Tools
|
|
373
|
+
{
|
|
374
|
+
name: "view_action_item",
|
|
375
|
+
description: "View action item(s) with all details. Supports single ID or multiple IDs.",
|
|
376
|
+
inputSchema: {
|
|
377
|
+
type: "object",
|
|
378
|
+
properties: {
|
|
379
|
+
action_item_id: { type: "string", description: "Single action item ID (UUID)" },
|
|
380
|
+
action_item_ids: { type: "array", items: { type: "string" }, description: "Multiple action item IDs (UUIDs)" },
|
|
381
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
382
|
+
},
|
|
383
|
+
additionalProperties: false,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
name: "list_action_items",
|
|
388
|
+
description: "List action items for an issue",
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: "object",
|
|
391
|
+
properties: {
|
|
392
|
+
issue_id: { type: "string", description: "Issue ID (UUID)" },
|
|
393
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
394
|
+
},
|
|
395
|
+
required: ["issue_id"],
|
|
396
|
+
additionalProperties: false,
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "create_action_item",
|
|
401
|
+
description: "Create a new action item for an issue",
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: "object",
|
|
404
|
+
properties: {
|
|
405
|
+
issue_id: { type: "string", description: "Issue ID (UUID)" },
|
|
406
|
+
title: { type: "string", description: "Action item title" },
|
|
407
|
+
description: { type: "string", description: "Detailed description" },
|
|
408
|
+
sql_action: { type: "string", description: "SQL command to execute, e.g. 'DROP INDEX CONCURRENTLY idx_unused;'" },
|
|
409
|
+
configs: {
|
|
410
|
+
type: "array",
|
|
411
|
+
items: {
|
|
412
|
+
type: "object",
|
|
413
|
+
properties: {
|
|
414
|
+
parameter: { type: "string" },
|
|
415
|
+
value: { type: "string" },
|
|
416
|
+
},
|
|
417
|
+
required: ["parameter", "value"],
|
|
418
|
+
},
|
|
419
|
+
description: "Configuration parameter changes",
|
|
420
|
+
},
|
|
421
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
422
|
+
},
|
|
423
|
+
required: ["issue_id", "title"],
|
|
424
|
+
additionalProperties: false,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: "update_action_item",
|
|
429
|
+
description: "Update an action item: mark as done/not done, approve/reject, or edit title/description",
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: "object",
|
|
432
|
+
properties: {
|
|
433
|
+
action_item_id: { type: "string", description: "Action item ID (UUID)" },
|
|
434
|
+
title: { type: "string", description: "New title" },
|
|
435
|
+
description: { type: "string", description: "New description" },
|
|
436
|
+
is_done: { type: "boolean", description: "Mark as done (true) or not done (false)" },
|
|
437
|
+
status: { type: "string", description: "Approval status: 'waiting_for_approval', 'approved', or 'rejected'" },
|
|
438
|
+
status_reason: { type: "string", description: "Reason for approval/rejection" },
|
|
439
|
+
debug: { type: "boolean", description: "Enable verbose debug logs" },
|
|
440
|
+
},
|
|
441
|
+
required: ["action_item_id"],
|
|
442
|
+
additionalProperties: false,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
84
445
|
],
|
|
85
446
|
};
|
|
86
447
|
});
|
|
87
448
|
|
|
449
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
450
|
server.setRequestHandler(CallToolRequestSchema, async (req: any) => {
|
|
89
|
-
|
|
90
|
-
const args = (req.params.arguments as Record<string, unknown>) || {};
|
|
91
|
-
|
|
92
|
-
const cfg = config.readConfig();
|
|
93
|
-
const apiKey = (rootOpts?.apiKey || process.env.PGAI_API_KEY || cfg.apiKey || "").toString();
|
|
94
|
-
const { apiBaseUrl } = resolveBaseUrls(rootOpts, cfg);
|
|
95
|
-
|
|
96
|
-
const debug = Boolean(args.debug ?? extra?.debug);
|
|
97
|
-
|
|
98
|
-
if (!apiKey) {
|
|
99
|
-
return {
|
|
100
|
-
content: [
|
|
101
|
-
{
|
|
102
|
-
type: "text",
|
|
103
|
-
text: "API key is required. Run 'pgai auth' or set PGAI_API_KEY.",
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
isError: true,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
if (toolName === "list_issues") {
|
|
112
|
-
const issues = await fetchIssues({ apiKey, apiBaseUrl, debug });
|
|
113
|
-
return { content: [{ type: "text", text: JSON.stringify(issues, null, 2) }] };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (toolName === "view_issue") {
|
|
117
|
-
const issueId = String(args.issue_id || "").trim();
|
|
118
|
-
if (!issueId) {
|
|
119
|
-
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
120
|
-
}
|
|
121
|
-
const issue = await fetchIssue({ apiKey, apiBaseUrl, issueId, debug });
|
|
122
|
-
if (!issue) {
|
|
123
|
-
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
|
|
124
|
-
}
|
|
125
|
-
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, debug });
|
|
126
|
-
const combined = { issue, comments };
|
|
127
|
-
return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (toolName === "post_issue_comment") {
|
|
131
|
-
const issueId = String(args.issue_id || "").trim();
|
|
132
|
-
const rawContent = String(args.content || "");
|
|
133
|
-
const parentCommentId = args.parent_comment_id ? String(args.parent_comment_id) : undefined;
|
|
134
|
-
if (!issueId) {
|
|
135
|
-
return { content: [{ type: "text", text: "issue_id is required" }], isError: true };
|
|
136
|
-
}
|
|
137
|
-
if (!rawContent) {
|
|
138
|
-
return { content: [{ type: "text", text: "content is required" }], isError: true };
|
|
139
|
-
}
|
|
140
|
-
const content = interpretEscapes(rawContent);
|
|
141
|
-
const result = await createIssueComment({ apiKey, apiBaseUrl, issueId, content, parentCommentId, debug });
|
|
142
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
throw new Error(`Unknown tool: ${toolName}`);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
148
|
-
return { content: [{ type: "text", text: message }], isError: true };
|
|
149
|
-
}
|
|
451
|
+
return handleToolCall(req, rootOpts, extra);
|
|
150
452
|
});
|
|
151
453
|
|
|
152
454
|
const transport = new StdioServerTransport();
|
|
153
455
|
await server.connect(transport);
|
|
154
456
|
}
|
|
155
|
-
|
|
156
|
-
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics loader for express checkup reports
|
|
3
|
+
*
|
|
4
|
+
* Loads SQL queries from embedded metrics data (generated from metrics.yml at build time).
|
|
5
|
+
* Provides version-aware query selection and row transformation utilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { METRICS, MetricDefinition } from "./metrics-embedded";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get SQL query for a specific metric, selecting the appropriate version.
|
|
12
|
+
*
|
|
13
|
+
* @param metricName - Name of the metric (e.g., "settings", "db_stats")
|
|
14
|
+
* @param pgMajorVersion - PostgreSQL major version (default: 16)
|
|
15
|
+
* @returns SQL query string
|
|
16
|
+
* @throws Error if metric not found or no compatible version available
|
|
17
|
+
*/
|
|
18
|
+
export function getMetricSql(metricName: string, pgMajorVersion: number = 16): string {
|
|
19
|
+
const metric = METRICS[metricName];
|
|
20
|
+
|
|
21
|
+
if (!metric) {
|
|
22
|
+
throw new Error(`Metric "${metricName}" not found. Available metrics: ${Object.keys(METRICS).join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Find the best matching version: highest version <= pgMajorVersion
|
|
26
|
+
const availableVersions = Object.keys(metric.sqls)
|
|
27
|
+
.map(v => parseInt(v, 10))
|
|
28
|
+
.sort((a, b) => b - a); // Sort descending
|
|
29
|
+
|
|
30
|
+
const matchingVersion = availableVersions.find(v => v <= pgMajorVersion);
|
|
31
|
+
|
|
32
|
+
if (matchingVersion === undefined) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`No compatible SQL version for metric "${metricName}" with PostgreSQL ${pgMajorVersion}. ` +
|
|
35
|
+
`Available versions: ${availableVersions.join(", ")}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return metric.sqls[matchingVersion];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get metric definition including all metadata.
|
|
44
|
+
*
|
|
45
|
+
* @param metricName - Name of the metric
|
|
46
|
+
* @returns MetricDefinition or undefined if not found
|
|
47
|
+
*/
|
|
48
|
+
export function getMetricDefinition(metricName: string): MetricDefinition | undefined {
|
|
49
|
+
return METRICS[metricName];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* List all available metric names.
|
|
54
|
+
*/
|
|
55
|
+
export function listMetricNames(): string[] {
|
|
56
|
+
return Object.keys(METRICS);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Metric names that correspond to express report checks.
|
|
61
|
+
* Maps check IDs and logical names to metric names in the METRICS object.
|
|
62
|
+
*/
|
|
63
|
+
export const METRIC_NAMES = {
|
|
64
|
+
// Index health checks
|
|
65
|
+
H001: "pg_invalid_indexes",
|
|
66
|
+
H002: "unused_indexes",
|
|
67
|
+
H004: "redundant_indexes",
|
|
68
|
+
// Settings and version info (A002, A003, A007, A013)
|
|
69
|
+
settings: "settings",
|
|
70
|
+
// Database statistics (A004)
|
|
71
|
+
dbStats: "db_stats",
|
|
72
|
+
dbSize: "db_size",
|
|
73
|
+
// Stats reset info (H002)
|
|
74
|
+
statsReset: "stats_reset",
|
|
75
|
+
} as const;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Transform a row from metrics query output to JSON report format.
|
|
79
|
+
* Metrics use `tag_` prefix for dimensions; we strip it for JSON reports.
|
|
80
|
+
* Also removes Prometheus-specific fields like epoch_ns, num, tag_datname.
|
|
81
|
+
*/
|
|
82
|
+
export function transformMetricRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
83
|
+
const result: Record<string, unknown> = {};
|
|
84
|
+
|
|
85
|
+
for (const [key, value] of Object.entries(row)) {
|
|
86
|
+
// Skip Prometheus-specific fields
|
|
87
|
+
if (key === "epoch_ns" || key === "num" || key === "tag_datname") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Strip tag_ prefix
|
|
92
|
+
const newKey = key.startsWith("tag_") ? key.slice(4) : key;
|
|
93
|
+
result[newKey] = value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Transform settings metric row to the format expected by express reports.
|
|
101
|
+
* The settings metric returns one row per setting with tag_setting_name as key.
|
|
102
|
+
*/
|
|
103
|
+
export function transformSettingsRow(row: Record<string, unknown>): {
|
|
104
|
+
name: string;
|
|
105
|
+
setting: string;
|
|
106
|
+
unit: string;
|
|
107
|
+
category: string;
|
|
108
|
+
vartype: string;
|
|
109
|
+
is_default: boolean;
|
|
110
|
+
} {
|
|
111
|
+
return {
|
|
112
|
+
name: String(row.tag_setting_name || ""),
|
|
113
|
+
setting: String(row.tag_setting_value || ""),
|
|
114
|
+
unit: String(row.tag_unit || ""),
|
|
115
|
+
category: String(row.tag_category || ""),
|
|
116
|
+
vartype: String(row.tag_vartype || ""),
|
|
117
|
+
is_default: row.is_default === 1 || row.is_default === true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Re-export types for convenience
|
|
122
|
+
export type { MetricDefinition } from "./metrics-embedded";
|
|
123
|
+
|
|
124
|
+
// Legacy export for backward compatibility
|
|
125
|
+
export function loadMetricsYml(): { metrics: Record<string, unknown> } {
|
|
126
|
+
return { metrics: METRICS };
|
|
127
|
+
}
|