Skip to content

Commit 270e42e

Browse files
committed
docs: add MCP implementation guide with architecture and message flow
Comprehensive reference for MCP connection paths (local/remote), message types, file map, config format, and common issues. Also includes temporary debug logs for relay path tracing.
1 parent acd569c commit 270e42e

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

docs/MCP-IMPLEMENTATION.md

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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

Comments
 (0)