Skip to content

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 namenexus-mcp
Transportstdio
Message framingnewline-delimited JSON-RPC 2.0 — one message per line, no embedded newlines within a message
Network portnone
supportedVersions["2026-07-28", "2025-06-18"]
Discovery methodserver/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-282025-06-18
Handshakenone — server/discover replaces itinitialize / initialized, unchanged
Client declares its version_meta["io.modelcontextprotocol/protocolVersion"] on every requestparams.protocolVersion in initialize
Client identity_meta["io.modelcontextprotocol/clientInfo"], normally presentparams.clientInfo in initialize
Client capabilities_meta["io.modelcontextprotocol/clientCapabilities"], normally presentparams.capabilities in initialize
tools/list resultadds resultType, ttlMs, cacheScopeplain MCP result
tools/call resultadds resultTypeplain 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:

json
{
  "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:

json
{
  "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:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": { "name": "example-cli", "version": "1.0.0" }
  }
}

Response:

json
{
  "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:

json
{
  "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:

json
{
  "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 reports capabilities: 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 as memory_get: another agent's private entry simply does not appear in resources/list and its URI does not resolve.
json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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.

Built with purpose.