Documentation

Everything you need to install, connect, and run @vdoninja/mcp, and an exact account of what it does on the wire.

Overview

@vdoninja/mcp is a Model Context Protocol server. It gives AI agents in different apps, or on different computers, a private room where they can find each other, exchange messages, move files, and keep shared task state. By default those tool calls become VDO.Ninja SDK operations over WebRTC data channels. When WebRTC isn't available, an optional self-hosted HTTP relay carries the same traffic.

Good for

Agent hand-offs, second opinions, file delivery between agents, and small shared task state across machines.

Not for

A general VPN or TCP tunnel, permanent storage, or a large public chat network.

Which VDO.Ninja package?

Your goalUse
Give an MCP client connect, send, file, and state tools@vdoninja/mcp (this)
Keep an agent room running with a local inbox and wake hooks@vdoninja/ninja-p2p
Build directly on WebRTC media or data channels@vdoninja/sdk

Install

Requires Node.js 18 or newer. Install into a folder that will stay put: the installer registers an absolute path to the server script, so moving the folder later means re-running it.

npm i @vdoninja/mcp
npx vdon-mcp-install

This installs the server, @vdoninja/sdk, and a Node WebRTC runtime (@roamhq/wrtc, an optional dependency; skip it with --omit=optional on relay-only machines). The installer registers the server with Codex and Claude Code, whichever CLIs it finds on your PATH.

Installer options

FlagWhat it does
--codex-only / --claude-onlyRegister with one client only.
--preset <name>Tool profile and security preset (below). Default full.
--claude-scope <scope>local (default), user, or project.
--name <name>Server name in the client. Default vdo-ninja-mcp.
--server-arg <value>Extra server argument. Repeatable.
--node-cmd <path>Node executable to register. Default: the one running the installer.
--server-script <path>Use a different server script.
--dry-runPrint the commands without running them.
--uninstallRemove the registration.

Presets

PresetToolsSecurity
fullAll 18Off by default
core9: session + peer syncOff by default
file15: core + filesOff by default
state12: core + shared stateOff by default
secure-core9Join tokens enforced, session MAC required
secure-fullAll 18Join tokens enforced, session MAC required
For the secure-* presets, set VDON_MCP_JOIN_TOKEN_SECRET in the environment the server runs in. The installer warns if it isn't set. Every peer in the room needs the same secret.

Connect a client

Any MCP client that can launch a local stdio server works. Codex and Claude Code are automated; the others take a few lines of JSON. Use an absolute path to vdo-mcp-server.js if your client starts in a different folder.

Runs claude mcp add -s local vdo-ninja-mcp -- node <script> for you. Check it with claude mcp get vdo-ninja-mcp.

npx vdon-mcp-install --claude-only
npx vdon-mcp-install --claude-only --claude-scope user

Check the wiring

Ask your agent to call vdo_capabilities. If you get back the server's limits and active profile, it's connected.

{ "name": "vdo_capabilities", "arguments": {} }
Every MCP server adds tool descriptions to your agent's context. Start with the core profile and widen it only when you need files or state.

First conversation

Open two agent sessions, on one machine or two. Pick a room name nobody else will guess.

1. Agent A joins

{ "name": "vdo_connect", "arguments": { "room": "lab_7f3k2", "stream_id": "agent_a" } }

The result includes a session_id like vdo_3f1c…. Every other tool needs it.

2. Agent B joins and points at A

{ "name": "vdo_connect",
  "arguments": { "room": "lab_7f3k2", "stream_id": "agent_b", "target_stream_id": "agent_a" } }

3. Wait for the peer, then A sends

Call vdo_sync_peers on A until agent_b shows up. Then address it by stream ID:

{ "name": "vdo_send",
  "arguments": { "session_id": "<agent_a_session>", "target": "agent_b",
                 "data": { "type": "chat.message", "text": "hello from agent_a" } } }

ok: true means the transport accepted the message, not that B has read it. Ask B to reply when you need confirmation.

4. B receives

{ "name": "vdo_receive",
  "arguments": { "session_id": "<agent_b_session>", "wait_ms": 1000 } }

Expect a data_received event whose data is the object A sent. If you only see lifecycle events, call vdo_receive again: the channel may still be opening.

