@bounded-systems/verbspec-mcp
README.md
# @bounded-systems/verbspec-mcp
Turn any [`@bounded-systems/verbspec`](https://jsr.io/@bounded-systems/verbspec)
verb **`Registry`** into a real **[MCP](https://modelcontextprotocol.io) server**.
verbspec authors each check/generator **once** as a typed `VerbSpec` (Zod
input/output + a business-meaning summary) and projects it to CLI, Anthropic,
OpenAPI, and OpenRPC surfaces — and to MCP *tool descriptors*. What it
deliberately does **not** ship is a server: the MCP wire protocol, the
transport, and result-wrapping. This package is that missing layer, built on the
official [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk).
Hand it a registry, get a server:
```ts
import { serveStdio } from "@bounded-systems/verbspec-mcp";
import { registry } from "./registry";
await serveStdio(registry, { name: "spd", version: "0.1.0" });
```
Every verb becomes an MCP tool whose `inputSchema`/`outputSchema` **are the
verb's own Zod schemas** (zero drift), and whose handler runs the verb. Object-
shaped outputs are returned as MCP `structuredContent` (and advertised as an
`outputSchema`); other outputs degrade to text. A verb that throws surfaces as an
`isError` tool result, not a transport crash.
## Where this sits
One verb spec, projected everywhere — and every MCP server in the org is the same
shape: **verbspec verbs → this base → an optional topic layer.**
```
verbspec author each verb once (Zod input/output + a summary)
└── verbspec-mcp the base — any verb Registry → MCP tools (this package)
└── <topic>-mcp optional, domain-specific layers on top, e.g.:
• verified static-site / website builds — adds read-only,
Sigstore-verified static responses + a resource catalog
(today: @bounded-systems/static-mcp)
```
`verbspec-mcp` is the only mandatory piece: it just **runs the verbs** (checks,
generators, mutations — whatever they do). Anything domain-specific is an
*option* stacked on top — still nothing but verbspec verbs plus this base plus
its own middleware. "Serve a verified website build" is one such topic, not the
core; use the base alone to expose any live verb registry as tools.
## Install
Published to both registries so either ecosystem can consume it:
```bash
deno add jsr:@bounded-systems/verbspec-mcp # Deno / JSR-native
npm install @bounded-systems/verbspec-mcp # Node / npm (+ Bun)
```
`@modelcontextprotocol/sdk` is a normal dependency; `@bounded-systems/verbspec`
is a **peer** — the server shares your project's verbspec instance (and its
`zod`), so there's no dual-package split.
## API
```ts
interface McpServerOptions {
name?: string; // handshake server name (default "verbspec-mcp")
version?: string; // handshake version (default "0.0.0")
instructions?: string; // handshake instructions (how to use the tools)
filter?: (verb: AnyVerbSpec) => boolean; // restrict which verbs are exposed
deps?: () => unknown; // inject a shared deps slice into every verb's run
mapResult?: (out, verb, args) => ToolResult | Promise<ToolResult>; // shape the tool result
dispatch?: boolean; // add a discover/dispatch pair covering the WHOLE registry
}
// Build the configured SDK server WITHOUT a transport — attach your own (e.g. HTTP),
// or register extra surfaces (resources) on the returned server.
function buildMcpServer(registry: Registry, opts?: McpServerOptions): McpServer;
// buildMcpServer + connectStdio. The one-liner most servers want.
function serveStdio(registry: Registry, opts?: McpServerOptions): Promise<void>;
// Connect a server you built yourself to stdio, with stdout hygiene. For servers that
// also register resources and can't use the serveStdio shortcut.
function connectStdio(server: McpServer): Promise<void>;
```
For an HTTP server, take `buildMcpServer(...)` and `.connect()` it to a
Streamable HTTP transport from the SDK.
### Topic layers: `deps` + `mapResult`
A domain-specific server stacks on the base through two seams instead of forking
it. `deps` injects a shared capability slice into every verb's `run(input, deps)`
(dependency injection — one client, or a test mock, for all verbs). `mapResult`
takes control of how a verb's output becomes the tool result (content,
`structuredContent`, `_meta`, `isError`). For example, the verified static-site
layer threads its verifying client via `deps` and renders verified bytes +
provenance `_meta` via `mapResult` — while the base still owns registration,
schema validation, and running the verb.
### A registry too big to register verb-by-verb: `dispatch`
Registering every verb directly means every verb's tool schema ships in every request of every
session. Fine for a handful of verbs; for hundreds, it degrades a client's tool-selection accuracy
and spends context nobody asked to spend. `dispatch: true` adds a fixed-size alternative — two
tools, `discover_verbs` and `dispatch_verb`, that together reach the whole registry regardless of
`filter`:
```ts
await serveStdio(registry, { name: "spd", version: "0.1.0", dispatch: true });
```
- **`discover_verbs`** searches by id substring, summary keyword, or `actor` (the one grouping a
`VerbSpec` already carries) and returns matching ids with their summary and input schema.
- **`dispatch_verb`** runs any verb by `id`, validating `args` against *that verb's own* input
schema, and returns the same result shape a directly-registered tool would.
This composes with `filter` rather than replacing it: register a small hot set directly (as today,
unchanged) and turn on `dispatch` to keep the rest reachable without paying its per-tool context
cost. Off by default — every existing behavior, including `filter`, is unchanged when omitted.
### stdout hygiene
`serveStdio` redirects `console.log`/`info`/`debug` to **stderr**. The SDK writes
JSON-RPC frames to `process.stdout`, so a verb that logs progress during `run()`
would otherwise interleave and corrupt the stream (in Bun, `console.log` writes
to fd 1 natively — patching `process.stdout` alone would miss it). Verbs must not
write to `process.stdout` directly; use stderr for diagnostics.
## License
MIT
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessNo issues