Status: Implemented. For the common development/debugging path, start with the local MCP guide. This page describes the transport, authentication, mode matrix, and implementation contract.
The Windows tray app now ships a local Model Context Protocol (MCP) server alongside its existing OpenClaw gateway client. Node capabilities the agent reaches over the OpenClaw gateway WebSocket (system.*, screen.*, canvas.*, camera.*, location.get, tts.*, stt.*, and device.*) are advertised, on the same machine, as MCP tools over http://127.0.0.1:8765/. browser.proxy is not in that MCP-only set. BrowserProxyActivation registers it only when the toggle is on, a gateway client is attached, the shared gateway token is set, and the browser endpoint is verified. Local-only app.* and app.connection.* tools are also exposed to MCP clients for tray automation and connection/pairing workflows. Those are not registered with the remote gateway node transport.
This means any local MCP client (Claude Desktop, Claude Code, Cursor, an MCP-aware CLI, a custom dev script) can reach into the running tray and drive Windows-native capabilities directly, without an OpenClaw gateway in the loop. The tray app can run in MCP-only mode with no gateway connection at all.
MCP and Gateway node mode share capability dispatch rather than maintaining
separate implementations. A new command still needs registration, curated MCP
descriptions, the shipped winnode reference, tests, and applicable live proof.
Follow the new-command checklist.
- Single source of truth for capabilities. A new
INodeCapabilityregistered withWindowsNodeClient.RegisterCapability(...)is reachable via every transport the tray supports. Today: gateway WebSocket and local MCP HTTP. Future transports (named pipe, gRPC, whatever) plug in the same way. - Local-first development. Capabilities can be exercised on Windows without standing up an OpenClaw gateway, without an account, without a gateway token, without pairing, and without a tunnel.
- Make MCP clients first-class consumers of the OpenClaw native node, not afterthoughts. The tooling investment in capabilities (camera consent flows, exec approval policy, canvas WebView2 plumbing) pays off in both directions: agent-via-gateway and agent-via-local-MCP.
- No remote authentication. Loopback bind + Origin/Host checks keep the endpoint unreachable from any other machine. A local bearer token guards against untrusted local processes on the same box (see Authentication below). We will revisit ACLs / multi-user when we want remote MCP, multiple users on one box, or shared dev VMs.
- No SSE or second listener. Unary calls remain plain JSON-RPC. Optional file duplex uses the authenticated bounded polling extension below.
- No per-tool input schemas. Capabilities don't expose schemas; MCP
inputSchemais permissive ({type: "object", additionalProperties: true}). When/ifINodeCapabilitygrows a schema property, the MCP bridge picks it up with no other changes. - No port configuration UI. Default
8765is hardcoded. Easy to lift intoSettingsManagerlater.
Edit the dual-transport diagram.
The capability list lives on NodeService, not on WindowsNodeClient. That single change is what makes MCP-only mode possible: the gateway client is now optional. When it exists, Register(cap) pushes capabilities into both the local list and the gateway client's registration message. When it doesn't (MCP-only), capabilities still populate the local list and the MCP bridge serves them.
OpenClaw.Shared/Mcp/McpToolBridge.cs is transport-agnostic JSON-RPC 2.0. It implements:
initialize- protocol version2024-11-05, server info.tools/list- flattens_capabilitiesinto MCP tools. Tool name = command name ("screen.snapshot"); known commands get curated descriptions fromMcpToolBridge.CommandDescriptions; unknown commands fall back to"{category} capability: {command}".inputSchemais permissive.tools/call- finds the capability viaINodeCapability.CanHandle(name), builds aNodeInvokeRequest(the same struct the gateway path uses), callsExecuteAsync, wraps the result as MCPcontent[].text. Tool failures come back asresult.isError = true, not JSON-RPC errors (per MCP spec - JSON-RPC errors are reserved for protocol issues).ping,notifications/initialized- protocol housekeeping.notifications/cancelled- cancels the active request whose JSON-RPC ID is supplied asparams.requestId. A cancelledtools/callcompletes with an MCP tool error containingcancelled. If concurrent HTTP scheduling processes cancellation just before an already-sent call registers, the notification records a pending cancellation for five seconds. The first matching registration atomically consumes that tombstone and returnscancelledbefore capability execution begins, so correctness does not depend on scheduler timing. Repeated notifications for the same pending ID do not extend its original five-second lifetime. Pending cancellations and recent-completion guards are each capped at 1,024 entries; expired entries are pruned first and the oldest remaining entry is evicted at capacity. Because JSON-RPC IDs are client-scoped but this stateless HTTP transport has no client identity, duplicate active IDs are allowed and cancellation is ignored when more than one active call matches; this prevents one local client from cancelling another client when the collision is already observable. Request matching uses the decoded JSON string value or normalized arbitrary-precision JSON number value, while preserving the distinction between string and numeric IDs. A tombstone cannot predict a later cross-client ID collision, so it cancels the first matching registration; complete isolation would require client identity in the transport. A recent completion guard also prevents a late notification from poisoning immediate ID reuse.
The bridge takes a Func<IReadOnlyList<INodeCapability>> rather than a snapshot. Every tools/list re-reads the live list. This is what guarantees zero-cost capability addition - register a new capability after server start and it appears on the next tools/list.
The System tools permission is also read live by the retained system capability.
Turning it Off removes system.run and system.run.prepare from local discovery
and rejects new calls without waiting for Gateway reconnect to rebuild registration.
Re-enabling takes effect only after settings persistence succeeds, so a failed
save cannot temporarily grant execution permission.
Approved requests recheck this permission after authorization and at the runner's
prelaunch revalidation boundary. This does not terminate an already-running command;
cancellation remains a separate operation.
Cancellation is cooperative and uses the same CancellationToken capability
contract as the gateway transport. Screen and camera operations propagate the
token through recording consent/countdown, camera admission, capture waits, and
recording shutdown. The HTTP request deadline remains a separate safety bound;
no command-specific transport deadlines are applied.
OpenClaw.Shared/Mcp/McpHttpServer.cs is System.Net.HttpListener bound to http://127.0.0.1:8765/. Loopback-only by construction; not reachable from any other machine even with firewall holes. A defensive IPAddress.IsLoopback check on each request acts as belt-and-suspenders.
GET / returns a friendly text probe. POST / is JSON-RPC. Anything else → 405. When a bearer token is configured, every verb must pass the token gate before method dispatch.
This OpenClaw-specific extension is advertised by initialize under
capabilities.experimental["openclaw/file-channel"]. It is not an MCP standard.
The existing authenticated HTTP listener serves it; no SSE, second port, or
WebSocket binary frames are involved. Clients not implementing it retain unary
file commands and preflight. Explicit binary/create execution requires a channel.
- Call
openclaw/files/openwith{v:1,command:"file.fetch"|"file.create",requestId:"unique-call-id",timeoutMs:60000}. Retain the unpredictableleasefrom the response. - Attach once to the real
tools/callwith that exact JSON-RPC request ID and command. Put{v:1,lease}inparams._meta["openclaw/file-channel"], not inside canonical commandarguments. - Call
openclaw/files/pollwith{v:1,lease,ack:0}. Polls return immediately with{v:1,frames:[{seq,payloadJSON}],completed}. Each subsequentackis the next output sequence after consumed frames. Unacknowledged frames are retained; retry a lost poll with the same cursor. Polls are not long-lived handlers. - Feed received JSON strings to the v1 framing parser. Send framed input with
openclaw/files/inputand{v:1,lease,seq:0,payloadJSON:"..."}. Input sequence increases per frame. Retrying the immediately preceding identical input is idempotent; changed or out-of-order input closes the transfer. - Call
openclaw/files/closewith{v:1,lease}on completion or cancellation. Timeout (at most ten minutes), token replacement, profile/server teardown also cancel channels. Sandbox settings changes do not cancel admitted work.
The HTTP server snapshots the bearer and file-channel epoch as one immutable authentication generation. Rotation first invalidates existing channels, then publishes the replacement generation. Requests holding an older snapshot cannot open or attach channels in the new generation, including requests concurrent with rotation.
Frames match upstream 82df040c9c584fce0decbac9ebf260ec134e04c4:
{v:1,kind:"ready"} or
{v:1,kind:"data",message:0,index:0,last:true,data:"canonical-padded-base64"}.
Exact keys, safe integer counters, strict ordering, 8192-byte nonfinal fragments
and a 16384-byte UTF-8 wire cap are enforced. File logical messages cap at 1 MiB.
Pre-listener buffering caps at eight messages and 1 MiB including unfinished
messages; active asynchronous delivery caps at eight messages and 2 MiB.
Readiness is sent only after the worker registers its listener. Fetch receives
opaque UTF-8 start, then repeatedly emits a nonempty logical message of up to
1 MiB and waits for an opaque UTF-8 ack before reading the next chunk. The client
must acknowledge every logical message, including the last, independently of
the wire-frame polling cursor above. An empty file still requires start but
emits no data messages and expects no ack. Fetch completes with the command
result; it does not send an empty EOF message. Create receives messages, ACKs
each nonempty one, and verifies size/hash at empty-message EOF. The client need
not send reciprocal readiness.
Gateway output sends at most sixteen ordered wire frames before waiting for
all correlated progress responses. Readiness still waits for its response before
delivering buffered input. Heartbeats share the same sequence and send gate.
This does not change the wire format or the logical-message ack requirement.
Local MCP retains its existing cursor-based backpressure.
There are four leases/streaming handlers, sixteen ordinary tool handlers, eight independent control handlers, and thirty-two bounded ingress readers. Authentication and bounded parsing precede classification; full tool slots cannot consume the control slots needed for ACK/cancellation. Each output queue retains at most sixteen wire frames and backpressures producers until a poll acknowledges them.
Normal bodies remain capped at 4 MiB. A client declaring an implemented file
command in X-OpenClaw-File-Command gets a file-specific bounded allowance
(FileTransportJson.EnvelopeBytes); the parsed tools/call command must match.
Only two unary file envelopes/results can be retained concurrently; declared
streaming calls release that reservation after their bounded metadata is parsed.
Responses use a
separate bounded file allowance accounting for base64 and nested MCP text.
The file response bound does not apply to other tools; their previous response
contract remains unchanged. File requests cannot escape their depth/size bounds
through generic JSON fallback. Gateway file results use the same command-specific
classification, including unary/preflight calls. Generic Gateway results retain
their original serializer casing, depth and escaping. Invalid or oversized file
result serialization settles the invoke with an explicit error on either
Gateway invoke envelope.
winnode sends this declaration automatically for file calls and supports
--input-file / no-clobber --output-file; see its shipped command reference.
Gateway transport uses the same frame parser with correlated
node.invoke.progress RPC acknowledgments, ordered node.invoke.input events
(id, not invokeId), and separate node.invoke.cancel events. Five-second
empty progress heartbeats share the progress sequence. Connection generation,
target node, deadline and cancellation are checked independently from file
arguments. Plugin inner failures remain unchanged on the Gateway wire but count
as telemetry failures and MCP isError, giving the CLI a nonzero exit.
The HTTP transport requires a bearer token on every request. Defense-in-depth on top of loopback bind + Origin/Host checks: if an attacker can run code in any local user context they can reach 127.0.0.1:8765, so we don't want the listener to be open-by-construction.
Where the token lives. %APPDATA%\OpenClawTray\mcp-token.txt. The exact path is composed by NodeService.McpTokenPath from SettingsManager.SettingsDirectoryPath, so the test-suite override OPENCLAW_TRAY_DATA_DIR isolates the token file too. The file inherits the parent directory's ACL - by default only the current user (and SYSTEM/Administrators) can read it.
When it's created. Lazily, on the first NodeService.StartMcpServer() call - i.e. the first time the user enables Local MCP Server in Settings and saves. Until that toggle has been on at least once, the file does not exist. This trips up users who try to grab the token before flipping the switch.
How long it is. 32 bytes of CSPRNG output, base64url-encoded with padding stripped → 43 ASCII characters (~256 bits of entropy). See McpAuthToken.Generate().
Lifetime. The token is persistent across tray restarts. It's only regenerated if the file is deleted or its contents are emptied. There is no automatic rotation.
On the wire. Every request must carry Authorization: Bearer <token> when the server has a configured token. Missing or wrong token → 401 Unauthorized with no body. GET / remains a "yes I'm here" probe after auth passes.
How users find it. Settings > Permissions > Integrations > Local MCP Server shows the endpoint and status, with Copy token and Copy URL actions. Agents should read the selected profile's token file without printing it rather than placing the token in a prompt or command line. For clients that require a literal bearer value in configuration, use Copy token and keep that configuration private.
Two independent toggles in SettingsData:
public bool EnableNodeMode { get; set; } // open WebSocket to gateway
public bool EnableMcpServer { get; set; } // run local MCP HTTP serverEnableNodeMode |
EnableMcpServer |
Behavior |
|---|---|---|
| false | false | Operator-only (legacy default) |
| false | true | MCP server only, no gateway |
| true | false | Gateway node, no MCP |
| true | true | Gateway node + MCP |
The Local MCP Server control lives in Permissions > Integrations. Its endpoint and status describe the actual selected app instance. The two flags above remain independent; a running MCP-only listener is not evidence of Gateway pairing.
A legacy McpOnlyMode field is migrated automatically on load and never re-written.
MCP startup is reported from the actual listener state. NodeService.McpStartupError is populated when capability registration or the HTTP listener fails, and MCP-only startup is not treated as successful unless the loopback MCP server is running. Tray, Permissions, and Command Center surfaces show local MCP-only separately from gateway connectivity so a working local MCP listener is never presented as a gateway connection.
The tray's most interesting code lives in capabilities: system.run (official MXC SDK execution on supported Windows, positive unsupported-OS compatibility, and the V2 exec-approval coordinator), screen.snapshot (Windows.Graphics.Capture + GraphicsCapturePicker), canvas.* (WebView2 with trusted origin enforcement), camera.snap/camera.clip (MediaCapture + consent prompt), and location.get (Windows.Devices.Geolocation). These have Windows-only behavior; unit tests do not substitute for native runtime proof.
Local MCP changes that. Concrete benefits:
- Manual smoke tests in seconds.
curl -s -X POST http://127.0.0.1:8765/ -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'validates that the capability dispatch path works, the WinUI dispatcher marshaling is correct, the result shape matches expectations. No gateway, no gateway token, no pairing, no SSH tunnel. - Reproducible bug reports. A repro becomes a
tools/callbody the bug filer can paste verbatim. No "what was the gateway doing at the time." - Integration tests against a real instance.
tests/OpenClaw.Tray.IntegrationTests(for exampleMcpHttpServerIntegrationTests) spawns the tray and drives MCP JSON-RPC over HTTP whenOPENCLAW_RUN_INTEGRATION=1. The same requests a developer runs by hand are the ones these tests send. The bridge logic -McpToolBridge- is also covered byMcpToolBridgeTestswith no UI involvement. - Coverage for the dispatch path itself.
WindowsNodeClient's capability-routing logic (CanHandle→ExecuteAsync) was previously only exercised against a live gateway. The MCP server hits the same code paths, so any local MCP test is implicit coverage of the gateway dispatch. - Bridge unit tests already exist.
tests/OpenClaw.Shared.Tests/McpToolBridgeTests.cscovers initialize, tools/list, runtime capability registration, tool calls, unknown tools, capability failures, JSON-RPC unknown method, notifications, and parse errors. These are pure C# unit tests with fake capabilities - no HTTP, no UI, no gateway.
The exact same node tools the OpenClaw gateway uses are now invocable by any local MCP-aware client:
-
Claude Code (this CLI). Add to
~/.claude.jsonor per-project.mcp.json:{ "mcpServers": { "openclaw-tray": { "type": "http", "url": "http://127.0.0.1:8765/" } } }The agent then sees
screen.snapshot,system.run,canvas.*, etc. as tools, with whatever arguments the capability accepts. -
Claude Desktop. Same config shape under MCP servers.
-
Cursor. Same.
-
GitHub Copilot CLI / Copilot in the terminal. As MCP support lands in those clients, the endpoint is already there.
-
Custom dev scripts. Anything that can speak HTTP + JSON-RPC. A 30-line Python or Node helper can drive the entire capability surface.
In all cases the user gets a Windows-native agent experience without OpenClaw infrastructure. They can be entirely offline w.r.t. an OpenClaw gateway and still hand the LLM a working set of "do something on my Windows box" tools.
The canonical command descriptions live in OpenClaw.Shared\Mcp\McpToolBridge.CommandDescriptions; OpenClaw.WinNode.Cli.Tests\SkillMdDriftTests keeps .agents\skills\winnode\SKILL.md in sync with that documented capability surface. The live tools/list output may also include newly registered capabilities with fallback descriptions before prose is updated.
Gateway/node command groups currently include:
system.notify,system.run,system.run.prepare,system.which,system.execApprovals.get,system.execApprovals.setcanvas.present,canvas.hide,canvas.navigate,canvas.eval,canvas.snapshot,canvas.a2ui.push,canvas.a2ui.reset,canvas.a2ui.dump,canvas.caps,canvas.a2ui.pushJSONLscreen.snapshot,screen.recordcamera.list,camera.snap,camera.clipstt.transcribe,stt.listen,stt.statustts.speak,tts.statuslocation.getdevice.info,device.statusbrowser.proxy(omitted fromtools/listuntilBrowserProxyActivationisRegistrationBlock.None: toggle on, gateway client attached, shared gateway token set, browser endpoint verified. MCP-only mode has no gateway client, sowinnode --command browser.proxyreturnsUnknown tool: browser.proxy.)
Local MCP-only app control commands currently include:
app.navigate,app.status,app.sessions,app.agents,app.nodes,app.config.get,app.settings.get,app.settings.set,app.menu,app.search,app.dashboard.url- Chat automation:
app.chat.snapshot,app.chat.send,app.chat.reset - Connection diagnostics/automation:
app.connection.status,app.connection.gateways,app.connection.applySetupCode,app.connection.connectSharedToken,app.connection.pendingApprovals,app.connection.approveDevicePairing,app.connection.rejectDevicePairing,app.connection.approveNodePairing,app.connection.rejectNodePairing,app.connection.reconnect,app.connection.reconnectNode
For connection troubleshooting, prefer app.connection.status over the older
app.status summary. It is read-only and returns the manager-owned
operator/node snapshot, active gateway metadata, credential source/status,
MCP listener state, browser-proxy shared-token caveat, pending approval commands,
retry hints inferred from recent diagnostics, and recent diagnostic events.
app.connection.gateways lists saved gateway records with credential presence
booleans only; it never returns token values.
Example chat automation smoke:
winnode --command app.chat.send --params '{"message":"hello from local MCP"}'This is the strongest argument for making MCP a first-class citizen, not an afterthought.
When a contributor adds a new capability - say, clipboard.read, clipboard.write, windows.list, audio.transcribe, git.status, office.draft_email - today the workflow looks like:
- Implement
INodeCapability. - Wire it into
NodeService.RegisterCapabilities(). - Stand up a gateway, authenticate, pair the device, etc., to test.
- Drive the capability from within an agent conversation, observing logs and taking screenshots to confirm correctness.
With MCP in-process the workflow shortens to:
- Implement
INodeCapability. - Wire it into
NodeService.RegisterCapabilities(). - Restart the tray. The new tool is immediately visible to any local MCP client (
tools/listre-reads the registry every call), and to manualcurltests.
The dev loop for capabilities is now identical to the dev loop for any local HTTP server: edit, restart, hit the endpoint with the local MCP bearer token, observe. No gateway, no agent, no gateway auth.
This compounds when you stack it with Claude Code or Cursor on the same machine. A contributor can:
- Open the repo in their IDE.
- Run the tray with
EnableMcpServer = true. - Have Claude Code connected to the same MCP endpoint.
- Iterate on a new capability while the agent - using that very capability - helps drive the iteration. The capability under development can be invoked by the assistant on the next turn after a tray restart. That's a tight self-hosted feedback loop.
It also reduces the cost of "speculative" capabilities. Today, adding a capability has a tax: it must be useful enough to justify the extra surface in the gateway/agent stack. With local MCP, a contributor can build a capability speculatively, validate it against their own MCP-aware agent, and only later decide whether to formalize it for gateway use. That lowers the bar for experimentation.
The server is built on several defensive layers, not just one. Loopback alone is not sufficient - a browser tab the user opens is also on the loopback interface, so a malicious page could otherwise reach http://127.0.0.1:8765/ directly.
-
Loopback bind.
HttpListeneris registered with the prefixhttp://127.0.0.1:8765/. The Windows kernel binds the listening socket to the loopback interface only - packets from other interfaces are not delivered to it. Firewall configuration is irrelevant. Defends against: another machine on the network. -
Defensive
IsLoopbackcheck. Each incoming request validatesctx.Request.RemoteEndPoint.Address. Belt-and-suspenders for #1. -
CSRF / browser gate. Each request is rejected if any of the following holds:
- the request carries an
Originheader (real MCP clients - Claude Desktop, Cursor, Claude Code, curl - never sendOrigin; browsers always do for cross-origin fetches); - the
Hostheader is anything other than127.0.0.1[:port]orlocalhost[:port](defends against DNS-rebinding pivots); - on
POST, theContent-Typeis anything other thanapplication/json(forces a CORS preflight from a browser, which we never satisfy). - the request body exceeds 4 MiB (DoS / OOM cap).
Together these three checks force a malicious cross-origin browser fetch into a CORS preflight that we deliberately do not honor (no
Access-Control-Allow-*is ever emitted), so the actual call is blocked before reaching capability code. - the request carries an
-
Bearer token. Every request must include the persistent local MCP bearer token (
Authorization: Bearer <token>) once the server has created%APPDATA%\OpenClawTray\mcp-token.txt. This blocks drive-by local clients that know the port but cannot read the per-user token file. -
Concurrency cap. A semaphore limits in-flight handlers to 8. A misbehaving local client cannot pin every threadpool thread on long-running screen/camera calls.
-
Capability-level controls remain in force. The V2 exec-approval coordinator still gates
system.run. Camera and screen capture still go through Windows consent flows. MCP doesn't bypass any of those.
system.run uses MXC on supported Windows. A positive unsupported-Windows OS
verdict instead selects approved direct uncontained execution, with a persistent
Node Sandbox warning and disabled permission controls. Sandbox file/network/
clipboard/UI restrictions are not enforced there; command approvals, deadlines,
bounded output and prelaunch revalidation remain active. SDK failures, unknown
probes, unsupported scopes and stale
permissions return an MCP tool result with isError=true, not a success-shaped
exit payload. A process that actually exits nonzero retains the existing
stdout, stderr, exitCode, timedOut, success, durationMs payload.
Cancellation propagates through the V2 prompt and revalidation to explicit
contained-process termination. A late native launch remains owned until its
streams, process and scratch are safely disposed. No policy-change global
cancellation is performed. Balanced/Open scope is the OS user profile plus
resolved redirected personal folders, not a drive or all users. Custom project
grants are explicit. No volume-root readonly grants are added.
Authentication is local bearer-token based. The token is persistent, generated by the tray, stored in the current user's OpenClawTray data directory, and verified before MCP method dispatch. It is defense-in-depth rather than a hard sandbox boundary: a malicious process already running as the same user may still be able to read user-profile files or invoke native APIs directly. If we need stronger isolation for shared machines or low-trust local processes, the next step is scoped or per-call tokens issued by the tray, not URL ACLs or HTTPS - both add deployment pain without solving the same-user trust problem.
These should all be rejected with 403 Forbidden:
# Browser pretending to come from another origin
curl -X POST http://127.0.0.1:8765/ -H "Origin: https://evil.com" -H "Content-Type: application/json" -d '{}'
# DNS rebinding attempt
curl -X POST http://127.0.0.1:8765/ -H "Host: evil.com" -H "Content-Type: application/json" -d '{}'This should be rejected with 415:
curl -X POST http://127.0.0.1:8765/ -H "Authorization: Bearer <token>" -H "Content-Type: text/plain" --data '{"jsonrpc":"2.0","id":1,"method":"ping"}'These should succeed:
curl http://127.0.0.1:8765/ -H "Authorization: Bearer <token>" # GET probe
curl -X POST http://127.0.0.1:8765/ -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"ping"}'These are reasonable next steps but explicitly out of scope for the initial implementation:
- Per-tool input schemas. Add an
IReadOnlyDictionary<string, JsonElement> InputSchemas(or per-command descriptor) toINodeCapability. The MCP bridge'sHandleToolsListpicks them up automatically. Until then, MCP clients see permissive schemas and the agent has to figure out arg shapes from descriptions and trial-and-error. Authentication.Implemented. See Authentication below.- Streamable HTTP / SSE. For long-running tools (
screen.record, futureaudio.transcribe), MCP supports streaming progress. The bridge needs to learn about it and the HTTP server needs to optionally upgrade. - Resource and prompt support. MCP has
resources/*andprompts/*methods we currently no-op. Notifications, recent activity, channel state could be modeled as MCP resources. - Configurable port. Move
McpDefaultPortintoSettingsManager. Probably also pick a free port at startup if the default is in use, and surface the actual port in the Settings UI. - Setup Wizard step. Today the Settings Advanced section is the only way to enable MCP. The Setup Wizard could offer it as a one-click option, especially attractive for users who don't run a gateway at all.
| File | Role |
|---|---|
src/OpenClaw.Shared/Mcp/McpToolBridge.cs |
Transport-agnostic JSON-RPC dispatcher. |
src/OpenClaw.Shared/SettingsData.cs |
Settings JSON model. Adds EnableMcpServer; deprecates McpOnlyMode. |
src/OpenClaw.Shared/Mcp/McpHttpServer.cs |
HttpListener-based loopback HTTP transport. |
src/OpenClaw.Tray.WinUI/Services/NodeService.cs |
Owns the capability list. Hosts the MCP server when enabled. |
src/OpenClaw.Tray.WinUI/Services/SettingsManager.cs |
In-memory settings model + load/save. Migrates legacy McpOnlyMode. |
src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml(.cs) |
Settings UI surface hosted by HubWindow. |
src/OpenClaw.Tray.WinUI/App.xaml.cs |
Bootstraps NodeService based on the new mode matrix. |
tests/OpenClaw.Shared.Tests/McpToolBridgeTests.cs |
Unit tests for the bridge. |
With the tray running and EnableMcpServer = true:
# Server is up
curl http://127.0.0.1:8765/ -H "Authorization: Bearer <token>"
# List tools
curl -s -X POST http://127.0.0.1:8765/ `
-H "Authorization: Bearer <token>" `
-H "Content-Type: application/json" `
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# Take a screenshot of the primary monitor
curl -s -X POST http://127.0.0.1:8765/ `
-H "Authorization: Bearer <token>" `
-H "Content-Type: application/json" `
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"screen.snapshot"}}'For a simpler local CLI smoke test, run winnode --list-tools; it loads the
same token file automatically. A connection refused on port 8765 means the
tray is not listening. Do not reinstall winnode. Start the tray with
EnableMcpServer true, set OPENCLAW_TRAY_DATA_DIR for both the tray and
winnode, and wait until mcp-token.txt exists and the port accepts a
connection. OPENCLAW_SKIP_UPDATE_CHECK=1 skips the startup update check.
For agent-driven validation, use the repo-local skill
.agents/skills/openclaw-proof-validation/SKILL.md. MCP/node changes need live
tool discovery plus invocation proof using winnode or raw MCP JSON-RPC.
Every new Windows node command must remain first-class over local MCP. Register
it in the capability path used by NodeService, update
McpToolBridge.CommandDescriptions, update
.agents/skills/winnode/SKILL.md, and add focused tests. SkillMdDriftTests
guards against drift between capabilities, MCP descriptions, and winnode docs.
PR proof for a new command should paste winnode --list-tools plus the command
invocation, or raw MCP tools/list plus tools/call; include gateway invoke
output when gateway-mediated behavior changed and a gateway is available.
For Claude Code, drop this into .mcp.json at the repo root or ~/.claude.json:
{
"mcpServers": {
"openclaw-tray": {
"type": "http",
"url": "http://127.0.0.1:8765/",
"headers": {
"Authorization": "Bearer <token>"
}
}
}
}