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.
Files changed (58) hide show
  1. package/bin/postgres-ai.ts +649 -310
  2. package/bun.lock +258 -0
  3. package/dist/bin/postgres-ai.js +29491 -1910
  4. package/dist/sql/01.role.sql +16 -0
  5. package/dist/sql/02.permissions.sql +37 -0
  6. package/dist/sql/03.optional_rds.sql +6 -0
  7. package/dist/sql/04.optional_self_managed.sql +8 -0
  8. package/dist/sql/05.helpers.sql +415 -0
  9. package/lib/auth-server.ts +58 -97
  10. package/lib/checkup-api.ts +175 -0
  11. package/lib/checkup.ts +837 -0
  12. package/lib/config.ts +3 -0
  13. package/lib/init.ts +106 -74
  14. package/lib/issues.ts +121 -194
  15. package/lib/mcp-server.ts +6 -17
  16. package/lib/metrics-loader.ts +156 -0
  17. package/package.json +13 -9
  18. package/sql/02.permissions.sql +9 -5
  19. package/sql/05.helpers.sql +415 -0
  20. package/test/checkup.test.ts +953 -0
  21. package/test/init.integration.test.ts +396 -0
  22. package/test/init.test.ts +345 -0
  23. package/test/schema-validation.test.ts +188 -0
  24. package/tsconfig.json +12 -20
  25. package/dist/bin/postgres-ai.d.ts +0 -3
  26. package/dist/bin/postgres-ai.d.ts.map +0 -1
  27. package/dist/bin/postgres-ai.js.map +0 -1
  28. package/dist/lib/auth-server.d.ts +0 -31
  29. package/dist/lib/auth-server.d.ts.map +0 -1
  30. package/dist/lib/auth-server.js +0 -263
  31. package/dist/lib/auth-server.js.map +0 -1
  32. package/dist/lib/config.d.ts +0 -45
  33. package/dist/lib/config.d.ts.map +0 -1
  34. package/dist/lib/config.js +0 -181
  35. package/dist/lib/config.js.map +0 -1
  36. package/dist/lib/init.d.ts +0 -85
  37. package/dist/lib/init.d.ts.map +0 -1
  38. package/dist/lib/init.js +0 -644
  39. package/dist/lib/init.js.map +0 -1
  40. package/dist/lib/issues.d.ts +0 -75
  41. package/dist/lib/issues.d.ts.map +0 -1
  42. package/dist/lib/issues.js +0 -336
  43. package/dist/lib/issues.js.map +0 -1
  44. package/dist/lib/mcp-server.d.ts +0 -9
  45. package/dist/lib/mcp-server.d.ts.map +0 -1
  46. package/dist/lib/mcp-server.js +0 -168
  47. package/dist/lib/mcp-server.js.map +0 -1
  48. package/dist/lib/pkce.d.ts +0 -32
  49. package/dist/lib/pkce.d.ts.map +0 -1
  50. package/dist/lib/pkce.js +0 -101
  51. package/dist/lib/pkce.js.map +0 -1
  52. package/dist/lib/util.d.ts +0 -27
  53. package/dist/lib/util.d.ts.map +0 -1
  54. package/dist/lib/util.js +0 -46
  55. package/dist/lib/util.js.map +0 -1
  56. package/dist/package.json +0 -46
  57. package/test/init.integration.test.cjs +0 -382
  58. 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
- return path.join(packageRootDirFromCompiled(), "sql");
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
- // Apply non-optional steps in a single transaction.
515
- await params.client.query("begin;");
516
- try {
517
- for (const step of params.plan.steps.filter((s) => !s.optional)) {
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(step.sql, step.params as any);
520
- applied.push(step.name);
521
- } catch (e) {
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
- await params.client.query("commit;");
555
- } catch (e) {
556
- // Rollback errors should never mask the original failure.
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 params.client.query("rollback;");
559
- } catch {
560
- // ignore
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 outside of the transaction so a failure doesn't abort everything.
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
- // Run each optional step in its own mini-transaction to avoid partial application.
569
- await params.client.query("begin;");
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
- const viewExistsRes = await params.client.query("select to_regclass('public.pg_statistic') is not null as ok");
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 public.pg_statistic exists");
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, 'public.pg_statistic', 'SELECT') as ok",
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 public.pg_statistic");
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
- return new Promise((resolve, reject) => {
89
- const req = https.request(
90
- url,
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
- return new Promise((resolve, reject) => {
172
- const req = https.request(
173
- url,
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
- return new Promise((resolve, reject) => {
257
- const req = https.request(
258
- url,
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
- return new Promise((resolve, reject) => {
360
- const req = https.request(
361
- url,
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 * as pkg from "../package.json";
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 * as path from "path";
9
- // Types schemas will be loaded dynamically from the SDK's CJS bundle
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
-