MCP Protocol
NEXUS exposes the app running on your machine to any MCP-compatible client through a local server that identifies itself as nexus-mcp.
Quick facts
| Server name | nexus-mcp |
| Transport | stdio |
| Message framing | newline-delimited JSON-RPC 2.0 — one message per line, no embedded newlines within a message |
| Network port | none |
supportedVersions | ["2026-07-28", "2025-06-18"] |
| Discovery method | server/discover (2026-07-28 era only) |
Transport
The server communicates over standard input and output. Every message is a single JSON-RPC 2.0 object serialized on one line; a message never contains a raw newline inside it. The server opens no network port — there is nothing to bind, firewall, or expose. Anything the process writes to standard output is a valid MCP message; nothing else is written there, so a client can safely treat every line on stdout as parseable JSON-RPC.
Version negotiation
NEXUS's MCP server supports two protocol eras at once. Which era a given request belongs to is decided per request, not per connection:
| 2026-07-28 | 2025-06-18 | |
|---|---|---|
| Handshake | none — server/discover replaces it | initialize / initialized, unchanged |
| Client declares its version | _meta["io.modelcontextprotocol/protocolVersion"] on every request | params.protocolVersion in initialize |
| Client identity | _meta["io.modelcontextprotocol/clientInfo"], normally present | params.clientInfo in initialize |
| Client capabilities | _meta["io.modelcontextprotocol/clientCapabilities"], normally present | params.capabilities in initialize |
tools/list result | adds resultType, ttlMs, cacheScope | plain MCP result |
tools/call result | adds resultType | plain MCP result |
The routing rule is simple and applies independently to every request: if _meta carries a valid string io.modelcontextprotocol/protocolVersion, the request is evaluated against the 2026-07-28 contract. If _meta has no such key at all, the request is served as legacy. There is no connection-wide "mode" the server remembers — a client is expected to pick one era and stay there, but each request is classified on its own, regardless of what a prior request on the same connection looked like.
2026-07-28: no handshake, server/discover is mandatory
A 2026-07-28 client does not call initialize. Instead it calls server/discover, which is required, and which returns the server's supported versions, capabilities, cache behavior for the discovery result itself, and server identity.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": {},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "example-cli", "version": "1.4.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "server/discover",
"supportedVersions": ["2026-07-28", "2025-06-18"],
"capabilities": { "tools": {} },
"ttlMs": 60000,
"cacheScope": "session",
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "nexus-mcp" }
}
}
}Treat resultType as an opaque discriminator to branch on rather than a literal value to hardcode against — this example illustrates the shape, not a guaranteed constant. ttlMs and cacheScope describe how long the discovery result itself may be cached and at what scope; they say nothing about the caching of any other result.
2025-06-18: initialize works unchanged
A legacy client performs the standard MCP handshake.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "example-cli", "version": "1.0.0" }
}
}Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": { "tools": {} },
"serverInfo": { "name": "nexus-mcp" }
}
}The client then sends its initialized notification, as with any MCP server.
tools/list
tools/list is served live from the running workspace — it is never a cached or stale catalog. This has a direct consequence: if no workspace is running, the call fails closed. It does not fall back to a previous catalog and does not return an empty list dressed up as success. The precise error code for this condition is not part of the stable contract — branch on "this call failed," not on a specific code, and treat it as a signal to start a workspace and retry rather than evidence of a broken client.
In the 2026-07-28 era, a successful result adds resultType, ttlMs, and cacheScope around the standard tools array:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "tools/list",
"ttlMs": 30000,
"cacheScope": "workspace",
"tools": [
{ "name": "canvas_get_state", "description": "[Local NEXUS app] Get a bounded summary (revision, role, element counts, labels) of one canvas.", "inputSchema": { "type": "object" } }
]
}
}Every description in the tools array opens with [Local NEXUS app], declaring which plane the tool acts on. See Tool Namespaces for why, and MCP Tools for the families themselves.
tools/call
In the 2026-07-28 era, a successful result adds resultType around the standard MCP tool-result shape:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "tools/call",
"content": [{ "type": "text", "text": "..." }],
"isError": false
}
}See MCP Tools for what each tool accepts and returns, and Canvas Roles for the outcome contract that canvas-mutating calls follow.
resources/list and resources/read
Resources are the read-only counterpart to tools: they expose data, never a mutation. resources/list returns every resource the running workspace can currently serve; resources/read takes a uri and returns its contents as MCP's standard contents array.
Three resource families ship today:
nexus://capabilities— the capability catalogue: every tool's name, description, and (where annotated) its required capability, approval level, side effects, reversibility, compatible window kinds, and restart implications. Read this before calling a tool you haven't used before. A tool not yet annotated reportscapabilities: null— an honest "not catalogued yet," never a guess.nexus://planes— which plane this server acts on, and which it does not: what it addresses, the description prefix every one of its tools carries, and a plain statement that a co-installed Chainabit cloud server addresses an unrelated account with an unrelated workspace identity. Read this once at the start of a session if you have both servers connected — see NEXUS local MCP vs Chainabit cloud MCP.nexus://memory/{entryId}— one shared-memory entry, in the same visibility asmemory_get: another agent's private entry simply does not appear inresources/listand its URI does not resolve.
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/read",
"params": { "uri": "nexus://capabilities" }
}In the 2026-07-28 era, results here add resultType only — ttlMs/cacheScope are tools/list-specific (see the table above).
prompts/list and prompts/get
Prompts are user-selected workflows: a named, reusable script the client can offer a human to run, rather than something an agent decides to call on its own. prompts/get takes a name and returns a description plus a messages array ready to seed a conversation.
Notifications never get a reply
A JSON-RPC notification — a message with no id — never receives a response, including when the server would otherwise have returned an error. If you send a malformed notification, nothing will tell you; do not wait on a reply to one.
Errors
Unsupported protocol version — -32022
Returned whenever a request declares a protocolVersion — via _meta in the 2026-07-28 style, or via initialize.params in the legacy style — that is not in supportedVersions:
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-06-18"],
"requested": "2024-11-05"
}
}
}Malformed _meta — -32602
Returned when _meta engages the io.modelcontextprotocol/ namespace — that is, it carries at least one key under that prefix — but does not carry a valid string protocolVersion. For example, this request supplies client identity but omits the version key:
{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/list",
"params": {},
"_meta": {
"io.modelcontextprotocol/clientInfo": { "name": "example-cli", "version": "1.4.0" }
}
}It is rejected rather than guessed at:
{
"jsonrpc": "2.0",
"id": 8,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"reason": "io.modelcontextprotocol/protocolVersion must be a string"
}
}
}A request that omits the io.modelcontextprotocol/ namespace entirely is not an error — it is simply routed as legacy.