postgresai 0.14.0-dev.43 → 0.14.0-dev.45
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/bin/postgres-ai.ts +649 -310
- package/bun.lock +258 -0
- package/dist/bin/postgres-ai.js +29491 -1910
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.permissions.sql +37 -0
- package/dist/sql/03.optional_rds.sql +6 -0
- package/dist/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/05.helpers.sql +415 -0
- package/lib/auth-server.ts +58 -97
- package/lib/checkup-api.ts +175 -0
- package/lib/checkup.ts +837 -0
- package/lib/config.ts +3 -0
- package/lib/init.ts +106 -74
- package/lib/issues.ts +121 -194
- package/lib/mcp-server.ts +6 -17
- package/lib/metrics-loader.ts +156 -0
- package/package.json +13 -9
- package/sql/02.permissions.sql +9 -5
- package/sql/05.helpers.sql +415 -0
- package/test/checkup.test.ts +953 -0
- package/test/init.integration.test.ts +396 -0
- package/test/init.test.ts +345 -0
- package/test/schema-validation.test.ts +188 -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 -85
- package/dist/lib/init.d.ts.map +0 -1
- package/dist/lib/init.js +0 -644
- 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 -382
- package/test/init.test.cjs +0 -392
package/lib/config.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface Config {
|
|
|
9
9
|
apiKey: string | null;
|
|
10
10
|
baseUrl: string | null;
|
|
11
11
|
orgId: number | null;
|
|
12
|
+
defaultProject: string | null;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -46,6 +47,7 @@ export function readConfig(): Config {
|
|
|
46
47
|
apiKey: null,
|
|
47
48
|
baseUrl: null,
|
|
48
49
|
orgId: null,
|
|
50
|
+
defaultProject: null,
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
// Try user-level config first
|
|
@@ -57,6 +59,7 @@ export function readConfig(): Config {
|
|
|
57
59
|
config.apiKey = parsed.apiKey || null;
|
|
58
60
|
config.baseUrl = parsed.baseUrl || null;
|
|
59
61
|
config.orgId = parsed.orgId || null;
|
|
62
|
+
config.defaultProject = parsed.defaultProject || null;
|
|
60
63
|
return config;
|
|
61
64
|
} catch (err) {
|
|
62
65
|
const message = err instanceof Error ? err.message : String(err);
|
package/lib/init.ts
CHANGED
|
@@ -159,13 +159,21 @@ export type InitPlan = {
|
|
|
159
159
|
steps: InitStep[];
|
|
160
160
|
};
|
|
161
161
|
|
|
162
|
-
function packageRootDirFromCompiled(): string {
|
|
163
|
-
// dist/lib/init.js -> <pkg>/dist/lib ; package root is ../..
|
|
164
|
-
return path.resolve(__dirname, "..", "..");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
162
|
function sqlDir(): string {
|
|
168
|
-
|
|
163
|
+
// Handle both development and production paths
|
|
164
|
+
// Development: lib/init.ts -> ../sql
|
|
165
|
+
// Production (bundled): dist/bin/postgres-ai.js -> ../sql (copied during build)
|
|
166
|
+
const candidates = [
|
|
167
|
+
path.resolve(__dirname, "..", "sql"), // bundled: dist/bin -> dist/sql
|
|
168
|
+
path.resolve(__dirname, "..", "..", "sql"), // dev from lib: lib -> ../sql
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
for (const candidate of candidates) {
|
|
172
|
+
if (fs.existsSync(candidate)) {
|
|
173
|
+
return candidate;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
function loadSqlTemplate(filename: string): string {
|
|
@@ -485,6 +493,12 @@ end $$;`;
|
|
|
485
493
|
sql: applyTemplate(loadSqlTemplate("02.permissions.sql"), vars),
|
|
486
494
|
});
|
|
487
495
|
|
|
496
|
+
// Helper functions (SECURITY DEFINER) for plan analysis and table info
|
|
497
|
+
steps.push({
|
|
498
|
+
name: "05.helpers",
|
|
499
|
+
sql: applyTemplate(loadSqlTemplate("05.helpers.sql"), vars),
|
|
500
|
+
});
|
|
501
|
+
|
|
488
502
|
if (params.includeOptionalPermissions) {
|
|
489
503
|
steps.push(
|
|
490
504
|
{
|
|
@@ -511,78 +525,70 @@ export async function applyInitPlan(params: {
|
|
|
511
525
|
const applied: string[] = [];
|
|
512
526
|
const skippedOptional: string[] = [];
|
|
513
527
|
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
528
|
+
// Helper to wrap a step execution in begin/commit
|
|
529
|
+
const executeStep = async (step: InitStep): Promise<void> => {
|
|
530
|
+
await params.client.query("begin;");
|
|
531
|
+
try {
|
|
532
|
+
await params.client.query(step.sql, step.params as any);
|
|
533
|
+
await params.client.query("commit;");
|
|
534
|
+
} catch (e) {
|
|
535
|
+
// Rollback errors should never mask the original failure.
|
|
518
536
|
try {
|
|
519
|
-
await params.client.query(
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
523
|
-
const errAny = e as any;
|
|
524
|
-
const wrapped: any = new Error(`Failed at step "${step.name}": ${msg}`);
|
|
525
|
-
// Preserve useful Postgres error fields so callers can provide better hints / diagnostics.
|
|
526
|
-
const pgErrorFields = [
|
|
527
|
-
"code",
|
|
528
|
-
"detail",
|
|
529
|
-
"hint",
|
|
530
|
-
"position",
|
|
531
|
-
"internalPosition",
|
|
532
|
-
"internalQuery",
|
|
533
|
-
"where",
|
|
534
|
-
"schema",
|
|
535
|
-
"table",
|
|
536
|
-
"column",
|
|
537
|
-
"dataType",
|
|
538
|
-
"constraint",
|
|
539
|
-
"file",
|
|
540
|
-
"line",
|
|
541
|
-
"routine",
|
|
542
|
-
] as const;
|
|
543
|
-
if (errAny && typeof errAny === "object") {
|
|
544
|
-
for (const field of pgErrorFields) {
|
|
545
|
-
if (errAny[field] !== undefined) wrapped[field] = errAny[field];
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
if (e instanceof Error && e.stack) {
|
|
549
|
-
wrapped.stack = e.stack;
|
|
550
|
-
}
|
|
551
|
-
throw wrapped;
|
|
537
|
+
await params.client.query("rollback;");
|
|
538
|
+
} catch {
|
|
539
|
+
// ignore
|
|
552
540
|
}
|
|
541
|
+
throw e;
|
|
553
542
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// Apply non-optional steps, each in its own transaction
|
|
546
|
+
for (const step of params.plan.steps.filter((s) => !s.optional)) {
|
|
557
547
|
try {
|
|
558
|
-
await
|
|
559
|
-
|
|
560
|
-
|
|
548
|
+
await executeStep(step);
|
|
549
|
+
applied.push(step.name);
|
|
550
|
+
} catch (e) {
|
|
551
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
552
|
+
const errAny = e as any;
|
|
553
|
+
const wrapped: any = new Error(`Failed at step "${step.name}": ${msg}`);
|
|
554
|
+
// Preserve useful Postgres error fields so callers can provide better hints / diagnostics.
|
|
555
|
+
const pgErrorFields = [
|
|
556
|
+
"code",
|
|
557
|
+
"detail",
|
|
558
|
+
"hint",
|
|
559
|
+
"position",
|
|
560
|
+
"internalPosition",
|
|
561
|
+
"internalQuery",
|
|
562
|
+
"where",
|
|
563
|
+
"schema",
|
|
564
|
+
"table",
|
|
565
|
+
"column",
|
|
566
|
+
"dataType",
|
|
567
|
+
"constraint",
|
|
568
|
+
"file",
|
|
569
|
+
"line",
|
|
570
|
+
"routine",
|
|
571
|
+
] as const;
|
|
572
|
+
if (errAny && typeof errAny === "object") {
|
|
573
|
+
for (const field of pgErrorFields) {
|
|
574
|
+
if (errAny[field] !== undefined) wrapped[field] = errAny[field];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (e instanceof Error && e.stack) {
|
|
578
|
+
wrapped.stack = e.stack;
|
|
579
|
+
}
|
|
580
|
+
throw wrapped;
|
|
561
581
|
}
|
|
562
|
-
throw e;
|
|
563
582
|
}
|
|
564
583
|
|
|
565
|
-
// Apply optional steps
|
|
584
|
+
// Apply optional steps, each in its own transaction (failure doesn't abort)
|
|
566
585
|
for (const step of params.plan.steps.filter((s) => s.optional)) {
|
|
567
586
|
try {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
try {
|
|
571
|
-
await params.client.query(step.sql, step.params as any);
|
|
572
|
-
await params.client.query("commit;");
|
|
573
|
-
applied.push(step.name);
|
|
574
|
-
} catch {
|
|
575
|
-
try {
|
|
576
|
-
await params.client.query("rollback;");
|
|
577
|
-
} catch {
|
|
578
|
-
// ignore rollback errors
|
|
579
|
-
}
|
|
580
|
-
skippedOptional.push(step.name);
|
|
581
|
-
// best-effort: ignore
|
|
582
|
-
}
|
|
587
|
+
await executeStep(step);
|
|
588
|
+
applied.push(step.name);
|
|
583
589
|
} catch {
|
|
584
|
-
// If we can't even begin/commit, treat as skipped.
|
|
585
590
|
skippedOptional.push(step.name);
|
|
591
|
+
// best-effort: ignore
|
|
586
592
|
}
|
|
587
593
|
}
|
|
588
594
|
|
|
@@ -642,16 +648,25 @@ export async function verifyInitSetup(params: {
|
|
|
642
648
|
missingRequired.push("SELECT on pg_catalog.pg_index");
|
|
643
649
|
}
|
|
644
650
|
|
|
645
|
-
|
|
651
|
+
// Check postgres_ai schema exists and is usable
|
|
652
|
+
const schemaExistsRes = await params.client.query(
|
|
653
|
+
"select has_schema_privilege($1, 'postgres_ai', 'USAGE') as ok",
|
|
654
|
+
[role]
|
|
655
|
+
);
|
|
656
|
+
if (!schemaExistsRes.rows?.[0]?.ok) {
|
|
657
|
+
missingRequired.push("USAGE on schema postgres_ai");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const viewExistsRes = await params.client.query("select to_regclass('postgres_ai.pg_statistic') is not null as ok");
|
|
646
661
|
if (!viewExistsRes.rows?.[0]?.ok) {
|
|
647
|
-
missingRequired.push("view
|
|
662
|
+
missingRequired.push("view postgres_ai.pg_statistic exists");
|
|
648
663
|
} else {
|
|
649
664
|
const viewPrivRes = await params.client.query(
|
|
650
|
-
"select has_table_privilege($1, '
|
|
665
|
+
"select has_table_privilege($1, 'postgres_ai.pg_statistic', 'SELECT') as ok",
|
|
651
666
|
[role]
|
|
652
667
|
);
|
|
653
668
|
if (!viewPrivRes.rows?.[0]?.ok) {
|
|
654
|
-
missingRequired.push("SELECT on view
|
|
669
|
+
missingRequired.push("SELECT on view postgres_ai.pg_statistic");
|
|
655
670
|
}
|
|
656
671
|
}
|
|
657
672
|
|
|
@@ -669,13 +684,30 @@ export async function verifyInitSetup(params: {
|
|
|
669
684
|
if (typeof spLine !== "string" || !spLine) {
|
|
670
685
|
missingRequired.push("role search_path is set");
|
|
671
686
|
} else {
|
|
672
|
-
// We accept any ordering as long as public and pg_catalog are included.
|
|
687
|
+
// We accept any ordering as long as postgres_ai, public, and pg_catalog are included.
|
|
673
688
|
const sp = spLine.toLowerCase();
|
|
674
|
-
if (!sp.includes("public") || !sp.includes("pg_catalog")) {
|
|
675
|
-
missingRequired.push("role search_path includes public and pg_catalog");
|
|
689
|
+
if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
|
|
690
|
+
missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
|
|
676
691
|
}
|
|
677
692
|
}
|
|
678
693
|
|
|
694
|
+
// Check for helper functions
|
|
695
|
+
const explainFnRes = await params.client.query(
|
|
696
|
+
"select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok",
|
|
697
|
+
[role]
|
|
698
|
+
);
|
|
699
|
+
if (!explainFnRes.rows?.[0]?.ok) {
|
|
700
|
+
missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const tableDescribeFnRes = await params.client.query(
|
|
704
|
+
"select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok",
|
|
705
|
+
[role]
|
|
706
|
+
);
|
|
707
|
+
if (!tableDescribeFnRes.rows?.[0]?.ok) {
|
|
708
|
+
missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
|
|
709
|
+
}
|
|
710
|
+
|
|
679
711
|
if (params.includeOptionalPermissions) {
|
|
680
712
|
// Optional RDS/Aurora extras
|
|
681
713
|
{
|
package/lib/issues.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import * as https from "https";
|
|
2
|
-
import { URL } from "url";
|
|
3
1
|
import { maskSecret, normalizeBaseUrl } from "./util";
|
|
4
2
|
|
|
5
3
|
export interface IssueActionItem {
|
|
@@ -75,59 +73,42 @@ export async function fetchIssues(params: FetchIssuesParams): Promise<IssueListI
|
|
|
75
73
|
|
|
76
74
|
if (debug) {
|
|
77
75
|
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
78
|
-
// eslint-disable-next-line no-console
|
|
79
76
|
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
80
|
-
// eslint-disable-next-line no-console
|
|
81
77
|
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
82
|
-
// eslint-disable-next-line no-console
|
|
83
78
|
console.log(`Debug: Auth scheme: access-token`);
|
|
84
|
-
// eslint-disable-next-line no-console
|
|
85
79
|
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
method: "GET",
|
|
93
|
-
headers,
|
|
94
|
-
},
|
|
95
|
-
(res) => {
|
|
96
|
-
let data = "";
|
|
97
|
-
res.on("data", (chunk) => (data += chunk));
|
|
98
|
-
res.on("end", () => {
|
|
99
|
-
if (debug) {
|
|
100
|
-
// eslint-disable-next-line no-console
|
|
101
|
-
console.log(`Debug: Response status: ${res.statusCode}`);
|
|
102
|
-
// eslint-disable-next-line no-console
|
|
103
|
-
console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
|
|
104
|
-
}
|
|
105
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
106
|
-
try {
|
|
107
|
-
const parsed = JSON.parse(data) as IssueListItem[];
|
|
108
|
-
resolve(parsed);
|
|
109
|
-
} catch {
|
|
110
|
-
reject(new Error(`Failed to parse issues response: ${data}`));
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
let errMsg = `Failed to fetch issues: HTTP ${res.statusCode}`;
|
|
114
|
-
if (data) {
|
|
115
|
-
try {
|
|
116
|
-
const errObj = JSON.parse(data);
|
|
117
|
-
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
118
|
-
} catch {
|
|
119
|
-
errMsg += `\n${data}`;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
reject(new Error(errMsg));
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
req.on("error", (err: Error) => reject(err));
|
|
129
|
-
req.end();
|
|
82
|
+
const response = await fetch(url.toString(), {
|
|
83
|
+
method: "GET",
|
|
84
|
+
headers,
|
|
130
85
|
});
|
|
86
|
+
|
|
87
|
+
if (debug) {
|
|
88
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
89
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data = await response.text();
|
|
93
|
+
|
|
94
|
+
if (response.ok) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(data) as IssueListItem[];
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error(`Failed to parse issues response: ${data}`);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
let errMsg = `Failed to fetch issues: HTTP ${response.status}`;
|
|
102
|
+
if (data) {
|
|
103
|
+
try {
|
|
104
|
+
const errObj = JSON.parse(data);
|
|
105
|
+
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
106
|
+
} catch {
|
|
107
|
+
errMsg += `\n${data}`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw new Error(errMsg);
|
|
111
|
+
}
|
|
131
112
|
}
|
|
132
113
|
|
|
133
114
|
|
|
@@ -158,59 +139,42 @@ export async function fetchIssueComments(params: FetchIssueCommentsParams): Prom
|
|
|
158
139
|
|
|
159
140
|
if (debug) {
|
|
160
141
|
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
161
|
-
// eslint-disable-next-line no-console
|
|
162
142
|
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
163
|
-
// eslint-disable-next-line no-console
|
|
164
143
|
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
165
|
-
// eslint-disable-next-line no-console
|
|
166
144
|
console.log(`Debug: Auth scheme: access-token`);
|
|
167
|
-
// eslint-disable-next-line no-console
|
|
168
145
|
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
169
146
|
}
|
|
170
147
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
method: "GET",
|
|
176
|
-
headers,
|
|
177
|
-
},
|
|
178
|
-
(res) => {
|
|
179
|
-
let data = "";
|
|
180
|
-
res.on("data", (chunk) => (data += chunk));
|
|
181
|
-
res.on("end", () => {
|
|
182
|
-
if (debug) {
|
|
183
|
-
// eslint-disable-next-line no-console
|
|
184
|
-
console.log(`Debug: Response status: ${res.statusCode}`);
|
|
185
|
-
// eslint-disable-next-line no-console
|
|
186
|
-
console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
|
|
187
|
-
}
|
|
188
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
189
|
-
try {
|
|
190
|
-
const parsed = JSON.parse(data) as IssueComment[];
|
|
191
|
-
resolve(parsed);
|
|
192
|
-
} catch {
|
|
193
|
-
reject(new Error(`Failed to parse issue comments response: ${data}`));
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
let errMsg = `Failed to fetch issue comments: HTTP ${res.statusCode}`;
|
|
197
|
-
if (data) {
|
|
198
|
-
try {
|
|
199
|
-
const errObj = JSON.parse(data);
|
|
200
|
-
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
201
|
-
} catch {
|
|
202
|
-
errMsg += `\n${data}`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
reject(new Error(errMsg));
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
req.on("error", (err: Error) => reject(err));
|
|
212
|
-
req.end();
|
|
148
|
+
const response = await fetch(url.toString(), {
|
|
149
|
+
method: "GET",
|
|
150
|
+
headers,
|
|
213
151
|
});
|
|
152
|
+
|
|
153
|
+
if (debug) {
|
|
154
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
155
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const data = await response.text();
|
|
159
|
+
|
|
160
|
+
if (response.ok) {
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(data) as IssueComment[];
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error(`Failed to parse issue comments response: ${data}`);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
let errMsg = `Failed to fetch issue comments: HTTP ${response.status}`;
|
|
168
|
+
if (data) {
|
|
169
|
+
try {
|
|
170
|
+
const errObj = JSON.parse(data);
|
|
171
|
+
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
172
|
+
} catch {
|
|
173
|
+
errMsg += `\n${data}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw new Error(errMsg);
|
|
177
|
+
}
|
|
214
178
|
}
|
|
215
179
|
|
|
216
180
|
export interface FetchIssueParams {
|
|
@@ -243,63 +207,47 @@ export async function fetchIssue(params: FetchIssueParams): Promise<IssueDetail
|
|
|
243
207
|
|
|
244
208
|
if (debug) {
|
|
245
209
|
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
246
|
-
// eslint-disable-next-line no-console
|
|
247
210
|
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
248
|
-
// eslint-disable-next-line no-console
|
|
249
211
|
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
250
|
-
// eslint-disable-next-line no-console
|
|
251
212
|
console.log(`Debug: Auth scheme: access-token`);
|
|
252
|
-
// eslint-disable-next-line no-console
|
|
253
213
|
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
254
214
|
}
|
|
255
215
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
{
|
|
260
|
-
method: "GET",
|
|
261
|
-
headers,
|
|
262
|
-
},
|
|
263
|
-
(res) => {
|
|
264
|
-
let data = "";
|
|
265
|
-
res.on("data", (chunk) => (data += chunk));
|
|
266
|
-
res.on("end", () => {
|
|
267
|
-
if (debug) {
|
|
268
|
-
// eslint-disable-next-line no-console
|
|
269
|
-
console.log(`Debug: Response status: ${res.statusCode}`);
|
|
270
|
-
// eslint-disable-next-line no-console
|
|
271
|
-
console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
|
|
272
|
-
}
|
|
273
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
274
|
-
try {
|
|
275
|
-
const parsed = JSON.parse(data);
|
|
276
|
-
if (Array.isArray(parsed)) {
|
|
277
|
-
resolve((parsed[0] as IssueDetail) ?? null);
|
|
278
|
-
} else {
|
|
279
|
-
resolve(parsed as IssueDetail);
|
|
280
|
-
}
|
|
281
|
-
} catch {
|
|
282
|
-
reject(new Error(`Failed to parse issue response: ${data}`));
|
|
283
|
-
}
|
|
284
|
-
} else {
|
|
285
|
-
let errMsg = `Failed to fetch issue: HTTP ${res.statusCode}`;
|
|
286
|
-
if (data) {
|
|
287
|
-
try {
|
|
288
|
-
const errObj = JSON.parse(data);
|
|
289
|
-
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
290
|
-
} catch {
|
|
291
|
-
errMsg += `\n${data}`;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
reject(new Error(errMsg));
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
req.on("error", (err: Error) => reject(err));
|
|
301
|
-
req.end();
|
|
216
|
+
const response = await fetch(url.toString(), {
|
|
217
|
+
method: "GET",
|
|
218
|
+
headers,
|
|
302
219
|
});
|
|
220
|
+
|
|
221
|
+
if (debug) {
|
|
222
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
223
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const data = await response.text();
|
|
227
|
+
|
|
228
|
+
if (response.ok) {
|
|
229
|
+
try {
|
|
230
|
+
const parsed = JSON.parse(data);
|
|
231
|
+
if (Array.isArray(parsed)) {
|
|
232
|
+
return (parsed[0] as IssueDetail) ?? null;
|
|
233
|
+
} else {
|
|
234
|
+
return parsed as IssueDetail;
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
throw new Error(`Failed to parse issue response: ${data}`);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
let errMsg = `Failed to fetch issue: HTTP ${response.status}`;
|
|
241
|
+
if (data) {
|
|
242
|
+
try {
|
|
243
|
+
const errObj = JSON.parse(data);
|
|
244
|
+
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
245
|
+
} catch {
|
|
246
|
+
errMsg += `\n${data}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw new Error(errMsg);
|
|
250
|
+
}
|
|
303
251
|
}
|
|
304
252
|
|
|
305
253
|
export interface CreateIssueCommentParams {
|
|
@@ -339,67 +287,46 @@ export async function createIssueComment(params: CreateIssueCommentParams): Prom
|
|
|
339
287
|
"access-token": apiKey,
|
|
340
288
|
"Prefer": "return=representation",
|
|
341
289
|
"Content-Type": "application/json",
|
|
342
|
-
"Content-Length": Buffer.byteLength(body).toString(),
|
|
343
290
|
};
|
|
344
291
|
|
|
345
292
|
if (debug) {
|
|
346
293
|
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
347
|
-
// eslint-disable-next-line no-console
|
|
348
294
|
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
349
|
-
// eslint-disable-next-line no-console
|
|
350
295
|
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
351
|
-
// eslint-disable-next-line no-console
|
|
352
296
|
console.log(`Debug: Auth scheme: access-token`);
|
|
353
|
-
// eslint-disable-next-line no-console
|
|
354
297
|
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
355
|
-
// eslint-disable-next-line no-console
|
|
356
298
|
console.log(`Debug: Request body: ${body}`);
|
|
357
299
|
}
|
|
358
300
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
method: "POST",
|
|
364
|
-
headers,
|
|
365
|
-
},
|
|
366
|
-
(res) => {
|
|
367
|
-
let data = "";
|
|
368
|
-
res.on("data", (chunk) => (data += chunk));
|
|
369
|
-
res.on("end", () => {
|
|
370
|
-
if (debug) {
|
|
371
|
-
// eslint-disable-next-line no-console
|
|
372
|
-
console.log(`Debug: Response status: ${res.statusCode}`);
|
|
373
|
-
// eslint-disable-next-line no-console
|
|
374
|
-
console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
|
|
375
|
-
}
|
|
376
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
377
|
-
try {
|
|
378
|
-
const parsed = JSON.parse(data) as IssueComment;
|
|
379
|
-
resolve(parsed);
|
|
380
|
-
} catch {
|
|
381
|
-
reject(new Error(`Failed to parse create comment response: ${data}`));
|
|
382
|
-
}
|
|
383
|
-
} else {
|
|
384
|
-
let errMsg = `Failed to create issue comment: HTTP ${res.statusCode}`;
|
|
385
|
-
if (data) {
|
|
386
|
-
try {
|
|
387
|
-
const errObj = JSON.parse(data);
|
|
388
|
-
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
389
|
-
} catch {
|
|
390
|
-
errMsg += `\n${data}`;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
reject(new Error(errMsg));
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
req.on("error", (err: Error) => reject(err));
|
|
400
|
-
req.write(body);
|
|
401
|
-
req.end();
|
|
301
|
+
const response = await fetch(url.toString(), {
|
|
302
|
+
method: "POST",
|
|
303
|
+
headers,
|
|
304
|
+
body,
|
|
402
305
|
});
|
|
403
|
-
}
|
|
404
306
|
|
|
307
|
+
if (debug) {
|
|
308
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
309
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
310
|
+
}
|
|
405
311
|
|
|
312
|
+
const data = await response.text();
|
|
313
|
+
|
|
314
|
+
if (response.ok) {
|
|
315
|
+
try {
|
|
316
|
+
return JSON.parse(data) as IssueComment;
|
|
317
|
+
} catch {
|
|
318
|
+
throw new Error(`Failed to parse create comment response: ${data}`);
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
let errMsg = `Failed to create issue comment: HTTP ${response.status}`;
|
|
322
|
+
if (data) {
|
|
323
|
+
try {
|
|
324
|
+
const errObj = JSON.parse(data);
|
|
325
|
+
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
326
|
+
} catch {
|
|
327
|
+
errMsg += `\n${data}`;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
throw new Error(errMsg);
|
|
331
|
+
}
|
|
332
|
+
}
|
package/lib/mcp-server.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import pkg from "../package.json";
|
|
2
2
|
import * as config from "./config";
|
|
3
3
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "./issues";
|
|
4
4
|
import { resolveBaseUrls } from "./util";
|
|
5
5
|
|
|
6
|
-
// MCP SDK imports
|
|
7
|
-
import { Server } from "@modelcontextprotocol/sdk/server";
|
|
8
|
-
import
|
|
9
|
-
|
|
6
|
+
// MCP SDK imports - Bun handles these directly
|
|
7
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
10
10
|
|
|
11
11
|
interface RootOptsLike {
|
|
12
12
|
apiKey?: string;
|
|
@@ -14,16 +14,6 @@ interface RootOptsLike {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?: boolean }): Promise<void> {
|
|
17
|
-
// Resolve stdio transport at runtime to avoid subpath export resolution issues
|
|
18
|
-
const serverEntry = require.resolve("@modelcontextprotocol/sdk/server");
|
|
19
|
-
const stdioPath = path.join(path.dirname(serverEntry), "stdio.js");
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
21
|
-
const { StdioServerTransport } = require(stdioPath);
|
|
22
|
-
// Load schemas dynamically to avoid subpath export resolution issues
|
|
23
|
-
const typesPath = path.resolve(path.dirname(serverEntry), "../types.js");
|
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
25
|
-
const { CallToolRequestSchema, ListToolsRequestSchema } = require(typesPath);
|
|
26
|
-
|
|
27
17
|
const server = new Server(
|
|
28
18
|
{ name: "postgresai-mcp", version: pkg.version },
|
|
29
19
|
{ capabilities: { tools: {} } }
|
|
@@ -85,6 +75,7 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
85
75
|
};
|
|
86
76
|
});
|
|
87
77
|
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
79
|
server.setRequestHandler(CallToolRequestSchema, async (req: any) => {
|
|
89
80
|
const toolName = req.params.name;
|
|
90
81
|
const args = (req.params.arguments as Record<string, unknown>) || {};
|
|
@@ -152,5 +143,3 @@ export async function startMcpServer(rootOpts?: RootOptsLike, extra?: { debug?:
|
|
|
152
143
|
const transport = new StdioServerTransport();
|
|
153
144
|
await server.connect(transport);
|
|
154
145
|
}
|
|
155
|
-
|
|
156
|
-
|