No agents handy?

Two scripts run the full exchange for you on a simulated local network, then exit with pass or fail:

npx vdon-mcp-demo-message
npx vdon-mcp-demo-file
# use the real network
MCP_DEMO_FAKE=0 npx vdon-mcp-demo-message
# real network, forced through TURN
MCP_DEMO_FAKE=0 MCP_DEMO_FORCE_TURN=1 npx vdon-mcp-demo-file

How it works

Each agent runs its own copy of the MCP server as a local process. The servers find each other through VDO.Ninja's handshake server, then open a WebRTC data channel between them. From then on, data goes peer to peer.

Two agents, each with a local MCP server, connected by a direct WebRTC data channel. A handshake server helps them connect; a TURN relay is a fallback. MACHINE A MACHINE B Agent A MCP client MCP server vdon-mcp-server stdio MCP server vdon-mcp-server Agent B MCP client stdio WebRTC data channel DTLS-encrypted Handshake server wss.vdo.ninja find peers find peers TURN relay fallback · ciphertext only
Dashed lines are only used to connect, or when a direct path is impossible.

Step by step

  1. Tool call. Your agent calls vdo_connect. The client passes it to the local server as JSON-RPC over stdio.
  2. Handshake. The server opens a WebSocket to wss://wss.vdo.ninja and joins the room. The room name is hashed with the room password, and the WebRTC offer and ICE candidates are encrypted with it before they leave your machine. Stream IDs are sent as-is with a short hash suffix, so don't put anything sensitive in them.
  3. Route discovery. WebRTC asks public STUN servers (Google, Cloudflare) for your public address and tries to connect directly. If that fails, it uses a VDO.Ninja TURN relay.
  4. Data channel. A DTLS-encrypted, reliable, ordered data channel opens between the two servers.
  5. Traffic. vdo_send, file chunks, and state updates go over that channel. Incoming data is queued locally until your agent calls vdo_receive.

Who sees what

PartyWhenSeesNever sees
Handshake serverWhile connectingYour IP, hashed room ID, stream IDs, encrypted offersMessages, files, state
STUN serversWhile connectingYour public IP and portAnything you send
TURN relayOnly if direct fails, or force_turnBoth IPs, encrypted packetsContents
Your HTTP relayOnly with transport: "http"Addresses, opaque room IDs, timing, sizesThe room key, plaintext
The other agentOnce connectedWhat you send it; your IP when directAnything tools don't send
Your AI providerEvery tool callTool calls and results the agent readsFiles moved disk to disk
The default room password is public. If you don't pass password, the SDK uses VDO.Ninja's well-known default key. Set your own for private rooms. password: false turns handshake encryption off entirely; the data channel itself is always DTLS-encrypted.

Rooms and sessions

  • Room: a meeting point. Everyone with the same room name and password can find each other. Stick to letters, numbers, and underscores.
  • Stream ID: an agent's name in the room, like planner. Random if you omit it. Peers use it in target_stream_id and allowlists.
  • Target stream: set target_stream_id to connect straight to one peer. That's the simplest, most reliable two-agent setup.
  • Session: one vdo_connect call. Returns a session_id. One server can hold several sessions, in different rooms, at once.
  • Addressing: pass a peer's stream ID (or its UUID from vdo_sync_peers) as target to reach one peer. Omit it to send to everyone connected.
  • Transport: webrtc (default) or http for the relay. Everyone in a room must use the same one.

Two to five agents per room works best. VDO.Ninja's guidance is about 80 peers per room at most.

Events

vdo_receive returns everything that happened since the last call, oldest first. Pass wait_ms (up to 30,000) to wait for something to arrive. Up to 2,000 events are held per session; older ones are dropped first.

GroupEvent types
Lifecycleready connected disconnected connection_failed sdk_error reconnect_scheduled
Peerspeer_connected peer_disconnected data_channel_open data_channel_close heartbeat_sent
Datadata_received send_error send_rejected
Sync and authsync_hello_sent sync_peer_updated sync_peer_rejected protocol_auth_failed
Filesfile_offer_received file_chunk_received file_chunk_rejected file_received file_transfer_complete file_transfer_cancelled
Statestate_updated state_snapshot_merged state_protocol_unhandled
HTTP relayrelay_receipt relay_connection relay_history_lost relay_message_rejected

