@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/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
- if (process.argv.includes("--init")) {
240
- // Guard: must be run from project root
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(process.cwd(), "package.json"));
259
+ await stat(path.join(cwd, ".vscode"));
260
+ ideDetection.vscode = true;
243
261
  }
244
- catch {
245
- console.error("Error: No package.json found in current directory.\n" +
246
- "--init must be run from your project root (the directory containing package.json).");
247
- process.exit(1);
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
- const cwd = process.cwd();
251
- // ---- Phase 1: IDE Detection ----
252
- const ideDetection = {
253
- claude: true,
254
- vscode: false,
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, ".vscode"));
260
- ideDetection.vscode = true;
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(path.join(cwd, ".cursor"));
265
- ideDetection.cursor = true;
305
+ await stat(fullPath);
306
+ fileExists = true;
266
307
  }
267
308
  catch { }
268
- if (!ideDetection.cursor && process.env.CURSOR_TRACE_DIR)
269
- ideDetection.cursor = true;
270
- try {
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
- catch { }
275
- if (!ideDetection.windsurf) {
313
+ if (fileExists) {
314
+ // Read and parse existing file
315
+ const raw = await readFile(fullPath, "utf-8");
316
+ let parsed;
276
317
  try {
277
- await stat(path.join(cwd, ".windsurfrules"));
278
- ideDetection.windsurf = true;
318
+ parsed = JSON.parse(raw);
279
319
  }
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;
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
- if (fileExists) {
314
- // Read and parse existing file
315
- const raw = await readFile(fullPath, "utf-8");
316
- let parsed;
317
- try {
318
- parsed = JSON.parse(raw);
319
- }
320
- catch {
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
- // 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" });
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
- console.log("\nMCP config files:");
355
- for (const entry of configActions) {
356
- console.log(` ${entry.path}: ${entry.action}`);
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
- // ---- 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"));
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
- 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);
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
- catch { /* file doesn't exist */ }
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
- // 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 custom pipeline directories ----
397
- const pipelinesDir = path.resolve(cwd, process.env.BAPI_PIPELINES_DIR ?? ".bridge/pipelines");
398
- const instrDir = path.join(path.dirname(pipelinesDir), "instructions");
399
- await mkdir(pipelinesDir, { recursive: true });
400
- await mkdir(instrDir, { recursive: true });
401
- const readmePath = path.join(pipelinesDir, "README.md");
402
- const examplePath = path.join(pipelinesDir, "example-pipeline.json");
403
- const readmeContent = `# Custom Pipelines
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
- const exampleContent = JSON.stringify({
466
- name: "Example Pipeline",
467
- description: "A sample pipeline demonstrating all step types and features.",
468
- variables: ["ticket_key"],
469
- steps: [
470
- {
471
- type: "mcp_call",
472
- tool: "get_ticket",
473
- params: { ticket_number: "{ticket_key}" },
474
- description: "Fetch the ticket details from Jira",
475
- on_error: "halt",
476
- },
477
- {
478
- type: "agent_task",
479
- instruction: "Read the ticket details above and summarize the key requirements in 3 bullet points.",
480
- description: "Summarize ticket requirements",
481
- on_error: "halt",
482
- },
483
- {
484
- type: "agent_task",
485
- instruction: "Search the codebase for files related to the ticket and list the top 5 most relevant files.",
486
- description: "Find relevant source files",
487
- on_error: "warn_and_continue",
488
- },
489
- {
490
- type: "mcp_call",
491
- tool: "add_comment",
492
- params: {
493
- ticket_number: "{ticket_key}",
494
- comment_text: "Pipeline analysis complete. See agent output for details.",
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
- }, null, 2) + "\n";
502
- try {
503
- await stat(readmePath);
504
- console.log(` ${path.relative(cwd, readmePath)} (skipped — already exists)`);
505
- }
506
- catch {
507
- await writeFile(readmePath, readmeContent, "utf-8");
508
- console.log(` ${path.relative(cwd, readmePath)} (written)`);
509
- }
510
- try {
511
- await stat(examplePath);
512
- console.log(` ${path.relative(cwd, examplePath)} (skipped — already exists)`);
513
- }
514
- catch {
515
- await writeFile(examplePath, exampleContent, "utf-8");
516
- console.log(` ${path.relative(cwd, examplePath)} (written)`);
517
- }
518
- console.log(` ${path.relative(cwd, instrDir)}/ (ensured)`);
519
- // ---- Phase 6: Final summary ----
520
- if (anyCreatedOrAdded) {
521
- console.log("\nUpdate BAPI_API_KEY and BAPI_REPO_NAME in your config files — " +
522
- "get these values from the Bridge API setup UI at https://bridgegpt-api.com");
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, and code review standards for the configured repository. " +
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("Target status name to transition to (case-insensitive match)"),
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(() => { });