I spent a full debugging session getting Claude Desktop on Windows to talk to an MCP server running on a Raspberry Pi via Tailscale. Sharing this Claude report as it can be useful to others attempting similar setups. Six attempts, six different failure modes. Writing this up because I couldn’t find a single clear resource on why this is hard, and the real blocker is non-obvious. TL;DR : Claude Desktop is an MSIX Windows Store app. Its child processes cannot access ~/.ssh/ . SSH silently fails with no error. The fix is a 25-line Node.js proxy that bridges Claude Desktop’s stdio transport to an HTTP server on the Pi. What I was building A personal knowledge graph on a Pi 4. I wanted Claude Desktop on my Windows machine to query it via MCP while I’m in a conversation. Attempt 1 — url field in config Tried: json “mcpServers”: { “self-graph”: { “url”: “http://100.x.x.x:8090/mcp” } } } Result : “not valid configuration” — immediate rejection. Claude Desktop only supports command / args (stdio transport). The url field works in Claude Code CLI and the SDK but not in Claude Desktop’s config parser. Dead end. Attempt 2 — SSH stdio The obvious approach: use SSH as the subprocess so Claude Desktop can talk to the remote Python script through the tunnel. json { “command”: “ssh”, “args”: [“pi@your-pi.your-tailnet.ts.net”, “/usr/bin/python3 /home/pi/self-graph/mcp_server.py”] } Result : Connected, received initialize , crash ~350ms later. Two bugs hit simultaneously: Bug A — SSH host key not in Windows known_hosts . With no TTY, SSH can’t prompt “Are you sure?”. It fails silently. Fixed by running the SSH command once manually in PowerShell. Bug B — mcp Python SDK 1.27.1 breaking change. The list_tools handler was returning plain dict objects. SDK 1.27.1 requires typed Tool(…) objects. The crash happened exactly one network roundtrip after initialize — enough time for Claude Desktop to send tools/list and get back ‘dict’ object has no attribute ‘name’ . Fixed both. Still broken. Attempt 3 — After SDK fix: asyncio/anyio transport dies through SSH pipes Result : Now crashes 42ms after initialize . That’s exactly one Tailscale roundtrip — Claude Desktop receives the response and the Python process exits before notifications/initialized arrives. The mcp SDK uses anyio 's stdio_server() which wraps stdin in an async memory stream. Something in the interaction between Windows SSH’s pipe handling and anyio’s task group lifecycle causes the session to terminate after the first response. Works perfectly locally ( printf ); function post(body) { return new Promise((resolve, reject) => { const data = JSON.stringify(body); const req = http.request({ hostname: ‘100.x.x.x’, port: 8090, path: ‘/mcp’, method: ‘POST’, headers: { ‘Content-Type’: ‘application/json’, ‘Content-Length’: Buffer.byteLength(data) } }, res => { let out = ‘’; res.on(‘data’, c => out += c); res.on(‘end’, () => resolve(out.trim())); }); req.on(‘error’, reject); req.write(data); req.end(); }); } rl.on(‘line’, async line => { line = line.trim(); if (!line) return; try { const msg = JSON.parse(line); // Notifications are fire-and-forget — never send a response if (msg.method && msg.method.startsWith(‘notifications/’)) return; const resp = await post(msg); if (resp) process.stdout.write(resp + ‘\n’); } catch (_) {} }); ``` Claude Desktop config : json { “mcpServers”: { “self-graph”: { “command”: “node”, “args”: [“C:\Users\USER\mcp_proxy.js”] } } } One gotcha: do not send notification responses back to Claude Desktop . The HTTP server returns {“id”: null, “result”: {}} for notifications/initialized . Claude Desktop’s Zod validator rejects any message with id: null (expects string or number) and also rejects result as an unrecognized key in its request schema. Skip notifications entirely in the proxy. The 8 things I wish I’d known Claude Desktop (MSIX) can’t use SSH keys from child processes — skip SSH, use HTTP mcp SDK 1.27.1 requires Tool / TextContent typed objects — plain dicts crash silently then hard-fail anyio’s stdio_server fails through SSH pipes — raw for line in stdin is more robust ** os.path.expanduser(“~”) is unreliable in SSH subprocess context** — use file Module-level open() with no guard crashes Python before anything runs — always use try/except MCP notifications must not generate responses in a proxy — id: null fails Zod SSH stderr is not captured by Claude Desktop — use file-based logging for diagnosis Test with C:\Windows\System32\OpenSSH\ssh.exe specifically , not just ssh from terminal — they may use different key stores submitted by /u/pcx_wave
Originally posted by u/pcx_wave on r/ClaudeCode
