Skip to main content
Every tool is a function of its arguments. The server has no third-party dependencies (the JSON-RPC transport is implemented in the package), never evaluates code and never writes to disk.

Run it

MCP clients start it with npx; nothing to install up front:

Register it in your client

Claude Code:
or, shared with the project, .mcp.json:
Cursor (.cursor/mcp.json) and Claude Desktop (claude_desktop_config.json) use the same mcpServers shape. VS Code (.vscode/mcp.json):
On Windows, a client that cannot start npx directly needs "command": "cmd", "args": ["/c", "npx", "-y", "@botmesh/mcp"].

Documentation tools

--docs <dir> (or BOTMESH_DOCS_DIR; the pre-rename BOTWA_DOCS_DIR is still read) points at a local Markdown tree, such as the docs/ folder of a clone of the repository; the guides are not bundled in the package. Without it the two docs tools are not advertised in tools/list, so a client never sees a tool that cannot work.

From a checkout

In VS Code the repo already ships .vscode/mcp.json: run pnpm build once, then start the botmesh server from the MCP view.

Programmatically

The main entry (createMcpServer, serveStream, runStdio, the protocol revisions, the server identity and EXAMPLE_FLOW_DOCUMENT) is covered by semver; the JSON-RPC framing, the tool catalogue and the docs search live in @botmesh/mcp/internal, with no semver guarantee.

Tools

flow_analyze runs the same describeFlows() and lintFlows() that bot.describe() and bot.lint() use, and flow_* wraps @botmesh/flow-schema, so the server can never disagree with the runtime or the deployment gate.

Valid or invalid is a result, not an error

An invalid document is a successful call whose payload says valid: false with the issues:
isError: true is reserved for the request (missing or mistyped arguments, an unmigratable version, a path outside the docs root), so an agent can tell “my document needs work” from “my call was wrong”. The arguments are checked against the inputSchema that tools/list advertises, before the tool runs: an unknown key, a missing required one, a value outside an enum or a range (format, direction, target up to the current document version, limit from 1 to 25) or a value nested more than 64 levels deep fails with one sentence that names it, for example invalid arguments for flow_validate: unknown argument "doc" (accepted: "document"). Error texts never carry absolute paths: a missing page answers page not found: guide/x.md.

Protocol

  • initialize answers with capabilities: { tools: { listChanged: false } }, the package version and an instructions block that states the invariants that are expensive to get wrong (the v1 API does not exist, documents are data, a handler must not await a conversation it starts, ids are opaque).
  • Supported revisions: 2025-06-18, 2025-03-26, 2024-11-05. A client asking for another one is answered with 2025-06-18 and decides what to do.
  • Every tool carries annotations (readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false), so a client may run them without asking for confirmation.
  • ping, tools/list and tools/call; anything else (resources/*, prompts/*, logging/setLevel) answers -32601, because those capabilities are not advertised.
  • Notifications are never answered (JSON-RPC forbids it), including notifications/initialized.
  • A response sent by the client (a message with result or error and no method) is ignored, never answered.
  • One JSON message per line. A malformed line answers -32700, a line longer than maxMessageLength (16 Mi characters by default, DEFAULT_MAX_MESSAGE_LENGTH) answers -32600 without being buffered, and the session continues. A JSON array is a batch (revision 2025-03-26): it is answered with an array of responses, notifications left out.
  • Nothing but protocol messages is written to stdout; diagnostics go to stderr. If the client closes the pipe, the server stops writing and exits when stdin ends, without an unhandled EPIPE. A --docs directory that does not exist is reported on stderr at start-up.
With --docs, docs_search indexes every .md under the directory except generated output (api/, dist/, .vitepress/) and the internal design notes (superpowers/), and splits them into heading-delimited sections (a # inside a fenced code block is not a heading). Ranking is deterministic and deliberately simple: accent- and case-insensitive terms, inverse document frequency so a rare identifier (appendLog) outranks a word that appears on every page (durabilidad), presence instead of raw counts, a doubled weight for heading hits, a coverage bonus and an exact-phrase bonus. English and Spanish pages are indexed together, so one query reaches both. docs_page only reads .md files and rejects any path that escapes the docs root.

Tests

pnpm --filter @botmesh/mcp test covers the framing (messages split across chunks, \r\n, a last line without a newline, UTF-8 chunks, batches, oversized lines, a seeded fuzz of raw chunks), the argument checks, every tool with valid and invalid input, the 14 step kinds, subflow resolution, migration of a versionless document, a bilingual search over a temporary tree (including ../ rejection) and the version lockstep with package.json.