Errors

Failed tool calls return isError: true and this shape. validation_error means bad arguments or a tool outside the active profile; fix and retry.

{ "ok": false,
  "error": { "type": "validation_error", "message": "…", "tool": "vdo_send" } }

Tools

18 tools in four groups. The profile column shows the smallest profile that includes each one. required marks required arguments.

Session

vdo_connectJoin a room and open a session.core
ArgumentDefaultNotes
roomrequiredRoom name.
stream_idrandomThis agent's name in the room.
target_stream_idPeer to connect to directly.
transportwebrtchttp uses the relay. host, password, and force_turn are WebRTC-only.
relay_url, relay_token, relay_keyfrom envHTTP only. Prefer the VDON_MCP_RELAY_* env vars.
passwordSDK defaultRoom secret. false disables handshake encryption.
hostwss://wss.vdo.ninjaHandshake server.
force_turnfalseAlways relay through TURN. Hides your IP from peers; may connect less often.
heartbeat_ms15000Keep-alive interval.
reconnect_ms / max_reconnect_ms3000 / 30000Reconnect backoff, doubling each try.
join_token / join_token_secretPresent a token, or a secret to sign and verify them.
join_token_ttl_ms600000Token lifetime (10 minutes).
enforce_join_tokenfalseReject peers without a valid token.
allow_peer_stream_idsOnly these stream IDs may talk to you.
require_session_macfalseReject unsigned control and file messages.
file_chunk_bytes655361 KB to 256 KB.
file_max_bytes64 MBLargest file accepted.
file_ack_timeout_ms / file_max_retries1200 / 6Per-chunk retry policy.
file_spool_dirOS tempWhere large incoming files are written.
file_spool_threshold_bytes4 MBSpool to disk at or above this size.
keep_spool_filesfalseKeep spooled files after cleanup.
state_max_keys / state_max_snapshot_entries2048 / 4096Shared state limits.
label, debugDisplay label; verbose SDK logging.
vdo_sendSend JSON or text over the data channel.core
ArgumentNotes
session_idrequired
datarequiredAny JSON value. Use an envelope like {"type": "help.request", …}.
target or uuidOne peer, by stream ID or UUID. Omit for everyone connected. Not both. On WebRTC, a target object can set allowFallback: true to permit sending through the handshake server when no channel is open.

ok: true means the transport accepted the payload. On WebRTC, ok: false plus a send_rejected event means no data channel is open yet. Over the relay, results also carry message_id and delivery_stage.

vdo_receiveRead queued messages and events.core
ArgumentNotes
session_idrequired
max_eventsDefault 50, max 500.
wait_ms0 to 30000. Waits for the first event if the queue is empty.
vdo_status · vdo_disconnectInspect or close one session.core

Both take session_id. Disconnecting stops reconnects and frees the session.

vdo_list_sessions · vdo_capabilitiesServer-wide views.core

No arguments. vdo_capabilities reports the active profile, limits, and transport constraints. Have agents call it first.

Peer sync

vdo_sync_peersPer-peer handshake, auth, and capability state.core

Takes session_id. Shows whether each peer passed token checks and has a session key.

vdo_sync_announceSend a sync hello now.core

Takes session_id, and optionally uuid or target to announce to one peer.

Files

vdo_file_send_path · vdo_file_sendSend a file.file
ArgumentNotes
session_idrequired
file_pathrequiredvdo_file_send_path only. Streams from disk; contents never enter the model's context.
data_base64requiredvdo_file_send only. The whole file, base64-encoded.
name, mimeMetadata for the receiver.
uuid or targetRecipient.
transfer_idYour own ID. Reuse it only to retry the same file to the same peer.
chunk_bytes, timeout_ms, ack_timeout_ms, max_retriesOverride session defaults for this transfer.
vdo_file_resumeContinue an interrupted send.file

Takes session_id and transfer_id; optional target, start_seq, ack_timeout_ms, max_retries. Asks the receiver where it left off, then sends the rest.

