diff --git a/docs/glama/SERVER_CONFIGURATION.md b/docs/glama/SERVER_CONFIGURATION.md index 4486355f..63df8825 100644 --- a/docs/glama/SERVER_CONFIGURATION.md +++ b/docs/glama/SERVER_CONFIGURATION.md @@ -12,6 +12,40 @@ | `BOJ_REST_PORT` | No | TCP port for REST API | `7700` | | `BOJ_MCP_BRIDGE` | No | Path to MCP bridge executable | `mcp-bridge/main.js` | +### Tool Surface Scoping + +| Name | Required | Description | Default | +|------|----------|-------------|---------| +| `BOJ_TOOL_SCOPE` | No | Controls which tools are *advertised* over `tools/list` | `full` | + +`BOJ_TOOL_SCOPE` is a coherence lever: it narrows the advertised tool +surface without removing any capability. It accepts three forms: + +- **`full`** (or unset) — advertise every tool. This is the default and + preserves backward compatibility with all existing clients. +- **`core`** — advertise only the always-present discovery/dispatch + core: `boj_health`, `boj_menu`, `boj_cartridges`, + `boj_cartridge_info`, `boj_cartridge_invoke`, plus **all** `coord_*` + tools (the local coordination surface is a single coherent unit). +- **CSV of domain prefixes** — e.g. `core,github,browser` advertises + the core plus the named explicit domain groups + (`github`, `gitlab`, `cloud`, `comms`, `ml`, `browser`, `research`, + `codeseeker`). `core` is always implied. + +The unified-endpoint thesis is preserved in every mode: each explicit +`boj__*` tool remains fully reachable through the generic +`boj_cartridge_invoke` dispatcher even when it is not advertised. +Narrowing the surface only changes discovery, not capability — which is +exactly what Glama's Server Coherence sub-score rewards. + +```bash +# Minimal surface — discovery/dispatch core + coordination only +docker run -e BOJ_TOOL_SCOPE=core boj-server + +# Core plus the GitHub and browser domain groups +docker run -e BOJ_TOOL_SCOPE=core,github,browser boj-server +``` + ### Authentication | Name | Required | Description | Default | diff --git a/glama.json b/glama.json index 4a3424c0..a5095b51 100644 --- a/glama.json +++ b/glama.json @@ -43,6 +43,11 @@ "type": "string", "description": "URL for the local coordination bus backend.", "default": "http://127.0.0.1:7745" + }, + "BOJ_TOOL_SCOPE": { + "type": "string", + "description": "Advertised tool-surface scope. 'full' (or unset) advertises every tool (default, backward-compatible). 'core' advertises only the discovery/dispatch core (boj_health, boj_menu, boj_cartridges, boj_cartridge_info, boj_cartridge_invoke) plus all coord_* tools. A CSV of domain prefixes (e.g. 'core,github,browser') advertises core plus the named explicit groups. Every explicit boj__* tool remains reachable via boj_cartridge_invoke regardless of scope.", + "default": "full" } } } diff --git a/mcp-bridge/lib/tools.js b/mcp-bridge/lib/tools.js index 35fc6bb1..2ea3aa75 100644 --- a/mcp-bridge/lib/tools.js +++ b/mcp-bridge/lib/tools.js @@ -5,12 +5,43 @@ // // Builds the MCP tool list from cartridge data. Separates tool schema // definitions from transport and dispatch logic. +// +// Every tool carries MCP-spec `annotations` (title + the four behaviour +// hints) and a JSON-Schema `outputSchema` describing the documented +// return shape. The full descriptions / inputSchemas are AAA-tier and +// must NOT be shortened — a coherence test enforces a 120-char min and +// 200-char mean description floor. + +// Common opaque-payload outputSchema for tools that pass an upstream +// cartridge / provider response straight through. We do not invent +// fields for these — the shape is owned by the downstream cartridge. +function passthrough(desc) { + return { type: "object", description: desc, additionalProperties: true }; +} + +// Standard coord-bus failure envelope shape, merged into coord_* +// outputSchemas so the documented `{error, hint}` path is described. /** - * Build the full MCP tool list. - * @returns {Array<{name: string, description: string, inputSchema: object}>} + * Build the MCP tool list. + * + * Scope filtering (Task: Teranga scoped surface) — controlled by the + * `BOJ_TOOL_SCOPE` environment variable: + * - unset / "full" : advertise every tool (DEFAULT, back-compat). + * - "core" : advertise only the discovery/dispatch core + * (boj_health, boj_menu, boj_cartridges, + * boj_cartridge_info, boj_cartridge_invoke) plus + * ALL coord_* tools. Every explicit boj__* + * tool is still reachable via boj_cartridge_invoke, + * so the unified-endpoint thesis is preserved. + * - CSV of prefixes : "core" plus the named explicit domain groups, + * e.g. "core,github,browser". + * + * @param {string} [scope] Optional explicit scope (overrides the env + * var; mainly for tests). When omitted, BOJ_TOOL_SCOPE is read. + * @returns {Array<{name: string, description: string, inputSchema: object, annotations: object, outputSchema: object}>} */ -function buildToolList() { +function buildToolList(scope) { const tools = []; // Core server tools @@ -18,18 +49,49 @@ function buildToolList() { name: "boj_health", description: "Ping the BoJ server and report liveness. Returns `{status:\"ok\", uptime_s, version}` when the REST backend at BOJ_URL is reachable; returns a structured error hint when the backend is offline. Zero-argument, read-only, no side effects — safe to call at any time. Use before invoking any other boj_* tool to confirm the cartridge fleet is live.", inputSchema: { type: "object", properties: {}, additionalProperties: false }, + annotations: { title: "Server Health", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Liveness report when the REST backend is reachable, or a structured error hint when it is offline.", + properties: { + status: { type: "string", description: "`\"ok\"` when the backend responded." }, + uptime_s: { type: "number", description: "Backend process uptime in seconds." }, + version: { type: "string", description: "BoJ backend version string." }, + error: { type: "string", description: "Present only when the backend is unreachable." }, + hint: { type: "string", description: "Remediation hint, present only on error." }, + }, + additionalProperties: true, + }, }); tools.push({ name: "boj_menu", description: "List every installed BoJ cartridge grouped by trust tier (Teranga, Shield, Ayo) with name, version, domain, supported protocols, and availability flag. Read-only snapshot — no side effects. Returns offline static manifest when the backend is unreachable, so the tool is always inspectable. Use this as the first call when an agent needs to discover what capabilities are mounted; follow with `boj_cartridge_info` for a specific cartridge's tools.", inputSchema: { type: "object", properties: {}, additionalProperties: false }, + annotations: { title: "Cartridge Menu", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Installed cartridges grouped by trust tier; falls back to the offline static manifest when the backend is unreachable.", + properties: { + tiers: { type: "object", description: "Map of tier name (Teranga/Shield/Ayo) to an array of cartridge summary objects {name, version, domain, protocols, available}." }, + }, + additionalProperties: true, + }, }); tools.push({ name: "boj_cartridges", description: "Return the BoJ capability matrix — a `protocol × domain` grid marking which cartridges serve each combination (e.g. MCP+Database → database-mcp). Complements `boj_menu` (flat list) with a two-dimensional view optimised for routing decisions. Read-only; no side effects. Use when selecting a cartridge by protocol (MCP / REST / gRPC / GraphQL / LSP / DAP / BSP) rather than by name.", inputSchema: { type: "object", properties: {}, additionalProperties: false }, + annotations: { title: "Capability Matrix", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "A protocol × domain capability grid mapping each combination to the serving cartridge name.", + properties: { + matrix: { type: "object", description: "Nested map keyed by protocol then domain, valued by cartridge identifier." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -49,6 +111,22 @@ function buildToolList() { required: ["name"], additionalProperties: false, }, + annotations: { title: "Cartridge Manifest", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Full manifest for the named cartridge, or `{error:\"not found\", hint}` for an unknown name.", + properties: { + name: { type: "string", description: "Cartridge identifier." }, + version: { type: "string", description: "Cartridge version." }, + domain: { type: "string", description: "Capability domain." }, + protocols: { type: "array", items: { type: "string" }, description: "Supported protocols." }, + tools: { type: "array", description: "Declared tool definitions with input schemas." }, + auth: { type: "string", description: "Auth model." }, + error: { type: "string", description: "`\"not found\"` for an unknown cartridge name." }, + hint: { type: "string", description: "Remediation hint, present only on error." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -72,6 +150,8 @@ function buildToolList() { required: ["name"], additionalProperties: false, }, + annotations: { title: "Invoke Cartridge", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: passthrough("Raw JSON response from the target cartridge — shape is owned by the downstream cartridge's manifest, so it is not enumerated here."), }); // Cloud providers @@ -91,6 +171,8 @@ function buildToolList() { required: ["operation"], additionalProperties: false, }, + annotations: { title: "Verpex Hosting", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: passthrough("Structured Verpex (cPanel UAPI) JSON for the requested operation, or `{error, hint}` on auth failure or missing parameters."), }); tools.push({ @@ -106,6 +188,8 @@ function buildToolList() { required: ["operation"], additionalProperties: false, }, + annotations: { title: "Cloudflare Edge", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: passthrough("Direct Cloudflare API response for the requested operation, or `{error, hint}` on failure."), }); tools.push({ @@ -121,6 +205,8 @@ function buildToolList() { required: ["operation"], additionalProperties: false, }, + annotations: { title: "Vercel Projects", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: passthrough("Direct Vercel API response for the requested read operation, or `{error, hint}` on failure."), }); // Communications @@ -137,6 +223,8 @@ function buildToolList() { required: ["operation"], additionalProperties: false, }, + annotations: { title: "Gmail", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: passthrough("Structured Gmail JSON containing message/thread data for the requested operation, or `{error, hint}` on failure."), }); tools.push({ @@ -152,6 +240,8 @@ function buildToolList() { required: ["operation"], additionalProperties: false, }, + annotations: { title: "Google Calendar", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: passthrough("Structured Calendar JSON with event arrays or free/busy availability windows for the requested operation."), }); // ML/AI @@ -168,6 +258,8 @@ function buildToolList() { required: ["operation"], additionalProperties: false, }, + annotations: { title: "Hugging Face Hub", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: passthrough("Structured Hugging Face JSON containing model/dataset metadata or hosted-inference results for the requested operation."), }); // Browser automation — all routed through browser-mcp to the Firefox WebDriver session @@ -175,26 +267,84 @@ function buildToolList() { name: "boj_browser_navigate", description: "Drive the controlled Firefox session to a new URL. Side-effectful; replaces the active tab's document and resets the click/type state machine. Waits for the browser `load` event before returning. Returns `{ok:true, final_url, title}` on success or `{error, hint}` on network failure. Always use this tool first when orienting to a new page before attempting clicks or text extraction.", inputSchema: { type: "object", properties: { url: { type: "string", description: "Absolute URL (http/https) to navigate to. Relative paths are rejected.", minLength: 7 } }, required: ["url"], additionalProperties: false }, + annotations: { title: "Browser Navigate", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: { + type: "object", + description: "Navigation result after the browser `load` event, or `{error, hint}` on network failure.", + properties: { + ok: { type: "boolean", description: "`true` on successful navigation." }, + final_url: { type: "string", description: "URL after redirects." }, + title: { type: "string", description: "Document title of the loaded page." }, + error: { type: "string", description: "Present only on failure." }, + hint: { type: "string", description: "Remediation hint, present only on error." }, + }, + additionalProperties: true, + }, }); tools.push({ name: "boj_browser_click", description: "Click the first element matching a CSS selector on the current page. Side-effectful; may trigger navigation, form submission, or JavaScript event handlers. Waits for the target element to be both visible and enabled before clicking. Returns `{ok:true, selector}` on success or `{error:\"not found\"|\"not visible\"}`. Use after `boj_browser_navigate` has settled and the page structure is known.", inputSchema: { type: "object", properties: { selector: { type: "string", description: "CSS selector of the target element, e.g. `button#submit`, `a[href$='.pdf']`. Must match at least one visible element.", minLength: 1 } }, required: ["selector"], additionalProperties: false }, + annotations: { title: "Browser Click", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: { + type: "object", + description: "Click result, or `{error}` when the element is not found or not visible.", + properties: { + ok: { type: "boolean", description: "`true` on a successful click." }, + selector: { type: "string", description: "The selector that was clicked." }, + error: { type: "string", description: "`\"not found\"` or `\"not visible\"` on failure." }, + }, + additionalProperties: true, + }, }); tools.push({ name: "boj_browser_type", description: "Type literal text into the first input, textarea, or contenteditable element matching a CSS selector. Side-effectful; fires `input` and `change` events as each character is delivered. Returns `{ok:true, selector}` on success or `{error:\"not editable\"|\"not found\"}`. Does not automatically submit forms; pair with `boj_browser_click` on the submit button for full interaction.", inputSchema: { type: "object", properties: { selector: { type: "string", description: "CSS selector of the target input element.", minLength: 1 }, text: { type: "string", description: "Literal text to deliver. Special keys like Enter or Tab are not interpreted; use `boj_browser_execute_js` for synthetic keyboard events.", minLength: 1 } }, required: ["selector", "text"], additionalProperties: false }, + annotations: { title: "Browser Type", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: { + type: "object", + description: "Type result, or `{error}` when the element is not found or not editable.", + properties: { + ok: { type: "boolean", description: "`true` on successful text entry." }, + selector: { type: "string", description: "The selector that received text." }, + error: { type: "string", description: "`\"not editable\"` or `\"not found\"` on failure." }, + }, + additionalProperties: true, + }, }); tools.push({ name: "boj_browser_read_page", description: "Extract the visible text content of the active tab. Read-only; no side effects. Concatenates body text, strips scripts/styles, and preserves rough reading order. Returns `{url, title, text, text_length}` with content capped at ~1 MB for context efficiency. Use after navigation has settled to provide page content to the LLM for analysis.", inputSchema: { type: "object", properties: {}, additionalProperties: false }, + annotations: { title: "Browser Read Page", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: { + type: "object", + description: "Extracted visible text of the active tab, capped at ~1 MB.", + properties: { + url: { type: "string", description: "Current tab URL." }, + title: { type: "string", description: "Document title." }, + text: { type: "string", description: "Concatenated visible text in rough reading order." }, + text_length: { type: "number", description: "Character length of `text`." }, + }, + additionalProperties: true, + }, }); tools.push({ name: "boj_browser_screenshot", description: "Capture a PNG screenshot of the active tab's current viewport. Read-only; no side effects. Returns base64-encoded image bytes under `{ok:true, image_base64, mime:\"image/png\"}`. Use when text extraction via `boj_browser_read_page` is insufficient, such as for analyzing complex layouts, charts, or visual bugs.", inputSchema: { type: "object", properties: {}, additionalProperties: false }, + annotations: { title: "Browser Screenshot", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: { + type: "object", + description: "PNG screenshot of the active tab viewport as base64 image bytes.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + image_base64: { type: "string", description: "Base64-encoded PNG bytes." }, + mime: { type: "string", description: "Always `image/png`." }, + }, + additionalProperties: true, + }, }); tools.push({ name: "boj_browser_tabs", @@ -209,47 +359,70 @@ function buildToolList() { required: ["operation"], additionalProperties: false, }, + annotations: { title: "Browser Tabs", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: { + type: "object", + description: "Tab operation result: tab array for `list`, new tab id for `create`, or bare ok for `close`.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + tabs: { type: "array", description: "Open tab descriptors, present for `list`." }, + tab_id: { type: "number", description: "Newly created tab id, present for `create`." }, + }, + additionalProperties: true, + }, }); tools.push({ name: "boj_browser_execute_js", description: "Execute a JavaScript snippet in the context of the active tab and return the value of the last expression. Side-effectful; can mutate the DOM, fire events, or inspect internal JS state. Returns `{ok:true, result}` (result must be JSON-serialisable) or `{error, stack}` on exception. Sandboxed to the target origin for security. Use when declarative actions like click or type are insufficient for complex interactions.", inputSchema: { type: "object", properties: { script: { type: "string", description: "JavaScript source code to execute. The last expression's value is returned; use an IIFE for multi-statement logic.", minLength: 1 } }, required: ["script"], additionalProperties: false }, + annotations: { title: "Browser Execute JS", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: { + type: "object", + description: "Value of the last evaluated expression, or `{error, stack}` on a thrown exception.", + properties: { + ok: { type: "boolean", description: "`true` when the script ran without throwing." }, + result: { description: "JSON-serialisable value of the last expression." }, + error: { type: "string", description: "Exception message, present only on failure." }, + stack: { type: "string", description: "Exception stack trace, present only on failure." }, + }, + additionalProperties: true, + }, }); // GitHub API tools — all routed via the authenticated backend token; read-only unless noted. const ghTools = [ - { name: "boj_github_list_repos", desc: "List repositories owned by or accessible to the authenticated GitHub user. Read-only; no side effects. Paginated; default 30, max 100 per page. Use `sort` to order by last-activity axis. Returns an array of repository objects `[{name, full_name, private, default_branch, ...}]`. Useful for project discovery.", props: { per_page: { type: "number", minimum: 1, maximum: 100, description: "Results per page (1..100, default 30)." }, sort: { type: "string", enum: ["updated", "created", "pushed", "full_name"], description: "Sort key: `updated` (metadata), `pushed` (commits), `created`, `full_name`." } } }, - { name: "boj_github_get_repo", desc: "Fetch detailed metadata for a single GitHub repository. Read-only; no side effects. Includes description, default branch, topics, visibility, stars, and language breakdown. Returns a full repository object or `{error, status}`. Use before operations that need `default_branch` (e.g. `boj_github_create_pr` with `base` omitted) to ensure correct targeting.", props: { owner: { type: "string", description: "Repo owner login (user or org)." }, repo: { type: "string", description: "Repo name (without owner prefix)." } }, req: ["owner", "repo"] }, - { name: "boj_github_create_issue", desc: "Open a new issue on a GitHub repository. Side-effectful; creates a new record. Returns the created issue metadata `{number, html_url, state, ...}` on success. Pair with `boj_github_comment_issue` for subsequent follow-ups or automated status updates.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, title: { type: "string", description: "Issue title. Keep under 80 chars.", minLength: 1 }, body: { type: "string", description: "Markdown body. Optional but recommended." }, labels: { type: "array", items: { type: "string" }, description: "Label names to attach. Labels must already exist on the repo." } }, req: ["owner", "repo", "title"] }, - { name: "boj_github_list_issues", desc: "List issues on a GitHub repository, filtered by state (open, closed, all). Read-only; no side effects. Does NOT include pull requests (use `boj_github_list_prs` for those). Paginated; default 30. Returns an array of issue objects `[{number, title, state, user, labels, ...}]`. Useful for triaging project status and checking open tasks.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, state: { type: "string", enum: ["open", "closed", "all"], description: "Issue state filter (default `open`)." }, per_page: { type: "number", minimum: 1, maximum: 100, description: "Results per page (1..100)." } }, req: ["owner", "repo"] }, - { name: "boj_github_get_issue", desc: "Fetch a single issue's full details. Read-only; no side effects. Includes title, body, state, labels, assignees, and comments count. Returns the issue object or `{error, status}`. Use before `boj_github_comment_issue` to confirm the issue is still open and to gather context from the description.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, issue_number: { type: "number", minimum: 1, description: "Issue number as shown in the URL `#`." } }, req: ["owner", "repo", "issue_number"] }, - { name: "boj_github_comment_issue", desc: "Post a new Markdown comment on a GitHub issue. Side-effectful; visible to all watchers and triggers notifications. Returns the created comment metadata `{id, html_url, user, ...}` on success. Useful for automated updates or recording investigation findings.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, issue_number: { type: "number", minimum: 1, description: "Issue number." }, body: { type: "string", description: "Markdown comment body. Supports full GitHub-flavored Markdown.", minLength: 1 } }, req: ["owner", "repo", "issue_number", "body"] }, - { name: "boj_github_create_pr", desc: "Open a pull request between two branches. Side-effectful; triggers CI workflows and notifies reviewers. `head` must already be pushed to the remote. Returns the created PR metadata `{number, html_url, state, ...}` on success. Essential for submitting code changes for review.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, title: { type: "string", description: "PR title (keep under 70 chars).", minLength: 1 }, body: { type: "string", description: "Markdown body — should include a summary of changes and a test plan." }, head: { type: "string", description: "Source branch (same-repo) or `owner:branch` (cross-fork)." }, base: { type: "string", description: "Target branch, usually `main`. Defaults to repo's `default_branch` when omitted." } }, req: ["owner", "repo", "title", "head"] }, - { name: "boj_github_list_prs", desc: "List pull requests on a GitHub repository, filtered by state. Read-only; no side effects. Does NOT include non-PR issues (use `boj_github_list_issues` for those). Returns an array of PR summary objects `[{number, title, state, user, head, base, html_url, ...}]`. Useful for monitoring active development and code reviews.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, state: { type: "string", enum: ["open", "closed", "all"], description: "PR state filter (default `open`)." } }, req: ["owner", "repo"] }, - { name: "boj_github_get_pr", desc: "Fetch a single pull request's detailed metadata. Read-only; no side effects. Includes title, body, state, head/base refs, mergeable state, and a summary of CI checks. Returns the PR object or `{error, status}`. Use before `boj_github_merge_pr` to confirm CI is green and the branch is mergeable.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, pull_number: { type: "number", minimum: 1, description: "PR number as shown in the URL." } }, req: ["owner", "repo", "pull_number"] }, - { name: "boj_github_merge_pr", desc: "Merge a pull request into the base branch. Side-effectful and hard-to-reverse; triggers branch deletion (if configured) and deployment workflows. Requires CI status to be green and mergeability confirmed. Returns `{sha, merged:true}` on success. Prefer `squash` for a cleaner linear history on the main branch.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, pull_number: { type: "number", minimum: 1, description: "PR number." }, method: { type: "string", enum: ["merge", "squash", "rebase"], description: "Merge strategy (default `merge`). `squash` collapses all commits into one; `rebase` replays commits linearly." } }, req: ["owner", "repo", "pull_number"] }, - { name: "boj_github_search_code", desc: "Search code across GitHub using the Code Search API v2. Read-only; no side effects. Query syntax supports `repo:`, `language:`, `path:`, `symbol:` qualifiers. Returns an object with `total_count` and an `items` array of match results. Rate-limited to 30 requests per minute. Excellent for finding usage patterns or specific symbols across repositories.", props: { query: { type: "string", description: "Code search query, e.g. `repo:hyperpolymath/boj-server \"coord_register\"`. See GitHub's Code Search syntax documentation.", minLength: 1 } }, req: ["query"] }, - { name: "boj_github_search_issues", desc: "Search issues and pull requests across GitHub. Read-only; no side effects. Query syntax supports `repo:`, `is:issue|pr`, `is:open|closed`, `author:`, `label:`. Returns an object with `total_count` and an `items` array of issues/PRs. Useful for finding existing reports or discussions related to a specific topic.", props: { query: { type: "string", description: "Issues search query, e.g. `repo:foo/bar is:pr is:open label:security`.", minLength: 1 } }, req: ["query"] }, - { name: "boj_github_get_file", desc: "Read a file's contents from a GitHub repository at a specific branch, tag, or commit. Read-only; no side effects. Decodes base64 content automatically for text files. Returns `{path, content, sha, encoding:\"utf-8\"}`. Binary files preserve the raw base64 encoding. Useful for inspecting code or reading configuration files without cloning.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, path: { type: "string", description: "Repo-relative path (no leading slash), e.g. `src/main.js`." }, ref: { type: "string", description: "Branch, tag, or commit SHA. Defaults to the repo's default branch if omitted." } }, req: ["owner", "repo", "path"] }, - { name: "boj_github_graphql", desc: "Execute an arbitrary GitHub GraphQL v4 query. Can be Read-only or Side-effectful depending on the query (query vs mutation). Use when REST endpoints are insufficient, such as for batching requests, fetching nested fragments, or custom data aggregates. Returns the raw GraphQL response `{data, errors?}`. Requires a valid schema-compliant query string.", props: { query: { type: "string", description: "GraphQL document. Supports `query` and `mutation` operations.", minLength: 1 }, variables: { type: "object", description: "Optional GraphQL variables object, keyed by `$name` references inside the query." } }, req: ["query"] }, + { name: "boj_github_list_repos", desc: "List repositories owned by or accessible to the authenticated GitHub user. Read-only; no side effects. Paginated; default 30, max 100 per page. Use `sort` to order by last-activity axis. Returns an array of repository objects `[{name, full_name, private, default_branch, ...}]`. Useful for project discovery.", props: { per_page: { type: "number", minimum: 1, maximum: 100, description: "Results per page (1..100, default 30)." }, sort: { type: "string", enum: ["updated", "created", "pushed", "full_name"], description: "Sort key: `updated` (metadata), `pushed` (commits), `created`, `full_name`." } }, ann: { title: "List GitHub Repos", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Array of GitHub repository objects `[{name, full_name, private, default_branch, ...}]`.") }, + { name: "boj_github_get_repo", desc: "Fetch detailed metadata for a single GitHub repository. Read-only; no side effects. Includes description, default branch, topics, visibility, stars, and language breakdown. Returns a full repository object or `{error, status}`. Use before operations that need `default_branch` (e.g. `boj_github_create_pr` with `base` omitted) to ensure correct targeting.", props: { owner: { type: "string", description: "Repo owner login (user or org)." }, repo: { type: "string", description: "Repo name (without owner prefix)." } }, req: ["owner", "repo"], ann: { title: "Get GitHub Repo", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Full GitHub repository object, or `{error, status}` on failure.") }, + { name: "boj_github_create_issue", desc: "Open a new issue on a GitHub repository. Side-effectful; creates a new record. Returns the created issue metadata `{number, html_url, state, ...}` on success. Pair with `boj_github_comment_issue` for subsequent follow-ups or automated status updates.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, title: { type: "string", description: "Issue title. Keep under 80 chars.", minLength: 1 }, body: { type: "string", description: "Markdown body. Optional but recommended." }, labels: { type: "array", items: { type: "string" }, description: "Label names to attach. Labels must already exist on the repo." } }, req: ["owner", "repo", "title"], ann: { title: "Create GitHub Issue", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, out: passthrough("Created GitHub issue metadata `{number, html_url, state, ...}`.") }, + { name: "boj_github_list_issues", desc: "List issues on a GitHub repository, filtered by state (open, closed, all). Read-only; no side effects. Does NOT include pull requests (use `boj_github_list_prs` for those). Paginated; default 30. Returns an array of issue objects `[{number, title, state, user, labels, ...}]`. Useful for triaging project status and checking open tasks.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, state: { type: "string", enum: ["open", "closed", "all"], description: "Issue state filter (default `open`)." }, per_page: { type: "number", minimum: 1, maximum: 100, description: "Results per page (1..100)." } }, req: ["owner", "repo"], ann: { title: "List GitHub Issues", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Array of GitHub issue objects `[{number, title, state, user, labels, ...}]` (excludes PRs).") }, + { name: "boj_github_get_issue", desc: "Fetch a single issue's full details. Read-only; no side effects. Includes title, body, state, labels, assignees, and comments count. Returns the issue object or `{error, status}`. Use before `boj_github_comment_issue` to confirm the issue is still open and to gather context from the description.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, issue_number: { type: "number", minimum: 1, description: "Issue number as shown in the URL `#`." } }, req: ["owner", "repo", "issue_number"], ann: { title: "Get GitHub Issue", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Full GitHub issue object, or `{error, status}` on failure.") }, + { name: "boj_github_comment_issue", desc: "Post a new Markdown comment on a GitHub issue. Side-effectful; visible to all watchers and triggers notifications. Returns the created comment metadata `{id, html_url, user, ...}` on success. Useful for automated updates or recording investigation findings.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, issue_number: { type: "number", minimum: 1, description: "Issue number." }, body: { type: "string", description: "Markdown comment body. Supports full GitHub-flavored Markdown.", minLength: 1 } }, req: ["owner", "repo", "issue_number", "body"], ann: { title: "Comment on GitHub Issue", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, out: passthrough("Created GitHub comment metadata `{id, html_url, user, ...}`.") }, + { name: "boj_github_create_pr", desc: "Open a pull request between two branches. Side-effectful; triggers CI workflows and notifies reviewers. `head` must already be pushed to the remote. Returns the created PR metadata `{number, html_url, state, ...}` on success. Essential for submitting code changes for review.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, title: { type: "string", description: "PR title (keep under 70 chars).", minLength: 1 }, body: { type: "string", description: "Markdown body — should include a summary of changes and a test plan." }, head: { type: "string", description: "Source branch (same-repo) or `owner:branch` (cross-fork)." }, base: { type: "string", description: "Target branch, usually `main`. Defaults to repo's `default_branch` when omitted." } }, req: ["owner", "repo", "title", "head"], ann: { title: "Create GitHub PR", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, out: passthrough("Created GitHub pull request metadata `{number, html_url, state, ...}`.") }, + { name: "boj_github_list_prs", desc: "List pull requests on a GitHub repository, filtered by state. Read-only; no side effects. Does NOT include non-PR issues (use `boj_github_list_issues` for those). Returns an array of PR summary objects `[{number, title, state, user, head, base, html_url, ...}]`. Useful for monitoring active development and code reviews.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, state: { type: "string", enum: ["open", "closed", "all"], description: "PR state filter (default `open`)." } }, req: ["owner", "repo"], ann: { title: "List GitHub PRs", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Array of GitHub PR summary objects `[{number, title, state, user, head, base, html_url, ...}]`.") }, + { name: "boj_github_get_pr", desc: "Fetch a single pull request's detailed metadata. Read-only; no side effects. Includes title, body, state, head/base refs, mergeable state, and a summary of CI checks. Returns the PR object or `{error, status}`. Use before `boj_github_merge_pr` to confirm CI is green and the branch is mergeable.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, pull_number: { type: "number", minimum: 1, description: "PR number as shown in the URL." } }, req: ["owner", "repo", "pull_number"], ann: { title: "Get GitHub PR", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Full GitHub pull request object including mergeable state and CI summary, or `{error, status}`.") }, + { name: "boj_github_merge_pr", desc: "Merge a pull request into the base branch. Side-effectful and hard-to-reverse; triggers branch deletion (if configured) and deployment workflows. Requires CI status to be green and mergeability confirmed. Returns `{sha, merged:true}` on success. Prefer `squash` for a cleaner linear history on the main branch.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, pull_number: { type: "number", minimum: 1, description: "PR number." }, method: { type: "string", enum: ["merge", "squash", "rebase"], description: "Merge strategy (default `merge`). `squash` collapses all commits into one; `rebase` replays commits linearly." } }, req: ["owner", "repo", "pull_number"], ann: { title: "Merge GitHub PR", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, out: { type: "object", description: "Merge result. Side-effectful and hard-to-reverse.", properties: { sha: { type: "string", description: "Resulting merge commit SHA." }, merged: { type: "boolean", description: "`true` when the merge succeeded." } }, additionalProperties: true } }, + { name: "boj_github_search_code", desc: "Search code across GitHub using the Code Search API v2. Read-only; no side effects. Query syntax supports `repo:`, `language:`, `path:`, `symbol:` qualifiers. Returns an object with `total_count` and an `items` array of match results. Rate-limited to 30 requests per minute. Excellent for finding usage patterns or specific symbols across repositories.", props: { query: { type: "string", description: "Code search query, e.g. `repo:hyperpolymath/boj-server \"coord_register\"`. See GitHub's Code Search syntax documentation.", minLength: 1 } }, req: ["query"], ann: { title: "Search GitHub Code", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: { type: "object", description: "GitHub code-search results.", properties: { total_count: { type: "number", description: "Total matches." }, items: { type: "array", description: "Match result objects." } }, additionalProperties: true } }, + { name: "boj_github_search_issues", desc: "Search issues and pull requests across GitHub. Read-only; no side effects. Query syntax supports `repo:`, `is:issue|pr`, `is:open|closed`, `author:`, `label:`. Returns an object with `total_count` and an `items` array of issues/PRs. Useful for finding existing reports or discussions related to a specific topic.", props: { query: { type: "string", description: "Issues search query, e.g. `repo:foo/bar is:pr is:open label:security`.", minLength: 1 } }, req: ["query"], ann: { title: "Search GitHub Issues", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: { type: "object", description: "GitHub issue/PR search results.", properties: { total_count: { type: "number", description: "Total matches." }, items: { type: "array", description: "Issue/PR result objects." } }, additionalProperties: true } }, + { name: "boj_github_get_file", desc: "Read a file's contents from a GitHub repository at a specific branch, tag, or commit. Read-only; no side effects. Decodes base64 content automatically for text files. Returns `{path, content, sha, encoding:\"utf-8\"}`. Binary files preserve the raw base64 encoding. Useful for inspecting code or reading configuration files without cloning.", props: { owner: { type: "string", description: "Repo owner login." }, repo: { type: "string", description: "Repo name." }, path: { type: "string", description: "Repo-relative path (no leading slash), e.g. `src/main.js`." }, ref: { type: "string", description: "Branch, tag, or commit SHA. Defaults to the repo's default branch if omitted." } }, req: ["owner", "repo", "path"], ann: { title: "Get GitHub File", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: { type: "object", description: "File contents at the requested ref; text decoded from base64, binaries kept as base64.", properties: { path: { type: "string", description: "Repo-relative path." }, content: { type: "string", description: "File contents (decoded UTF-8 or raw base64 for binary)." }, sha: { type: "string", description: "Blob SHA." }, encoding: { type: "string", description: "`utf-8` for text, `base64` for binary." } }, additionalProperties: true } }, + { name: "boj_github_graphql", desc: "Execute an arbitrary GitHub GraphQL v4 query. Can be Read-only or Side-effectful depending on the query (query vs mutation). Use when REST endpoints are insufficient, such as for batching requests, fetching nested fragments, or custom data aggregates. Returns the raw GraphQL response `{data, errors?}`. Requires a valid schema-compliant query string.", props: { query: { type: "string", description: "GraphQL document. Supports `query` and `mutation` operations.", minLength: 1 }, variables: { type: "object", description: "Optional GraphQL variables object, keyed by `$name` references inside the query." } }, req: ["query"], ann: { title: "GitHub GraphQL", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, out: { type: "object", description: "Raw GitHub GraphQL response. Can be a query (read) or a mutation (write) depending on the supplied document.", properties: { data: { type: "object", description: "GraphQL data payload." }, errors: { type: "array", description: "GraphQL errors, present only on failure." } }, additionalProperties: true } }, ]; for (const t of ghTools) { - tools.push({ name: t.name, description: t.desc, inputSchema: { type: "object", properties: t.props, required: t.req || [], additionalProperties: false } }); + tools.push({ name: t.name, description: t.desc, inputSchema: { type: "object", properties: t.props, required: t.req || [], additionalProperties: false }, annotations: t.ann, outputSchema: t.out }); } // GitLab API tools — routed via authenticated backend token; read-only unless noted. const glTools = [ - { name: "boj_gitlab_list_projects", desc: "List GitLab projects accessible to the authenticated user. Read-only; no side effects. Includes personal, group, and starred projects. Paginated; default 20 per page. Returns an array of project objects `[{id, path_with_namespace, visibility, default_branch, ...}]`. Useful for finding projects within a GitLab instance.", props: { per_page: { type: "number", minimum: 1, maximum: 100, description: "Results per page (1..100, default 20)." } } }, - { name: "boj_gitlab_get_project", desc: "Fetch metadata for a single GitLab project. Read-only; no side effects. Includes description, default branch, visibility, topics, and repository statistics. Returns the project object or `{error, status}`. Use before operations that need `default_branch` or `web_url` to ensure correct targeting.", props: { project_id: { type: "string", description: "Either numeric project id or the URL-encoded full path (e.g. `group%2Fsubgroup%2Frepo`).", minLength: 1 } }, req: ["project_id"] }, - { name: "boj_gitlab_create_issue", desc: "Open a new issue on a GitLab project. Side-effectful; creates a new record. Returns the created issue metadata `{iid, web_url, state, ...}` on success. Pair with follow-up comments via the GitLab GraphQL API for complex discussions.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, title: { type: "string", description: "Issue title. Keep under 80 chars.", minLength: 1 }, description: { type: "string", description: "Markdown description. Optional but recommended." } }, req: ["project_id", "title"] }, - { name: "boj_gitlab_list_issues", desc: "List issues on a GitLab project, filtered by state. Read-only; no side effects. Does NOT include merge requests (use `boj_gitlab_list_mrs` for those). Returns an array of issue objects `[{iid, title, state, author, ...}]`. Useful for project tracking and identifying open tasks.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, state: { type: "string", enum: ["opened", "closed", "all"], description: "Issue state filter (default `opened`)." } }, req: ["project_id"] }, - { name: "boj_gitlab_create_mr", desc: "Open a merge request (MR) on a GitLab project. Side-effectful; triggers CI/CD pipelines and notifies reviewers. `source` branch must already be pushed. Returns the created MR metadata `{iid, web_url, state, ...}` on success. Essential for submitting code for review and integration.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, title: { type: "string", description: "MR title (keep under 70 chars).", minLength: 1 }, source: { type: "string", description: "Source branch name.", minLength: 1 }, target: { type: "string", description: "Target branch. Defaults to the project's default branch if omitted." } }, req: ["project_id", "title", "source"] }, - { name: "boj_gitlab_list_mrs", desc: "List merge requests on a GitLab project, filtered by state. Read-only; no side effects. Returns an array of MR summary objects `[{iid, title, state, author, source_branch, target_branch, ...}]`. Useful for monitoring code review progress and integration status.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, state: { type: "string", enum: ["opened", "closed", "merged", "all"], description: "MR state filter (default `opened`)." } }, req: ["project_id"] }, - { name: "boj_gitlab_list_pipelines", desc: "List recent CI/CD pipelines for a GitLab project. Read-only; no side effects. Returns an array of pipeline objects `[{id, status, ref, sha, web_url, created_at}]`. Use after a push or MR creation to monitor automated build and test status.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." } }, req: ["project_id"] }, - { name: "boj_gitlab_setup_mirror", desc: "Configure a GitLab project to mirror its repository to an external URL. Side-effectful; subsequent pushes to GitLab will be automatically mirrored. Returns `{mirror_id, enabled:true}` on success. Confirm destination repository ownership before invoking to avoid unintentional data exposure.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, target_url: { type: "string", description: "External git URL to mirror to (e.g. `https://github.com/owner/repo.git`). Credentials can be embedded as `https://user:token@host/...`." } }, req: ["project_id", "target_url"] }, + { name: "boj_gitlab_list_projects", desc: "List GitLab projects accessible to the authenticated user. Read-only; no side effects. Includes personal, group, and starred projects. Paginated; default 20 per page. Returns an array of project objects `[{id, path_with_namespace, visibility, default_branch, ...}]`. Useful for finding projects within a GitLab instance.", props: { per_page: { type: "number", minimum: 1, maximum: 100, description: "Results per page (1..100, default 20)." } }, ann: { title: "List GitLab Projects", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Array of GitLab project objects `[{id, path_with_namespace, visibility, default_branch, ...}]`.") }, + { name: "boj_gitlab_get_project", desc: "Fetch metadata for a single GitLab project. Read-only; no side effects. Includes description, default branch, visibility, topics, and repository statistics. Returns the project object or `{error, status}`. Use before operations that need `default_branch` or `web_url` to ensure correct targeting.", props: { project_id: { type: "string", description: "Either numeric project id or the URL-encoded full path (e.g. `group%2Fsubgroup%2Frepo`).", minLength: 1 } }, req: ["project_id"], ann: { title: "Get GitLab Project", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Full GitLab project object, or `{error, status}` on failure.") }, + { name: "boj_gitlab_create_issue", desc: "Open a new issue on a GitLab project. Side-effectful; creates a new record. Returns the created issue metadata `{iid, web_url, state, ...}` on success. Pair with follow-up comments via the GitLab GraphQL API for complex discussions.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, title: { type: "string", description: "Issue title. Keep under 80 chars.", minLength: 1 }, description: { type: "string", description: "Markdown description. Optional but recommended." } }, req: ["project_id", "title"], ann: { title: "Create GitLab Issue", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, out: passthrough("Created GitLab issue metadata `{iid, web_url, state, ...}`.") }, + { name: "boj_gitlab_list_issues", desc: "List issues on a GitLab project, filtered by state. Read-only; no side effects. Does NOT include merge requests (use `boj_gitlab_list_mrs` for those). Returns an array of issue objects `[{iid, title, state, author, ...}]`. Useful for project tracking and identifying open tasks.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, state: { type: "string", enum: ["opened", "closed", "all"], description: "Issue state filter (default `opened`)." } }, req: ["project_id"], ann: { title: "List GitLab Issues", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Array of GitLab issue objects `[{iid, title, state, author, ...}]` (excludes MRs).") }, + { name: "boj_gitlab_create_mr", desc: "Open a merge request (MR) on a GitLab project. Side-effectful; triggers CI/CD pipelines and notifies reviewers. `source` branch must already be pushed. Returns the created MR metadata `{iid, web_url, state, ...}` on success. Essential for submitting code for review and integration.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, title: { type: "string", description: "MR title (keep under 70 chars).", minLength: 1 }, source: { type: "string", description: "Source branch name.", minLength: 1 }, target: { type: "string", description: "Target branch. Defaults to the project's default branch if omitted." } }, req: ["project_id", "title", "source"], ann: { title: "Create GitLab MR", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, out: passthrough("Created GitLab merge request metadata `{iid, web_url, state, ...}`.") }, + { name: "boj_gitlab_list_mrs", desc: "List merge requests on a GitLab project, filtered by state. Read-only; no side effects. Returns an array of MR summary objects `[{iid, title, state, author, source_branch, target_branch, ...}]`. Useful for monitoring code review progress and integration status.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, state: { type: "string", enum: ["opened", "closed", "merged", "all"], description: "MR state filter (default `opened`)." } }, req: ["project_id"], ann: { title: "List GitLab MRs", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Array of GitLab MR summary objects `[{iid, title, state, author, source_branch, target_branch, ...}]`.") }, + { name: "boj_gitlab_list_pipelines", desc: "List recent CI/CD pipelines for a GitLab project. Read-only; no side effects. Returns an array of pipeline objects `[{id, status, ref, sha, web_url, created_at}]`. Use after a push or MR creation to monitor automated build and test status.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." } }, req: ["project_id"], ann: { title: "List GitLab Pipelines", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, out: passthrough("Array of GitLab pipeline objects `[{id, status, ref, sha, web_url, created_at}]`.") }, + { name: "boj_gitlab_setup_mirror", desc: "Configure a GitLab project to mirror its repository to an external URL. Side-effectful; subsequent pushes to GitLab will be automatically mirrored. Returns `{mirror_id, enabled:true}` on success. Confirm destination repository ownership before invoking to avoid unintentional data exposure.", props: { project_id: { type: "string", description: "Project id or URL-encoded full path." }, target_url: { type: "string", description: "External git URL to mirror to (e.g. `https://github.com/owner/repo.git`). Credentials can be embedded as `https://user:token@host/...`." } }, req: ["project_id", "target_url"], ann: { title: "Setup GitLab Mirror", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, out: { type: "object", description: "Mirror configuration result. Side-effectful — subsequent pushes are auto-mirrored to the external URL.", properties: { mirror_id: { type: "string", description: "Identifier of the configured mirror." }, enabled: { type: "boolean", description: "`true` when mirroring is active." } }, additionalProperties: true } }, ]; for (const t of glTools) { - tools.push({ name: t.name, description: t.desc, inputSchema: { type: "object", properties: t.props, required: t.req || [], additionalProperties: false } }); + tools.push({ name: t.name, description: t.desc, inputSchema: { type: "object", properties: t.props, required: t.req || [], additionalProperties: false }, annotations: t.ann, outputSchema: t.out }); } // Code Intelligence (CodeSeeker) @@ -272,6 +445,8 @@ function buildToolList() { required: ["operation"], additionalProperties: false, }, + annotations: { title: "CodeSeeker", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: passthrough("Structured CodeSeeker results: code snippets, symbol relations, traversal paths, or Graph-RAG answers depending on the operation."), }); // Research @@ -288,6 +463,8 @@ function buildToolList() { required: ["operation"], additionalProperties: false, }, + annotations: { title: "Academic Research", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + outputSchema: passthrough("Structured Semantic Scholar JSON: paper metadata, citation graphs, references, or author data depending on the operation."), }); // Local coordination (localhost multi-instance AI coordination — local-coord-mcp cartridge) @@ -305,6 +482,18 @@ function buildToolList() { required: ["client_kind"], additionalProperties: false, }, + annotations: { title: "Register Coord Peer", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Newly created peer identity on the loopback coord bus.", + properties: { + peer_id: { type: "string", description: "Generated peer ID `-<4hex>[@]`." }, + token: { type: "string", description: "Session token — must be passed to all subsequent coord_* calls." }, + error: { type: "string", description: "Present only on failure." }, + hint: { type: "string", description: "Remediation hint, present only on error." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -318,6 +507,15 @@ function buildToolList() { required: ["token"], additionalProperties: false, }, + annotations: { title: "List Coord Peers", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "All currently-registered peers on the coord bus.", + properties: { + peers: { type: "array", description: "Peer objects with peer_id, role, status, and capabilities." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -333,6 +531,16 @@ function buildToolList() { required: ["token", "target", "message"], additionalProperties: false, }, + annotations: { title: "Send Coord Message", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Queue result for the sent message.", + properties: { + status: { type: "string", description: "`\"queued\"` on success." }, + recipients: { type: "array", items: { type: "string" }, description: "Peer IDs the message was enqueued for." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -346,6 +554,18 @@ function buildToolList() { required: ["token"], additionalProperties: false, }, + annotations: { title: "Receive Coord Message", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Next inbox message (removed from the queue), or `{empty:true}` when the inbox is empty.", + properties: { + from: { type: "string", description: "Sender peer ID." }, + message: { type: "string", description: "Message payload." }, + ts: { type: "number", description: "Enqueue timestamp." }, + empty: { type: "boolean", description: "`true` when no message was available." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -363,6 +583,17 @@ function buildToolList() { required: ["token", "task"], additionalProperties: false, }, + annotations: { title: "Claim Coord Task", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Claim result, or `{error:\"already claimed\"}` when another peer holds the task.", + properties: { + holder: { type: "string", description: "Peer ID now holding the task." }, + ttl_s: { type: "number", description: "Watchdog TTL in seconds for this claim." }, + error: { type: "string", description: "`\"already claimed\"` on contention." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -377,6 +608,15 @@ function buildToolList() { required: ["token", "status"], additionalProperties: false, }, + annotations: { title: "Set Coord Status", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Acknowledgement that the peer's status string was updated.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + }, + additionalProperties: true, + }, }); // ── Supervision tools ────────────────────────────────────────── @@ -392,6 +632,17 @@ function buildToolList() { required: ["token", "secret"], additionalProperties: false, }, + annotations: { title: "Promote to Master", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Promotion result; demotes the previous master and emits an audit record.", + properties: { + role: { type: "string", description: "`\"master\"` on success." }, + error: { type: "string", description: "Present only on failure (e.g. bad secret)." }, + hint: { type: "string", description: "Remediation hint, present only on error." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -408,6 +659,16 @@ function buildToolList() { required: ["token", "target", "message", "risk_tier"], additionalProperties: false, }, + annotations: { title: "Send Gated Envelope", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Delivery result; tier 2+ from apprentices is quarantined for review.", + properties: { + status: { type: "string", description: "`\"delivered\"` or `\"quarantined\"`." }, + request_id: { type: "integer", description: "Quarantine entry ID, present when status is `quarantined`." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -421,6 +682,15 @@ function buildToolList() { required: ["token"], additionalProperties: false, }, + annotations: { title: "List Quarantine", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Quarantined envelopes awaiting a master/journeyman decision.", + properties: { + requests: { type: "array", description: "Request objects with sender ID, risk tier, and message preview." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -435,6 +705,18 @@ function buildToolList() { required: ["token", "request_id"], additionalProperties: false, }, + annotations: { title: "Read Quarantine Entry", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Full body and metadata of one quarantined envelope; the entry stays queued until approved or rejected.", + properties: { + message: { type: "string", description: "Full message payload." }, + from: { type: "string", description: "Sender peer ID." }, + risk_tier: { type: "integer", description: "Declared risk tier 0..4." }, + ts: { type: "number", description: "Arrival timestamp." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -449,6 +731,16 @@ function buildToolList() { required: ["token", "request_id"], additionalProperties: false, }, + annotations: { title: "Approve Quarantine", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Approval result; removes the entry from quarantine and triggers delivery.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + delivered_to: { type: "string", description: "Target peer ID the message was delivered to." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -464,6 +756,15 @@ function buildToolList() { required: ["token", "request_id", "reason"], additionalProperties: false, }, + annotations: { title: "Reject Quarantine", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Rejection result; removes the entry without delivery and logs the decision to the audit stream.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + }, + additionalProperties: true, + }, }); // ── Track record / affinity tools (Task #13) ─────────────────── @@ -483,6 +784,15 @@ function buildToolList() { required: ["token", "tag", "outcome", "risk_tier"], additionalProperties: false, }, + annotations: { title: "Report Task Outcome", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Acknowledgement that the outcome was recorded into the client_kind-keyed track record.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -496,6 +806,15 @@ function buildToolList() { required: ["token"], additionalProperties: false, }, + annotations: { title: "Get Affinity Scores", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Computed effective_affinity scores over the trailing window (max of last 20 attempts or 7 days).", + properties: { + affinities: { type: "array", description: "(client_kind, tag) → score entries." }, + }, + additionalProperties: true, + }, }); // ── Reassignment engine (Task #14) ───────────────────────────── @@ -511,6 +830,16 @@ function buildToolList() { required: ["token", "tags"], additionalProperties: false, }, + annotations: { title: "Set Declared Affinities", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Acknowledgement with the now-active declared affinity tags.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + tags: { type: "array", items: { type: "string" }, description: "The peer's updated declared tags." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -524,6 +853,16 @@ function buildToolList() { required: ["token"], additionalProperties: false, }, + annotations: { title: "Scan Reassignments", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Reassignment-scanner pass result.", + properties: { + scanned: { type: "integer", description: "Number of (peer, tag) pairs compared." }, + enqueued: { type: "integer", description: "Number of fyi/clarify envelopes enqueued on divergence." }, + }, + additionalProperties: true, + }, }); // ── Master handoff (Task #35) ────────────────────────────────── @@ -540,6 +879,16 @@ function buildToolList() { required: ["token", "new_peer_id", "secret"], additionalProperties: false, }, + annotations: { title: "Transfer Master Role", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Master-handoff result; emits a MASTER_HANDOFF audit record.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + new_master: { type: "string", description: "Peer ID that is now master." }, + }, + additionalProperties: true, + }, }); // ── Variant + capability advertisement (Tasks #33 + #34) ─────── @@ -560,6 +909,16 @@ function buildToolList() { required: ["token", "variant"], additionalProperties: false, }, + annotations: { title: "Set Peer Variant", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Acknowledgement; the variant label is broadcast-visible to all peers.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + error: { type: "string", description: "`\"invalid variant\"` when the label fails validation." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -589,6 +948,15 @@ function buildToolList() { required: ["token"], additionalProperties: false, }, + annotations: { title: "Set Peer Capabilities", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Acknowledgement that the capability profile was advertised for cold-start routing.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -606,6 +974,20 @@ function buildToolList() { required: ["token", "peer_id"], additionalProperties: false, }, + annotations: { title: "Get Peer Capabilities", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Another peer's advertised capability profile, or `{error:\"peer not found\"}`.", + properties: { + client_kind: { type: "string", description: "Peer client family." }, + variant: { type: "string", description: "Free-form model/variant label." }, + class: { type: "array", items: { type: "string" }, description: "Capability classes." }, + tier: { type: "integer", description: "Advertised capability tier." }, + prover_strengths: { type: "array", items: { type: "string" }, description: "Prover/verifier strength tags." }, + error: { type: "string", description: "`\"peer not found\"` when unknown." }, + }, + additionalProperties: true, + }, }); // ── Operational observability ────────────────────────────────── @@ -620,6 +1002,18 @@ function buildToolList() { required: ["token"], additionalProperties: false, }, + annotations: { title: "Coord Bus Health", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Operational snapshot of the coordination bus.", + properties: { + peers: { type: "integer", description: "Registered peer count." }, + quarantine_depth: { type: "integer", description: "Pending quarantined envelopes." }, + active_claims: { type: "integer", description: "Currently held task claims." }, + track_record_fill: { type: "number", description: "Track-record table fill ratio." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -637,6 +1031,17 @@ function buildToolList() { required: ["token", "task"], additionalProperties: false, }, + annotations: { title: "Refresh Claim TTL", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Watchdog-refresh result, or `{error:\"not holder\"}` when the peer does not hold the claim.", + properties: { + ok: { type: "boolean", description: "`true` on success." }, + new_deadline_ms: { type: "number", description: "Refreshed deadline as an epoch-ms timestamp." }, + error: { type: "string", description: "`\"not holder\"` when the peer is not the claim holder." }, + }, + additionalProperties: true, + }, }); tools.push({ @@ -650,9 +1055,74 @@ function buildToolList() { required: ["token"], additionalProperties: false, }, + annotations: { title: "Sweep Watchdog", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, + outputSchema: { + type: "object", + description: "Watchdog-tick result; released claims emit audit records.", + properties: { + released: { type: "integer", description: "Number of expired claims auto-released this tick." }, + }, + additionalProperties: true, + }, }); - return tools; + // ───────────────────────────────────────────────────────────────── + // Teranga scoped tool surface (coherence lever) + // + // The unified-endpoint thesis holds regardless of scope: every + // explicit boj__* tool remains reachable through the generic + // boj_cartridge_invoke dispatcher. Narrowing the *advertised* surface + // only changes discovery, not capability — which is exactly what + // Glama's Server Coherence sub-score rewards. + // ───────────────────────────────────────────────────────────────── + + // Always-present discovery/dispatch core. coord_* is treated as one + // coherent local-coordination unit and is always in `core`. + const CORE_EXACT = new Set([ + "boj_health", + "boj_menu", + "boj_cartridges", + "boj_cartridge_info", + "boj_cartridge_invoke", + ]); + const isCore = (name) => CORE_EXACT.has(name) || name.startsWith("coord_"); + + // Map a tool name to its domain group token (used by CSV scopes). + // e.g. boj_github_list_repos -> "github", boj_browser_click -> "browser". + function domainOf(name) { + if (isCore(name)) return "core"; + const m = /^boj_([a-z]+)_/.exec(name); + if (m) return m[1]; // github | gitlab | cloud | comms | ml | browser + if (name === "boj_research") return "research"; + if (name === "boj_codeseeker") return "codeseeker"; + return "other"; + } + + // Resolve the effective scope: explicit arg > env var > "full". + const rawScope = ( + scope ?? + (typeof process !== "undefined" ? process.env?.BOJ_TOOL_SCOPE : undefined) ?? + "full" + ).trim(); + + // "full" (or unset) — advertise everything, preserving backward + // compatibility with every existing client. + if (rawScope === "" || rawScope.toLowerCase() === "full") { + return tools; + } + + // Otherwise parse a CSV of domain group tokens. "core" is always + // implied so the discovery/dispatch surface is never lost. + const groups = new Set( + rawScope + .toLowerCase() + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0), + ); + groups.add("core"); + + return tools.filter((t) => groups.has(domainOf(t.name))); } export { buildToolList }; diff --git a/mcp-bridge/main.js b/mcp-bridge/main.js index 751be413..5837674e 100755 --- a/mcp-bridge/main.js +++ b/mcp-bridge/main.js @@ -317,7 +317,7 @@ async function handleMessage(line) { sendResult(id, { protocolVersion: "2024-11-05", capabilities: { - tools: { listChanged: false }, + tools: { listChanged: true }, resources: { subscribe: false }, prompts: { listChanged: false }, logging: { @@ -345,6 +345,11 @@ async function handleMessage(line) { } case "tools/list": { + // buildToolList() returns whole tool objects (name, description, + // inputSchema, annotations, outputSchema) and reads BOJ_TOOL_SCOPE + // internally for the Teranga scoped-surface lever. We pass the + // objects through verbatim so annotations + outputSchema reach the + // wire unmodified — no field whitelisting here by design. const tools = buildToolList(); sendResult(id, { tools }); break; diff --git a/mcp-bridge/tests/dispatch_test.js b/mcp-bridge/tests/dispatch_test.js index 6a9b6b3a..d5f1ba02 100644 --- a/mcp-bridge/tests/dispatch_test.js +++ b/mcp-bridge/tests/dispatch_test.js @@ -163,3 +163,129 @@ test("tool names are unique", () => { const dup = names.filter((n, i) => names.indexOf(n) !== i); assert.deepEqual(dup, [], `Duplicate tool names: ${dup.join(", ")}`); }); + +// ----------------------------------------------------------------- +// 7. MCP annotations — every advertised tool must carry the full +// MCP-spec annotations block: a non-empty Title-Case `title` +// string plus the four boolean behaviour hints. Glama and modern +// MCP clients surface these for safety/UX; a missing or +// wrong-typed hint is a coherence regression. +// ----------------------------------------------------------------- +test("every tool has a complete annotations block", () => { + const HINTS = [ + "readOnlyHint", + "destructiveHint", + "idempotentHint", + "openWorldHint", + ]; + for (const t of tools) { + assert.ok( + t.annotations && typeof t.annotations === "object", + `${t.name}: missing annotations object`, + ); + assert.equal( + typeof t.annotations.title, + "string", + `${t.name}: annotations.title must be a string`, + ); + assert.ok( + t.annotations.title.length > 0, + `${t.name}: annotations.title must be non-empty`, + ); + for (const h of HINTS) { + assert.equal( + typeof t.annotations[h], + "boolean", + `${t.name}: annotations.${h} must be a boolean`, + ); + } + } +}); + +// ----------------------------------------------------------------- +// 8. outputSchema — every advertised tool must declare a JSON Schema +// object describing its return, with a non-empty top-level +// description and an explicit additionalProperties flag (Glama +// scores documented return shapes). +// ----------------------------------------------------------------- +test("every tool has a typed, described outputSchema", () => { + for (const t of tools) { + assert.ok( + t.outputSchema && typeof t.outputSchema === "object", + `${t.name}: missing outputSchema`, + ); + assert.equal( + t.outputSchema.type, + "object", + `${t.name}: outputSchema.type must be "object"`, + ); + assert.equal( + typeof t.outputSchema.description, + "string", + `${t.name}: outputSchema.description must be a string`, + ); + assert.ok( + t.outputSchema.description.length > 0, + `${t.name}: outputSchema.description must be non-empty`, + ); + assert.equal( + typeof t.outputSchema.additionalProperties, + "boolean", + `${t.name}: outputSchema.additionalProperties must be set explicitly`, + ); + } +}); + +// ----------------------------------------------------------------- +// 9. BOJ_TOOL_SCOPE back-compat — with the env var unset (and no +// explicit scope arg), buildToolList() must advertise the FULL +// surface. This guards the default-preserving contract: scoping +// is opt-in and must never silently shrink an existing client's +// tool list. +// ----------------------------------------------------------------- +test("BOJ_TOOL_SCOPE unset advertises the full surface (back-compat)", () => { + const saved = process.env.BOJ_TOOL_SCOPE; + try { + delete process.env.BOJ_TOOL_SCOPE; + const full = buildToolList(); + // The full surface includes explicit domain tools that `core` + // would drop — assert a representative explicit tool is present. + const names = new Set(full.map((t) => t.name)); + assert.ok( + names.has("boj_github_list_repos"), + "explicit boj_github_* tools must be advertised when scope is unset", + ); + assert.ok( + names.has("boj_browser_navigate"), + "explicit boj_browser_* tools must be advertised when scope is unset", + ); + assert.equal( + full.length, + tools.length, + "unset scope must match the module-load full surface length", + ); + + // And `core` must be a strict subset (the scope lever works). + const core = buildToolList("core"); + assert.ok( + core.length < full.length, + "core scope must advertise fewer tools than full", + ); + const coreNames = new Set(core.map((t) => t.name)); + assert.ok( + !coreNames.has("boj_github_list_repos"), + "core scope must NOT advertise explicit boj_github_* tools", + ); + assert.ok( + coreNames.has("boj_cartridge_invoke"), + "core scope must keep boj_cartridge_invoke (unified endpoint)", + ); + assert.ok( + [...coreNames].some((n) => n.startsWith("coord_")), + "core scope must keep the coord_* coordination unit", + ); + } finally { + if (saved === undefined) delete process.env.BOJ_TOOL_SCOPE; + else process.env.BOJ_TOOL_SCOPE = saved; + } +});