15596df7e4
Anade el binario standalone cmd/devicemesh-mcp/ que expone via JSON-RPC
sobre stdio el catalogo de devicemesh tools (exec, shell.eval, fs.*,
git.*, pkg.*, proc.*, docker.*) al claude -p parent.
Arquitectura issue 0145:
- main.go: flags (--device-agent, --mode, --tools-allowed, --server-name),
inicializa devicemesh.Client + RegisterBuiltins + FilterByAllowed, lanza
server.ServeStdio del SDK mark3labs/mcp-go (ya dep).
- bridge.go: registra cada ToolSpec como mcp.Tool con WithRawInputSchema +
handler que invoca ToolRegistry.Call (validate->map->HTTP->map). Resultado
serializado a NewToolResultText, errores como NewToolResultError para que
el modelo se autocorrija.
Razon: hoy claude -p ve nuestras tool names solo como TEXTO en el system
prompt y las imita sin ejecutar. Con --mcp-config apuntando a este binario,
claude las descubre via tools/list e invoca via tools/call REALMENTE.
Smoke OK: initialize frame produce {capabilities:{tools:{listChanged:true}},
serverInfo:{name:"devicemesh",version:"0.1.0"}}.
Issue doc 0145 incluido con aceptacion A3 anti-hallucination + DoD triada.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
7.8 KiB
Markdown
152 lines
7.8 KiB
Markdown
---
|
|
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/<agent_id>-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:"<JSON>"}]}
|
|
→ 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(<json>)` o `mcp.NewToolResultError(<msg>)`.
|
|
- 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/<agent_id>-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 <path>`.
|
|
- 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__<server>__<tool>`, 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/<agent_id>-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/<agent_id>-mcp-config.json` generado + cfg overrides aplicados |
|
|
| claude args | `--mcp-config <path>` + `--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 `<agent_dir>/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.
|