--- id: "0145" title: "MCP bridge claude-code → devicemesh tools" status: pending type: feature domain: - agents - llm - mcp - devicemesh scope: app priority: high depends: - "0134" - "0144" related_flows: - "0009" related_issues: - "0134" - "0144" created: 2026-05-24 updated: 2026-05-24 tags: [mcp, claude-code, devicemesh, agents] flow: "0009" --- # 0145 — MCP bridge claude-code → devicemesh tools ## Objetivo Hacer que `claude -p` (subprocess que usa el provider `claude-code` de cada agent) **invoque REALMENTE** las 14+ tools de `pkg/tools/devicemesh` (`exec`, `shell.eval`, `fs.*`, `git.*`, `pkg.*`, `proc.*`, `docker.*`) en lugar de imitar el formato como texto. Esto se logra exponiendo el `ToolRegistry` per-agent como un **servidor MCP** (Model Context Protocol) que claude descubre via `--mcp-config` y consume via JSON-RPC stdio. ## Contexto Hoy `claude -p` se invoca con `disable_tools: true` → `--tools ""`, y las tools de device-mesh viven solo en el system prompt como **descripcion textual**. Resultado: - claude **imita** el formato (`{"tool": "exec", ...}`) pero **NO ejecuta** nada. - El audit chain del `device_agent` queda **vacio** tras un "exec" anunciado por el bot. - Anti-criterio A3 del flow 0009 (anti-hallucination) **falla**: el bot dice que hizo algo, el device no recibe nada. El fix correcto es darle a claude un **transporte real** para invocar tools. MCP es el contrato nativo de claude-code: 1. Cada agent levanta su propio MCP server (binario Go child de `claude`). 2. claude descubre tools via `tools/list`, invoca via `tools/call`. 3. El binario MCP traduce `tools/call` → `ToolRegistry.Call` → HTTP al `device_agent` remoto. 4. claude ve los resultados reales, audit DB se llena, anti-hallucination pasa. ## Arquitectura ``` agents_and_robots (VPS) ├─ launcher (Go) │ └─ devagents.New(cfg) │ ├─ buildDeviceMeshRegistry() -- per-agent ToolRegistry │ ├─ buildMCPConfig() -- escribe /tmp/-mcp-config.json │ └─ override cfg.LLM.Primary.ClaudeCode (MCPConfigPath, AllowedTools, DisableTools=false) │ └─ bin/devicemesh-mcp (binario standalone) ├─ stdin ← JSON-RPC frames del claude parent ├─ stdout → JSON-RPC responses ├─ tools/list → enumera 14+ tools del registry filtered └─ tools/call → dispatch HTTP al device_agent via pkg/tools/devicemesh.NewClient + RegisterBuiltins ``` Flujo real una vez activado: ``` operator → Matrix DM → agent-wsl-lucas → claude -p --mcp-config /tmp/agent-wsl-lucas-mcp-config.json --allowedTools "mcp__devicemesh__exec" ... → claude spawna ./bin/devicemesh-mcp como child → claude envia tools/list → devicemesh-mcp responde con 14 tools → claude decide ejecutar exec → claude envia tools/call name=exec args={argv:["ls"]} → devicemesh-mcp llama ToolRegistry.Call("exec", {argv:["ls"]}) → POST http://10.42.0.10:7474/capability {capability:"shell.exec", args:{argv:["ls"]}} → device_agent ejecuta, registra en audit.db, devuelve resultado → devicemesh-mcp empaqueta como MCP {content:[{type:"text", text:""}]} → claude recibe resultado real, lo razona, responde al operador ``` ## Tareas ### Pieza 1 — Binario `cmd/devicemesh-mcp/` - `cmd/devicemesh-mcp/main.go` — entrypoint con flags `--device-agent`, `--mode`, `--tools-allowed`. Inicializa `Client` + `RegisterBuiltins` + `FilterByAllowed`. Lanza loop stdio via `mcp-go server.ServeStdio`. - `cmd/devicemesh-mcp/bridge.go` — adapter: itera `ToolRegistry.List()` y registra cada spec como MCP tool, con handler que invoca `reg.Call(ctx, name, args)` y devuelve `mcp.NewToolResultText()` o `mcp.NewToolResultError()`. - Build target: `bin/devicemesh-mcp`. ### Pieza 2 — Schema config - `internal/config/schema.go`: - `ClaudeCodeCfg`: anadir `MCPConfigPath string` y `MCPServerName string` (default "devicemesh"). - `DeviceMeshConfig`: anadir `ExposeViaMCP *bool` (puntero para distinguir "no establecido" vs "false explicito"). Helper `ShouldExposeViaMCP()` que devuelve true cuando enabled && (nil || *true). ### Pieza 3 — Launcher integration - `devagents/mcp_bridge.go` — funcion `BuildMCPBridge(cfg, logger)` que: - Resuelve binario `bin/devicemesh-mcp` relativo al ejecutable del launcher. - Resuelve URL device_agent (env override igual que `buildDeviceMeshRegistry`). - Construye lista de tools allowed. - Genera el JSON de mcp-config en `/tmp/-mcp-config.json` (mode 0600). - Devuelve `(configPath, allowedToolNames, err)`. - `devagents/runtime.go` o `cmd/launcher/main.go`: tras cargar config si `DeviceMesh.Enabled && ShouldExposeViaMCP`, llamar `BuildMCPBridge` y aplicar overrides a `cfg.LLM.Primary.ClaudeCode` (MCPConfigPath, AllowedTools, DisableTools=false). Logging explicito. ### Pieza 4 — `shell/llm/claudecode.go` - En `buildClaudeArgs`: si `cfg.MCPConfigPath != ""`, append `--mcp-config `. - Validacion defensiva: si `DisableTools=true` y `AllowedTools` no vacio, log warning + ignorar DisableTools (AllowedTools tiene prioridad). ### Pieza 5 — Tests - `cmd/devicemesh-mcp/main_test.go`: - `TestInitialize` — frame initialize → serverInfo + capabilities. - `TestToolsList` — frame tools/list → 14+ tools con `inputSchema`. Mock device-agent via httptest. - `TestToolsCallExec` — tools/call name=exec → device-agent devuelve stdout=hi → assert MCP content contiene "hi". - `TestToolsCallInvalidTool` — tools/call name=nonexistent → assert isError. - `TestNotificationsInitialized` — notification (no id) → assert NO response. - `TestUserModeFilter` — --mode user → pkg.install NO listado; --mode sudo → si. - `cmd/devicemesh-mcp/integration_test.go` — spawn subprocess + secuencia completa. - `devagents/mcp_bridge_test.go` — assert config JSON valido, allowed_tools formato `mcp____`, override DisableTools. ### Pieza 6 — Build + smoke 1. `go build -tags goolm -o bin/devicemesh-mcp ./cmd/devicemesh-mcp` clean. 2. `go build -tags goolm -o bin/launcher ./cmd/launcher` clean. 3. Smoke test del binario: `echo '{"jsonrpc":"2.0","id":1,"method":"initialize",...}' | bin/devicemesh-mcp` produce JSON-RPC response. 4. Deploy a VPS + restart `agents_and_robots.service`. 5. Verificar `/tmp/agent-wsl-lucas-mcp-config.json` se genera tras restart + logs muestran tools registered + claude-code-with-MCP. ## Aceptacion (anti-criterio A3 anti-hallucination) - Al pedirle a `agent-wsl-lucas` que ejecute `ls`, una entry aparece en `audit.db` del device dentro de 5s. - `claude -p` logs muestran `tool_use: mcp__devicemesh__exec` (no texto imitado). - `/tmp/-mcp-config.json` valido, mode 0600. - `bin/devicemesh-mcp` standalone responde a `initialize`/`tools/list`/`tools/call` en JSON-RPC. ## DoD triada por capas | Capa | Verificacion | |---|---| | Binario MCP | `bin/devicemesh-mcp` build clean + tests passing | | Launcher | `/tmp/-mcp-config.json` generado + cfg overrides aplicados | | claude args | `--mcp-config ` + `--allowedTools mcp__devicemesh__*` presentes | | Smoke real | Audit DB del device crece tras prompt al agent | ## Decisiones de diseno 1. **MCP via mcp-go SDK** en vez de implementar JSON-RPC raw. La dep `github.com/mark3labs/mcp-go v0.44.1` ya existe (`shell/mcp/server.go` ya la usa). Usar `server.ServeStdio` reduce superficie de bugs y test surface. 2. **Binario standalone** (`cmd/devicemesh-mcp/`) en vez de embebido en el launcher. Razon: claude lo lanza como child via `--mcp-config` — necesita un ejecutable separado. Tambien permite debuggear en aislamiento (`echo ... | bin/devicemesh-mcp`). 3. **MCPConfigPath en `/tmp/`** (no en `/data/`). El path es runtime-only, regenerable cada arranque, contiene path absoluto al binario del launcher actual + URL devicemesh. Persistirlo en repo crea drift PC↔VPS.