|
| 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