Build on Vyre
The MCP hub
Claude reaches Vyre through MCP. Every tool a module registers shows up in Claude Code as a tool of one MCP server, vyre, with no per-tool wiring. The other direction, Vyre running other MCP servers for you with credentials from the vault, is the MCP hub (the mcp module, core/mcp/). Its tools come through the same vyre server, so a session has one entry for everything.
Vyre as an MCP server
The Harness plugin declares one server in harness/.mcp.json:
{ "mcpServers": { "vyre": { "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/mcp/server.js"] } } }
harness/mcp/server.js is a stdio JSON-RPC server with no dependencies and no tools of its own. It lists what vyred has and forwards calls, so a module added to vyred appears in the next session without a change here.
- Listing. On
tools/listit asks vyred forGET /v1/toolsas the callermcpand offers each tool with its description and input schema. If vyred is not running, it starts it and asks again. - Names. MCP names allow letters, digits,
_and-, so dots become underscores, cut to 64 characters:recall.searchisrecall_search. Claude Code shows it asmcp__vyre__recall_search, ormcp__plugin_vyre_vyre__recall_searchwhen the plugin is installed through Claude Code's plugin system. - Calling. On
tools/callit maps the name back and posts to/v1/tools/<tool>. The result comes back as text (JSON, pretty-printed) and, for an object, asstructuredContent. An error comes back asisErrorwith the text<code>: <message>. - Which session. Each call says which session it comes from, with the key the session's
SessionStarthook was given for itsclaudeprocess. vyred checks it and passes the verifiedthreadto the tool. The key is read on every call, since/clearstarts a new session. - Timeouts. 120 seconds per call;
agents.askgets 10 minutes, because it waits for a whole turn of another session. - Hub tools. Beside the module tools it lists
mcp.tools, from the hub's cache, so listing never starts a server. Each is named<server>__<tool>, which no module tool can take (module tool names have one underscore between words). A tool the hub will hold starts its description with "(held for approval) ". A call goes tomcp.callwith the session key; a held call answers with the held item as text and instructuredContent.held, not as an error. - Changes.
listChangedis false. A tool added while a session runs appears in the next session. vyre mcp. Runs this same server on stdio for aclaudestarted outside Vyre. It imports the server rather than spawning it, so the server's parent is stillclaudeand it finds its session.vyre mcp installprintsclaude mcp add -s user vyre -- vyre mcpand runs it only with--yes.
What is not offered
- Tools named
harness.*: they are the hooks' own. - Tools whose
callerslist leaves outmcp, andinternalorhooktools. - Inside an agent's thread, when the agent is not the assistant:
threads.*andagents.*, the tools that drive other sessions.
Inside an agent's thread
The Switchboard sets VYRE_AGENT and a key in the agent's environment. The server then calls as mcp:agent:<name>, and vyred believes that name only with the key of a live thread of that agent. recall.search is held to the folders of the agent's projects (VYRE_PROJECTS, VYRE_SCOPE_CWDS); an agent with no project folders searches nothing.
Where the server is loaded
Every Claude Code session Vyre starts, from the vyre home, the Vyre app, Chat, Lumen or an agent, runs claude with --plugin-dir pointing at harness/. Your global Claude Code setup is never modified. A lean one-question thread (lean: true on threads.start) loads no plugin and no MCP servers at all (--strict-mcp-config with no config).
Every Claude session Vyre starts, lean or not, runs with --strict-mcp-config and an explicit config that names only Vyre's own server (the Agent SDK path sets strictMcpConfig the same way). So the session never loads the connectors a Claude account carries (Gmail, Drive, Slack and the like), the person's own user-scope servers, or the servers of other plugins. A server the person wants in Vyre sessions is added through the hub, and its tools go through the Gate. Two things are outside this: a Claude session you start yourself in a terminal is your own and still loads that account's connectors, and scripts/claude-connector-check.mjs should be rerun after each Claude Code upgrade. Grok sessions start with Grok's import of Claude and Cursor MCP servers switched off; Codex reads only the account's own Vyre-made home.
The hub
The mcp module runs any number of MCP servers (stdio, streamable HTTP and SSE) behind Vyre. People add them with mcp.add, vyre connect add mcp or the Vyre app; see connectors.
- Rows hold names, never values. A server row names vault items:
auth { type: bearer | env | oauth | service-account, item }, andenv { VAR: item }for a stdio server.mcp.addrefuses a header, env value, argument or url that looks like a credential, and anyVYRE_variable. Plain http is allowed only to this machine, the private network (100.64.0.0/10) or an origin undermcp.httpHosts. - Credentials at call time. Values are fetched under the
mcpgrant when a server starts or a request is sent: an env for one stdio child, a header per HTTP request. A stdio child getsPATH,HOME,LANG,TMPDIR, its own env and nothing of vyred's. A 401 invalidates the token, reconnects and retries once. Every result and error is scrubbed of every value touched, and a result is cut at 256 KB. - Light. Nothing starts at boot. A server starts on first use, its tools are cached, and it stops after
idle(10 minutes by default) on one timer. A server that fails more than three times in five minutes stays failed untilmcp.restart. - Scope. A row has
scope { projects, agents }. For a model caller, scope follows what vyred verified: the agent from its key, the thread from the session key and the thread's project from the Switchboard. A claim in the input is never used.mcp.callchecks scope again, whatever was listed. - Who may manage.
mcp.add,mcp.update,mcp.remove,mcp.testandmcp.restartare for people and modules (cli,local,deck,capsule,module), never a model.mcp.servers,mcp.toolsandmcp.callare open to all, scoped as above.
What goes through the Gate
- A tool is a read when its name has a read verb (
list,get,search,read,find,fetch,query,describe,lookup,view,show), or its annotations sayreadOnlyHint: true, and it has no send, write or delete word. Everything else is outward. Unknown is outward. - A person can set
tools.modeper tool:read,write(held) oroff(hidden). A tool with a send word (send,post,reply,forward,publish,share,invite,tweet,dm,comment) is held whatever the mode says, andmcp.addrefusesreadfor one. - An outward call becomes
gate.request { kind, via: "mcp:<server>", to, content: { tool, arguments } }.kindisdelete,spendorsendby the tool's name.tois the first of the argumentsto,channel,recipient,email,address,url, else the server's name. The model gets{ held, message }. - Each server is a Gate sender,
mcp:<server>, registered withgate.offer. On approval the Gate callsmcp.release(internal,module:gateonly), which runs exactly the approved arguments on the server the item was held for.
Never a route around the floor
The Harness Rules ask about any MCP tool whose name means sending, and gate.route denies such a tool to an agent. Both step aside for mcp__vyre__<server>__<tool> (and mcp__plugin_vyre_vyre__...), because the hub already holds every such call at the Gate and its rule is stricter than the name rule. Everything else in the floor still applies to hub tools: a path or command that reaches the vault is denied whatever the tool.
That step-aside is sound only because:
- every hub tool with a send word is held, whatever the person's mode;
- Vyre's own MCP server is never a hub server:
mcp.addrefusesvyre mcpandharness/mcp/server.js, every hub child carriesVYRE_HUB_CHILD=1, and the Vyre MCP server answers every request with an error when it sees it; - a model can never add or change a server, release a held call or approve one.
Other MCP servers in your own Claude Code setup are not the hub's: the Rules watch their tools by name. See the security floor, rules 1 and 2.
The google module (core/google/) is the other connector: Calendar and Gmail over REST with an OAuth refresh token or a domain-wide-delegation service account from the vault. It mints the narrowest scope each call needs, holds google.mail.send and any event with attendees at the Gate as google:<account>, and adds accounts through google.add or the google.connect sign-in flow. See connectors.
Next
- Tools and events: offering a tool Claude can call.
- The module contract:
callers,internaland the checks every call passes. - Agents: what an agent's thread can see.
- Tools reference: every
mcp.*andgoogle.*tool.