|
| 1 | +# MCP Implementation Guide |
| 2 | + |
| 3 | +> How MCP servers connect to Clay. Read this before working on any MCP-related code. |
| 4 | +
|
| 5 | +--- |
| 6 | + |
| 7 | +## Architecture Overview |
| 8 | + |
| 9 | +MCP servers let Mates (and Claude Code sessions) use external tools (GitHub, Notion, filesystem, etc.). There are two connection paths depending on whether the user is local or remote. |
| 10 | + |
| 11 | +``` |
| 12 | +LOCAL USER (same machine as Clay server): |
| 13 | + Clay Server -> spawns MCP process directly -> stdin/stdout JSON-RPC |
| 14 | +
|
| 15 | +REMOTE USER (browser on different machine): |
| 16 | + Clay Server -> WebSocket -> Webapp -> Extension -> Native Host -> MCP process |
| 17 | +``` |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## Connection Path: Local |
| 22 | + |
| 23 | +When `ws._clayLocal` is true (client IP is 127.0.0.1 / ::1), Clay server manages MCP processes directly via `lib/mcp-local.js`. |
| 24 | + |
| 25 | +``` |
| 26 | +lib/mcp-local.js reads ~/.clay/mcp.json, spawns processes, relays JSON-RPC |
| 27 | +lib/project.js creates _localMcp, initializes on local client connect |
| 28 | +lib/project-mcp.js builds SDK proxy servers from local tools (createLocalToolHandler) |
| 29 | +``` |
| 30 | + |
| 31 | +**Flow:** |
| 32 | +1. Local client connects -> `ws._clayLocal = true` (set in server.js upgrade handler) |
| 33 | +2. `handleConnection` in project.js calls `_localMcp.initialize()` |
| 34 | +3. `mcp-local.js` reads `~/.clay/mcp.json`, spawns all configured servers |
| 35 | +4. Each server goes through MCP handshake (initialize -> notifications/initialized -> tools/list) |
| 36 | +5. When ready, `_mcp.rebuildAndBroadcast()` builds SDK proxy servers |
| 37 | +6. `createLocalToolHandler` returns a function that calls `localMcp.callTool()` directly |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## Connection Path: Remote |
| 42 | + |
| 43 | +When `ws._clayLocal` is false, MCP processes run on the user's machine via the Native Host bridge. |
| 44 | + |
| 45 | +### Components |
| 46 | + |
| 47 | +| Component | Location | Role | |
| 48 | +|-----------|----------|------| |
| 49 | +| `lib/project-mcp.js` | Clay server | Builds SDK proxy servers, relays tool calls via WebSocket | |
| 50 | +| `lib/public/modules/app-misc.js` | Webapp (browser) | Forwards messages between server WS and Extension | |
| 51 | +| `clay-chrome/content.js` | Extension content script | Port bridge between webapp and service worker | |
| 52 | +| `clay-chrome/background.js` | Extension service worker | Connects to Native Host, relays messages | |
| 53 | +| `clay-mcp-bridge/host.js` | Native Host (user's machine) | Spawns MCP processes, manages config | |
| 54 | + |
| 55 | +### Message Flow: Server -> MCP Process |
| 56 | + |
| 57 | +``` |
| 58 | +1. SDK needs a tool call |
| 59 | +2. project-mcp.js createToolHandler() sends to extension WS: |
| 60 | + { type: "mcp_tool_call", callId, server, method, params } |
| 61 | +
|
| 62 | +3. Webapp app-misc.js handleMcpToolCallMessage() receives via WS |
| 63 | +4. Calls forwardMcpToolCall() -> window.postMessage: |
| 64 | + { source: "clay-page", payload: { type: "clay_mcp_tool_call", ... } } |
| 65 | +
|
| 66 | +5. content.js receives, forwards via port.postMessage to background.js |
| 67 | +
|
| 68 | +6. background.js matches "clay_mcp_tool_call", calls mcpRelayToolCall() |
| 69 | +7. mcpRelayToolCall() sends to Native Host via mcpSendNative(): |
| 70 | + { type: "mcp_request", server, method, params, callId } |
| 71 | +
|
| 72 | +8. Native Host relayToolCall() sends JSON-RPC to MCP process stdin: |
| 73 | + { jsonrpc: "2.0", id: N, method: "tools/call", params: { name, arguments } } |
| 74 | +``` |
| 75 | + |
| 76 | +### Message Flow: MCP Process -> Server (response) |
| 77 | + |
| 78 | +``` |
| 79 | +1. MCP process writes JSON-RPC response to stdout |
| 80 | +
|
| 81 | +2. Native Host drainJsonRpc() parses, handleMcpResponse() matches rpcId |
| 82 | +3. Sends back: { type: "mcp_response", callId, result/error } |
| 83 | +
|
| 84 | +4. background.js mcpHandleNativeMessage() matches callId to pending callback |
| 85 | +5. Callback calls sendToClayTab(): |
| 86 | + { type: "mcp_tool_result", callId, result, error } |
| 87 | +
|
| 88 | +6. content.js receives via port, forwards to page via window.postMessage |
| 89 | +
|
| 90 | +7. app-misc.js receives "mcp_tool_result", forwards to server via WS: |
| 91 | + { type: "mcp_tool_result" or "mcp_tool_error", callId, result/error } |
| 92 | +
|
| 93 | +8. project-mcp.js handleToolResult() resolves the pending Promise |
| 94 | +``` |
| 95 | + |
| 96 | +### Message Flow: Server List (Extension -> Server) |
| 97 | + |
| 98 | +``` |
| 99 | +1. Native Host auto-starts servers from ~/.clay/mcp.json on launch |
| 100 | +2. When a server finishes MCP handshake, Native Host sends: |
| 101 | + { type: "server_ready", server, tools } |
| 102 | +
|
| 103 | +3. background.js receives, calls broadcastMcpServers() |
| 104 | +4. broadcastMcpServers() calls get_servers on Native Host, gets full list |
| 105 | +5. Broadcasts to Clay tabs: |
| 106 | + { type: "mcp_servers_available", servers: [...], hostConnected: true } |
| 107 | +
|
| 108 | +6. content.js forwards to page |
| 109 | +
|
| 110 | +7. app-misc.js receives "mcp_servers_available", forwards to server via WS |
| 111 | +
|
| 112 | +8. project-mcp.js handleServersAvailable() stores in _availableServers |
| 113 | +9. rebuildProxyServers() creates SDK proxy servers with tools |
| 114 | +10. broadcastMcpState() sends mcp_servers_state to all clients (for UI) |
| 115 | +``` |
| 116 | + |
| 117 | +--- |
| 118 | + |
| 119 | +## Message Type Reference |
| 120 | + |
| 121 | +### Webapp <-> Extension (window.postMessage) |
| 122 | + |
| 123 | +| Direction | Type | Purpose | |
| 124 | +|-----------|------|---------| |
| 125 | +| Ext -> Page | `clay_ext_tab_list` | Browser tab list (includes extensionId) | |
| 126 | +| Ext -> Page | `clay_ext_result` | Extension command result | |
| 127 | +| Ext -> Page | `clay_ext_disconnected` | Extension context invalidated | |
| 128 | +| Ext -> Page | `mcp_servers_available` | MCP server list from Native Host | |
| 129 | +| Ext -> Page | `mcp_tool_result` | MCP tool call result | |
| 130 | +| Page -> Ext | `clay_ext_command` | Browser automation command | |
| 131 | +| Page -> Ext | `clay_mcp_tool_call` | MCP tool call to relay | |
| 132 | + |
| 133 | +### Server <-> Webapp (WebSocket) |
| 134 | + |
| 135 | +| Direction | Type | Purpose | |
| 136 | +|-----------|------|---------| |
| 137 | +| S -> W | `mcp_tool_call` | Tool call for Extension to relay | |
| 138 | +| S -> W | `mcp_servers_state` | Full server state (for UI rendering) | |
| 139 | +| W -> S | `browser_tab_list` | Tab list (sets _extensionWs, extensionId) | |
| 140 | +| W -> S | `mcp_servers_available` | Server list from Extension | |
| 141 | +| W -> S | `mcp_tool_result` | Tool result from Extension relay | |
| 142 | +| W -> S | `mcp_tool_error` | Tool error from Extension relay | |
| 143 | +| W -> S | `mcp_toggle_server` | Toggle server enabled for project | |
| 144 | + |
| 145 | +### Extension <-> Native Host (Chrome Native Messaging) |
| 146 | + |
| 147 | +| Direction | Type | Purpose | |
| 148 | +|-----------|------|---------| |
| 149 | +| Ext -> NH | `ping` | Health check | |
| 150 | +| Ext -> NH | `get_servers` | Get all configured servers with status | |
| 151 | +| Ext -> NH | `add_server` | Add server to ~/.clay/mcp.json | |
| 152 | +| Ext -> NH | `remove_server` | Remove server from config | |
| 153 | +| Ext -> NH | `import_config` | Add external config to include list | |
| 154 | +| Ext -> NH | `get_imports` | Get include list | |
| 155 | +| Ext -> NH | `remove_import` | Remove from include list | |
| 156 | +| Ext -> NH | `mcp_request` | Relay tool call to MCP process | |
| 157 | +| NH -> Ext | `pong` | Health check response | |
| 158 | +| NH -> Ext | `server_ready` | Server finished MCP handshake | |
| 159 | +| NH -> Ext | `server_status` | Server started/crashed/exited | |
| 160 | +| NH -> Ext | `mcp_response` | Tool call result from MCP process | |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +## Key Files |
| 165 | + |
| 166 | +| File | Purpose | |
| 167 | +|------|---------| |
| 168 | +| `lib/mcp-local.js` | Local MCP process manager (localhost clients) | |
| 169 | +| `lib/project-mcp.js` | MCP bridge module, proxy server builder, toggle handler | |
| 170 | +| `lib/project.js` | Creates _localMcp, wires to _mcp, detects local clients | |
| 171 | +| `lib/project-user-message.js` | Handles browser_tab_list (sets _extensionWs) | |
| 172 | +| `lib/server.js` | Sets ws._clayLocal, passes MCP callbacks to project context | |
| 173 | +| `lib/daemon.js` | onGetProjectMcpServers / onSetProjectMcpServers (config persistence) | |
| 174 | +| `lib/public/modules/app-misc.js` | Webapp MCP message forwarding | |
| 175 | +| `lib/public/modules/mcp-ui.js` | MCP Servers modal (setup wizard + toggle list) | |
| 176 | +| `native-host/clay-mcp-host.js` | Native Host source (also in clay-mcp-bridge npm package) | |
| 177 | + |
| 178 | +## Config |
| 179 | + |
| 180 | +### ~/.clay/mcp.json (managed by Native Host) |
| 181 | + |
| 182 | +```json |
| 183 | +{ |
| 184 | + "mcpServers": { |
| 185 | + "filesystem": { |
| 186 | + "command": "npx", |
| 187 | + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/chad"], |
| 188 | + "env": {} |
| 189 | + } |
| 190 | + }, |
| 191 | + "include": [ |
| 192 | + "~/.claude/claude_desktop_config.json" |
| 193 | + ] |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +### daemon.json (per-project enabled list) |
| 198 | + |
| 199 | +```json |
| 200 | +{ |
| 201 | + "projects": [ |
| 202 | + { |
| 203 | + "slug": "my-project", |
| 204 | + "enabledMcpServers": ["filesystem", "github"] |
| 205 | + } |
| 206 | + ] |
| 207 | +} |
| 208 | +``` |
| 209 | + |
| 210 | +--- |
| 211 | + |
| 212 | +## Setup Wizard (MCP Servers Modal) |
| 213 | + |
| 214 | +The modal shows a 3-step wizard when setup is incomplete: |
| 215 | + |
| 216 | +1. **Install Chrome Extension** - connected via browser_tab_list detection |
| 217 | +2. **Install MCP Bridge** - `npx clay-mcp-bridge install <extension-id>` (remote only, local auto-completes) |
| 218 | +3. **Add MCP Servers** - via Extension popup (+) button or import existing config |
| 219 | + |
| 220 | +When all steps are done, wizard hides and shows server toggle list. |
| 221 | + |
| 222 | +State tracked by: `_extensionConnected`, `_nativeHostConnected` (from hostConnected in mcp_servers_state), server count. |
| 223 | + |
| 224 | +--- |
| 225 | + |
| 226 | +## Common Issues |
| 227 | + |
| 228 | +- **Extension not detected**: Clay page must be refreshed after extension install/reload |
| 229 | +- **Native Host not found**: Browser must be restarted after `npx clay-mcp-bridge install` |
| 230 | +- **npx not found by Native Host**: Chrome uses minimal PATH. The install script writes absolute node path in wrapper |
| 231 | +- **Tool call timeout**: Check message type alignment (clay_mcp_tool_call vs mcp_tool_call) |
| 232 | +- **Toggle not persisting**: Verify onSetProjectMcpServers is wired through server.js to project context |
| 233 | +- **Service worker state lost**: MV3 SWs lose memory on sleep. Use URL pattern matching for tab detection, not in-memory Sets |
0 commit comments