Skip to content

Commit 217fc48

Browse files
feat(mcp-bridge): implement resources + prompts surface (epic #87 item 1) (#89)
## Summary Closes the first item of epic #87. The bridge has been declaring \`resources\` and \`prompts\` capabilities in its MCP \`initialize\` response since v0.4.5, but never actually served any. This wires up real handlers so clients can: - **Inspect BoJ state without LLM round-trips** — cartridge index, capability matrix, proof obligations via \`resources/read\` - **Instantiate high-frequency multi-cartridge workflows** — six BoJ-shaped templates via \`prompts/get\` ## Resources surface (6 URIs) | URI | Content | Backend? | |---|---|---| | \`boj://cartridges\` | Full cartridge index | REST when live; offline-menu fallback | | \`boj://cartridges/<name>\` | Per-cartridge manifest | REST; name validated against \`^[a-z0-9][a-z0-9-]*-mcp$\` | | \`boj://capabilities/matrix\` | Protocol × domain grid | REST; offline-hint fallback | | \`boj://capabilities/tools\` | Bridge tools grouped by domain | Always offline-readable | | \`boj://proofs/manifest\` | P-01..P-07 + \`believe_me\` audit | Always offline-readable | | \`boj://server/info\` | Server identity + capabilities | Always offline-readable | | \`boj://docs/architecture\` | ADR + topology pointers (Markdown) | Always offline-readable | ## Prompts surface (6 templates) | Name | Composes | |---|---| | \`audit-repo\` | hypatia-mcp + panic-attack-mcp + secrets-mcp | | \`convene-cluster\` | All \`coord_*\` tools — register, claim, broadcast, supervise | | \`deploy-with-dns-ssl\` | Provider cartridge (fly/render/railway/vercel) + cloudflare-mcp | | \`summarize-channel\` | slack/discord/matrix/telegram via \`boj_cartridge_invoke\` | | \`triage-issues\` | github-api-mcp / gitlab-api-mcp | | \`proof-status\` | \`boj://proofs/manifest\` + \`boj://cartridges\` (resource-first) | Each template names the exact tools the LLM should call — no free-form orchestration. ## What's in the diff - **New** \`mcp-bridge/lib/resources.js\` (~200 LOC) — resource definitions + URI router - **New** \`mcp-bridge/lib/prompts.js\` (~200 LOC) — prompt definitions + argument validation + template builders - **Edit** \`mcp-bridge/main.js\` — replaced empty list stubs with real handlers; added \`resources/read\` and \`prompts/get\` methods; wired error sanitization - **Edit** \`mcp-bridge/tests/dispatch_test.js\` — 2 new coherence tests covering resource/prompt well-formedness + error paths ## Verification End-to-end with \`deno run -A mcp-bridge/main.js\` + manual JSON-RPC fixtures: - ✅ \`initialize\` → unchanged, declares same capabilities - ✅ \`resources/list\` → returns 6 resources with proper schema - ✅ \`resources/read\` → returns content for each URI; unknown URI → \`-32602\`; non-\`boj://\` URI rejected - ✅ \`prompts/list\` → returns 6 prompts with argument schemas - ✅ \`prompts/get\` → valid args produce non-empty message; missing required arg → \`-32602\`; unknown name → \`-32602\` - ✅ Existing test suite: **11/11 pass** (no regressions) - ✅ New tests: 2/2 pass (resources well-formedness + prompts well-formedness + arg validation) ## Tier-payoff This is the lowest-effort item in epic #87 Tier A and the highest coherence payoff — BoJ becomes a fully realised MCP citizen (tools + resources + prompts + logging), not just a tool surface. ## Sequencing Independent of #88 (publish workflow fix) and of v0.4.7 tagging. Can land at any time. Future cartridges may extend the resource catalogue (e.g. \`boj://policies/...\` once item 4's policy DSL lands). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 76e15b1 commit 217fc48

4 files changed

Lines changed: 603 additions & 2 deletions

File tree

mcp-bridge/lib/prompts.js

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// BoJ Server — MCP prompts surface
5+
//
6+
// Implements MCP prompts/list + prompts/get. Each template encodes a
7+
// high-frequency BoJ workflow as a structured set of messages that
8+
// guides the connected LLM through a multi-cartridge composition.
9+
//
10+
// Templates name the tools they expect to call so the LLM has explicit
11+
// guidance, not just free-form orchestration.
12+
13+
const PROMPT_DEFS = [
14+
{
15+
name: "audit-repo",
16+
description: "Run a security + quality + compliance audit on a GitHub or GitLab repository, composing hypatia-mcp, panic-attack-mcp, and secrets-mcp findings into a single report.",
17+
arguments: [
18+
{ name: "owner", description: "Repo owner / org login", required: true },
19+
{ name: "repo", description: "Repo name (no owner prefix)", required: true },
20+
{ name: "forge", description: "github | gitlab (default github)", required: false },
21+
],
22+
},
23+
{
24+
name: "convene-cluster",
25+
description: "Stand up a multi-agent BoJ cluster: register this peer, discover others, claim a task, broadcast readiness, and start the supervision loop.",
26+
arguments: [
27+
{ name: "task_topic", description: "Free-form description of the work to coordinate on", required: true },
28+
{ name: "role_hint", description: "apprentice | journeyman | master (default journeyman)", required: false },
29+
{ name: "variant", description: "Model variant label (e.g. opus-4.7, sonnet-4.6, flash-2.5)", required: false },
30+
],
31+
},
32+
{
33+
name: "deploy-with-dns-ssl",
34+
description: "Deploy an application to a cloud provider with custom-domain DNS + SSL wired through Cloudflare. Composes the chosen provider's cartridge with cloudflare-mcp.",
35+
arguments: [
36+
{ name: "app_name", description: "Application identifier on the deploy target", required: true },
37+
{ name: "domain", description: "Custom domain (e.g. app.example.com)", required: true },
38+
{ name: "provider", description: "fly | render | railway | vercel (default fly)", required: false },
39+
],
40+
},
41+
{
42+
name: "summarize-channel",
43+
description: "Pull recent history from a messaging channel and produce a concise summary of decisions, action items, and unresolved threads.",
44+
arguments: [
45+
{ name: "platform", description: "slack | discord | matrix | telegram", required: true },
46+
{ name: "channel_id", description: "Channel/room identifier", required: true },
47+
{ name: "hours", description: "Lookback window in hours (default 24)", required: false },
48+
],
49+
},
50+
{
51+
name: "triage-issues",
52+
description: "Triage open issues on a repository: enumerate, classify by severity/effort/staleness, propose labels, and surface the top N for action.",
53+
arguments: [
54+
{ name: "owner", description: "Repo owner / org login", required: true },
55+
{ name: "repo", description: "Repo name", required: true },
56+
{ name: "forge", description: "github | gitlab (default github)", required: false },
57+
{ name: "top_n", description: "How many issues to surface (default 10)", required: false },
58+
],
59+
},
60+
{
61+
name: "proof-status",
62+
description: "Report the formal-verification posture of one cartridge or the whole BoJ ABI — proof obligations, discharge state, believe_me-axiom count, and outstanding work.",
63+
arguments: [
64+
{ name: "cartridge", description: "Cartridge name (omit for whole-server view)", required: false },
65+
],
66+
},
67+
];
68+
69+
function listPrompts() {
70+
return PROMPT_DEFS.map((p) => ({ ...p }));
71+
}
72+
73+
function buildAuditRepo(args) {
74+
const forge = args.forge ?? "github";
75+
const owner = args.owner;
76+
const repo = args.repo;
77+
const text = `You are auditing the ${forge} repository \`${owner}/${repo}\` for security, quality, and compliance issues. Compose findings from three cartridges into a single report.
78+
79+
Step 1 — Fetch repo metadata via \`boj_${forge}_get_repo\` (owner=${owner}, repo=${repo}) to confirm visibility, default branch, and topics.
80+
81+
Step 2 — Run the Hypatia neurosymbolic scanner via \`boj_cartridge_invoke\` against \`hypatia-mcp\` to enumerate security/quality/compliance issues for this repo.
82+
83+
Step 3 — Run \`boj_cartridge_invoke\` against \`panic-attack-mcp\` to surface dangerous patterns, banned constructs, and proof drift.
84+
85+
Step 4 — Run \`boj_cartridge_invoke\` against \`secrets-mcp\` to verify no committed secrets are detectable.
86+
87+
Step 5 — Produce a single Markdown report with three sections (Security / Quality / Compliance), each listing findings with severity + file path + recommended remediation. If any tool returns an error or unavailable hint, surface that clearly and continue with the other tools.
88+
89+
Do not modify the repo. Read-only operations only.`;
90+
return {
91+
description: `Audit ${owner}/${repo} via hypatia + panic-attack + secrets`,
92+
messages: [
93+
{ role: "user", content: { type: "text", text } },
94+
],
95+
};
96+
}
97+
98+
function buildConveneCluster(args) {
99+
const task = args.task_topic;
100+
const role = args.role_hint ?? "journeyman";
101+
const variant = args.variant ?? "unset";
102+
const text = `You are joining a BoJ multi-agent coordination cluster on the local coord bus (127.0.0.1:7745) to work on: ${task}
103+
104+
Step 1 — Register as a peer via \`coord_register\` with client_kind matching your model family (claude/gemini/copilot/openai/mistral/custom), variant="${variant}", and declared_affinities matching this task. Capture the returned token.
105+
106+
Step 2 — Call \`coord_list_peers\` to see who else is already on the bus.
107+
108+
Step 3 — Call \`coord_set_capabilities\` to advertise your class/tier/prover_strengths so cold-start routing can find you.
109+
110+
Step 4 — Call \`coord_claim_task\` for the topic above with confidence∈[0,1] and an honest task_difficulty (trivial/routine/challenging/novel). Watchdog TTL will be applied based on your role (apprentice 30s / journeyman 5m / master none).
111+
112+
Step 5 — Broadcast readiness via \`coord_send\` (target="*") with a JSON A2ML envelope announcing your variant + claimed task.
113+
114+
Step 6 — Enter the receive loop: call \`coord_receive\` periodically. If your role is "${role}" and you handle a high-risk operation (tier 2+), route it via \`coord_send_gated\` so the master can quarantine-review.
115+
116+
Step 7 — Heartbeat via \`coord_progress\` before your watchdog TTL expires. Report outcomes via \`coord_report_outcome\` to build your track record.
117+
118+
Maintain coordination etiquette: only one peer claims a task at a time; respect quarantine decisions; do not promote yourself to master without the explicit secret.`;
119+
return {
120+
description: `Convene ${role} on BoJ coord bus for: ${task}`,
121+
messages: [
122+
{ role: "user", content: { type: "text", text } },
123+
],
124+
};
125+
}
126+
127+
function buildDeployWithDnsSsl(args) {
128+
const app = args.app_name;
129+
const domain = args.domain;
130+
const provider = args.provider ?? "fly";
131+
const providerCartridge = `${provider}-mcp`;
132+
const text = `Deploy \`${app}\` to ${provider} with custom domain \`${domain}\` and SSL via Cloudflare.
133+
134+
Step 1 — Verify your auth posture: \`boj_cloud_cloudflare\` (operation=authenticate) and \`boj_cartridge_invoke\` against \`${providerCartridge}\` with an auth check op.
135+
136+
Step 2 — Deploy the application via \`boj_cartridge_invoke\` against \`${providerCartridge}\`. Use the deploy/release op appropriate to that provider (Fly: deploy; Render: create-service; Railway: deploy; Vercel: redeploy).
137+
138+
Step 3 — Once the deployment reports a public hostname (e.g. \`${app}.fly.dev\`), point Cloudflare DNS at it. Call \`boj_cloud_cloudflare\` with operation="list-dns-zones" to find the zone for \`${domain}\`, then operation="add-dns-record" with type=CNAME, name=\`${domain}\`, content=<provider-hostname>, proxied=true.
139+
140+
Step 4 — On the provider side, register the custom domain so the provider's edge accepts traffic for \`${domain}\` (Fly: certs create; Render: add custom domain; Railway: add domain; Vercel: assign domain).
141+
142+
Step 5 — Verify HTTPS resolves and the provider's cert issuance has completed (Cloudflare-proxied → Cloudflare cert is sufficient end-to-end; for grey-clouded, wait for provider issuance).
143+
144+
Step 6 — Smoke-test via \`boj_browser_navigate\` to \`https://${domain}\` and confirm 200 + correct content.
145+
146+
If anything fails, do not roll back automatically. Report the failed step and ask before destructive actions.`;
147+
return {
148+
description: `Deploy ${app} to ${provider} + ${domain} via Cloudflare DNS/SSL`,
149+
messages: [
150+
{ role: "user", content: { type: "text", text } },
151+
],
152+
};
153+
}
154+
155+
function buildSummarizeChannel(args) {
156+
const platform = args.platform;
157+
const channel = args.channel_id;
158+
const hours = args.hours ?? "24";
159+
const cartridge = `${platform}-mcp`;
160+
const text = `Summarize the last ${hours} hours of activity in ${platform} channel \`${channel}\`.
161+
162+
Step 1 — Call \`boj_cartridge_invoke\` against \`${cartridge}\` with an op that fetches recent messages (Slack: read-history; Discord: channel-history; Matrix: room-messages; Telegram: get-updates) bounded by the ${hours}-hour window.
163+
164+
Step 2 — Produce a Markdown summary with these sections:
165+
- **Decisions made** — explicit choices with who said what
166+
- **Action items** — TODOs with owner + deadline when stated
167+
- **Unresolved threads** — questions or debates that didn't reach closure
168+
- **Notable links / files** — anything shared that's worth surfacing
169+
170+
Step 3 — At the end, list any participants who appeared but didn't post substantively (silent presence).
171+
172+
Keep it under 800 words. Do not invent details that aren't in the message log. Quote exact words for any decision or commitment.`;
173+
return {
174+
description: `Summarize ${platform}#${channel} (${hours}h)`,
175+
messages: [
176+
{ role: "user", content: { type: "text", text } },
177+
],
178+
};
179+
}
180+
181+
function buildTriageIssues(args) {
182+
const forge = args.forge ?? "github";
183+
const owner = args.owner;
184+
const repo = args.repo;
185+
const topN = args.top_n ?? "10";
186+
const text = `Triage open issues on ${forge}:${owner}/${repo}. Surface the top ${topN} for action.
187+
188+
Step 1 — List open issues via \`boj_${forge}_list_issues\` (state=open, owner=${owner}, repo=${repo}, per_page=100). Page if there are more.
189+
190+
Step 2 — For each issue, classify by:
191+
- **Severity** — critical / high / normal / low (signal: security keywords, "broken", user-blocking, vs. nice-to-have)
192+
- **Effort** — small / medium / large (signal: scope of files mentioned, dependency complexity)
193+
- **Staleness** — fresh (<7 days) / mid (7-30) / stale (>30) — uses createdAt + last-activity
194+
195+
Step 3 — Recommend labels using these rules: any critical/high → \`priority:high\`; any small + stale → \`good-first-issue\`; any unanswered + >14 days → \`needs-triage\`.
196+
197+
Step 4 — Surface the top ${topN} prioritized as: \`[severity, -staleness_days, effort_asc]\`. For each, output:
198+
- Issue # + title
199+
- Severity / Effort / Staleness classification
200+
- Why it's prioritized
201+
- Recommended labels
202+
- Suggested first step
203+
204+
Do not apply labels automatically. This is recommendation-only; the human must approve before any \`boj_${forge}_*\` mutating call.`;
205+
return {
206+
description: `Triage top ${topN} issues on ${forge}:${owner}/${repo}`,
207+
messages: [
208+
{ role: "user", content: { type: "text", text } },
209+
],
210+
};
211+
}
212+
213+
function buildProofStatus(args) {
214+
const cartridge = args.cartridge;
215+
const scope = cartridge ? `cartridge \`${cartridge}\`` : "the whole BoJ ABI surface";
216+
const text = `Report the formal-verification posture of ${scope}.
217+
218+
Step 1 — Read \`boj://proofs/manifest\` via \`resources/read\` to get the canonical obligation list and discharge state.
219+
220+
${cartridge ? `Step 2 — Read \`boj://cartridges/${cartridge}\` to confirm this cartridge exists and check its declared proof-obligation set in its manifest.` : `Step 2 — Read \`boj://cartridges\` to enumerate every cartridge and note which ones declare proof obligations in their manifests.`}
221+
222+
Step 3 — Produce a report with:
223+
- **Obligations**: ID, title, status (discharged / outstanding), evidence file
224+
- **believe_me audit**: current axiom count vs. initial, with a brief note on each remaining axiom and whether discharge is tractable
225+
- **Outstanding work**: any obligation marked outstanding, plus the relevant tracking issue (epic #87 item 11 for residual axioms, item 12 for composition proof)
226+
227+
Do not invent obligations not present in the manifest. If a cartridge has no proof obligations declared, say so explicitly rather than fabricating any.`;
228+
return {
229+
description: `Proof-verification posture for ${scope}`,
230+
messages: [
231+
{ role: "user", content: { type: "text", text } },
232+
],
233+
};
234+
}
235+
236+
const BUILDERS = {
237+
"audit-repo": buildAuditRepo,
238+
"convene-cluster": buildConveneCluster,
239+
"deploy-with-dns-ssl": buildDeployWithDnsSsl,
240+
"summarize-channel": buildSummarizeChannel,
241+
"triage-issues": buildTriageIssues,
242+
"proof-status": buildProofStatus,
243+
};
244+
245+
function validateRequired(name, args) {
246+
const def = PROMPT_DEFS.find((p) => p.name === name);
247+
if (!def) return { code: -32602, message: `Unknown prompt: ${name}` };
248+
for (const a of def.arguments) {
249+
if (a.required && (args?.[a.name] === undefined || args?.[a.name] === "")) {
250+
return { code: -32602, message: `Prompt '${name}' requires argument '${a.name}'` };
251+
}
252+
}
253+
return null;
254+
}
255+
256+
function getPrompt(name, args) {
257+
const err = validateRequired(name, args ?? {});
258+
if (err) return { error: err };
259+
const builder = BUILDERS[name];
260+
if (!builder) return { error: { code: -32602, message: `Unknown prompt: ${name}` } };
261+
return { result: builder(args ?? {}) };
262+
}
263+
264+
export { listPrompts, getPrompt };

0 commit comments

Comments
 (0)