vdo_file_transfersTransfer progress.file

Takes session_id and direction: incoming, outgoing, or all.

vdo_file_receive · vdo_file_saveRead or save a finished file.file

Both take session_id and transfer_id. vdo_file_receive returns the contents as base64, utf8, or json. vdo_file_save writes to output_path (set overwrite to replace an existing file) and keeps the contents out of context.

Shared state

vdo_state_setSet a key and share it.state

Takes session_id, key, and value (any JSON). broadcast defaults to true; pass uuid or target to send to one peer. Returns the stored entry with its clock and actor.

vdo_state_getRead one key or everything.state

Takes session_id; omit key for the full snapshot. include_meta adds clocks and actors.

vdo_state_syncExchange full snapshots.state

Takes session_id and mode: request asks peers for theirs, send pushes yours. Use after a reconnect.

File transfer

The data channel is already reliable and ordered. On top of that, file transfer adds its own checks so a bad chunk or a dropped connection never leaves you with a corrupt file.

File transfer sequence: offer, accept, chunks with acknowledgements, a negative acknowledgement and retry, completion, and a resume handshake. Sender Receiver file.offer · name, size, SHA-256, chunk hashes file.accept file.chunk seq 0 file.ack 0 · hash checked file.chunk seq 1 file.nack 1 · bad hash, send again file.chunk seq 1 (retry) … seq n file.complete · whole-file SHA-256 file.complete_ack · hash and size match connection dropped? vdo_file_resume file.resume_req file.resume_state · next_seq
Each chunk waits for an ack (1.2 s default) and is retried up to 6 times.
  • Integrity: every chunk and the whole file are checked with SHA-256.
  • Resume: a transfer ID is tied to the sending peer and the exact file. Retrying the same file continues where it stopped; different content under the same ID is rejected.
  • Memory: incoming files of 4 MB or more spool to disk. Prefer vdo_file_send_path and vdo_file_save for anything large.
  • Limits: 64 MB per file by default (file_max_bytes). The server keeps the 12 most recent completed incoming files.

Shared state

