@bridge_gpt/mcp-server 0.1.8 → 0.1.10
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 +156 -67
- package/build/agent-utils.js +110 -0
- package/build/agents.generated.js +13 -0
- package/build/commands.generated.js +9 -8
- package/build/decision-page-template.js +628 -0
- package/build/index.js +548 -201
- package/build/pipeline-utils.js +1 -1
- package/build/pipelines.generated.js +106 -5
- package/build/update-check.js +80 -0
- package/build/version.generated.js +2 -0
- package/package.json +6 -3
- package/pipelines/implement-ticket.json +10 -0
- package/pipelines/learn-repository.json +22 -0
- package/pipelines/plan-epic.json +44 -0
- package/pipelines/review-ticket.json +11 -1
package/build/index.js
CHANGED
|
@@ -14,9 +14,16 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
import { writeFile, mkdir, readFile, stat } from "fs/promises";
|
|
16
16
|
import path from "path";
|
|
17
|
+
import { execSync } from "child_process";
|
|
18
|
+
import { createRequire } from "module";
|
|
17
19
|
import { PIPELINES as BUNDLED_PIPELINES, INSTRUCTIONS as BUNDLED_INSTRUCTIONS } from "./pipelines.generated.js";
|
|
18
20
|
import { COMMANDS } from "./commands.generated.js";
|
|
21
|
+
import { AGENTS } from "./agents.generated.js";
|
|
22
|
+
import { VERSION } from "./version.generated.js";
|
|
23
|
+
import { checkForUpdate } from "./update-check.js";
|
|
24
|
+
import { reconstructAgentMarkdown, translateAgentToCopilot } from "./agent-utils.js";
|
|
19
25
|
import { resolveRecipe, loadCustomPipelines } from "./pipeline-utils.js";
|
|
26
|
+
import { generateDecisionPageHtml } from "./decision-page-template.js";
|
|
20
27
|
// Mutable pipeline/instruction state — starts with bundled, merged with user at startup
|
|
21
28
|
const PIPELINES = { ...BUNDLED_PIPELINES };
|
|
22
29
|
const INSTRUCTIONS = { ...BUNDLED_INSTRUCTIONS };
|
|
@@ -236,171 +243,220 @@ async function ensureGitignored(cwd, filePath) {
|
|
|
236
243
|
const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
237
244
|
await writeFile(gitignorePath, content + separator + entry + "\n", "utf-8");
|
|
238
245
|
}
|
|
239
|
-
|
|
240
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Core initialization logic shared by --init and --upgrade.
|
|
248
|
+
* Runs all scaffolding phases without calling process.exit().
|
|
249
|
+
*/
|
|
250
|
+
async function runInit(cwd) {
|
|
251
|
+
// ---- Phase 1: IDE Detection ----
|
|
252
|
+
const ideDetection = {
|
|
253
|
+
claude: true,
|
|
254
|
+
vscode: false,
|
|
255
|
+
cursor: false,
|
|
256
|
+
windsurf: false,
|
|
257
|
+
};
|
|
241
258
|
try {
|
|
242
|
-
await stat(path.join(
|
|
259
|
+
await stat(path.join(cwd, ".vscode"));
|
|
260
|
+
ideDetection.vscode = true;
|
|
243
261
|
}
|
|
244
|
-
catch {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
262
|
+
catch { }
|
|
263
|
+
try {
|
|
264
|
+
await stat(path.join(cwd, ".cursor"));
|
|
265
|
+
ideDetection.cursor = true;
|
|
248
266
|
}
|
|
267
|
+
catch { }
|
|
268
|
+
if (!ideDetection.cursor && process.env.CURSOR_TRACE_DIR)
|
|
269
|
+
ideDetection.cursor = true;
|
|
249
270
|
try {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
cursor: false,
|
|
256
|
-
windsurf: false,
|
|
257
|
-
};
|
|
271
|
+
await stat(path.join(cwd, ".windsurf"));
|
|
272
|
+
ideDetection.windsurf = true;
|
|
273
|
+
}
|
|
274
|
+
catch { }
|
|
275
|
+
if (!ideDetection.windsurf) {
|
|
258
276
|
try {
|
|
259
|
-
await stat(path.join(cwd, ".
|
|
260
|
-
ideDetection.
|
|
277
|
+
await stat(path.join(cwd, ".windsurfrules"));
|
|
278
|
+
ideDetection.windsurf = true;
|
|
261
279
|
}
|
|
262
280
|
catch { }
|
|
281
|
+
}
|
|
282
|
+
const detectedIDEs = Object.entries(ideDetection)
|
|
283
|
+
.filter(([, v]) => v)
|
|
284
|
+
.map(([k]) => k);
|
|
285
|
+
console.log(`Bridge API --init: detected IDEs: ${detectedIDEs.join(", ")}`);
|
|
286
|
+
// ---- Phase 2: Windsurf manual instructions ----
|
|
287
|
+
if (ideDetection.windsurf) {
|
|
288
|
+
const windsurfSnippet = JSON.stringify({ mcpServers: { "bridge-api": buildBridgeApiEntry(cwd) } }, null, 2);
|
|
289
|
+
console.log("\n⚠ Windsurf does not support project-local MCP configuration.\n" +
|
|
290
|
+
"Add the following to ~/.codeium/windsurf/mcp_config.json:\n\n" +
|
|
291
|
+
windsurfSnippet + "\n");
|
|
292
|
+
}
|
|
293
|
+
// ---- Phase 3: Config file handling ----
|
|
294
|
+
const configTargets = [
|
|
295
|
+
{ path: ".mcp.json", topLevelKey: "mcpServers", shouldCreate: true },
|
|
296
|
+
{ path: ".vscode/mcp.json", topLevelKey: "servers", shouldCreate: ideDetection.vscode },
|
|
297
|
+
{ path: ".cursor/mcp.json", topLevelKey: "mcpServers", shouldCreate: ideDetection.cursor },
|
|
298
|
+
];
|
|
299
|
+
const configActions = [];
|
|
300
|
+
let anyCreatedOrAdded = false;
|
|
301
|
+
for (const target of configTargets) {
|
|
302
|
+
const fullPath = path.join(cwd, target.path);
|
|
303
|
+
let fileExists = false;
|
|
263
304
|
try {
|
|
264
|
-
await stat(
|
|
265
|
-
|
|
305
|
+
await stat(fullPath);
|
|
306
|
+
fileExists = true;
|
|
266
307
|
}
|
|
267
308
|
catch { }
|
|
268
|
-
if (!
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
await stat(path.join(cwd, ".windsurf"));
|
|
272
|
-
ideDetection.windsurf = true;
|
|
309
|
+
if (!target.shouldCreate && !fileExists) {
|
|
310
|
+
configActions.push({ path: target.path, action: "skipped — IDE not detected" });
|
|
311
|
+
continue;
|
|
273
312
|
}
|
|
274
|
-
|
|
275
|
-
|
|
313
|
+
if (fileExists) {
|
|
314
|
+
// Read and parse existing file
|
|
315
|
+
const raw = await readFile(fullPath, "utf-8");
|
|
316
|
+
let parsed;
|
|
276
317
|
try {
|
|
277
|
-
|
|
278
|
-
ideDetection.windsurf = true;
|
|
318
|
+
parsed = JSON.parse(raw);
|
|
279
319
|
}
|
|
280
|
-
catch {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
.filter(([, v]) => v)
|
|
284
|
-
.map(([k]) => k);
|
|
285
|
-
console.log(`Bridge API --init: detected IDEs: ${detectedIDEs.join(", ")}`);
|
|
286
|
-
// ---- Phase 2: Windsurf manual instructions ----
|
|
287
|
-
if (ideDetection.windsurf) {
|
|
288
|
-
const windsurfSnippet = JSON.stringify({ mcpServers: { "bridge-api": buildBridgeApiEntry(cwd) } }, null, 2);
|
|
289
|
-
console.log("\n⚠ Windsurf does not support project-local MCP configuration.\n" +
|
|
290
|
-
"Add the following to ~/.codeium/windsurf/mcp_config.json:\n\n" +
|
|
291
|
-
windsurfSnippet + "\n");
|
|
292
|
-
}
|
|
293
|
-
// ---- Phase 3: Config file handling ----
|
|
294
|
-
const configTargets = [
|
|
295
|
-
{ path: ".mcp.json", topLevelKey: "mcpServers", shouldCreate: true },
|
|
296
|
-
{ path: ".vscode/mcp.json", topLevelKey: "servers", shouldCreate: ideDetection.vscode },
|
|
297
|
-
{ path: ".cursor/mcp.json", topLevelKey: "mcpServers", shouldCreate: ideDetection.cursor },
|
|
298
|
-
];
|
|
299
|
-
const configActions = [];
|
|
300
|
-
let anyCreatedOrAdded = false;
|
|
301
|
-
for (const target of configTargets) {
|
|
302
|
-
const fullPath = path.join(cwd, target.path);
|
|
303
|
-
let fileExists = false;
|
|
304
|
-
try {
|
|
305
|
-
await stat(fullPath);
|
|
306
|
-
fileExists = true;
|
|
307
|
-
}
|
|
308
|
-
catch { }
|
|
309
|
-
if (!target.shouldCreate && !fileExists) {
|
|
310
|
-
configActions.push({ path: target.path, action: "skipped — IDE not detected" });
|
|
320
|
+
catch {
|
|
321
|
+
console.warn(` ${target.path} skipped — invalid JSON format`);
|
|
322
|
+
configActions.push({ path: target.path, action: "skipped — invalid JSON" });
|
|
311
323
|
continue;
|
|
312
324
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.warn(` ${target.path} skipped — invalid JSON format`);
|
|
322
|
-
configActions.push({ path: target.path, action: "skipped — invalid JSON" });
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
const topLevel = parsed[target.topLevelKey];
|
|
326
|
-
if (topLevel && topLevel["bridge-api"]) {
|
|
327
|
-
// Entry exists — update BAPI_PROJECT_ROOT only
|
|
328
|
-
if (!topLevel["bridge-api"].env)
|
|
329
|
-
topLevel["bridge-api"].env = {};
|
|
330
|
-
topLevel["bridge-api"].env.BAPI_PROJECT_ROOT = cwd;
|
|
331
|
-
await writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
332
|
-
configActions.push({ path: target.path, action: "updated BAPI_PROJECT_ROOT" });
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
// Entry missing — add it, preserving existing content
|
|
336
|
-
if (!parsed[target.topLevelKey])
|
|
337
|
-
parsed[target.topLevelKey] = {};
|
|
338
|
-
parsed[target.topLevelKey]["bridge-api"] = buildBridgeApiEntry(cwd);
|
|
339
|
-
await writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
340
|
-
configActions.push({ path: target.path, action: "added entry" });
|
|
341
|
-
anyCreatedOrAdded = true;
|
|
342
|
-
}
|
|
325
|
+
const topLevel = parsed[target.topLevelKey];
|
|
326
|
+
if (topLevel && topLevel["bridge-api"]) {
|
|
327
|
+
// Entry exists — update BAPI_PROJECT_ROOT only
|
|
328
|
+
if (!topLevel["bridge-api"].env)
|
|
329
|
+
topLevel["bridge-api"].env = {};
|
|
330
|
+
topLevel["bridge-api"].env.BAPI_PROJECT_ROOT = cwd;
|
|
331
|
+
await writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
332
|
+
configActions.push({ path: target.path, action: "updated BAPI_PROJECT_ROOT" });
|
|
343
333
|
}
|
|
344
334
|
else {
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
await
|
|
350
|
-
configActions.push({ path: target.path, action: "
|
|
335
|
+
// Entry missing — add it, preserving existing content
|
|
336
|
+
if (!parsed[target.topLevelKey])
|
|
337
|
+
parsed[target.topLevelKey] = {};
|
|
338
|
+
parsed[target.topLevelKey]["bridge-api"] = buildBridgeApiEntry(cwd);
|
|
339
|
+
await writeFile(fullPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
340
|
+
configActions.push({ path: target.path, action: "added entry" });
|
|
351
341
|
anyCreatedOrAdded = true;
|
|
352
342
|
}
|
|
353
343
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
344
|
+
else {
|
|
345
|
+
// Create new file
|
|
346
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
347
|
+
const content = { [target.topLevelKey]: { "bridge-api": buildBridgeApiEntry(cwd) } };
|
|
348
|
+
await writeFile(fullPath, JSON.stringify(content, null, 2) + "\n", "utf-8");
|
|
349
|
+
await ensureGitignored(cwd, target.path);
|
|
350
|
+
configActions.push({ path: target.path, action: "created" });
|
|
351
|
+
anyCreatedOrAdded = true;
|
|
357
352
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
353
|
+
}
|
|
354
|
+
console.log("\nMCP config files:");
|
|
355
|
+
for (const entry of configActions) {
|
|
356
|
+
console.log(` ${entry.path}: ${entry.action}`);
|
|
357
|
+
}
|
|
358
|
+
// ---- Phase 4: Scaffold slash command directories ----
|
|
359
|
+
const commandDirs = [path.join(cwd, ".claude", "commands")];
|
|
360
|
+
if (ideDetection.cursor) {
|
|
361
|
+
commandDirs.push(path.join(cwd, ".cursor", "commands"));
|
|
362
|
+
}
|
|
363
|
+
const writtenFiles = new Set();
|
|
364
|
+
const skippedFiles = new Set();
|
|
365
|
+
const overwrittenFiles = new Set();
|
|
366
|
+
for (const dir of commandDirs) {
|
|
367
|
+
await mkdir(dir, { recursive: true });
|
|
368
|
+
for (const [filename, content] of Object.entries(COMMANDS)) {
|
|
369
|
+
const target = path.join(dir, filename);
|
|
370
|
+
try {
|
|
371
|
+
const existing = await readFile(target, "utf-8");
|
|
372
|
+
if (existing === content) {
|
|
373
|
+
skippedFiles.add(filename);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
overwrittenFiles.add(filename);
|
|
377
|
+
}
|
|
378
|
+
catch { /* file doesn't exist */ }
|
|
379
|
+
await writeFile(target, content, "utf-8");
|
|
380
|
+
writtenFiles.add(filename);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// A file written in any directory takes priority over skipped
|
|
384
|
+
for (const f of writtenFiles)
|
|
385
|
+
skippedFiles.delete(f);
|
|
386
|
+
const total = Object.keys(COMMANDS).length;
|
|
387
|
+
const dirNames = commandDirs.map((d) => path.relative(cwd, d)).join(" and ");
|
|
388
|
+
console.log(`\nSlash commands: scaffolded ${total} commands into ${commandDirs.length} director${commandDirs.length === 1 ? "y" : "ies"}`);
|
|
389
|
+
if (writtenFiles.size > 0)
|
|
390
|
+
console.log(` Written: ${writtenFiles.size}`);
|
|
391
|
+
if (overwrittenFiles.size > 0)
|
|
392
|
+
console.log(` Overwritten (content changed): ${overwrittenFiles.size}`);
|
|
393
|
+
if (skippedFiles.size > 0)
|
|
394
|
+
console.log(` Skipped (unchanged): ${skippedFiles.size}`);
|
|
395
|
+
console.log(` ${dirNames}`);
|
|
396
|
+
// ---- Phase 5: Scaffold agent directories ----
|
|
397
|
+
const agentWritten = new Set();
|
|
398
|
+
const agentSkipped = new Set();
|
|
399
|
+
const agentOverwritten = new Set();
|
|
400
|
+
// Always scaffold to .claude/agents/
|
|
401
|
+
const claudeAgentsDir = path.join(cwd, ".claude", "agents");
|
|
402
|
+
await mkdir(claudeAgentsDir, { recursive: true });
|
|
403
|
+
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
404
|
+
const filename = `${key}.md`;
|
|
405
|
+
const content = reconstructAgentMarkdown(agent.frontmatter, agent.body);
|
|
406
|
+
const target = path.join(claudeAgentsDir, filename);
|
|
407
|
+
try {
|
|
408
|
+
const existing = await readFile(target, "utf-8");
|
|
409
|
+
if (existing === content) {
|
|
410
|
+
agentSkipped.add(filename);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
agentOverwritten.add(filename);
|
|
362
414
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
415
|
+
catch { /* file doesn't exist */ }
|
|
416
|
+
await writeFile(target, content, "utf-8");
|
|
417
|
+
agentWritten.add(filename);
|
|
418
|
+
}
|
|
419
|
+
// Scaffold to .github/agents/ for VS Code / Copilot
|
|
420
|
+
if (ideDetection.vscode) {
|
|
421
|
+
const copilotAgentsDir = path.join(cwd, ".github", "agents");
|
|
422
|
+
await mkdir(copilotAgentsDir, { recursive: true });
|
|
423
|
+
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
424
|
+
const filename = `${key}.agent.md`;
|
|
425
|
+
const content = translateAgentToCopilot(agent.frontmatter, agent.body);
|
|
426
|
+
const target = path.join(copilotAgentsDir, filename);
|
|
427
|
+
try {
|
|
428
|
+
const existing = await readFile(target, "utf-8");
|
|
429
|
+
if (existing === content) {
|
|
430
|
+
agentSkipped.add(filename);
|
|
431
|
+
continue;
|
|
377
432
|
}
|
|
378
|
-
|
|
379
|
-
await writeFile(target, content, "utf-8");
|
|
380
|
-
writtenFiles.add(filename);
|
|
433
|
+
agentOverwritten.add(filename);
|
|
381
434
|
}
|
|
435
|
+
catch { /* file doesn't exist */ }
|
|
436
|
+
await writeFile(target, content, "utf-8");
|
|
437
|
+
agentWritten.add(filename);
|
|
382
438
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
console.log(` ${
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
439
|
+
}
|
|
440
|
+
// TODO: Add Cursor agent scaffolding when Cursor publishes a formal agent spec
|
|
441
|
+
// A file written in any directory takes priority over skipped
|
|
442
|
+
for (const f of agentWritten)
|
|
443
|
+
agentSkipped.delete(f);
|
|
444
|
+
const agentTotal = Object.keys(AGENTS).length;
|
|
445
|
+
console.log(`\nAgents: scaffolded ${agentTotal} agent${agentTotal === 1 ? "" : "s"}`);
|
|
446
|
+
if (agentWritten.size > 0)
|
|
447
|
+
console.log(` Written: ${agentWritten.size}`);
|
|
448
|
+
if (agentOverwritten.size > 0)
|
|
449
|
+
console.log(` Overwritten (content changed): ${agentOverwritten.size}`);
|
|
450
|
+
if (agentSkipped.size > 0)
|
|
451
|
+
console.log(` Skipped (unchanged): ${agentSkipped.size}`);
|
|
452
|
+
// ---- Phase 6: Scaffold custom pipeline directories ----
|
|
453
|
+
const pipelinesDir = path.resolve(cwd, process.env.BAPI_PIPELINES_DIR ?? ".bridge/pipelines");
|
|
454
|
+
const instrDir = path.join(path.dirname(pipelinesDir), "instructions");
|
|
455
|
+
await mkdir(pipelinesDir, { recursive: true });
|
|
456
|
+
await mkdir(instrDir, { recursive: true });
|
|
457
|
+
const readmePath = path.join(pipelinesDir, "README.md");
|
|
458
|
+
const examplePath = path.join(pipelinesDir, "example-pipeline.json");
|
|
459
|
+
const readmeContent = `# Custom Pipelines
|
|
404
460
|
|
|
405
461
|
Place custom pipeline JSON files in this directory. They will be loaded at
|
|
406
462
|
server startup and available alongside the bundled pipelines.
|
|
@@ -462,65 +518,80 @@ automatically provided by the server.
|
|
|
462
518
|
- \`"halt"\` (default) — stop the pipeline immediately on failure
|
|
463
519
|
- \`"warn_and_continue"\` — log a warning and proceed to the next step
|
|
464
520
|
`;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
},
|
|
496
|
-
description: "Post a summary comment to the ticket",
|
|
497
|
-
on_error: "warn_and_continue",
|
|
498
|
-
requires_approval: true,
|
|
521
|
+
const exampleContent = JSON.stringify({
|
|
522
|
+
name: "Example Pipeline",
|
|
523
|
+
description: "A sample pipeline demonstrating all step types and features.",
|
|
524
|
+
variables: ["ticket_key"],
|
|
525
|
+
steps: [
|
|
526
|
+
{
|
|
527
|
+
type: "mcp_call",
|
|
528
|
+
tool: "get_ticket",
|
|
529
|
+
params: { ticket_number: "{ticket_key}" },
|
|
530
|
+
description: "Fetch the ticket details from Jira",
|
|
531
|
+
on_error: "halt",
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
type: "agent_task",
|
|
535
|
+
instruction: "Read the ticket details above and summarize the key requirements in 3 bullet points.",
|
|
536
|
+
description: "Summarize ticket requirements",
|
|
537
|
+
on_error: "halt",
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
type: "agent_task",
|
|
541
|
+
instruction: "Search the codebase for files related to the ticket and list the top 5 most relevant files.",
|
|
542
|
+
description: "Find relevant source files",
|
|
543
|
+
on_error: "warn_and_continue",
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
type: "mcp_call",
|
|
547
|
+
tool: "add_comment",
|
|
548
|
+
params: {
|
|
549
|
+
ticket_number: "{ticket_key}",
|
|
550
|
+
comment_text: "Pipeline analysis complete. See agent output for details.",
|
|
499
551
|
},
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
552
|
+
description: "Post a summary comment to the ticket",
|
|
553
|
+
on_error: "warn_and_continue",
|
|
554
|
+
requires_approval: true,
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
}, null, 2) + "\n";
|
|
558
|
+
try {
|
|
559
|
+
await stat(readmePath);
|
|
560
|
+
console.log(` ${path.relative(cwd, readmePath)} (skipped — already exists)`);
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
await writeFile(readmePath, readmeContent, "utf-8");
|
|
564
|
+
console.log(` ${path.relative(cwd, readmePath)} (written)`);
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
await stat(examplePath);
|
|
568
|
+
console.log(` ${path.relative(cwd, examplePath)} (skipped — already exists)`);
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
await writeFile(examplePath, exampleContent, "utf-8");
|
|
572
|
+
console.log(` ${path.relative(cwd, examplePath)} (written)`);
|
|
573
|
+
}
|
|
574
|
+
console.log(` ${path.relative(cwd, instrDir)}/ (ensured)`);
|
|
575
|
+
// ---- Phase 7: Final summary ----
|
|
576
|
+
if (anyCreatedOrAdded) {
|
|
577
|
+
console.log("\nUpdate BAPI_API_KEY and BAPI_REPO_NAME in your config files — " +
|
|
578
|
+
"get these values from the Bridge API setup UI at https://bridgegpt-api.com");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// CLI: --init
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
if (process.argv.includes("--init")) {
|
|
585
|
+
try {
|
|
586
|
+
await stat(path.join(process.cwd(), "package.json"));
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
console.error("Error: No package.json found in current directory.\n" +
|
|
590
|
+
"--init must be run from your project root (the directory containing package.json).");
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
await runInit(process.cwd());
|
|
524
595
|
process.exit(0);
|
|
525
596
|
}
|
|
526
597
|
catch (err) {
|
|
@@ -530,6 +601,43 @@ automatically provided by the server.
|
|
|
530
601
|
}
|
|
531
602
|
}
|
|
532
603
|
// ---------------------------------------------------------------------------
|
|
604
|
+
// CLI: --upgrade
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
if (process.argv.includes("--upgrade")) {
|
|
607
|
+
try {
|
|
608
|
+
await stat(path.join(process.cwd(), "package.json"));
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
console.error("Error: No package.json found in current directory.\n" +
|
|
612
|
+
"--upgrade must be run from your project root (the directory containing package.json).");
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const cwd = process.cwd();
|
|
617
|
+
const oldVersion = VERSION;
|
|
618
|
+
console.log("Upgrading @bridge_gpt/mcp-server to latest...\n");
|
|
619
|
+
execSync("npm i @bridge_gpt/mcp-server@latest", { stdio: "inherit" });
|
|
620
|
+
// Read the newly installed version from node_modules
|
|
621
|
+
const require = createRequire(cwd + "/");
|
|
622
|
+
const newPkgPath = require.resolve("@bridge_gpt/mcp-server/package.json");
|
|
623
|
+
const newPkg = JSON.parse(await readFile(newPkgPath, "utf-8"));
|
|
624
|
+
const newVersion = newPkg.version;
|
|
625
|
+
console.log(`\n@bridge_gpt/mcp-server: ${oldVersion} -> ${newVersion}`);
|
|
626
|
+
if (oldVersion === newVersion) {
|
|
627
|
+
console.log("Already up-to-date.");
|
|
628
|
+
}
|
|
629
|
+
console.log("\nRefreshing scaffolded artifacts...\n");
|
|
630
|
+
await runInit(cwd);
|
|
631
|
+
console.log("\nUpgrade complete.");
|
|
632
|
+
process.exit(0);
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
636
|
+
console.error(`Bridge API --upgrade failed: ${msg}`);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
533
641
|
// Server
|
|
534
642
|
// ---------------------------------------------------------------------------
|
|
535
643
|
const server = new McpServer({
|
|
@@ -553,8 +661,8 @@ server.registerTool("ping", {
|
|
|
553
661
|
return { content: [{ type: "text", text }] };
|
|
554
662
|
});
|
|
555
663
|
server.registerTool("get_project_standards", {
|
|
556
|
-
description: "Retrieve project-specific coding standards, architecture guidelines, testing standards,
|
|
557
|
-
"Returns structured markdown with sections for architecture instructions, code review correctness standards, testing stack information, and build analysis. " +
|
|
664
|
+
description: "Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. " +
|
|
665
|
+
"Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. " +
|
|
558
666
|
"Only sections with configured values are included. Returns 404 if no standards are configured. " +
|
|
559
667
|
"Consult these standards before writing or reviewing code to ensure compliance with project conventions.",
|
|
560
668
|
inputSchema: {},
|
|
@@ -963,6 +1071,119 @@ server.registerTool("upload_attachment", {
|
|
|
963
1071
|
const text = await handleResponse(resp);
|
|
964
1072
|
return { content: [{ type: "text", text: text + resolved.note }] };
|
|
965
1073
|
});
|
|
1074
|
+
server.registerTool("list_attachments", {
|
|
1075
|
+
description: "List attachments on a Jira ticket. " +
|
|
1076
|
+
"Returns metadata (ID, filename, MIME type, size, created date) for each attachment. " +
|
|
1077
|
+
"By default, AI-generated attachments are excluded. " +
|
|
1078
|
+
"Use this to discover available attachments before downloading.",
|
|
1079
|
+
inputSchema: {
|
|
1080
|
+
ticket_number: z
|
|
1081
|
+
.string()
|
|
1082
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123)"),
|
|
1083
|
+
include_ai_generated: z
|
|
1084
|
+
.boolean()
|
|
1085
|
+
.optional()
|
|
1086
|
+
.describe("Include AI-generated attachments in the list (default: false)"),
|
|
1087
|
+
},
|
|
1088
|
+
}, async ({ ticket_number, include_ai_generated }) => {
|
|
1089
|
+
const params = { repo_name: REPO_NAME };
|
|
1090
|
+
if (include_ai_generated) {
|
|
1091
|
+
params.include_ai_generated = "true";
|
|
1092
|
+
}
|
|
1093
|
+
const url = buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments`, params);
|
|
1094
|
+
const resp = await fetch(url, { headers: GET_HEADERS });
|
|
1095
|
+
const text = await handleResponse(resp);
|
|
1096
|
+
return { content: [{ type: "text", text }] };
|
|
1097
|
+
});
|
|
1098
|
+
const MAX_INLINE_TEXT_LENGTH = 50_000;
|
|
1099
|
+
server.registerTool("download_attachment", {
|
|
1100
|
+
description: "Download an attachment from a Jira ticket and save it to disk. " +
|
|
1101
|
+
"Specify either attachment_id or filename (not both). " +
|
|
1102
|
+
"For text files, the content is returned inline (truncated at ~50KB) and also saved to disk. " +
|
|
1103
|
+
"For binary files, the file is saved to disk and the path is returned. " +
|
|
1104
|
+
"Default save location: {BAPI_DOCS_DIR}/attachments/{ticket_number}/{filename}.",
|
|
1105
|
+
inputSchema: {
|
|
1106
|
+
ticket_number: z
|
|
1107
|
+
.string()
|
|
1108
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123)"),
|
|
1109
|
+
attachment_id: z
|
|
1110
|
+
.string()
|
|
1111
|
+
.optional()
|
|
1112
|
+
.describe("Jira attachment ID. Mutually exclusive with filename."),
|
|
1113
|
+
filename: z
|
|
1114
|
+
.string()
|
|
1115
|
+
.optional()
|
|
1116
|
+
.describe("Attachment filename. If multiple exist, returns the most recent. Mutually exclusive with attachment_id."),
|
|
1117
|
+
file_path: z
|
|
1118
|
+
.string()
|
|
1119
|
+
.optional()
|
|
1120
|
+
.describe("Override the default save location. If omitted, saves to {BAPI_DOCS_DIR}/attachments/{ticket_number}/{filename}."),
|
|
1121
|
+
},
|
|
1122
|
+
}, async ({ ticket_number, attachment_id, filename, file_path }) => {
|
|
1123
|
+
if (!attachment_id && !filename) {
|
|
1124
|
+
return {
|
|
1125
|
+
content: [{
|
|
1126
|
+
type: "text",
|
|
1127
|
+
text: JSON.stringify({
|
|
1128
|
+
error: "VALIDATION_ERROR",
|
|
1129
|
+
message: "Provide either attachment_id or filename (at least one is required).",
|
|
1130
|
+
}),
|
|
1131
|
+
}],
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
const params = { repo_name: REPO_NAME };
|
|
1135
|
+
if (attachment_id)
|
|
1136
|
+
params.attachment_id = attachment_id;
|
|
1137
|
+
if (filename)
|
|
1138
|
+
params.filename = filename;
|
|
1139
|
+
const url = buildGetUrl(`/ticket/${encodeURIComponent(ticket_number)}/attachments/download`, params);
|
|
1140
|
+
const resp = await fetch(url, { headers: GET_HEADERS });
|
|
1141
|
+
if (!resp.ok) {
|
|
1142
|
+
const text = await handleResponse(resp);
|
|
1143
|
+
return { content: [{ type: "text", text }] };
|
|
1144
|
+
}
|
|
1145
|
+
const body = await resp.json();
|
|
1146
|
+
const serverFilename = body.filename;
|
|
1147
|
+
const content = body.content;
|
|
1148
|
+
const isText = body.is_text;
|
|
1149
|
+
const mimeType = body.mime_type;
|
|
1150
|
+
const size = body.size;
|
|
1151
|
+
const safeFileName = path.basename(serverFilename);
|
|
1152
|
+
const safeTicket = path.basename(ticket_number);
|
|
1153
|
+
const savePath = file_path
|
|
1154
|
+
? file_path
|
|
1155
|
+
: path.join(BAPI_DOCS_DIR, "attachments", safeTicket, safeFileName);
|
|
1156
|
+
const resolvedSave = path.resolve(savePath);
|
|
1157
|
+
const resolvedRoot = path.resolve(PROJECT_ROOT);
|
|
1158
|
+
if (!resolvedSave.startsWith(resolvedRoot + path.sep) && resolvedSave !== resolvedRoot) {
|
|
1159
|
+
return {
|
|
1160
|
+
content: [{
|
|
1161
|
+
type: "text",
|
|
1162
|
+
text: JSON.stringify({
|
|
1163
|
+
error: "VALIDATION_ERROR",
|
|
1164
|
+
message: `Save path "${savePath}" is outside the project root. Refusing to write.`,
|
|
1165
|
+
}),
|
|
1166
|
+
}],
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
await mkdir(path.dirname(resolvedSave), { recursive: true });
|
|
1170
|
+
if (isText) {
|
|
1171
|
+
await writeFile(resolvedSave, content, "utf-8");
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
await writeFile(resolvedSave, Buffer.from(content, "base64"));
|
|
1175
|
+
}
|
|
1176
|
+
let resultText = `File saved to: ${resolvedSave}\nFilename: ${safeFileName}\nMIME type: ${mimeType}\nSize: ${size} bytes`;
|
|
1177
|
+
if (isText) {
|
|
1178
|
+
if (content.length > MAX_INLINE_TEXT_LENGTH) {
|
|
1179
|
+
resultText += `\n\n${content.slice(0, MAX_INLINE_TEXT_LENGTH)}\n\n[Content truncated. Full content saved to ${resolvedSave}]`;
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
resultText += `\n\n${content}`;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return { content: [{ type: "text", text: resultText }] };
|
|
1186
|
+
});
|
|
966
1187
|
server.registerTool("request_plan_generation", {
|
|
967
1188
|
description: "Request AI-generated implementation plan for a Jira ticket. " +
|
|
968
1189
|
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
@@ -1457,11 +1678,13 @@ server.registerTool("update_jira_status", {
|
|
|
1457
1678
|
description: "Transition a Jira ticket to a specified target status by executing a workflow transition. " +
|
|
1458
1679
|
"Provide either target_status (matched case-insensitively against available transitions) or transition_id (used directly). " +
|
|
1459
1680
|
"If transition_id is provided, it takes precedence over target_status. " +
|
|
1681
|
+
'Pass target_status as "auto" to trigger server-side status resolution via LLM — the server determines the correct ' +
|
|
1682
|
+
"post-PR status automatically. If auto-resolve finds no match, returns status: skipped (not an error). " +
|
|
1460
1683
|
"Returns the from/to status on success, or an error listing available transitions if no match is found. " +
|
|
1461
1684
|
"The repo_name is automatically injected from the configured environment.",
|
|
1462
1685
|
inputSchema: {
|
|
1463
1686
|
ticket_number: z.string().describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123)"),
|
|
1464
|
-
target_status: z.string().optional().describe(
|
|
1687
|
+
target_status: z.string().optional().describe('Target status name to transition to (case-insensitive match). Pass "auto" to resolve the target status server-side via LLM agent.'),
|
|
1465
1688
|
transition_id: z.string().optional().describe("Specific transition ID to execute (takes precedence over target_status)"),
|
|
1466
1689
|
},
|
|
1467
1690
|
}, async ({ ticket_number, target_status, transition_id }) => {
|
|
@@ -1511,6 +1734,7 @@ const VALID_CONFIG_FIELDS = [
|
|
|
1511
1734
|
"unit_testing_instructions", "e2e_testing_instructions",
|
|
1512
1735
|
"frontend_correctness_standards", "backend_correctness_standards",
|
|
1513
1736
|
"template_correctness_standards", "style_correctness_standards",
|
|
1737
|
+
"design_principles",
|
|
1514
1738
|
"post_pr_target_status", "ci_check_config",
|
|
1515
1739
|
].join(", ");
|
|
1516
1740
|
server.registerTool("list_config_fields", {
|
|
@@ -1955,6 +2179,117 @@ server.registerTool("get_pipeline_recipe", {
|
|
|
1955
2179
|
}
|
|
1956
2180
|
});
|
|
1957
2181
|
// ---------------------------------------------------------------------------
|
|
2182
|
+
// generate_decision_page
|
|
2183
|
+
// ---------------------------------------------------------------------------
|
|
2184
|
+
server.registerTool("generate_decision_page", {
|
|
2185
|
+
description: "Generate a local HTML decision page for capturing user decisions on ticket review findings. " +
|
|
2186
|
+
"Renders recommendation-driven review decisions with custom options from resolution guide " +
|
|
2187
|
+
"decision trees, plus confirmed improvements. The user opens the HTML file " +
|
|
2188
|
+
"in a browser, makes selections, and copies the resulting JSON output back to the agent.",
|
|
2189
|
+
inputSchema: {
|
|
2190
|
+
ticket_key: z.string().describe("Jira ticket key, e.g. BAPI-123"),
|
|
2191
|
+
actionable_items: z
|
|
2192
|
+
.array(z.object({
|
|
2193
|
+
id: z.string().min(1),
|
|
2194
|
+
question: z.string().min(1),
|
|
2195
|
+
context: z.string().min(1),
|
|
2196
|
+
source: z.string().min(1).describe("Source reference from the evaluation, e.g. 'Clarifying Q3 (initial round)'"),
|
|
2197
|
+
recommendation_index: z.number().int().min(0).describe("0-based index of the recommended option in the options array"),
|
|
2198
|
+
options: z.array(z.string().min(1)).min(1).describe("Option labels from resolution guide decision tree branches. Values are auto-generated."),
|
|
2199
|
+
}))
|
|
2200
|
+
.optional()
|
|
2201
|
+
.default([])
|
|
2202
|
+
.describe("Actionable review decisions with option labels from resolution guide decision trees. 'None of these' auto-appended."),
|
|
2203
|
+
clear_improvements: z
|
|
2204
|
+
.array(z.object({
|
|
2205
|
+
id: z.string().min(1),
|
|
2206
|
+
title: z.string().min(1),
|
|
2207
|
+
action: z.string().min(1),
|
|
2208
|
+
confidence: z.string().min(1),
|
|
2209
|
+
source: z.string().min(1).describe("Source reference from the evaluation"),
|
|
2210
|
+
}))
|
|
2211
|
+
.optional()
|
|
2212
|
+
.default([])
|
|
2213
|
+
.describe("Confirmed improvements displayed as informational list, not submitted."),
|
|
2214
|
+
},
|
|
2215
|
+
}, async (input) => {
|
|
2216
|
+
const validationError = (message) => ({
|
|
2217
|
+
content: [{
|
|
2218
|
+
type: "text",
|
|
2219
|
+
text: JSON.stringify({ error: "VALIDATION_ERROR", status: 400, message }),
|
|
2220
|
+
}],
|
|
2221
|
+
});
|
|
2222
|
+
// Validate ticket_key format (reject instead of silently sanitizing)
|
|
2223
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(input.ticket_key)) {
|
|
2224
|
+
return validationError(`Invalid ticket_key "${input.ticket_key}": must start with a letter and contain only letters, digits, hyphens, or underscores.`);
|
|
2225
|
+
}
|
|
2226
|
+
// No-decisions fast path: return structured response without writing a file
|
|
2227
|
+
if (input.actionable_items.length === 0) {
|
|
2228
|
+
return {
|
|
2229
|
+
content: [{
|
|
2230
|
+
type: "text",
|
|
2231
|
+
text: JSON.stringify({
|
|
2232
|
+
status: "no_decisions_needed",
|
|
2233
|
+
ticket_key: input.ticket_key,
|
|
2234
|
+
clear_improvements_count: input.clear_improvements.length,
|
|
2235
|
+
}),
|
|
2236
|
+
}],
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
// Validate actionable_items
|
|
2240
|
+
const seenIds = new Set();
|
|
2241
|
+
for (const item of input.actionable_items) {
|
|
2242
|
+
if (seenIds.has(item.id)) {
|
|
2243
|
+
return validationError(`Duplicate actionable_items id: "${item.id}"`);
|
|
2244
|
+
}
|
|
2245
|
+
seenIds.add(item.id);
|
|
2246
|
+
if (item.recommendation_index >= item.options.length) {
|
|
2247
|
+
return validationError(`Item "${item.id}": recommendation_index ${item.recommendation_index} is out of bounds (${item.options.length} options).`);
|
|
2248
|
+
}
|
|
2249
|
+
const noneLabel = item.options.find((label) => label.toLowerCase() === "none of these");
|
|
2250
|
+
if (noneLabel) {
|
|
2251
|
+
return validationError(`Item "${item.id}": option label "${noneLabel}" is reserved and auto-appended by the tool.`);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
// Read design assets and base64-encode for embedding
|
|
2255
|
+
const assetsDir = path.join(PROJECT_ROOT, "design-assets");
|
|
2256
|
+
const fontsDir = path.join(PROJECT_ROOT, "public", "fonts");
|
|
2257
|
+
let faviconBase64 = "";
|
|
2258
|
+
let logoBase64 = "";
|
|
2259
|
+
try {
|
|
2260
|
+
const faviconBuf = await readFile(path.join(assetsDir, "favicon", "favicon-32x32.png"));
|
|
2261
|
+
faviconBase64 = faviconBuf.toString("base64");
|
|
2262
|
+
}
|
|
2263
|
+
catch { /* favicon optional */ }
|
|
2264
|
+
try {
|
|
2265
|
+
const logoBuf = await readFile(path.join(assetsDir, "just-logo-rough-draft.png"));
|
|
2266
|
+
logoBase64 = logoBuf.toString("base64");
|
|
2267
|
+
}
|
|
2268
|
+
catch { /* logo optional */ }
|
|
2269
|
+
// Compute relative path from output dir to fonts dir
|
|
2270
|
+
const docsPath = getDocsPath("review");
|
|
2271
|
+
const fontsRelPath = path.relative(docsPath, fontsDir);
|
|
2272
|
+
const html = generateDecisionPageHtml(input, {
|
|
2273
|
+
faviconBase64,
|
|
2274
|
+
logoBase64,
|
|
2275
|
+
fontsRelPath,
|
|
2276
|
+
});
|
|
2277
|
+
const filePath = path.join(docsPath, `${input.ticket_key}-decisions.html`);
|
|
2278
|
+
await mkdir(docsPath, { recursive: true });
|
|
2279
|
+
await writeFile(filePath, html, "utf-8");
|
|
2280
|
+
return {
|
|
2281
|
+
content: [{
|
|
2282
|
+
type: "text",
|
|
2283
|
+
text: JSON.stringify({
|
|
2284
|
+
status: "decision_page_generated",
|
|
2285
|
+
file_path: filePath,
|
|
2286
|
+
actionable_items_count: input.actionable_items.length,
|
|
2287
|
+
clear_improvements_count: input.clear_improvements.length,
|
|
2288
|
+
}),
|
|
2289
|
+
}],
|
|
2290
|
+
};
|
|
2291
|
+
});
|
|
2292
|
+
// ---------------------------------------------------------------------------
|
|
1958
2293
|
// Entry point
|
|
1959
2294
|
// ---------------------------------------------------------------------------
|
|
1960
2295
|
// Load custom user pipelines before accepting connections
|
|
@@ -1971,3 +2306,15 @@ userPipelineKeys = customResult.userPipelineKeys;
|
|
|
1971
2306
|
const transport = new StdioServerTransport();
|
|
1972
2307
|
await server.connect(transport);
|
|
1973
2308
|
console.error("Bridge API MCP server running on stdio");
|
|
2309
|
+
// Fire-and-forget update check — delay to let MCP client attach listeners
|
|
2310
|
+
(async () => {
|
|
2311
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
2312
|
+
const result = await checkForUpdate();
|
|
2313
|
+
if (result?.updateAvailable) {
|
|
2314
|
+
server.server.sendLoggingMessage({
|
|
2315
|
+
level: "notice",
|
|
2316
|
+
logger: "bridge-api",
|
|
2317
|
+
data: `Update available: @bridge_gpt/mcp-server ${result.currentVersion} -> ${result.latestVersion}. Run: npx -y @bridge_gpt/mcp-server --upgrade`,
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
})().catch(() => { });
|