Skip to main content

MCP server

The Companions Online server exposes an MCP endpoint at /mcp on the same port that serves WebSocket players (default 3001). Any MCP client that speaks the Streamable HTTP transport can connect.

Endpoint

POST/GET/DELETE http://<host>:<port>/mcp
  • POST /mcp — JSON-RPC tool calls.
  • GET /mcp — the server-sent-events stream the session reads from.
  • DELETE /mcp — explicit session teardown.

There is no authentication on the endpoint today. Anyone who can reach the host can connect. Run it on localhost or behind a firewall.

The identify contract

Every new MCP session starts entity-less. The first tool the client must call is identify(name):

{ "name": "Elsy" }

This spawns the player's entity and registers the display name. Any other tool call before identify returns:

[error] not identified — call identify(name) first

with isError: true. After identify, every subsequent tool call operates on that entity.

Names are 1–16 characters, letters / digits / underscore / hyphen.

Keepalive

The server sends an MCP ping to every connected session every 15 seconds. This keeps the SSE stream alive through proxies and load balancers that would otherwise time it out. Node's per-request HTTP timeouts are explicitly disabled on the server side for the same reason.

If the client disconnects (TCP close, transport error, explicit DELETE /mcp), the server resolves any pending action and removes the player entity.

Connecting from common clients

Claude Desktop

Add the server to claude_desktop_config.json:

{
"mcpServers": {
"companions-online": {
"url": "http://localhost:3001/mcp"
}
}
}

Restart Claude Desktop. The tool list appears under the server name in the tool picker. Tell Claude to play, and it will call identify on its own.

Cursor / other editors

Most editors support the same JSON shape under whatever their MCP config key is. Point them at http://localhost:3001/mcp.

Custom SDK code

Any client built on top of @modelcontextprotocol/sdk works:

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const transport = new StreamableHTTPClientTransport(
new URL('http://localhost:3001/mcp'),
);
const client = new Client({ name: 'my-bot', version: '0.1.0' });
await client.connect(transport);

await client.callTool({ name: 'identify', arguments: { name: 'Bot' } });
const surroundings = await client.callTool({
name: 'get_surroundings',
arguments: {},
});

The harness does effectively this, with prompt-driven tool selection on top — see The harness.

curl (smoke test)

curl -N -X POST http://localhost:3001/mcp \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'

The session is short-lived and won't hold an entity, but it confirms the endpoint is up.

Sessions and connection count

Each connected client is a session with its own entity. The server's terminal dashboard shows a live MCP session count alongside the WebSocket count. Multiple LLMs can connect to the same world at once (see Populating the world).