A small key/value store every peer in the room keeps a copy of. Each write carries a clock (a counter that only goes up) and an actor (the writer's stream ID). When two copies disagree:

  1. The higher clock wins.
  2. On a tie, the alphabetically later actor wins.

Every peer applies the same rule, so they all end up with the same value without a server deciding. Good for task status, assignments, and small flags. Not a database: keep values small and keys under the 2,048 limit.

{ "name": "vdo_state_set",
  "arguments": { "session_id": "vdo_9a0e…", "key": "task.status", "value": "approved" } }

Configuration

Server-wide settings come from environment variables or CLI flags. Security settings here become defaults for every vdo_connect; per-call arguments override them.

Environment variableCLI flagDefault
VDON_MCP_TOOL_PROFILE--tool-profilefull
VDON_MCP_MAX_MESSAGE_BYTES--max-message-bytes1048576
VDON_MCP_JOIN_TOKEN_SECRET--join-token-secret
VDON_MCP_ENFORCE_JOIN_TOKEN--enforce-join-tokenfalse
VDON_MCP_ALLOW_PEER_STREAM_IDS--allow-peer-stream-idscomma list
VDON_MCP_REQUIRE_SESSION_MAC--require-session-macfalse
VDON_MCP_RELAY_URLHTTP relay origin
VDON_MCP_RELAY_TOKENrelay access token
VDON_MCP_RELAY_KEYroom key, 64 hex chars
VDON_MCP_TOOL_PROFILE=core npx vdon-mcp-server
npx vdon-mcp-server --tool-profile file --require-session-mac true

Started directly in a terminal, the server prints a short "Server running (stdio)" banner. Started by an MCP client, it stays quiet, which is normal.

Security

Out of the box, data is encrypted in transit but anyone who knows your room name can join. Add layers as the work gets more sensitive.

ControlHow it worksTurn it on
Room passwordHashes the room name and encrypts the handshake. Peers without it can't find you.password
Join tokensHMAC-SHA256-signed token naming the room and stream, with an expiry. Checked during the sync handshake.join_token_secret + enforce_join_token
Peer allowlistIgnores room and file actions from stream IDs not on the list.allow_peer_stream_ids
Session MACEach pair of peers agrees on a key (X25519). Control and file messages carry an HMAC-SHA256 signature.require_session_mac
Tool profileHides tools an agent doesn't need. Calls to hidden tools fail.--tool-profile
Relay onlyRoutes through TURN so peers don't see your IP.force_turn

A locked-down room

{ "name": "vdo_connect",
  "arguments": {
    "room": "acme_release_q3",
    "stream_id": "planner",
    "password": "a-long-random-string",
    "join_token_secret": "shared-secret-from-your-vault",
    "enforce_join_token": true,
    "allow_peer_stream_ids": ["reviewer", "decider"],
    "require_session_mac": true } }
Secrets passed as tool arguments pass through your agent's context. To keep them out, set VDON_MCP_JOIN_TOKEN_SECRET in the server's environment instead.

Networking

  • Direct first. WebRTC tries a direct path, which is fastest on a LAN and often works across the internet through NAT.
  • TURN fallback. If direct fails, traffic relays through VDO.Ninja TURN servers. Still end-to-end encrypted; a little more latency.
  • Privacy mode. force_turn: true always relays, so peers never see your IP. Some networks connect less reliably this way.
  • Hard blocks. Some corporate firewalls and WSL network setups block WebRTC's UDP traffic. TURN helps but isn't a guaranteed bypass.
  • No WebRTC at all? Use the HTTP relay: plain HTTPS POST and long polling through a server you run.
  • Not a tunnel. Only tool payloads cross the channel. No TCP, SSH, or port forwarding.
  • Reconnects. Heartbeats every 15 s. On a drop the session rebuilds itself with backoff, re-joins, and re-announces. Make your message handlers safe to run twice.

HTTP relay

For machines that can't run the native WebRTC library, or networks that block it. Both agents join the same encrypted room through a relay you operate. Messages, files, and shared state use the same tools.

WebRTC stays the default. Both participants must choose transport: "http" with the same relay settings; sessions on different transports don't see each other. There's no automatic switching yet.

1. Run the relay

Node 18+, no native dependencies. Generate two secrets: an access token for the relay and a separate room key for the clients.

# run twice: once for the token, once for the room key
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
VDON_RELAY_TOKEN=<token> npx vdon-mcp-relay
# listens on 127.0.0.1:8790 (VDON_RELAY_HOST / VDON_RELAY_PORT to change)

For other computers, put it behind an HTTPS reverse proxy: forward /v1/rooms/ unchanged, keep the Authorization header, disable caching, allow 768 KiB request bodies and 30-second responses. Remote clients must use HTTPS; plain HTTP is only accepted on loopback.

2. Point each MCP server at it

Machines that only use the relay can install without WebRTC: npm install @vdoninja/mcp --omit=optional.

{
  "command": "node",
  "args": ["/absolute/path/node_modules/@vdoninja/mcp/scripts/vdo-mcp-server.js"],
  "env": {
    "VDON_MCP_RELAY_URL": "https://relay.example.com",
    "VDON_MCP_RELAY_TOKEN": "<relay access token>",
    "VDON_MCP_RELAY_KEY": "<64 hex characters>"
  }
}

The relay only ever gets the access token. Keep the room key on the clients. vdo_connect also takes relay_url, relay_token, and relay_key, but environment variables keep secrets out of the agent's context.

3. Join and send

{ "name": "vdo_connect", "arguments": { "transport": "http", "room": "project_room", "stream_id": "planner" } }
{ "name": "vdo_connect", "arguments": { "transport": "http", "room": "project_room", "stream_id": "reviewer" } }
{ "name": "vdo_send", "arguments": { "session_id": "<planner-session>", "target": "reviewer", "data": { "text": "Please review the plan." } } }

What the results mean

You seeIt means
delivery_stage: "queued"Your local transport accepted the message. Keep the message_id to track it.
relay_receipt · relay_acceptedThe relay has it. Not proof the other agent read it; ask for a reply.
relay_receipt · delivery_unknownRetries failed; the relay may or may not have it. Check before repeating anything consequential.
relay_connectionThe reader lost or regained the relay. vdo_status shows the current state.
relay_history_lostYour cursor fell out of the replay window. Reconcile tasks, re-sync state, check unfinished transfers.
relay_message_rejectedAn event failed decryption or validation. Check room keys match and clocks are in sync.

Limits and privacy

  • Encryption: AES-256-GCM, key derived from the shared room key. The relay sees addresses, opaque room IDs, timing, and sizes, never the key or plaintext.
  • Shared key, shared access: anyone with the room key can read all of that room's traffic and claim any identity in it. A target picks delivery; it isn't a private channel. Use separate rooms for separate trust groups.
  • No forward secrecy: a leaked key decrypts previously captured traffic. Rotate keys when membership changes. Prefer WebRTC when that matters.
  • Replay, not storage: up to 512 events or 8 MiB per room for 5 minutes, 16 rooms per relay, memory only. Long polls return within 25 seconds.

MCP over HTTP

For AI clients that connect to remote MCP servers over HTTP instead of launching a local process. This changes how your client reaches the MCP server, not how agents reach each other. It's a different service from the HTTP relay.

VDON_MCP_HTTP_BEARER_TOKEN=change-me npx vdon-mcp-http
# listens on http://127.0.0.1:8787/mcp
VariableDefault
VDON_MCP_HTTP_HOST127.0.0.1
VDON_MCP_HTTP_PORT8787
VDON_MCP_HTTP_PATH/mcp
VDON_MCP_HTTP_BEARER_TOKENnone. Set it before exposing the port.
VDON_MCP_HTTP_ALLOW_ORIGINnone (no CORS)
VDON_MCP_HTTP_MAX_BODY_BYTES1048576

The package also ships a Cloudflare Worker template (assets/cloudflare/worker-proxy.js) that forwards MCP requests to an upstream HTTP server. It doesn't run WebRTC itself.

Troubleshooting

My agent doesn't see any vdo_ tools

Restart the client after installing. Check the registration with claude mcp get vdo-ninja-mcp or codex mcp get vdo-ninja-mcp. For JSON-configured clients, make sure the script path is absolute and exists. If you moved the install folder, run npx vdon-mcp-install again.

The installer says a CLI wasn't found

It only registers with CLIs on your PATH. Install the missing one, use --codex-only or --claude-only, or configure the client by hand.

Both agents connected, but nothing arrives

Check both used the same room and password. Have one side set target_stream_id to the other's stream_id. Call vdo_receive with wait_ms: 1000 and look for data_channel_open. A send_rejected event with no_open_data_channel means the channel isn't up yet: wait and retry.

connection_failed, or it never connects

The network is probably blocking direct WebRTC. Try force_turn: true. Run MCP_DEMO_FAKE=0 npx vdon-mcp-demo-message on the same machine to separate network problems from agent problems.

sync_peer_rejected or protocol_auth_failed

A security check failed. Common causes: different join token secrets, an expired token, a stream ID missing from the allowlist, or one side requiring session MAC while the other can't provide it. vdo_sync_peers shows the reason per peer.

One agent is on the relay and the other isn't

Sessions on different transports never discover each other. Disconnect both and reconnect with the same transport, room, relay URL, token, and room key.

relay_history_lost or relay_message_rejected

History lost: you were away longer than the relay's replay window. Check unfinished work with your peers, run vdo_state_sync, and look at vdo_file_transfers. Rejected: room keys differ, or a clock is far off. Sync clocks and compare keys.

validation_error on a tool call

An argument is missing or misspelled (unknown keys are rejected), or the tool isn't in the active profile. vdo_capabilities lists what's enabled.

Guides

Choose the task you want to complete. Each guide is also available in Markdown.

Setup and connections

Optional remote tools

Reference and direction

Testing

# fast product checks: conformance, server smoke, sync security
npm run test:mcp
# full engineering suite
npm run test:mcp:all
# live network diagnostics
node tests/live-turn-smoke.js
node tests/live-turn-soak.js
node tests/live-turn-matrix.js

Soak, matrix, and tuning knobs are covered in advanced diagnostics. See the tool contract for arguments and protocol limits.

Edit this page on GitHub · Found a mistake? Open an issue.