@bridge_gpt/mcp-server 0.2.0 → 0.2.1
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 +56 -54
- package/build/agent-launchers/claude.js +25 -17
- package/build/agent-launchers/cursor.js +65 -0
- package/build/agent-launchers/index.js +23 -8
- package/build/agent-registry.js +68 -0
- package/build/command-catalog.js +376 -0
- package/build/commands.generated.js +8 -5
- package/build/index.js +316 -116
- package/build/mcp-provisioning.js +94 -1
- package/build/pipeline-utils.js +0 -33
- package/build/pipelines.generated.js +2 -31
- package/build/schedule-run.js +436 -88
- package/build/schedule-store.js +41 -1
- package/build/scheduled-prompt.js +109 -0
- package/build/scheduler-backends/at-fallback.js +5 -10
- package/build/scheduler-backends/escaping.js +40 -10
- package/build/scheduler-backends/launchd.js +23 -14
- package/build/scheduler-backends/systemd-user.js +32 -19
- package/build/scheduler-backends/task-scheduler.js +8 -13
- package/build/start-tickets.js +459 -30
- package/build/version.generated.js +1 -1
- package/package.json +3 -2
- package/pipelines/implement-ticket.json +2 -28
- package/smoke-test/SMOKE-TEST.md +61 -18
package/build/index.js
CHANGED
|
@@ -87,11 +87,18 @@ async function getResolvedApiKey() {
|
|
|
87
87
|
}
|
|
88
88
|
/** GET auth headers, with the API key resolved once via {@link getResolvedApiKey}. */
|
|
89
89
|
async function getGetHeaders() {
|
|
90
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
"X-API-Key": await getResolvedApiKey(),
|
|
92
|
+
"X-Bridge-MCP-Version": VERSION,
|
|
93
|
+
};
|
|
91
94
|
}
|
|
92
95
|
/** POST auth headers (API key + JSON content type). */
|
|
93
96
|
async function getPostHeaders() {
|
|
94
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
"X-API-Key": await getResolvedApiKey(),
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
"X-Bridge-MCP-Version": VERSION,
|
|
101
|
+
};
|
|
95
102
|
}
|
|
96
103
|
/** Set true immediately after the server connection (transport) completes. */
|
|
97
104
|
let serverConnected = false;
|
|
@@ -313,8 +320,8 @@ async function saveLocally(dir, filename, content) {
|
|
|
313
320
|
// Heavy-read truncate-and-save helpers (BAPI-342)
|
|
314
321
|
// ---------------------------------------------------------------------------
|
|
315
322
|
//
|
|
316
|
-
//
|
|
317
|
-
// list_attachments) can return very large
|
|
323
|
+
// Five heavy read tools (get_project_standards, get_tickets, get_ticket,
|
|
324
|
+
// get_comments, list_attachments) can return very large payloads. To avoid flooding the
|
|
318
325
|
// agent's context, when a successful payload exceeds MAX_INLINE_TEXT_LENGTH the
|
|
319
326
|
// FULL payload is saved to disk first and only then is a truncated,
|
|
320
327
|
// markdown-fenced inline preview returned. Nothing is lost: if the save fails
|
|
@@ -502,6 +509,18 @@ const TICKET_ARTIFACTS = {
|
|
|
502
509
|
`Use get_architecture with ticket_number "${n}" to retrieve the architecture plan once processing completes.`,
|
|
503
510
|
pollLabel: (n) => `Architecture generation for ${n}`,
|
|
504
511
|
},
|
|
512
|
+
fsd: {
|
|
513
|
+
kind: "single",
|
|
514
|
+
generateEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/generate-fsd`,
|
|
515
|
+
getEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/fsd`,
|
|
516
|
+
saveSubdir: "fsd",
|
|
517
|
+
filename: (n) => `${n}-fsd-plan.md`,
|
|
518
|
+
requestErrorPrefix: "Failed to request FSD generation: ",
|
|
519
|
+
confirmationText: (n) => `FSD generation requested for ${n}. ` +
|
|
520
|
+
`Processing typically takes 2-4 minutes. ` +
|
|
521
|
+
`Use get_design_doc with ticket_number "${n}" and doc_type "fsd" to retrieve the functional specification document once processing completes.`,
|
|
522
|
+
pollLabel: (n) => `FSD generation for ${n}`,
|
|
523
|
+
},
|
|
505
524
|
clarifying_questions: {
|
|
506
525
|
kind: "single",
|
|
507
526
|
generateEndpoint: (n) => `/ticket/${encodeURIComponent(n)}/generate-clarifying-questions`,
|
|
@@ -576,6 +595,8 @@ async function getTicketArtifactDocsPath(subdir) {
|
|
|
576
595
|
return getDocsPath("plans");
|
|
577
596
|
case "architecture":
|
|
578
597
|
return getDocsPath("architecture");
|
|
598
|
+
case "fsd":
|
|
599
|
+
return getDocsPath("fsd");
|
|
579
600
|
case "clarifying-questions":
|
|
580
601
|
return getDocsPath("clarifying-questions");
|
|
581
602
|
case "ticket-critiques":
|
|
@@ -584,6 +605,14 @@ async function getTicketArtifactDocsPath(subdir) {
|
|
|
584
605
|
return getDocsPath("reimplementations");
|
|
585
606
|
}
|
|
586
607
|
}
|
|
608
|
+
// Map the public unified design-doc `doc_type` to the internal artifact key.
|
|
609
|
+
// `tdd` reuses the existing "architecture" artifact (the TDD/architecture
|
|
610
|
+
// document) WITHOUT renaming it, preserving the back-compatible
|
|
611
|
+
// request_architecture / get_architecture tools. `fsd` routes to the new "fsd"
|
|
612
|
+
// artifact.
|
|
613
|
+
function resolveDesignDocArtifactType(docType) {
|
|
614
|
+
return docType === "fsd" ? "fsd" : "architecture";
|
|
615
|
+
}
|
|
587
616
|
// Shared request flow for the five single-artifact request_* tools: POST the
|
|
588
617
|
// generate endpoint, return the per-tool error prefix on a non-OK POST, and on
|
|
589
618
|
// wait_for_result poll the get endpoint (900_000 ms) and optionally save.
|
|
@@ -1366,15 +1395,6 @@ const registerTool = ((name, config, handler) => {
|
|
|
1366
1395
|
}
|
|
1367
1396
|
return toolHandle;
|
|
1368
1397
|
});
|
|
1369
|
-
// ---------------------------------------------------------------------------
|
|
1370
|
-
// Tools
|
|
1371
|
-
// ---------------------------------------------------------------------------
|
|
1372
|
-
// SECURITY: The `annotations` objects below (readOnlyHint, destructiveHint,
|
|
1373
|
-
// idempotentHint, openWorldHint) are UNTRUSTED UX / model-routing hints only.
|
|
1374
|
-
// They are advisory metadata forwarded verbatim to the MCP client and MUST
|
|
1375
|
-
// NEVER be used for authorization, permission checks, access control, tool
|
|
1376
|
-
// enable/disable decisions, or any other security decision. Authorization is
|
|
1377
|
-
// enforced server-side via the API key + repo access checks, never here.
|
|
1378
1398
|
registerTool("ping", {
|
|
1379
1399
|
annotations: {
|
|
1380
1400
|
readOnlyHint: true,
|
|
@@ -1384,6 +1404,9 @@ registerTool("ping", {
|
|
|
1384
1404
|
},
|
|
1385
1405
|
description: "Test connectivity to Bridge API. Validates that the API key is accepted and the configured repository is accessible. " +
|
|
1386
1406
|
"Returns JSON with {status: 'ok', repo_name: '<configured repo>'}. " +
|
|
1407
|
+
"The response also reports MCP version metadata: mcp_version (your installed MCP server version), " +
|
|
1408
|
+
"latest_mcp_version (latest published), upgrade_available, upgrade_advice (a short, optional note when a newer version is available), " +
|
|
1409
|
+
"and release_state_stale. These are informational — an available upgrade is not an error. " +
|
|
1387
1410
|
"Use this as a quick health check before other operations, or to verify your Bridge API configuration is working. " +
|
|
1388
1411
|
"A 403 response means the API key is invalid or the repo is not authorized. " +
|
|
1389
1412
|
"If the server is unreachable, check that BAPI_BASE_URL points to a running Bridge API instance.",
|
|
@@ -1391,8 +1414,28 @@ registerTool("ping", {
|
|
|
1391
1414
|
}, async () => {
|
|
1392
1415
|
const url = buildGetUrl("/ping", { repo_name: REPO_NAME });
|
|
1393
1416
|
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
1394
|
-
|
|
1395
|
-
|
|
1417
|
+
// Preserve existing behavior for non-OK responses (structured error relay).
|
|
1418
|
+
if (!resp.ok) {
|
|
1419
|
+
const text = await handleResponse(resp);
|
|
1420
|
+
return { content: [{ type: "text", text }] };
|
|
1421
|
+
}
|
|
1422
|
+
// OK: keep the first content item independently JSON.parse()-able, and emit
|
|
1423
|
+
// any upgrade advice as a SEPARATE text item so prose never pollutes the JSON.
|
|
1424
|
+
const raw = await resp.text();
|
|
1425
|
+
try {
|
|
1426
|
+
const body = JSON.parse(raw);
|
|
1427
|
+
const content = [
|
|
1428
|
+
{ type: "text", text: JSON.stringify(body, null, 2) },
|
|
1429
|
+
];
|
|
1430
|
+
if (typeof body.upgrade_advice === "string" && body.upgrade_advice.length > 0) {
|
|
1431
|
+
content.push({ type: "text", text: body.upgrade_advice });
|
|
1432
|
+
}
|
|
1433
|
+
return { content };
|
|
1434
|
+
}
|
|
1435
|
+
catch {
|
|
1436
|
+
// Unexpected non-JSON 200 — fall back to the raw text in one content item.
|
|
1437
|
+
return { content: [{ type: "text", text: raw }] };
|
|
1438
|
+
}
|
|
1396
1439
|
});
|
|
1397
1440
|
registerTool("second_opinion", {
|
|
1398
1441
|
annotations: {
|
|
@@ -1448,6 +1491,7 @@ registerTool("generate_image", {
|
|
|
1448
1491
|
"This tool spends provider credits on every call — cost scales with quality (low/medium/high). " +
|
|
1449
1492
|
"Defaults to low quality to minimize provider spend; increase quality only when fidelity matters. " +
|
|
1450
1493
|
"Returns native MCP image content (type: 'image') so the caller receives the image directly. " +
|
|
1494
|
+
"The image is always also saved to the local BAPI_DOCS_DIR/images/ directory. " +
|
|
1451
1495
|
"Google Imagen outputs (provider='gemini') include an invisible SynthID watermark applied server-side by Google.",
|
|
1452
1496
|
inputSchema: {
|
|
1453
1497
|
prompt: z
|
|
@@ -1470,13 +1514,8 @@ registerTool("generate_image", {
|
|
|
1470
1514
|
.optional()
|
|
1471
1515
|
.default("1024x1024")
|
|
1472
1516
|
.describe("Image dimensions. Defaults to '1024x1024'."),
|
|
1473
|
-
save_locally: z
|
|
1474
|
-
.boolean()
|
|
1475
|
-
.optional()
|
|
1476
|
-
.default(false)
|
|
1477
|
-
.describe("When true, save the generated image to the local docs/images directory."),
|
|
1478
1517
|
},
|
|
1479
|
-
}, async ({ prompt, provider, quality, size
|
|
1518
|
+
}, async ({ prompt, provider, quality, size }) => {
|
|
1480
1519
|
const resp = await fetch(buildApiUrl("/llm/generate-image"), {
|
|
1481
1520
|
method: "POST",
|
|
1482
1521
|
headers: await getPostHeaders(),
|
|
@@ -1510,21 +1549,21 @@ registerTool("generate_image", {
|
|
|
1510
1549
|
const content = [
|
|
1511
1550
|
{ type: "image", data: imageBase64, mimeType },
|
|
1512
1551
|
];
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1552
|
+
// Always persist to BAPI_DOCS_DIR/images/. A write failure must not discard
|
|
1553
|
+
// the already-paid-for generation, so warn and still return the inline image.
|
|
1554
|
+
try {
|
|
1555
|
+
const imagesDir = await getDocsPath("images");
|
|
1556
|
+
const filename = `generated-image-${safeTimestampForFilename()}.png`;
|
|
1557
|
+
const filePath = `${imagesDir}/${filename}`;
|
|
1558
|
+
await mkdir(imagesDir, { recursive: true });
|
|
1559
|
+
await writeFile(filePath, Buffer.from(imageBase64, "base64"));
|
|
1560
|
+
content.push({ type: "text", text: `Saved to ${filePath}` });
|
|
1561
|
+
}
|
|
1562
|
+
catch (saveErr) {
|
|
1563
|
+
content.push({
|
|
1564
|
+
type: "text",
|
|
1565
|
+
text: `Warning: image generated successfully but local save failed: ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`,
|
|
1566
|
+
});
|
|
1528
1567
|
}
|
|
1529
1568
|
return { content };
|
|
1530
1569
|
});
|
|
@@ -1538,12 +1577,19 @@ registerTool("get_project_standards", {
|
|
|
1538
1577
|
description: "Retrieve project-specific coding standards, architecture guidelines, testing standards, code review standards, and project context (platform, version, project description) for the configured repository. " +
|
|
1539
1578
|
"Returns structured markdown with sections for project context, architecture instructions, code review correctness standards, testing stack information, and build analysis. " +
|
|
1540
1579
|
"Only sections with configured values are included. Returns 404 if no standards are configured. " +
|
|
1541
|
-
"Consult these standards before writing or reviewing code to ensure compliance with project conventions."
|
|
1580
|
+
"Consult these standards before writing or reviewing code to ensure compliance with project conventions. " +
|
|
1581
|
+
"Successful oversized output is automatically saved under the local docs directory and returned inline as a truncated preview.",
|
|
1542
1582
|
inputSchema: {},
|
|
1543
1583
|
}, async () => {
|
|
1544
1584
|
const url = buildGetUrl("/project-standards", { repo_name: REPO_NAME });
|
|
1545
1585
|
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
1546
|
-
const
|
|
1586
|
+
const ok = resp.ok;
|
|
1587
|
+
let text = await handleResponse(resp);
|
|
1588
|
+
if (ok) {
|
|
1589
|
+
const safeRepo = safeTicketFileSegment(REPO_NAME || "repo");
|
|
1590
|
+
const filename = `${safeRepo}-project-standards.md`;
|
|
1591
|
+
text = await truncateAndSaveIfNeeded(text, await getDocsPath("project-standards"), filename);
|
|
1592
|
+
}
|
|
1547
1593
|
return { content: [{ type: "text", text }] };
|
|
1548
1594
|
});
|
|
1549
1595
|
registerTool("get_tickets", {
|
|
@@ -1647,6 +1693,29 @@ registerTool("get_ticket", {
|
|
|
1647
1693
|
}
|
|
1648
1694
|
return { content: [{ type: "text", text }] };
|
|
1649
1695
|
});
|
|
1696
|
+
registerTool("get_ticket_model_tier", {
|
|
1697
|
+
annotations: {
|
|
1698
|
+
readOnlyHint: true,
|
|
1699
|
+
destructiveHint: false,
|
|
1700
|
+
idempotentHint: true,
|
|
1701
|
+
openWorldHint: true,
|
|
1702
|
+
},
|
|
1703
|
+
description: "Resolve the coarse implementation-model TIER for a Jira ticket from its difficulty. " +
|
|
1704
|
+
"Returns { difficulty: int|null, tier: \"cheap\"|\"basic\"|\"premium\"|null, source: \"cached\"|\"computed\"|\"fallback\" }. " +
|
|
1705
|
+
"The backend computes difficulty on demand (and caches it) when missing, and never returns a model id — " +
|
|
1706
|
+
"the tier->model mapping is owned by the start-tickets CLI. A null tier (source=\"fallback\") means the " +
|
|
1707
|
+
"model could not be resolved and callers should use the agent's default model.",
|
|
1708
|
+
inputSchema: {
|
|
1709
|
+
ticket_number: z
|
|
1710
|
+
.string()
|
|
1711
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123)"),
|
|
1712
|
+
},
|
|
1713
|
+
}, async ({ ticket_number }) => {
|
|
1714
|
+
const url = buildGetUrl(`/tickets/${encodeURIComponent(ticket_number)}/model-tier`, { repo_name: REPO_NAME });
|
|
1715
|
+
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
1716
|
+
const text = await handleResponse(resp);
|
|
1717
|
+
return { content: [{ type: "text", text }] };
|
|
1718
|
+
});
|
|
1650
1719
|
registerTool("get_comments", {
|
|
1651
1720
|
annotations: {
|
|
1652
1721
|
readOnlyHint: true,
|
|
@@ -2000,7 +2069,7 @@ registerTool("upload_attachment", {
|
|
|
2000
2069
|
"Optionally syncs the content to Bridge API's tickets_links table so retrieval endpoints " +
|
|
2001
2070
|
"(get_clarifying_questions, get_ticket_critique, get_plan) return the updated content without re-generation. " +
|
|
2002
2071
|
"Use link_type to specify which retrieval endpoint should serve this content. " +
|
|
2003
|
-
"Known link_type values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md.",
|
|
2072
|
+
"Known link_type values: clarifying-questions.md, debugging-guidance.md, ticket-quality-critique.md, architecture-plan.md, fsd-plan.md.",
|
|
2004
2073
|
inputSchema: {
|
|
2005
2074
|
ticket_number: z
|
|
2006
2075
|
.string()
|
|
@@ -2283,6 +2352,87 @@ registerTool("request_architecture", {
|
|
|
2283
2352
|
}, async (args) => {
|
|
2284
2353
|
return requestTicketArtifact("architecture", args);
|
|
2285
2354
|
});
|
|
2355
|
+
registerTool("request_design_doc", {
|
|
2356
|
+
annotations: {
|
|
2357
|
+
readOnlyHint: false,
|
|
2358
|
+
destructiveHint: false,
|
|
2359
|
+
idempotentHint: false,
|
|
2360
|
+
openWorldHint: true,
|
|
2361
|
+
},
|
|
2362
|
+
description: "START (or refresh) async generation of a design document for a Jira ticket, routed by doc_type. " +
|
|
2363
|
+
"Use doc_type 'tdd' for a Technical Design Document (architecture-focused, for engineers) or 'fsd' for a " +
|
|
2364
|
+
"Functional Specification Document (product/functional-focused, for PMs, designers, and QA). " +
|
|
2365
|
+
"This triggers an asynchronous background job — results are NOT immediate. " +
|
|
2366
|
+
"Processing typically takes 2-4 minutes depending on ticket complexity. " +
|
|
2367
|
+
"The matching get_design_doc tool retrieves the generated document later (call get_design_doc with the same ticket_number and doc_type) — unless you set wait_for_result, in which case this call blocks and returns it directly. " +
|
|
2368
|
+
"Returns 202 if the request was accepted, 404 if the ticket does not exist in Jira, or 403 if the API key is unauthorized.",
|
|
2369
|
+
inputSchema: {
|
|
2370
|
+
ticket_number: z
|
|
2371
|
+
.string()
|
|
2372
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123) to generate a design document for"),
|
|
2373
|
+
doc_type: z
|
|
2374
|
+
.enum(["tdd", "fsd"])
|
|
2375
|
+
.describe("Which design document to generate: 'tdd' (Technical Design Document, engineer audience) or " +
|
|
2376
|
+
"'fsd' (Functional Specification Document, product/functional audience)."),
|
|
2377
|
+
wait_for_result: z
|
|
2378
|
+
.boolean()
|
|
2379
|
+
.optional()
|
|
2380
|
+
.default(false)
|
|
2381
|
+
.describe("When true, the tool blocks and polls until the document is ready (typically 2-4 minutes), " +
|
|
2382
|
+
"then returns the full content directly. When false (default), returns immediately " +
|
|
2383
|
+
"with a confirmation message — use get_design_doc later to retrieve results."),
|
|
2384
|
+
save_locally: z
|
|
2385
|
+
.boolean()
|
|
2386
|
+
.optional()
|
|
2387
|
+
.default(true)
|
|
2388
|
+
.describe("When wait_for_result is true, whether to save the generated document to a local file under " +
|
|
2389
|
+
"BAPI_DOCS_DIR. Defaults to true. Only takes effect when wait_for_result is true."),
|
|
2390
|
+
second_opinion: z
|
|
2391
|
+
.string()
|
|
2392
|
+
.optional()
|
|
2393
|
+
.describe("Provider routing override for THIS artifact-generation request " +
|
|
2394
|
+
"(e.g. 'anthropic', 'openai', 'gemini'). When set, the artifact is " +
|
|
2395
|
+
"generated by the named provider and, where supported, a cross-provider " +
|
|
2396
|
+
"second-opinion pass is applied to this request only. " +
|
|
2397
|
+
"Takes precedence over `provider` when both are set."),
|
|
2398
|
+
provider: z.string().optional().describe("Pure provider switch — use a specific LLM provider (openai, anthropic, gemini) without " +
|
|
2399
|
+
"triggering second-opinion semantics. If both provider and second_opinion are set, " +
|
|
2400
|
+
"second_opinion takes precedence."),
|
|
2401
|
+
},
|
|
2402
|
+
}, async (args) => {
|
|
2403
|
+
return requestTicketArtifact(resolveDesignDocArtifactType(args.doc_type), args);
|
|
2404
|
+
});
|
|
2405
|
+
registerTool("get_design_doc", {
|
|
2406
|
+
annotations: {
|
|
2407
|
+
readOnlyHint: true,
|
|
2408
|
+
destructiveHint: false,
|
|
2409
|
+
idempotentHint: true,
|
|
2410
|
+
openWorldHint: true,
|
|
2411
|
+
},
|
|
2412
|
+
description: "RETRIEVE an already-generated design document for a Jira ticket, routed by doc_type. " +
|
|
2413
|
+
"Use doc_type 'tdd' for the Technical Design Document or 'fsd' for the Functional Specification Document. " +
|
|
2414
|
+
"This tool only fetches an existing document — it does NOT start or trigger generation. " +
|
|
2415
|
+
"If no document exists yet (or you need a fresh one), call `request_design_doc` first with the same doc_type. " +
|
|
2416
|
+
"Returns the full document as markdown text — present it verbatim without summarizing. " +
|
|
2417
|
+
"Returns a 404 / not-found response when no document is ready yet — that means generation has not run, not that this tool failed.",
|
|
2418
|
+
inputSchema: {
|
|
2419
|
+
ticket_number: z
|
|
2420
|
+
.string()
|
|
2421
|
+
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123, BAPI-42)"),
|
|
2422
|
+
doc_type: z
|
|
2423
|
+
.enum(["tdd", "fsd"])
|
|
2424
|
+
.describe("Which design document to retrieve: 'tdd' (Technical Design Document) or " +
|
|
2425
|
+
"'fsd' (Functional Specification Document)."),
|
|
2426
|
+
save_locally: z
|
|
2427
|
+
.boolean()
|
|
2428
|
+
.optional()
|
|
2429
|
+
.default(true)
|
|
2430
|
+
.describe("Whether to save the retrieved document to a local file under BAPI_DOCS_DIR. " +
|
|
2431
|
+
"Defaults to true. Set to false to skip saving."),
|
|
2432
|
+
},
|
|
2433
|
+
}, async (args) => {
|
|
2434
|
+
return getTicketArtifact(resolveDesignDocArtifactType(args.doc_type), args);
|
|
2435
|
+
});
|
|
2286
2436
|
registerTool("request_clarifying_questions", {
|
|
2287
2437
|
annotations: {
|
|
2288
2438
|
readOnlyHint: false,
|
|
@@ -2705,6 +2855,7 @@ registerTool("resolve_target_status", {
|
|
|
2705
2855
|
// ---------------------------------------------------------------------------
|
|
2706
2856
|
const VALID_CONFIG_FIELDS = [
|
|
2707
2857
|
"review_instructions", "documentation_instructions", "architecture_instructions",
|
|
2858
|
+
"tdd_document_instructions", "fsd_document_instructions",
|
|
2708
2859
|
"unit_testing_instructions", "e2e_testing_instructions",
|
|
2709
2860
|
"unit_testing_stack", "e2e_testing_stack",
|
|
2710
2861
|
"frontend_correctness_standards", "backend_correctness_standards",
|
|
@@ -2714,7 +2865,16 @@ const VALID_CONFIG_FIELDS = [
|
|
|
2714
2865
|
"allow_mutating_smoke_ops",
|
|
2715
2866
|
"selected_mcp_slugs",
|
|
2716
2867
|
"base_branch",
|
|
2717
|
-
"
|
|
2868
|
+
"difficulty_model_routing_enabled",
|
|
2869
|
+
"difficulty_model_tier_overrides",
|
|
2870
|
+
// BAPI-356 easy-install bootstrap fields (also exposed via the registry).
|
|
2871
|
+
"working_in",
|
|
2872
|
+
"version_control_system",
|
|
2873
|
+
"version",
|
|
2874
|
+
"project_description",
|
|
2875
|
+
"custom_directories",
|
|
2876
|
+
"exclude_directories",
|
|
2877
|
+
"exclude_file_extensions",
|
|
2718
2878
|
].join(", ");
|
|
2719
2879
|
registerTool("list_config_fields", {
|
|
2720
2880
|
annotations: {
|
|
@@ -2761,7 +2921,8 @@ registerTool("get_config_field", {
|
|
|
2761
2921
|
},
|
|
2762
2922
|
description: "Read the current value and metadata for a specific configuration field. " +
|
|
2763
2923
|
"Returns the field's current database value (or null if not set), along with a description of its purpose and examples of helpful content. " +
|
|
2764
|
-
"Use this before update_config_field to understand the current state and build upon it rather than overwriting blindly."
|
|
2924
|
+
"Use this before update_config_field to understand the current state and build upon it rather than overwriting blindly. " +
|
|
2925
|
+
"For install bootstrap, prefer get_install_manifest (one read of all bootstrap fields) over many individual get_config_field reads.",
|
|
2765
2926
|
inputSchema: {
|
|
2766
2927
|
field_name: z.string().describe(`The configuration field to read. Valid options: ${VALID_CONFIG_FIELDS}`),
|
|
2767
2928
|
},
|
|
@@ -2781,19 +2942,30 @@ registerTool("update_config_field", {
|
|
|
2781
2942
|
description: "Update a specific configuration field in the Bridge database. " +
|
|
2782
2943
|
"These fields control LLM behavior during code review, planning, and documentation. " +
|
|
2783
2944
|
"Always call get_config_field first to read the current value and build upon it. " +
|
|
2945
|
+
"For install bootstrap, prefer apply_install_manifest (one atomic write of all bootstrap fields, " +
|
|
2946
|
+
"with server-owned skip/conflict/confirmation semantics) over many individual update_config_field writes. " +
|
|
2784
2947
|
"Returns 400 if the field name is invalid, 404 if the repository has no configuration row yet.",
|
|
2785
2948
|
inputSchema: {
|
|
2786
2949
|
field_name: z.string().describe(`The configuration field to update. Valid options: ${VALID_CONFIG_FIELDS}`),
|
|
2787
|
-
value: z.union([
|
|
2788
|
-
|
|
2950
|
+
value: z.union([
|
|
2951
|
+
z.string(),
|
|
2952
|
+
z.boolean(),
|
|
2953
|
+
z.array(z.string()),
|
|
2954
|
+
z.record(z.string(), z.union([z.string(), z.null()])),
|
|
2955
|
+
]).optional().describe("The new value for the configuration field. Provide either value or file_path, not both. " +
|
|
2956
|
+
"Most fields take a string; scalar boolean fields (e.g. allow_mutating_smoke_ops, " +
|
|
2957
|
+
"difficulty_model_routing_enabled) take true/false. " +
|
|
2789
2958
|
"The selected_mcp_slugs field takes a JSON array of supported MCP validation manual slug strings " +
|
|
2790
2959
|
"(e.g. [\"b2c-commerce-developer\", \"playwright-mcp\", \"pwa-kit-mcp\"]) — pass an array of strings, " +
|
|
2791
2960
|
"not a comma-delimited string; an empty array clears the selection. " +
|
|
2961
|
+
"The difficulty_model_tier_overrides field takes a JSON object mapping tier names " +
|
|
2962
|
+
"(\"cheap\"/\"basic\"/\"premium\") to per-repo model aliases (e.g. {\"premium\": \"opus\"}) — pass " +
|
|
2963
|
+
"an object, not a string; an empty object clears all overrides. " +
|
|
2964
|
+
"The difficulty_model_routing_enabled field enables difficulty-based /start-tickets model routing " +
|
|
2965
|
+
"(default ON); pass true/false. " +
|
|
2792
2966
|
"The base_branch field is a string/null field controlling the development base branch used by PR " +
|
|
2793
2967
|
"creation (/create-pr) and start-tickets worktree creation; an empty/null value clears it and " +
|
|
2794
2968
|
"automations fall back to 'main'. " +
|
|
2795
|
-
"The tiered_execution field is a string enum with valid values 'off', 'claude_code_only', and " +
|
|
2796
|
-
"'all_capable'; omitting/clearing the value resolves to the backend default 'claude_code_only'. " +
|
|
2797
2969
|
"For string fields, omit both value and file_path to set the field to NULL (clearing it). " +
|
|
2798
2970
|
"Scalar boolean fields are NOT NULL and have no clear/null state: omitting the value writes false " +
|
|
2799
2971
|
"(matching the API-layer coercion), so pass true/false explicitly."),
|
|
@@ -2801,8 +2973,17 @@ registerTool("update_config_field", {
|
|
|
2801
2973
|
"Useful for large configuration values like detailed review instructions. " +
|
|
2802
2974
|
"The file must be UTF-8 encoded and under 1MB. " +
|
|
2803
2975
|
"Not supported for scalar boolean fields like allow_mutating_smoke_ops."),
|
|
2976
|
+
only_if_null: z.boolean().optional().describe("Secondary conditional-write guard: when true, the field is updated only if its column is " +
|
|
2977
|
+
"currently NULL (returns status 'skipped'/reason 'already_set' otherwise). Legal only for " +
|
|
2978
|
+
"nullable columns (HTTP 422 otherwise). This is NOT the install bootstrap write path — for " +
|
|
2979
|
+
"easy install, prefer apply_install_manifest, which owns skip/conflict/confirmation semantics."),
|
|
2804
2980
|
},
|
|
2805
|
-
}, async ({ field_name, value, file_path }) => {
|
|
2981
|
+
}, async ({ field_name, value, file_path, only_if_null }) => {
|
|
2982
|
+
// Build the PUT body, including only_if_null only when explicitly true so
|
|
2983
|
+
// the default (false) request shape is unchanged for existing callers.
|
|
2984
|
+
const withGuard = (v) => only_if_null === true
|
|
2985
|
+
? { repo_name: REPO_NAME, value: v, only_if_null: true }
|
|
2986
|
+
: { repo_name: REPO_NAME, value: v };
|
|
2806
2987
|
// JSONB array config fields (e.g. selected_mcp_slugs): forward the array value
|
|
2807
2988
|
// as JSON. Never join into a comma-delimited string — the backend expects a
|
|
2808
2989
|
// real JSON array and validates each slug against the mcp_docs allowlist.
|
|
@@ -2825,14 +3006,39 @@ registerTool("update_config_field", {
|
|
|
2825
3006
|
const resp = await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`), {
|
|
2826
3007
|
method: "PUT",
|
|
2827
3008
|
headers: await getPostHeaders(),
|
|
2828
|
-
body: JSON.stringify(
|
|
3009
|
+
body: JSON.stringify(withGuard(arrayValue)),
|
|
3010
|
+
});
|
|
3011
|
+
const text = await handleResponse(resp);
|
|
3012
|
+
return { content: [{ type: "text", text }] };
|
|
3013
|
+
}
|
|
3014
|
+
// JSONB object config fields (e.g. difficulty_model_tier_overrides): forward the
|
|
3015
|
+
// object value as JSON. Reject file_path; the backend validates/normalizes keys
|
|
3016
|
+
// and alias values. An omitted value clears all overrides (empty object).
|
|
3017
|
+
const JSON_OBJECT_CONFIG_FIELDS = ["difficulty_model_tier_overrides"];
|
|
3018
|
+
if (JSON_OBJECT_CONFIG_FIELDS.includes(field_name)) {
|
|
3019
|
+
if (file_path) {
|
|
3020
|
+
return {
|
|
3021
|
+
isError: true,
|
|
3022
|
+
content: [{
|
|
3023
|
+
type: "text",
|
|
3024
|
+
text: JSON.stringify({
|
|
3025
|
+
error: `'${field_name}' is a JSON object field; file_path updates are not supported. Pass value as an object mapping tier names to model aliases.`,
|
|
3026
|
+
}),
|
|
3027
|
+
}],
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
const objectValue = value === undefined ? {} : value;
|
|
3031
|
+
const resp = await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`), {
|
|
3032
|
+
method: "PUT",
|
|
3033
|
+
headers: await getPostHeaders(),
|
|
3034
|
+
body: JSON.stringify(withGuard(objectValue)),
|
|
2829
3035
|
});
|
|
2830
3036
|
const text = await handleResponse(resp);
|
|
2831
3037
|
return { content: [{ type: "text", text }] };
|
|
2832
3038
|
}
|
|
2833
3039
|
// Scalar boolean config fields: reject file-path updates and normalize boolean
|
|
2834
3040
|
// true/false and string "true"/"false" to a real boolean before persisting.
|
|
2835
|
-
const BOOLEAN_CONFIG_FIELDS = ["allow_mutating_smoke_ops"];
|
|
3041
|
+
const BOOLEAN_CONFIG_FIELDS = ["allow_mutating_smoke_ops", "difficulty_model_routing_enabled"];
|
|
2836
3042
|
if (BOOLEAN_CONFIG_FIELDS.includes(field_name)) {
|
|
2837
3043
|
if (file_path) {
|
|
2838
3044
|
return {
|
|
@@ -2874,7 +3080,7 @@ registerTool("update_config_field", {
|
|
|
2874
3080
|
const resp = await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`), {
|
|
2875
3081
|
method: "PUT",
|
|
2876
3082
|
headers: await getPostHeaders(),
|
|
2877
|
-
body: JSON.stringify(
|
|
3083
|
+
body: JSON.stringify(withGuard(boolValue)),
|
|
2878
3084
|
});
|
|
2879
3085
|
const text = await handleResponse(resp);
|
|
2880
3086
|
return { content: [{ type: "text", text }] };
|
|
@@ -2892,11 +3098,71 @@ registerTool("update_config_field", {
|
|
|
2892
3098
|
const resp = await fetch(buildUrl(`/config-field/${encodeURIComponent(field_name)}`), {
|
|
2893
3099
|
method: "PUT",
|
|
2894
3100
|
headers: await getPostHeaders(),
|
|
2895
|
-
body: JSON.stringify(
|
|
3101
|
+
body: JSON.stringify(withGuard(finalValue)),
|
|
2896
3102
|
});
|
|
2897
3103
|
const text = await handleResponse(resp);
|
|
2898
3104
|
return { content: [{ type: "text", text: text + note }] };
|
|
2899
3105
|
});
|
|
3106
|
+
// ---------------------------------------------------------------------------
|
|
3107
|
+
// Easy Install (BAPI-356): read-the-manifest / apply-the-manifest
|
|
3108
|
+
// ---------------------------------------------------------------------------
|
|
3109
|
+
registerTool("get_install_manifest", {
|
|
3110
|
+
annotations: {
|
|
3111
|
+
readOnlyHint: true,
|
|
3112
|
+
destructiveHint: false,
|
|
3113
|
+
idempotentHint: true,
|
|
3114
|
+
openWorldHint: true,
|
|
3115
|
+
},
|
|
3116
|
+
description: "Read the easy-install configuration manifest for the configured repository in one call. " +
|
|
3117
|
+
"Returns ordered field groups (each bootstrap field with its current value, is_set flag, agent " +
|
|
3118
|
+
"guidance, examples, and validation summary), a list of deferred fields (owned by " +
|
|
3119
|
+
"/learn-repository or set deliberately), a next_step pointer, done_criteria, and a signed " +
|
|
3120
|
+
"snapshot_token. Pass that exact snapshot_token to apply_install_manifest. " +
|
|
3121
|
+
"Prefer this over many individual get_config_field reads during install bootstrap.",
|
|
3122
|
+
inputSchema: {
|
|
3123
|
+
save_locally: z.boolean().optional().default(true).describe("When true (default), also save the full JSON manifest under BAPI_DOCS_DIR/install/."),
|
|
3124
|
+
},
|
|
3125
|
+
}, async ({ save_locally }) => {
|
|
3126
|
+
const url = buildGetUrl("/config/install-manifest", { repo_name: REPO_NAME });
|
|
3127
|
+
const resp = await fetch(url, { headers: await getGetHeaders() });
|
|
3128
|
+
const ok = resp.ok;
|
|
3129
|
+
let text = await handleResponse(resp);
|
|
3130
|
+
if (ok && save_locally !== false) {
|
|
3131
|
+
const safeRepo = safeTicketFileSegment(REPO_NAME || "repo");
|
|
3132
|
+
const filename = `${safeRepo}-install-manifest-${safeTimestampForFilename()}.json`;
|
|
3133
|
+
const note = await saveLocally(await getDocsPath("install"), filename, text);
|
|
3134
|
+
text = text + note;
|
|
3135
|
+
}
|
|
3136
|
+
return { content: [{ type: "text", text }] };
|
|
3137
|
+
});
|
|
3138
|
+
registerTool("apply_install_manifest", {
|
|
3139
|
+
annotations: {
|
|
3140
|
+
readOnlyHint: false,
|
|
3141
|
+
destructiveHint: true,
|
|
3142
|
+
idempotentHint: false,
|
|
3143
|
+
openWorldHint: true,
|
|
3144
|
+
},
|
|
3145
|
+
description: "Apply easy-install configuration in one atomic call. Pass the snapshot_token returned by " +
|
|
3146
|
+
"get_install_manifest plus a fields object. Each field value is either a scalar " +
|
|
3147
|
+
"(e.g. \"base_branch\": \"main\") or an object (e.g. \"project_description\": {\"value\": \"...\", " +
|
|
3148
|
+
"\"confirmed\": true}). project_description MUST be passed as {value, confirmed: true} and only " +
|
|
3149
|
+
"after explicit human approval. The server owns skip-if-set, conflict detection, and " +
|
|
3150
|
+
"confirmation semantics and returns six buckets: applied, skipped, conflict, rejected, " +
|
|
3151
|
+
"deferred, needs_confirmation. Only bootstrap-eligible fields are accepted (others are rejected).",
|
|
3152
|
+
inputSchema: {
|
|
3153
|
+
snapshot_token: z.string().describe("The exact snapshot_token returned by get_install_manifest for this repository."),
|
|
3154
|
+
fields: z.record(z.string(), z.any()).describe("Map of field_name to value. A value is either a scalar or an object {value, confirmed}. " +
|
|
3155
|
+
"Pass project_description only as {value: \"...\", confirmed: true} after human approval."),
|
|
3156
|
+
},
|
|
3157
|
+
}, async ({ snapshot_token, fields }) => {
|
|
3158
|
+
const resp = await fetch(buildUrl("/config/apply-install-manifest"), {
|
|
3159
|
+
method: "POST",
|
|
3160
|
+
headers: await getPostHeaders(),
|
|
3161
|
+
body: JSON.stringify({ repo_name: REPO_NAME, snapshot_token, fields }),
|
|
3162
|
+
});
|
|
3163
|
+
const text = await handleResponse(resp);
|
|
3164
|
+
return { content: [{ type: "text", text }] };
|
|
3165
|
+
});
|
|
2900
3166
|
function formatDeepResearchProviderReason(meta) {
|
|
2901
3167
|
if (!meta)
|
|
2902
3168
|
return "";
|
|
@@ -4125,72 +4391,6 @@ registerTool("generate_decision_page", {
|
|
|
4125
4391
|
};
|
|
4126
4392
|
});
|
|
4127
4393
|
// ---------------------------------------------------------------------------
|
|
4128
|
-
// Tiered section-executor telemetry (BAPI-346, Ticket 2)
|
|
4129
|
-
// ---------------------------------------------------------------------------
|
|
4130
|
-
/**
|
|
4131
|
-
* POST one per-section execution-telemetry row through the Bridge API HTTP
|
|
4132
|
-
* boundary (`/jira/tiered-section-metrics`). Keeps the MCP layer free of any
|
|
4133
|
-
* Python DAL import — the server route owns the dormant BAPI-345 DAL seam.
|
|
4134
|
-
*/
|
|
4135
|
-
async function recordTieredSectionMetric(args) {
|
|
4136
|
-
const payload = {
|
|
4137
|
-
repo_name: REPO_NAME,
|
|
4138
|
-
ticket_number: args.ticket_number,
|
|
4139
|
-
section_id: args.section_id,
|
|
4140
|
-
tier_assigned: args.tier_assigned,
|
|
4141
|
-
mode_run: args.mode_run,
|
|
4142
|
-
metrics: args.metrics ?? {},
|
|
4143
|
-
};
|
|
4144
|
-
const resp = await fetch(buildUrl("/tiered-section-metrics"), {
|
|
4145
|
-
method: "POST",
|
|
4146
|
-
headers: await getPostHeaders(),
|
|
4147
|
-
body: JSON.stringify(payload),
|
|
4148
|
-
});
|
|
4149
|
-
const text = await handleResponse(resp);
|
|
4150
|
-
return { content: [{ type: "text", text }] };
|
|
4151
|
-
}
|
|
4152
|
-
registerTool("record_tiered_section_metric", {
|
|
4153
|
-
annotations: {
|
|
4154
|
-
readOnlyHint: false,
|
|
4155
|
-
destructiveHint: false,
|
|
4156
|
-
idempotentHint: false,
|
|
4157
|
-
openWorldHint: true,
|
|
4158
|
-
},
|
|
4159
|
-
description: "Record one per-section execution-telemetry row for the Claude Code tiered " +
|
|
4160
|
-
"section executor (BAPI-346). This writes server-side telemetry state only — " +
|
|
4161
|
-
"it does NOT mutate Jira, code, or any ticket. It is intended to be called by " +
|
|
4162
|
-
"the tiered section executor after each section attempt; the executor Warns and " +
|
|
4163
|
-
"continues if recording fails, so telemetry never blocks an implementation. " +
|
|
4164
|
-
"Typed columns are ticket_number, section_id, tier_assigned, and mode_run; put " +
|
|
4165
|
-
"any additional per-section detail in the flexible `metrics` object.",
|
|
4166
|
-
inputSchema: {
|
|
4167
|
-
ticket_number: z
|
|
4168
|
-
.string()
|
|
4169
|
-
.describe("Jira ticket key in PROJECT-NUMBER format (e.g. PROJ-123, BAPI-42)"),
|
|
4170
|
-
section_id: z
|
|
4171
|
-
.string()
|
|
4172
|
-
.describe("The BAPI-345 section graph `id` of the section this metric is for."),
|
|
4173
|
-
tier_assigned: z
|
|
4174
|
-
.enum(["cheap", "basic", "premium"])
|
|
4175
|
-
.describe("The tier resolved for the section (cheap→haiku, basic→sonnet, premium→opus)."),
|
|
4176
|
-
mode_run: z
|
|
4177
|
-
.enum(["sub_agent", "inline_tiered", "inline_default"])
|
|
4178
|
-
.describe("The execution mode the section actually ran in."),
|
|
4179
|
-
metrics: z
|
|
4180
|
-
.record(z.string(), z.unknown())
|
|
4181
|
-
.optional()
|
|
4182
|
-
.describe("Optional flexible JSON detail (e.g. contract_version, mode_intended, " +
|
|
4183
|
-
"model_resolved, tokens, cache, verification, escalation_count, " +
|
|
4184
|
-
"budget_snapshot, files_changed, handoff, degraded_reason)."),
|
|
4185
|
-
},
|
|
4186
|
-
}, async ({ ticket_number, section_id, tier_assigned, mode_run, metrics }) => recordTieredSectionMetric({
|
|
4187
|
-
ticket_number,
|
|
4188
|
-
section_id,
|
|
4189
|
-
tier_assigned,
|
|
4190
|
-
mode_run,
|
|
4191
|
-
metrics,
|
|
4192
|
-
}));
|
|
4193
|
-
// ---------------------------------------------------------------------------
|
|
4194
4394
|
// Entry point
|
|
4195
4395
|
// ---------------------------------------------------------------------------
|
|
4196
4396
|
const transport = new StdioServerTransport();
|