diff --git a/.claude/mcp.json b/.claude/mcp.json index d38cdfc97c..b12b04a017 100644 --- a/.claude/mcp.json +++ b/.claude/mcp.json @@ -9,6 +9,11 @@ "Authorization": "Bearer ${CIPHER_API_TOKEN}" } }, + "agent-zero": { + "_purpose": "Agent Zero orchestrator for cross-agent task dispatch and memory operations.", + "type": "http", + "url": "http://localhost:8080/mcp" + }, "_pmoves-cipher-legacy-python-wrapper": { "_disabled": "Replaced 2026-05-15 with direct SSE to Cipher's native MCP endpoint. The Python wrapper called non-existent /api/memory routes. Kept here commented for rollback; remove in follow-up after SSE validated for 2 weeks.", "command": "uv", diff --git a/.gitignore b/.gitignore index 7c0e43fcc0..86687df0e3 100644 --- a/.gitignore +++ b/.gitignore @@ -123,6 +123,7 @@ yarn-error.log* # Per-node MCP-config backups created by `docker mcp client connect` flow .claude/mcp.json.pre-toolkit-connect.bak .mcp.json.pre-toolkit-connect.bak +*.pre-mcp-bootstrap.bak # Docker MCP Toolkit's per-node Claude Code MCP registration. Generated by # `docker mcp client connect claude-code --profile ` and contains diff --git a/.kimi/mcp.json b/.kimi/mcp.json index 0d5d570593..dd551b0f41 100644 --- a/.kimi/mcp.json +++ b/.kimi/mcp.json @@ -6,12 +6,86 @@ "headers": { "Authorization": "Bearer ${CIPHER_API_TOKEN}" }, - "description": "Agent memory (Neo4j knowledge graph) via Tailscale" + "description": "Agent memory (Neo4j knowledge graph)" }, "agent-zero": { "type": "http", "url": "http://${TS_Z890}:8080/mcp", - "description": "Agent Zero orchestrator via Tailscale" + "description": "Agent Zero orchestrator" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_ROLE_KEY:-${SUPABASE_SERVICE_KEY}}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } } -} \ No newline at end of file +} diff --git a/kilo.json b/kilo.json index 1bd13b760b..9d4f8a1ca9 100644 --- a/kilo.json +++ b/kilo.json @@ -18,14 +18,28 @@ "apiKey": "${Z_AI_API_KEY}" }, "models": { - "glm-5.2": { "name": "GLM-5.2 (Latest)" }, - "glm-5-turbo": { "name": "GLM-5 Turbo (Agentic)" }, - "glm-5.1": { "name": "GLM-5.1 (Reasoning)" }, - "glm-5v-turbo": { "name": "GLM-5V Turbo (Vision+Coding)" }, - "glm-4.7": { "name": "GLM-4.7 (Balanced Coding)" }, - "glm-4-air": { "name": "GLM-4 Air (Fast)" } + "glm-5.2": { + "name": "GLM-5.2 (Latest)" + }, + "glm-5-turbo": { + "name": "GLM-5 Turbo (Agentic)" + }, + "glm-5.1": { + "name": "GLM-5.1 (Reasoning)" + }, + "glm-5v-turbo": { + "name": "GLM-5V Turbo (Vision+Coding)" + }, + "glm-4.7": { + "name": "GLM-4.7 (Balanced Coding)" + }, + "glm-4-air": { + "name": "GLM-4 Air (Fast)" + } }, - "whitelist": ["glm-*"] + "whitelist": [ + "glm-*" + ] }, "minimax": { "options": { @@ -34,16 +48,26 @@ "apiKey": "${MINIMAX_API_KEY}" }, "models": { - "minimax-m2.7": { "name": "MiniMax-M2.7 (1M Context)" }, - "minimax-m2.1": { "name": "MiniMax-M2.1 (100K Context)" } + "minimax-m2.7": { + "name": "MiniMax-M2.7 (1M Context)" + }, + "minimax-m2.1": { + "name": "MiniMax-M2.1 (100K Context)" + } }, - "whitelist": ["minimax-*"] + "whitelist": [ + "minimax-*" + ] } }, "mcp": { "zai-vision": { "type": "local", - "command": ["npx", "-y", "@z_ai/mcp-server"], + "command": [ + "npx", + "-y", + "@z_ai/mcp-server" + ], "environment": { "Z_AI_API_KEY": "${Z_AI_API_KEY}", "Z_AI_MODE": "ZAI" @@ -52,40 +76,110 @@ "zai-web-search": { "type": "remote", "url": "https://api.z.ai/api/mcp/web_search_prime/mcp", - "headers": { "Authorization": "Bearer ${Z_AI_API_KEY}" } + "headers": { + "Authorization": "Bearer ${Z_AI_API_KEY}" + } }, "zai-web-reader": { "type": "remote", "url": "https://api.z.ai/api/mcp/web_reader/mcp", - "headers": { "Authorization": "Bearer ${Z_AI_API_KEY}" } + "headers": { + "Authorization": "Bearer ${Z_AI_API_KEY}" + } }, "zai-zread": { "type": "remote", "url": "https://api.z.ai/api/mcp/zread/mcp", - "headers": { "Authorization": "Bearer ${Z_AI_API_KEY}" } + "headers": { + "Authorization": "Bearer ${Z_AI_API_KEY}" + } }, "pmoves-cipher": { "type": "remote", "url": "http://${TS_Z890}:8105/mcp/sse", - "headers": { "Authorization": "Bearer ${CIPHER_API_TOKEN}" } + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } }, "tailscale": { "type": "local", - "command": ["npx", "-y", "tailscale-mcp@2026.4.10-1"], + "command": [ + "npx", + "-y", + "tailscale-mcp@2026.4.10-1" + ], "environment": { - "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}" + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" } }, "huggingface": { "type": "local", - "command": ["npx", "-y", "@llmindset/hf-mcp-server@0.3.30"], + "command": [ + "npx", + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], "environment": { "HF_TOKEN": "${HF_TOKEN}" } + }, + "agent-zero": { + "type": "remote", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "type": "local", + "command": [ + "uv", + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "environment": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "type": "local", + "command": [ + "npx", + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "type": "local", + "command": [ + "uvx", + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "environment": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "pmoves-docker-gateway-sse": { + "type": "remote", + "url": "http://localhost:8090/sse", + "headers": { + "Authorization": "Bearer ${MCP_GATEWAY_AUTH_TOKEN}" + } } }, "skills": { - "paths": [".kilocode/skills"], + "paths": [ + ".kilocode/skills" + ], "urls": [] }, "permission": { @@ -100,6 +194,11 @@ "zai-zread_*": "allow", "pmoves-cipher_*": "allow", "tailscale_*": "allow", - "huggingface_*": "allow" + "huggingface_*": "allow", + "agent-zero_*": "allow", + "pmoves-nats-fleet_*": "allow", + "pmoves-supabase_*": "allow", + "supabase-db_*": "allow", + "pmoves-docker-gateway-sse_*": "allow" } } diff --git a/pmoves/Makefile b/pmoves/Makefile index cf279f8b3f..3bce56bf7f 100644 --- a/pmoves/Makefile +++ b/pmoves/Makefile @@ -871,7 +871,7 @@ bootstrap-data: ## Umbrella: Supabase SQL + Neo4j seeds + Qdrant/Meili demo data @echo " ๐Ÿ’ก Run 'make smoke' to verify installation" .PHONY: first-run -first-run: ## Guided first-run: env setup, Supabase, core services, agents, demo data +first-run: ## Guided first-run: env setup, Supabase, core services, agents, MCPs, demo data @echo "๐Ÿš€ PMOVES First-Run Bootstrap" @echo "================================" @echo "" @@ -883,6 +883,8 @@ first-run: ## Guided first-run: env setup, Supabase, core services, agents, demo @echo " โœ“ Start core services (data tier, workers)" @echo " โœ“ Start agents (Agent Zero, Archon)" @echo " โœ“ Run auth bootstrap (mode=$(AUTH_BOOTSTRAP_MODE), email=$(SUPABASE_BOOT_USER_EMAIL))" + @echo " โœ“ Bootstrap Docker MCP Toolkit profile" + @echo " โœ“ Register native PMOVES MCP servers (Claude/Kimi/KiloCode configs)" @echo " โœ“ Seed Agent Zero MCP servers" @echo "" @$(MAKE) --no-print-directory check-tools @@ -892,6 +894,8 @@ first-run: ## Guided first-run: env setup, Supabase, core services, agents, demo @$(MAKE) --no-print-directory up @$(MAKE) --no-print-directory up-agents-ui @$(MAKE) --no-print-directory auth-bootstrap + @$(MAKE) --no-print-directory mcp-toolkit-bootstrap || true + @$(MAKE) --no-print-directory mcp-config-bootstrap @$(MAKE) --no-print-directory a0-mcp-seed @echo "" @echo "โœ… First-run complete!" diff --git a/pmoves/config/mcp_inventory.json b/pmoves/config/mcp_inventory.json new file mode 100644 index 0000000000..b9e049b3cb --- /dev/null +++ b/pmoves/config/mcp_inventory.json @@ -0,0 +1,151 @@ +{ + "version": 1, + "note": "Canonical inventory of PMOVES MCP servers. Used by pmoves/tools/mcp_config_generator.py to produce client-native config snippets for Claude, Kimi, KiloCode, Hermes, and Crush.", + "defaults": { + "cipher_local_url": "http://localhost:8105/mcp/sse", + "cipher_fleet_url": "http://${TS_Z890}:8105/mcp/sse", + "agent_zero_local_url": "http://localhost:8080/mcp", + "agent_zero_fleet_url": "http://${TS_Z890}:8080/mcp", + "docker_gateway_port": 8090 + }, + "groups": { + "core_pmoves": { + "description": "Native PMOVES MCP servers available on every node or via the fleet tailnet.", + "servers": [ + { + "key": "pmoves-cipher", + "description": "Agent memory (Neo4j knowledge graph)", + "transport": "sse", + "endpoint": "fleet", + "endpoint_prefix": "cipher", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + { + "key": "pmoves-cipher-local", + "description": "Local agent memory on this node", + "transport": "sse", + "endpoint": "local", + "endpoint_prefix": "cipher", + "url": "http://localhost:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + }, + "clients": ["hermes"] + }, + { + "key": "agent-zero", + "description": "Agent Zero orchestrator", + "transport": "http", + "endpoint": "fleet", + "endpoint_prefix": "agent_zero" + }, + { + "key": "pmoves-nats-fleet", + "description": "NATS fleet messaging bridge", + "transport": "stdio", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + { + "key": "pmoves-supabase", + "description": "Supabase PostgREST API", + "transport": "stdio", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "${SUPABASE_REST_URL:-http://localhost:8000/rest/v1}", + "--apiKey", + "${SUPABASE_SERVICE_ROLE_KEY:-${SUPABASE_SERVICE_KEY}}", + "--schema", + "public" + ] + }, + { + "key": "supabase-db", + "description": "Supabase Postgres database", + "transport": "stdio", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + { + "key": "huggingface", + "description": "HuggingFace model/paper/space search", + "transport": "stdio", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + { + "key": "tailscale", + "description": "Tailscale network administration", + "transport": "stdio", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + } + ] + }, + "docker_toolkit": { + "description": "Docker MCP Toolkit gateway exposing the canonical PMOVES profile.", + "profile": "pmoves_5090_web", + "servers": [ + { + "key": "pmoves-docker-gateway", + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "transport": "stdio", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ], + "clients": ["claude", "kimi", "opencode", "crush"] + }, + { + "key": "pmoves-docker-gateway-sse", + "description": "Docker MCP Toolkit gateway (SSE) for Hermes/KiloCode remote clients", + "transport": "sse", + "endpoint": "local", + "url": "http://localhost:8090/sse", + "headers": { + "Authorization": "Bearer ${MCP_GATEWAY_AUTH_TOKEN}" + }, + "clients": ["hermes", "kilocode"] + } + ] + } + } +} diff --git a/pmoves/config/profiles/hermes/4090.yaml b/pmoves/config/profiles/hermes/4090.yaml index 26a5f7134d..34387c69fa 100644 --- a/pmoves/config/profiles/hermes/4090.yaml +++ b/pmoves/config/profiles/hermes/4090.yaml @@ -86,3 +86,28 @@ security: redact_secrets: true approvals: mode: manual + +# === MCP SERVERS (auto-generated by PMOVES bootstrap) === +mcp_servers: + pmoves-cipher-local: + type: sse + url: http://localhost:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: true + pmoves-cipher-fleet: + type: sse + url: http://${TS_Z890}:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: false + agent-zero: + type: http + url: http://${TS_Z890}:8080/mcp + enabled: true + docker_mcp_gateway: + type: sse + url: http://localhost:8090/sse + auth_token: ${MCP_GATEWAY_AUTH_TOKEN} + tools: "*" + enabled: true diff --git a/pmoves/config/profiles/hermes/5090.yaml b/pmoves/config/profiles/hermes/5090.yaml index 7f1a6670e1..647a7d923a 100644 --- a/pmoves/config/profiles/hermes/5090.yaml +++ b/pmoves/config/profiles/hermes/5090.yaml @@ -102,3 +102,28 @@ security: redact_secrets: true approvals: mode: smart + +# === MCP SERVERS (auto-generated by PMOVES bootstrap) === +mcp_servers: + pmoves-cipher-local: + type: sse + url: http://localhost:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: true + pmoves-cipher-fleet: + type: sse + url: http://${TS_Z890}:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: false + agent-zero: + type: http + url: http://${TS_Z890}:8080/mcp + enabled: true + docker_mcp_gateway: + type: sse + url: http://localhost:8090/sse + auth_token: ${MCP_GATEWAY_AUTH_TOKEN} + tools: "*" + enabled: true diff --git a/pmoves/config/profiles/hermes/b850.yaml b/pmoves/config/profiles/hermes/b850.yaml index 09cbda8855..d0be2095fe 100644 --- a/pmoves/config/profiles/hermes/b850.yaml +++ b/pmoves/config/profiles/hermes/b850.yaml @@ -115,3 +115,28 @@ rocm: - ollama # host-native; requires OLLAMA_HOST override for container reach - llama-cpp-rocm # gfx1201 fork, :8090 (8080 is Agent Zero's) - vllm # via vllm-orchestrator dynamic TZ registration + +# === MCP SERVERS (auto-generated by PMOVES bootstrap) === +mcp_servers: + pmoves-cipher-local: + type: sse + url: http://localhost:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: true + pmoves-cipher-fleet: + type: sse + url: http://${TS_Z890}:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: false + agent-zero: + type: http + url: http://${TS_Z890}:8080/mcp + enabled: true + docker_mcp_gateway: + type: sse + url: http://localhost:8090/sse + auth_token: ${MCP_GATEWAY_AUTH_TOKEN} + tools: "*" + enabled: true diff --git a/pmoves/config/profiles/hermes/elder-melchor.yaml b/pmoves/config/profiles/hermes/elder-melchor.yaml index d063e64cdb..b61aca79a7 100644 --- a/pmoves/config/profiles/hermes/elder-melchor.yaml +++ b/pmoves/config/profiles/hermes/elder-melchor.yaml @@ -235,3 +235,28 @@ platform_toolsets: - tts - vision - web + +# === MCP SERVERS (auto-generated by PMOVES bootstrap) === +mcp_servers: + pmoves-cipher-local: + type: sse + url: http://localhost:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: true + pmoves-cipher-fleet: + type: sse + url: http://${TS_Z890}:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: false + agent-zero: + type: http + url: http://${TS_Z890}:8080/mcp + enabled: true + docker_mcp_gateway: + type: sse + url: http://localhost:8090/sse + auth_token: ${MCP_GATEWAY_AUTH_TOKEN} + tools: "*" + enabled: true diff --git a/pmoves/config/profiles/hermes/kvm4-1.yaml b/pmoves/config/profiles/hermes/kvm4-1.yaml index f2d9fc4a89..6a651c6825 100644 --- a/pmoves/config/profiles/hermes/kvm4-1.yaml +++ b/pmoves/config/profiles/hermes/kvm4-1.yaml @@ -82,3 +82,28 @@ security: redact_secrets: true approvals: mode: smart + +# === MCP SERVERS (auto-generated by PMOVES bootstrap) === +mcp_servers: + pmoves-cipher-local: + type: sse + url: http://localhost:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: true + pmoves-cipher-fleet: + type: sse + url: http://${TS_Z890}:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: false + agent-zero: + type: http + url: http://${TS_Z890}:8080/mcp + enabled: true + docker_mcp_gateway: + type: sse + url: http://localhost:8090/sse + auth_token: ${MCP_GATEWAY_AUTH_TOKEN} + tools: "*" + enabled: true diff --git a/pmoves/config/profiles/hermes/spark.yaml b/pmoves/config/profiles/hermes/spark.yaml index 1b7249e366..27b5c874a5 100644 --- a/pmoves/config/profiles/hermes/spark.yaml +++ b/pmoves/config/profiles/hermes/spark.yaml @@ -118,3 +118,28 @@ security: # model-registry :8110 (CHIT-signed, hf-mem sized for 128GB unified memory) # and promoted to registry_worker_* lanes. No pinned IDs here. local_models: dynamic-model-plane + +# === MCP SERVERS (auto-generated by PMOVES bootstrap) === +mcp_servers: + pmoves-cipher-local: + type: sse + url: http://localhost:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: true + pmoves-cipher-fleet: + type: sse + url: http://${TS_Z890}:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: false + agent-zero: + type: http + url: http://${TS_Z890}:8080/mcp + enabled: true + docker_mcp_gateway: + type: sse + url: http://localhost:8090/sse + auth_token: ${MCP_GATEWAY_AUTH_TOKEN} + tools: "*" + enabled: true diff --git a/pmoves/config/profiles/hermes/z890.yaml b/pmoves/config/profiles/hermes/z890.yaml index 408927e1e3..6fdd29653d 100644 --- a/pmoves/config/profiles/hermes/z890.yaml +++ b/pmoves/config/profiles/hermes/z890.yaml @@ -99,3 +99,28 @@ security: redact_secrets: true approvals: mode: smart + +# === MCP SERVERS (auto-generated by PMOVES bootstrap) === +mcp_servers: + pmoves-cipher-local: + type: sse + url: http://localhost:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: true + pmoves-cipher-fleet: + type: sse + url: http://${TS_Z890}:8105/mcp/sse + headers: + Authorization: Bearer ${CIPHER_API_TOKEN} + enabled: false + agent-zero: + type: http + url: http://${TS_Z890}:8080/mcp + enabled: true + docker_mcp_gateway: + type: sse + url: http://localhost:8090/sse + auth_token: ${MCP_GATEWAY_AUTH_TOKEN} + tools: "*" + enabled: true diff --git a/pmoves/configs/claws/opencode-4090.json b/pmoves/configs/claws/opencode-4090.json index 6661e5f379..2b865ec98d 100644 --- a/pmoves/configs/claws/opencode-4090.json +++ b/pmoves/configs/claws/opencode-4090.json @@ -1,15 +1,92 @@ { - "$schema": "KiloCode opencode.json โ€” Field Agent Node (4090 Laptop)", + "$schema": "KiloCode opencode.json \u2014 Field Agent Node (4090 Laptop)", "mcpServers": { "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", "type": "sse", "url": "http://${TS_Z890}:8105/mcp/sse", - "description": "Agent memory (Neo4j knowledge graph) via Tailscale" + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } }, "agent-zero": { + "description": "Agent Zero orchestrator", "type": "http", - "url": "http://${TS_Z890}:8080/mcp", - "description": "Agent Zero orchestrator via Tailscale" + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "modes": { diff --git a/pmoves/configs/claws/opencode-5090.json b/pmoves/configs/claws/opencode-5090.json index a3adcb62bb..93cfb40908 100644 --- a/pmoves/configs/claws/opencode-5090.json +++ b/pmoves/configs/claws/opencode-5090.json @@ -1,25 +1,25 @@ { - "$schema": "KiloCode opencode.json โ€” GPU Inference Node (5090)", + "$schema": "KiloCode opencode.json \u2014 GPU Inference Node (5090)", "mcpServers": { - "pmoves-cipher": { - "type": "sse", - "url": "http://${TS_Z890}:8105/mcp/sse", - "description": "Agent memory (Neo4j knowledge graph) via Tailscale" - }, - "agent-zero": { - "type": "http", - "url": "http://${TS_Z890}:8080/mcp", - "description": "Agent Zero orchestrator via Tailscale" - }, "docker": { "command": "docker", - "args": ["run", "--rm", "-i", "--mount", "type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock", "mcp/docker"], + "args": [ + "run", + "--rm", + "-i", + "--mount", + "type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock", + "mcp/docker" + ], "description": "Docker container management" }, "zai-vision": { "type": "stdio", "command": "npx", - "args": ["-y", "@z_ai/mcp-server"], + "args": [ + "-y", + "@z_ai/mcp-server" + ], "env": { "Z_AI_API_KEY": "${Z_AI_API_KEY}", "Z_AI_MODE": "ZAI" @@ -40,6 +40,93 @@ "type": "streamable-http", "url": "https://api.z.ai/api/mcp/zread/mcp", "description": "GitHub repo search and file reading" + }, + "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "modes": { diff --git a/pmoves/configs/claws/opencode-kvm2.json b/pmoves/configs/claws/opencode-kvm2.json index f129ed22ad..bac12e6457 100644 --- a/pmoves/configs/claws/opencode-kvm2.json +++ b/pmoves/configs/claws/opencode-kvm2.json @@ -1,6 +1,94 @@ { - "$schema": "KiloCode opencode.json โ€” Exit Proxy Node (KVM2)", - "mcpServers": {}, + "$schema": "KiloCode opencode.json \u2014 Exit Proxy Node (KVM2)", + "mcpServers": { + "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] + } + }, "modes": { "default": "pmoves-debug", "available": [ diff --git a/pmoves/configs/claws/opencode-kvm4-1.json b/pmoves/configs/claws/opencode-kvm4-1.json index dc69b5f526..592bd9c194 100644 --- a/pmoves/configs/claws/opencode-kvm4-1.json +++ b/pmoves/configs/claws/opencode-kvm4-1.json @@ -1,15 +1,92 @@ { - "$schema": "KiloCode opencode.json โ€” API Gateway Node (KVM4-1)", + "$schema": "KiloCode opencode.json \u2014 API Gateway Node (KVM4-1)", "mcpServers": { "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", "type": "sse", "url": "http://${TS_Z890}:8105/mcp/sse", - "description": "Agent memory (Neo4j knowledge graph) via Tailscale" + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } }, "agent-zero": { + "description": "Agent Zero orchestrator", "type": "http", - "url": "http://${TS_Z890}:8080/mcp", - "description": "Agent Zero orchestrator via Tailscale" + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "modes": { diff --git a/pmoves/configs/claws/opencode-kvm4-2.json b/pmoves/configs/claws/opencode-kvm4-2.json index 04607215af..4a712f5793 100644 --- a/pmoves/configs/claws/opencode-kvm4-2.json +++ b/pmoves/configs/claws/opencode-kvm4-2.json @@ -1,10 +1,92 @@ { - "$schema": "KiloCode opencode.json โ€” Data/Storage Node (KVM4-2)", + "$schema": "KiloCode opencode.json \u2014 Data/Storage Node (KVM4-2)", "mcpServers": { "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", "type": "sse", - "url": "http://cipher-memory:8105/mcp/sse", - "description": "Agent memory (Neo4j knowledge graph)" + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "modes": { diff --git a/pmoves/configs/claws/opencode-nemoclaw.json b/pmoves/configs/claws/opencode-nemoclaw.json index 6146521bc0..1d3c47fd01 100644 --- a/pmoves/configs/claws/opencode-nemoclaw.json +++ b/pmoves/configs/claws/opencode-nemoclaw.json @@ -1,6 +1,94 @@ { - "$schema": "KiloCode opencode.json โ€” NemoClaw Edge GPU Node (Jetson)", - "mcpServers": {}, + "$schema": "KiloCode opencode.json \u2014 NemoClaw Edge GPU Node (Jetson)", + "mcpServers": { + "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] + } + }, "modes": { "default": "pmoves-code", "available": [ @@ -21,7 +109,9 @@ ] }, "nats": { - "subscribe": ["mesh.gpu.command.v1"], + "subscribe": [ + "mesh.gpu.command.v1" + ], "publish": [ "mesh.gpu.status.v1", "mesh.gpu.model.loaded.v1", diff --git a/pmoves/configs/claws/opencode-nemotron-claw.json b/pmoves/configs/claws/opencode-nemotron-claw.json index 61aa537c4c..53e2a331b6 100644 --- a/pmoves/configs/claws/opencode-nemotron-claw.json +++ b/pmoves/configs/claws/opencode-nemotron-claw.json @@ -2,14 +2,91 @@ "$schema": "KiloCode opencode.json \u2014 Nemotron Claw GPU Node", "mcpServers": { "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", "type": "sse", "url": "http://${TS_Z890}:8105/mcp/sse", - "description": "Agent memory via Tailscale" + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } }, "agent-zero": { + "description": "Agent Zero orchestrator", "type": "http", - "url": "http://${TS_Z890}:8080/mcp", - "description": "Agent Zero orchestrator via Tailscale" + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "modes": { diff --git a/pmoves/configs/claws/opencode-spark.json b/pmoves/configs/claws/opencode-spark.json new file mode 100644 index 0000000000..ca43b24b48 --- /dev/null +++ b/pmoves/configs/claws/opencode-spark.json @@ -0,0 +1,92 @@ +{ + "$schema": "KiloCode opencode.json \u2014 DGX Spark Node (GB10)", + "mcpServers": { + "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] + } + } +} diff --git a/pmoves/configs/claws/scopes/4090.json b/pmoves/configs/claws/scopes/4090.json index d16dde61df..7535fa6a90 100644 --- a/pmoves/configs/claws/scopes/4090.json +++ b/pmoves/configs/claws/scopes/4090.json @@ -2,7 +2,7 @@ "identity": { "node": "4090", "role": "gpu-inference", - "description": "Active GPU contributor โ€” UI/CLI specialist, mobile inference, Crush companion", + "description": "Active GPU contributor \u2014 UI/CLI specialist, mobile inference, Crush companion", "hostname": "pmoves-laptop" }, "openclaw_overrides": { @@ -10,37 +10,69 @@ "tools.profile": "standard", "tools.exec.ask": "auto-confirm", "channels": { - "telegram": { "enabled": true }, - "discord": { "enabled": true } + "telegram": { + "enabled": true + }, + "discord": { + "enabled": true + } } }, "exec_approvals": { "main": { "allowlist": [ - { "pattern": "/usr/bin/docker", "note": "Docker container management and compose" }, - { "pattern": "/usr/bin/make", "note": "Make targets" }, - { "pattern": "/usr/bin/git", "note": "Git operations" }, - { "pattern": "/usr/bin/gh", "note": "GitHub CLI โ€” PR and issue management" }, - { "pattern": "/usr/bin/curl", "note": "Health checks and API calls" }, - { "pattern": "/usr/bin/node", "note": "Node.js runtime" }, - { "pattern": "/usr/bin/python3", "note": "Python runtime" }, - { "pattern": "/usr/bin/nvidia-smi", "note": "GPU monitoring (RTX 4090 Laptop)" }, - { "pattern": "/usr/bin/ollama", "note": "Local model serving (GPU)" }, - { "pattern": "/usr/bin/tailscale", "note": "Tailscale mesh status" }, - { "pattern": "/usr/bin/ffmpeg", "note": "Video encoding (NVENC pipeline)" }, - { "pattern": "/usr/bin/nats", "note": "NATS CLI โ€” GPU mesh message bus" } + { + "pattern": "/usr/bin/docker", + "note": "Docker container management and compose" + }, + { + "pattern": "/usr/bin/make", + "note": "Make targets" + }, + { + "pattern": "/usr/bin/git", + "note": "Git operations" + }, + { + "pattern": "/usr/bin/gh", + "note": "GitHub CLI \u2014 PR and issue management" + }, + { + "pattern": "/usr/bin/curl", + "note": "Health checks and API calls" + }, + { + "pattern": "/usr/bin/node", + "note": "Node.js runtime" + }, + { + "pattern": "/usr/bin/python3", + "note": "Python runtime" + }, + { + "pattern": "/usr/bin/nvidia-smi", + "note": "GPU monitoring (RTX 4090 Laptop)" + }, + { + "pattern": "/usr/bin/ollama", + "note": "Local model serving (GPU)" + }, + { + "pattern": "/usr/bin/tailscale", + "note": "Tailscale mesh status" + }, + { + "pattern": "/usr/bin/ffmpeg", + "note": "Video encoding (NVENC pipeline)" + }, + { + "pattern": "/usr/bin/nats", + "note": "NATS CLI \u2014 GPU mesh message bus" + } ] } }, "mcp_servers": { - "pmoves-cipher": { - "type": "sse", - "url": "http://${TS_Z890}:8105/sse" - }, - "agent-zero": { - "type": "http", - "url": "http://${TS_Z890}:8080/mcp" - }, "gpu-mesh": { "type": "nats", "subjects": [ @@ -52,6 +84,93 @@ "mesh.gpu.status.v1" ], "url": "nats://${TS_Z890}:4222" + }, + "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "damage_control_hooks": true, diff --git a/pmoves/configs/claws/scopes/5090.json b/pmoves/configs/claws/scopes/5090.json index fff4371a1c..afc310a4eb 100644 --- a/pmoves/configs/claws/scopes/5090.json +++ b/pmoves/configs/claws/scopes/5090.json @@ -2,7 +2,7 @@ "identity": { "node": "5090", "role": "gpu-inference", - "description": "GPU inference specialist โ€” TTS mesh routing, model integration, ToKenism bridge", + "description": "GPU inference specialist \u2014 TTS mesh routing, model integration, ToKenism bridge", "hostname": "pmoves-5090" }, "openclaw_overrides": { @@ -10,43 +10,80 @@ "tools.profile": "full", "tools.exec.ask": "auto", "channels": { - "discord": { "enabled": true } + "discord": { + "enabled": true + } } }, "exec_approvals": { "main": { "allowlist": [ - { "pattern": "/usr/bin/docker", "note": "Docker container management" }, - { "pattern": "/usr/bin/make", "note": "Make targets" }, - { "pattern": "/usr/bin/git", "note": "Git operations" }, - { "pattern": "/usr/bin/gh", "note": "GitHub CLI โ€” PR and issue management" }, - { "pattern": "/usr/bin/curl", "note": "Health checks and API calls" }, - { "pattern": "/usr/bin/nats", "note": "NATS CLI โ€” message bus operations" }, - { "pattern": "/usr/bin/python3", "note": "Python runtime" }, - { "pattern": "/usr/bin/node", "note": "Node.js runtime" }, - { "pattern": "/usr/bin/tailscale", "note": "Tailscale mesh management" }, - { "pattern": "/usr/bin/nvidia-smi", "note": "GPU monitoring and diagnostics" }, - { "pattern": "/usr/bin/ollama", "note": "Local model serving (GPU)" } + { + "pattern": "/usr/bin/docker", + "note": "Docker container management" + }, + { + "pattern": "/usr/bin/make", + "note": "Make targets" + }, + { + "pattern": "/usr/bin/git", + "note": "Git operations" + }, + { + "pattern": "/usr/bin/gh", + "note": "GitHub CLI \u2014 PR and issue management" + }, + { + "pattern": "/usr/bin/curl", + "note": "Health checks and API calls" + }, + { + "pattern": "/usr/bin/nats", + "note": "NATS CLI \u2014 message bus operations" + }, + { + "pattern": "/usr/bin/python3", + "note": "Python runtime" + }, + { + "pattern": "/usr/bin/node", + "note": "Node.js runtime" + }, + { + "pattern": "/usr/bin/tailscale", + "note": "Tailscale mesh management" + }, + { + "pattern": "/usr/bin/nvidia-smi", + "note": "GPU monitoring and diagnostics" + }, + { + "pattern": "/usr/bin/ollama", + "note": "Local model serving (GPU)" + } ] } }, "mcp_servers": { - "pmoves-cipher": { - "type": "sse", - "url": "http://${TS_Z890}:8105/sse" - }, - "agent-zero": { - "type": "http", - "url": "http://${TS_Z890}:8080/mcp" - }, "docker": { "command": "docker", - "args": ["run", "--rm", "-i", "--mount", "type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock", "mcp/docker"] + "args": [ + "run", + "--rm", + "-i", + "--mount", + "type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock", + "mcp/docker" + ] }, "zai-vision": { "type": "stdio", "command": "npx", - "args": ["-y", "@z_ai/mcp-server"], + "args": [ + "-y", + "@z_ai/mcp-server" + ], "env": { "Z_AI_API_KEY": "${Z_AI_API_KEY}", "Z_AI_MODE": "ZAI" @@ -67,6 +104,93 @@ "type": "streamable-http", "url": "https://api.z.ai/api/mcp/zread/mcp", "description": "GitHub repo search and file reading" + }, + "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "damage_control_hooks": true, diff --git a/pmoves/configs/claws/scopes/kvm2.json b/pmoves/configs/claws/scopes/kvm2.json index 4f3e879c1f..4119061894 100644 --- a/pmoves/configs/claws/scopes/kvm2.json +++ b/pmoves/configs/claws/scopes/kvm2.json @@ -2,7 +2,7 @@ "identity": { "node": "kvm2", "role": "exit-proxy", - "description": "Exit proxy node โ€” network edge, Cloudflare, headscale, VPN", + "description": "Exit proxy node \u2014 network edge, Cloudflare, headscale, VPN", "hostname": "kvm2" }, "openclaw_overrides": { @@ -14,18 +14,130 @@ "exec_approvals": { "main": { "allowlist": [ - { "pattern": "/usr/bin/tailscale", "note": "Tailscale management" }, - { "pattern": "/usr/bin/wg", "note": "WireGuard diagnostics" }, - { "pattern": "/usr/bin/cloudflared", "note": "Cloudflare tunnel management" }, - { "pattern": "/usr/bin/dig", "note": "DNS lookups" }, - { "pattern": "/usr/bin/traceroute", "note": "Network path diagnostics" }, - { "pattern": "/usr/bin/curl", "note": "HTTP probes and health checks" }, - { "pattern": "/usr/bin/ss", "note": "Socket statistics" }, - { "pattern": "/usr/bin/ip", "note": "Network interface inspection" } + { + "pattern": "/usr/bin/tailscale", + "note": "Tailscale management" + }, + { + "pattern": "/usr/bin/wg", + "note": "WireGuard diagnostics" + }, + { + "pattern": "/usr/bin/cloudflared", + "note": "Cloudflare tunnel management" + }, + { + "pattern": "/usr/bin/dig", + "note": "DNS lookups" + }, + { + "pattern": "/usr/bin/traceroute", + "note": "Network path diagnostics" + }, + { + "pattern": "/usr/bin/curl", + "note": "HTTP probes and health checks" + }, + { + "pattern": "/usr/bin/ss", + "note": "Socket statistics" + }, + { + "pattern": "/usr/bin/ip", + "note": "Network interface inspection" + } + ] + } + }, + "mcp_servers": { + "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" ] } }, - "mcp_servers": {}, "damage_control_hooks": true, "sign_trail": false } diff --git a/pmoves/configs/claws/scopes/kvm4-1.json b/pmoves/configs/claws/scopes/kvm4-1.json index c500da7d4f..2715e206ca 100644 --- a/pmoves/configs/claws/scopes/kvm4-1.json +++ b/pmoves/configs/claws/scopes/kvm4-1.json @@ -2,7 +2,7 @@ "identity": { "node": "kvm4-1", "role": "api-gateway", - "description": "API gateway node โ€” orchestration, health checks, NATS routing", + "description": "API gateway node \u2014 orchestration, health checks, NATS routing", "hostname": "kvm4-1" }, "openclaw_overrides": { @@ -10,31 +10,136 @@ "tools.profile": "restricted", "tools.exec.ask": "auto", "channels": { - "discord": { "enabled": true } + "discord": { + "enabled": true + } } }, "exec_approvals": { "main": { "allowlist": [ - { "pattern": "/usr/bin/curl", "note": "Health checks and API calls" }, - { "pattern": "/usr/bin/gh", "note": "GitHub CLI โ€” PR and issue management" }, - { "pattern": "/usr/bin/nats", "note": "NATS CLI โ€” message bus operations" }, - { "pattern": "/usr/bin/dig", "note": "DNS diagnostics" }, - { "pattern": "/usr/bin/systemctl", "note": "Service management" }, - { "pattern": "/usr/bin/journalctl", "note": "Log inspection" }, - { "pattern": "/usr/bin/git", "note": "Git operations" }, - { "pattern": "/usr/bin/tailscale", "note": "Tailscale status (read-only)" } + { + "pattern": "/usr/bin/curl", + "note": "Health checks and API calls" + }, + { + "pattern": "/usr/bin/gh", + "note": "GitHub CLI \u2014 PR and issue management" + }, + { + "pattern": "/usr/bin/nats", + "note": "NATS CLI \u2014 message bus operations" + }, + { + "pattern": "/usr/bin/dig", + "note": "DNS diagnostics" + }, + { + "pattern": "/usr/bin/systemctl", + "note": "Service management" + }, + { + "pattern": "/usr/bin/journalctl", + "note": "Log inspection" + }, + { + "pattern": "/usr/bin/git", + "note": "Git operations" + }, + { + "pattern": "/usr/bin/tailscale", + "note": "Tailscale status (read-only)" + } ] } }, "mcp_servers": { "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", "type": "sse", - "url": "http://${TS_Z890}:8105/sse" + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } }, "agent-zero": { + "description": "Agent Zero orchestrator", "type": "http", "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "damage_control_hooks": true, diff --git a/pmoves/configs/claws/scopes/kvm4-2.json b/pmoves/configs/claws/scopes/kvm4-2.json index 1a67a49103..75fdb6d760 100644 --- a/pmoves/configs/claws/scopes/kvm4-2.json +++ b/pmoves/configs/claws/scopes/kvm4-2.json @@ -2,7 +2,7 @@ "identity": { "node": "kvm4-2", "role": "data-storage", - "description": "Data and storage node โ€” Supabase, MinIO, Qdrant, Meilisearch, Neo4j", + "description": "Data and storage node \u2014 Supabase, MinIO, Qdrant, Meilisearch, Neo4j", "hostname": "kvm4-2" }, "openclaw_overrides": { @@ -14,19 +14,120 @@ "exec_approvals": { "main": { "allowlist": [ - { "pattern": "/usr/bin/psql", "note": "PostgreSQL client โ€” Supabase queries" }, - { "pattern": "/usr/bin/mc", "note": "MinIO client โ€” object storage ops" }, - { "pattern": "/usr/bin/curl", "note": "API calls to data services" }, - { "pattern": "/usr/bin/python3", "note": "Python scripts for data ops" }, - { "pattern": "/usr/bin/git", "note": "Git operations" }, - { "pattern": "/usr/bin/tailscale", "note": "Tailscale status (read-only)" } + { + "pattern": "/usr/bin/psql", + "note": "PostgreSQL client \u2014 Supabase queries" + }, + { + "pattern": "/usr/bin/mc", + "note": "MinIO client \u2014 object storage ops" + }, + { + "pattern": "/usr/bin/curl", + "note": "API calls to data services" + }, + { + "pattern": "/usr/bin/python3", + "note": "Python scripts for data ops" + }, + { + "pattern": "/usr/bin/git", + "note": "Git operations" + }, + { + "pattern": "/usr/bin/tailscale", + "note": "Tailscale status (read-only)" + } ] } }, "mcp_servers": { "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", "type": "sse", - "url": "http://cipher-memory:8105/sse" + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "damage_control_hooks": true, diff --git a/pmoves/configs/claws/scopes/nemoclaw.json b/pmoves/configs/claws/scopes/nemoclaw.json index 233ed55617..bc7f726884 100644 --- a/pmoves/configs/claws/scopes/nemoclaw.json +++ b/pmoves/configs/claws/scopes/nemoclaw.json @@ -2,7 +2,7 @@ "identity": { "node": "jetson", "role": "edge-gpu", - "description": "NemoClaw โ€” NVIDIA Jetson edge GPU node for local inference and model ops", + "description": "NemoClaw \u2014 NVIDIA Jetson edge GPU node for local inference and model ops", "hostname": "nemoclaw" }, "openclaw_overrides": { @@ -21,20 +21,72 @@ "exec_approvals": { "main": { "allowlist": [ - { "pattern": "/usr/bin/nvidia-smi", "note": "GPU status and monitoring" }, - { "pattern": "/usr/bin/tegrastats", "note": "Jetson system telemetry" }, - { "pattern": "/usr/bin/jetson_clocks", "note": "Jetson clock management" }, - { "pattern": "/usr/bin/python3", "note": "Python runtime for model ops" }, - { "pattern": "/usr/bin/trtexec", "note": "TensorRT engine builder" }, - { "pattern": "/usr/local/bin/ollama", "note": "Ollama local model server" }, - { "pattern": "/usr/bin/curl", "note": "Health checks to local services" }, - { "pattern": "/usr/bin/git", "note": "Git operations (model repos)" } + { + "pattern": "/usr/bin/nvidia-smi", + "note": "GPU status and monitoring" + }, + { + "pattern": "/usr/bin/tegrastats", + "note": "Jetson system telemetry" + }, + { + "pattern": "/usr/bin/jetson_clocks", + "note": "Jetson clock management" + }, + { + "pattern": "/usr/bin/python3", + "note": "Python runtime for model ops" + }, + { + "pattern": "/usr/bin/trtexec", + "note": "TensorRT engine builder" + }, + { + "pattern": "/usr/local/bin/ollama", + "note": "Ollama local model server" + }, + { + "pattern": "/usr/bin/curl", + "note": "Health checks to local services" + }, + { + "pattern": "/usr/bin/git", + "note": "Git operations (model repos)" + } ] } }, - "mcp_servers": {}, + "mcp_servers": { + "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + } + }, "nats_subjects": { - "subscribe": ["mesh.gpu.command.v1"], + "subscribe": [ + "mesh.gpu.command.v1" + ], "publish": [ "mesh.gpu.status.v1", "mesh.gpu.model.loaded.v1", diff --git a/pmoves/configs/claws/scopes/nemotron-claw.json b/pmoves/configs/claws/scopes/nemotron-claw.json index 8bcdcf7284..de20ed8da1 100644 --- a/pmoves/configs/claws/scopes/nemotron-claw.json +++ b/pmoves/configs/claws/scopes/nemotron-claw.json @@ -58,12 +58,91 @@ }, "mcp_servers": { "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", "type": "sse", - "url": "http://${TS_Z890}:8105/sse" + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } }, "agent-zero": { + "description": "Agent Zero orchestrator", "type": "http", "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "nats_subjects": { diff --git a/pmoves/configs/claws/scopes/spark.json b/pmoves/configs/claws/scopes/spark.json new file mode 100644 index 0000000000..fb236808bd --- /dev/null +++ b/pmoves/configs/claws/scopes/spark.json @@ -0,0 +1,135 @@ +{ + "identity": { + "node": "spark", + "role": "gpu-inference", + "description": "DGX Spark \u2014 GB10 Grace-Blackwell primary inference node", + "hostname": "pmoves-spark" + }, + "openclaw_overrides": { + "agents.defaults.model.primary": "claude-sonnet-4", + "tools.profile": "full", + "tools.exec.ask": "auto", + "channels": {} + }, + "exec_approvals": { + "main": { + "allowlist": [ + { + "pattern": "/usr/bin/docker", + "note": "Docker container management" + }, + { + "pattern": "/usr/bin/make", + "note": "Make targets" + }, + { + "pattern": "/usr/bin/git", + "note": "Git operations" + }, + { + "pattern": "/usr/bin/curl", + "note": "Health checks and API calls" + }, + { + "pattern": "/usr/bin/python3", + "note": "Python runtime" + }, + { + "pattern": "/usr/bin/nvidia-smi", + "note": "GPU monitoring" + } + ] + } + }, + "mcp_servers": { + "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } + }, + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://${TS_Z890}:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] + } + }, + "damage_control_hooks": true, + "sign_trail": true +} diff --git a/pmoves/configs/claws/scopes/z890.json b/pmoves/configs/claws/scopes/z890.json index 4129031516..e7181292d7 100644 --- a/pmoves/configs/claws/scopes/z890.json +++ b/pmoves/configs/claws/scopes/z890.json @@ -2,7 +2,7 @@ "identity": { "node": "z890", "role": "full-infra", - "description": "Primary development workstation โ€” full infrastructure access", + "description": "Primary development workstation \u2014 full infrastructure access", "hostname": "z890" }, "openclaw_overrides": { @@ -10,36 +10,165 @@ "tools.profile": "full", "tools.exec.ask": "off", "channels": { - "telegram": { "enabled": true }, - "discord": { "enabled": true } + "telegram": { + "enabled": true + }, + "discord": { + "enabled": true + } }, - "hooks.presets": ["gmail"] + "hooks.presets": [ + "gmail" + ] }, "exec_approvals": { "main": { "allowlist": [ - { "pattern": "/usr/bin/docker", "note": "Full Docker access" }, - { "pattern": "/usr/bin/make", "note": "All make targets" }, - { "pattern": "/usr/bin/ssh", "note": "SSH to all nodes" }, - { "pattern": "/usr/bin/git", "note": "Full git access" }, - { "pattern": "/usr/bin/gh", "note": "GitHub CLI" }, - { "pattern": "/usr/bin/curl", "note": "HTTP requests" }, - { "pattern": "/usr/bin/nats", "note": "NATS CLI" }, - { "pattern": "/usr/bin/python3", "note": "Python runtime" }, - { "pattern": "/usr/bin/node", "note": "Node.js runtime" }, - { "pattern": "/usr/bin/tailscale", "note": "Tailscale management" }, - { "pattern": "/usr/bin/nvidia-smi", "note": "GPU monitoring" } + { + "pattern": "/usr/bin/docker", + "note": "Full Docker access" + }, + { + "pattern": "/usr/bin/make", + "note": "All make targets" + }, + { + "pattern": "/usr/bin/ssh", + "note": "SSH to all nodes" + }, + { + "pattern": "/usr/bin/git", + "note": "Full git access" + }, + { + "pattern": "/usr/bin/gh", + "note": "GitHub CLI" + }, + { + "pattern": "/usr/bin/curl", + "note": "HTTP requests" + }, + { + "pattern": "/usr/bin/nats", + "note": "NATS CLI" + }, + { + "pattern": "/usr/bin/python3", + "note": "Python runtime" + }, + { + "pattern": "/usr/bin/node", + "note": "Node.js runtime" + }, + { + "pattern": "/usr/bin/tailscale", + "note": "Tailscale management" + }, + { + "pattern": "/usr/bin/nvidia-smi", + "note": "GPU monitoring" + } ] } }, "mcp_servers": { + "docker": { + "command": "docker", + "args": [ + "run", + "--rm", + "-i", + "--mount", + "type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock", + "mcp/docker" + ] + }, "pmoves-cipher": { + "description": "Agent memory (Neo4j knowledge graph)", "type": "sse", - "url": "http://localhost:8105/sse" + "url": "http://localhost:8105/mcp/sse", + "headers": { + "Authorization": "Bearer ${CIPHER_API_TOKEN}" + } }, - "docker": { + "agent-zero": { + "description": "Agent Zero orchestrator", + "type": "http", + "url": "http://localhost:8080/mcp" + }, + "pmoves-nats-fleet": { + "description": "NATS fleet messaging bridge", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server" + ], + "env": { + "NATS_URL": "${NATS_URL}" + } + }, + "pmoves-supabase": { + "description": "Supabase PostgREST API", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "http://localhost:8000/rest/v1", + "--apiKey", + "${SUPABASE_SERVICE_KEY}", + "--schema", + "public" + ] + }, + "supabase-db": { + "description": "Supabase Postgres database", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted" + ], + "env": { + "DATABASE_URI": "${SUPABASE_DB_URI}" + } + }, + "huggingface": { + "description": "HuggingFace model/paper/space search", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30" + ], + "env": { + "HF_TOKEN": "${HF_TOKEN}" + } + }, + "tailscale": { + "description": "Tailscale network administration", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1" + ], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}" + } + }, + "pmoves-docker-gateway": { + "description": "Docker MCP Toolkit gateway (stdio) for Claude/Kimi/OpenCode/Crush", "command": "docker", - "args": ["run", "--rm", "-i", "--mount", "type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock", "mcp/docker"] + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web" + ] } }, "damage_control_hooks": true, diff --git a/pmoves/docs/operations/MCP_TOOLKIT.md b/pmoves/docs/operations/MCP_TOOLKIT.md index 3c9d4fb458..147cb0348e 100644 --- a/pmoves/docs/operations/MCP_TOOLKIT.md +++ b/pmoves/docs/operations/MCP_TOOLKIT.md @@ -220,20 +220,20 @@ Upstream Docker docs note: **"E2B sandboxes now include direct access to the Doc | Target | What it does | |---|---| -| `make mcp-toolkit-bootstrap` | Verifies `docker mcp` CLI present, pulls `pmoves_5090_web` profile from OCI, imports it. Idempotent. Per-node. | +| `make mcp-toolkit-bootstrap` | Verifies `docker mcp` CLI present, pulls `pmoves_5090_web` profile from OCI, imports it. Also writes Kimi + KiloCode configs for the native PMOVES MCP stack. Idempotent. Per-node. | | `make mcp-toolkit-secrets-sync` | Reads `pmoves/env.shared` (override via `PMOVES_TIER_FILE`), populates `docker-pass`-style Toolkit secrets non-interactively. Skips OAuth-style servers (see ยง 5). | | `make mcp-toolkit-status` | `docker mcp profile ls && docker mcp client ls && docker mcp secret ls` + gateway PID โ€” single-shot health. | | `make mcp-toolkit-gateway-start` | Run gateway in SSE on a network port (background). See ยง 6 for security model. | | `make mcp-toolkit-gateway-stop` | Graceful stop + force-kill fallback. | | `make mcp-toolkit-gateway-tail` | `tail -f` the gateway log. | -| Target | What it does | Added by | -|---|---|---| -| `make mcp-toolkit-bootstrap` | Verifies `docker mcp` CLI present, pulls `pmoves_5090_web` profile from OCI, imports it. Idempotent. Per-node. | PR #1553 | -| `make mcp-toolkit-secrets-sync` | Reads `pmoves/env.shared` (override via `PMOVES_TIER_FILE`), populates `docker-pass`-style Toolkit secrets non-interactively. Skips OAuth-style servers (see ยง 5). | PR #1553 | -| `make mcp-toolkit-connect` | Pre-flights CLI + profile presence, backs up pre-existing `.mcp.json` and `.claude/mcp.json`, runs `docker mcp client connect claude-code --profile $(PROFILE)`. Override profile: `PROFILE=`. Writes a project-scoped `.mcp.json` (gitignored, host-specific env paths). | This PR (Lane A) | -| `make mcp-toolkit-status` | `docker mcp profile ls && docker mcp client ls && docker mcp secret ls` โ€” single-shot health. | PR #1553 | - -Targets live in `pmoves/Makefile`. Scripts live in `pmoves/scripts/mcp-toolkit-*.sh`. +| `make mcp-toolkit-connect` | Pre-flights CLI + profile presence, backs up pre-existing `.mcp.json` and `.claude/mcp.json`, runs `docker mcp client connect claude-code --profile $(PROFILE)`. Override profile: `PROFILE=`. Writes a project-scoped `.mcp.json` (gitignored, host-specific env paths). | +| `make mcp-config-bootstrap` | Writes Kimi + KiloCode `.kimi/mcp.json` and `kilo.json`, plus all `pmoves/configs/claws/opencode-*.json` node configs, from the canonical inventory (`pmoves/config/mcp_inventory.json`). Safe to re-run. | +| `make mcp-bootstrap` | Umbrella: Toolkit profile + native PMOVES MCP configs. Runs even when Docker Toolkit is unavailable. | +| `make mcp-bootstrap-check` | Validates the imported Toolkit profile, generated configs, and key presence. | +| `make hermes-crush-bootstrap` | Updates Hermes Agent `~/.hermes/profiles/pmoves-hermes/config.yaml` and Crush CLI `~/.config/crush/crush.json` MCP sections from the inventory. | +| `make opencode-bootstrap` | Updates all `pmoves/configs/claws/opencode-*.json` node configs with canonical PMOVES MCPs (preserves existing zai/docker entries). | + +Targets live in `pmoves/Makefile` (included from `pmoves/mk/mcp-toolkit.mk`). Scripts live in `pmoves/scripts/mcp-toolkit-*.sh` and `pmoves/scripts/bootstrap-hermes-crush.sh`. --- diff --git a/pmoves/mk/kilo.mk b/pmoves/mk/kilo.mk index 2c2adbd071..b498a1edd7 100644 --- a/pmoves/mk/kilo.mk +++ b/pmoves/mk/kilo.mk @@ -97,12 +97,12 @@ kilo-parity-check: ## Report parity gaps between KiloCode GLM and Kimi/Codex/Cla echo " โŒ KiloCode cross-linked in .kimi/AGENTS.md"; gaps=$$((gaps+1)); \ fi; \ echo ""; \ + mcp_gaps=$$(PYTHONPATH="$(CURDIR)/.." $(PYTHON) -m pmoves.tools.kilo_parity_mcp_check); \ + gaps=$$((gaps + mcp_gaps)); \ if [ $$blocked -gt 0 ]; then \ echo "[*] Results: $$gaps gap(s) found, $$blocked item(s) blocked by platform (expected)"; \ else \ echo "[*] Results: $$gaps gap(s) found"; \ fi; \ - if [ $$blocked -gt 0 ]; then \ - echo "[!] Hook implementation blocked โ€” see .kilo/hooks/damage-control/README.md for activation plan"; \ - fi; \ + exit $$gaps exit $$gaps diff --git a/pmoves/mk/mcp-toolkit.mk b/pmoves/mk/mcp-toolkit.mk index 6bdb8ed3a8..b52dffce4a 100644 --- a/pmoves/mk/mcp-toolkit.mk +++ b/pmoves/mk/mcp-toolkit.mk @@ -1,18 +1,18 @@ # pmoves/mk/mcp-toolkit.mk # -# Docker MCP Toolkit fleet operations. See pmoves/docs/operations/MCP_TOOLKIT.md -# for the full operational guide. +# Docker MCP Toolkit fleet operations + PMOVES MCP server bootstrap. +# See pmoves/docs/operations/MCP_TOOLKIT.md for the full operational guide. # # mcp-toolkit-connect is the only target that MUTATES a client's MCP config # (writes .mcp.json at the repo root). It is gated on operator authorization โ€” # call explicitly, not as part of a wider chain. -.PHONY: mcp-toolkit-bootstrap mcp-toolkit-secrets-sync mcp-toolkit-status mcp-toolkit-help mcp-toolkit-gateway-start mcp-toolkit-gateway-stop mcp-toolkit-gateway-tail .PHONY: mcp-toolkit-bootstrap mcp-toolkit-secrets-sync mcp-toolkit-status mcp-toolkit-connect mcp-toolkit-help -.PHONY: mcp-toolkit-bootstrap mcp-toolkit-secrets-sync mcp-toolkit-status mcp-toolkit-verify mcp-toolkit-help +.PHONY: mcp-toolkit-gateway-start mcp-toolkit-gateway-stop mcp-toolkit-gateway-tail mcp-toolkit-verify +.PHONY: mcp-core-bootstrap mcp-config-bootstrap mcp-bootstrap mcp-bootstrap-check hermes-crush-bootstrap opencode-bootstrap openclaw-scope-bootstrap openclaw-scope-check -mcp-toolkit-help: ## Show Docker MCP Toolkit Make targets - @echo "Docker MCP Toolkit targets:" +mcp-toolkit-help: ## Show Docker MCP Toolkit + PMOVES MCP bootstrap targets + @echo "Docker MCP Toolkit + PMOVES MCP targets:" @echo " mcp-toolkit-bootstrap Pull + import the canonical PMOVES profile (idempotent)" @echo " Override: PMOVES_MCP_PROFILE_REF= PMOVES_MCP_REFRESH=1" @echo " mcp-toolkit-secrets-sync Populate docker-pass-style secrets from pmoves/env.shared" @@ -28,6 +28,14 @@ mcp-toolkit-help: ## Show Docker MCP Toolkit Make targets @echo " mcp-toolkit-gateway-tail Tail the background gateway log" @echo " mcp-toolkit-verify End-to-end fixture: 5 phases (profile, connect, tools, gateway, call)" @echo " Override: PROFILE= MCP_GATEWAY_PORT= PROBE_TOOL= PROBE_TOOL_ARG=" + @echo " mcp-core-bootstrap Register native PMOVES MCP servers (idempotent)" + @echo " mcp-config-bootstrap Write agent-stack MCP configs from canonical inventory" + @echo " mcp-bootstrap Umbrella: Toolkit + core + config bootstrap" + @echo " mcp-bootstrap-check Validate imported profile + generated configs + reachability" + @echo " hermes-crush-bootstrap Update Hermes Agent and Crush CLI MCP configs" + @echo " opencode-bootstrap Update all pmoves/configs/claws/opencode-*.json MCP configs" + @echo " openclaw-scope-bootstrap Update all pmoves/configs/claws/scopes/*.json MCP configs" + @echo " openclaw-scope-check Validate scope MCP configs against tier expectations" @echo " mcp-toolkit-help This message" @echo @echo "Full guide: pmoves/docs/operations/MCP_TOOLKIT.md" @@ -46,6 +54,7 @@ mcp-toolkit-gateway-stop: ## Stop the background gateway started by mcp-toolkit- mcp-toolkit-gateway-tail: ## Tail the background gateway log @tail -f $${PMOVES_MCP_GATEWAY_LOG:-/tmp/pmoves-mcp-gateway.log} + mcp-toolkit-connect: ## Connect claude-code to the imported profile (per-node; writes .mcp.json โ€” gitignored) @bash scripts/mcp-toolkit-connect.sh @@ -73,3 +82,77 @@ mcp-toolkit-status: ## Show docker mcp profile / client / secret status mcp-toolkit-verify: ## End-to-end MCP Toolkit fixture (5 phases โ€” see tools/verify_pmoves_5090_web_mcp_integration.sh) @bash tools/verify_pmoves_5090_web_mcp_integration.sh + +# --------------------------------------------------------------------------- +# PMOVES MCP server bootstrap +# --------------------------------------------------------------------------- + +mcp-core-bootstrap: mcp-config-bootstrap ## Alias: register native PMOVES MCP servers (writes Kimi + KiloCode configs) + +mcp-config-bootstrap: ## Write Kimi + KiloCode + OpenCode + OpenClaw scope MCP configs from canonical inventory + @PYTHONPATH="$(CURDIR)/.." $(PYTHON) -m pmoves.tools.mcp_config_generator --client kimi + @PYTHONPATH="$(CURDIR)/.." $(PYTHON) -m pmoves.tools.mcp_config_generator --client kilocode + @$(MAKE) --no-print-directory opencode-bootstrap + @$(MAKE) --no-print-directory openclaw-scope-bootstrap + +opencode-bootstrap: ## Update all pmoves/configs/claws/opencode-*.json MCP configs + @PYTHONPATH="$(CURDIR)/.." $(PYTHON) -m pmoves.tools.bootstrap_opencode + +openclaw-scope-bootstrap: ## Update all pmoves/configs/claws/scopes/*.json MCP configs + @PYTHONPATH="$(CURDIR)/.." $(PYTHON) -m pmoves.tools.bootstrap_openclaw_scopes + +openclaw-scope-check: ## Validate OpenClaw scope MCP configs against tier expectations + @PYTHONPATH="$(CURDIR)/.." $(PYTHON) -m pmoves.tools.bootstrap_openclaw_scopes --check + +mcp-bootstrap: ## Umbrella: Docker Toolkit profile + native PMOVES MCP servers + agent configs + @$(MAKE) --no-print-directory mcp-toolkit-bootstrap || true + @$(MAKE) --no-print-directory mcp-config-bootstrap + +mcp-bootstrap-check: ## Validate imported profile, generated configs, and basic reachability + @echo "[*] MCP bootstrap check ..." + @ok=0; fail=0; \ + if docker mcp version >/dev/null 2>&1; then \ + if docker mcp profile ls 2>/dev/null | grep -qF "pmoves_5090_web"; then \ + echo " โœ“ Docker MCP Toolkit profile 'pmoves_5090_web' imported"; ok=$$((ok+1)); \ + else \ + echo " โœ— Docker MCP Toolkit profile 'pmoves_5090_web' not found"; fail=$$((fail+1)); \ + fi; \ + else \ + echo " โš  docker mcp CLI not available โ€” skipping Toolkit profile check"; \ + fi; \ + for f in $(REPO_ROOT)/.claude/mcp.json $(REPO_ROOT)/.kimi/mcp.json $(REPO_ROOT)/kilo.json; do \ + if [ -f "$$f" ]; then \ + echo " โœ“ $$f exists"; ok=$$((ok+1)); \ + else \ + echo " โœ— $$f missing"; fail=$$((fail+1)); \ + fi; \ + done; \ + if grep -q '"agent-zero"' $(REPO_ROOT)/.claude/mcp.json; then \ + echo " โœ“ .claude/mcp.json contains agent-zero"; ok=$$((ok+1)); \ + else \ + echo " โœ— .claude/mcp.json missing agent-zero"; fail=$$((fail+1)); \ + fi; \ + for key in pmoves-cipher agent-zero pmoves-nats-fleet; do \ + if grep -q "\"$$key\"" $(REPO_ROOT)/.kimi/mcp.json; then \ + echo " โœ“ .kimi/mcp.json contains $$key"; ok=$$((ok+1)); \ + else \ + echo " โœ— .kimi/mcp.json missing $$key"; fail=$$((fail+1)); \ + fi; \ + done; \ + if grep -q "pmoves-docker-gateway" $(REPO_ROOT)/.kimi/mcp.json; then \ + echo " โœ“ .kimi/mcp.json contains Docker gateway"; ok=$$((ok+1)); \ + else \ + echo " โœ— .kimi/mcp.json missing Docker gateway"; fail=$$((fail+1)); \ + fi; \ + for cfg in $(REPO_ROOT)/pmoves/configs/claws/opencode-*.json; do \ + if grep -q "pmoves-cipher" "$$cfg" && grep -q "agent-zero" "$$cfg" && grep -q "pmoves-docker-gateway" "$$cfg"; then \ + echo " โœ“ $$(basename $$cfg) has canonical PMOVES MCPs"; ok=$$((ok+1)); \ + else \ + echo " โœ— $$(basename $$cfg) missing canonical PMOVES MCPs"; fail=$$((fail+1)); \ + fi; \ + done; \ + echo "[*] Results: $$ok passed, $$fail failed"; \ + exit $$fail + +hermes-crush-bootstrap: ## Update Hermes Agent and Crush CLI MCP configs from canonical inventory + @bash scripts/bootstrap-hermes-crush.sh diff --git a/pmoves/scripts/bootstrap-hermes-crush.sh b/pmoves/scripts/bootstrap-hermes-crush.sh new file mode 100755 index 0000000000..9f1694bcd5 --- /dev/null +++ b/pmoves/scripts/bootstrap-hermes-crush.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Bootstrap Hermes Agent and Crush CLI MCP configurations from the canonical +# PMOVES inventory. Idempotent โ€” safe to re-run. +# +# Usage: +# bash pmoves/scripts/bootstrap-hermes-crush.sh +# make -C pmoves hermes-crush-bootstrap +# +# Environment: +# PMOVES_HERMES_PROFILE Hermes profile name (default: pmoves-hermes) +# PMOVES_CRUSH_CONFIG Crush config path (default: ~/.config/crush/crush.json) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +HERMES_PROFILE="${PMOVES_HERMES_PROFILE:-pmoves-hermes}" +CRUSH_CONFIG="${PMOVES_CRUSH_CONFIG:-${HOME}/.config/crush/crush.json}" +HERMES_CONFIG="${HOME}/.hermes/profiles/${HERMES_PROFILE}/config.yaml" + +info() { printf '\033[1;34m[hermes-crush-bootstrap]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[hermes-crush-bootstrap] WARN:\033[0m %s\n' "$*" >&2; } +fail() { printf '\033[1;31m[hermes-crush-bootstrap] FAIL:\033[0m %s\n' "$*" >&2; exit 1; } + +info "Bootstrapping Hermes (${HERMES_PROFILE}) and Crush MCP configs" + +# Ensure Python tooling is available. +if ! command -v python3 >/dev/null 2>&1; then + fail "python3 not found on PATH" +fi + +# Set PYTHONPATH so 'python3 -m pmoves.tools.*' resolves from the repo root +export PYTHONPATH="${REPO_ROOT}:${PYTHONPATH:-}" + +# โ”€โ”€ Crush โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +info "Updating Crush config: ${CRUSH_CONFIG}" +python3 -m pmoves.tools.mcp_config_generator --client crush --output "${CRUSH_CONFIG}" || fail "Crush config update failed" + +# โ”€โ”€ Hermes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +info "Updating Hermes config: ${HERMES_CONFIG}" + +python3 - </dev/null 2>&1; then fi info "Bootstrap complete. Profile '${PROFILE_ID}' is available." + +# Write agent-stack MCP configs (Cipher, Agent Zero, NATS, Supabase, etc.) +# so that clients see both the Docker Toolkit profile and the core PMOVES stack. +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +info "Writing PMOVES MCP configs for Kimi + KiloCode" +if make -C "${REPO_ROOT}/pmoves" --no-print-directory mcp-config-bootstrap; then + info "PMOVES MCP configs updated" +else + warn "PMOVES MCP config update reported errors โ€” continuing" +fi + echo echo "Next steps:" echo " โ€ข Populate secrets non-interactively: make mcp-toolkit-secrets-sync" echo " โ€ข Inspect connection state: make mcp-toolkit-status" echo " โ€ข Connect Claude Code to profile: docker mcp client connect claude-code --profile ${PROFILE_ID}" echo " (this mutates .claude/mcp.json โ€” review the diff before committing)" +echo " โ€ข Bootstrap Hermes + Crush configs: make -C pmoves hermes-crush-bootstrap" echo echo "OAuth-mediated Cloudflare servers (13 of 25) need a one-time interactive" echo "browser authorize per node. See pmoves/docs/operations/MCP_TOOLKIT.md ยง 5." diff --git a/pmoves/tensorzero/config/tensorzero.toml b/pmoves/tensorzero/config/tensorzero.toml index e699592c45..559851bff2 100644 --- a/pmoves/tensorzero/config/tensorzero.toml +++ b/pmoves/tensorzero/config/tensorzero.toml @@ -330,6 +330,7 @@ api_base = "https://api.z.ai/api/coding/paas/v4" model_name = "glm-5v-turbo" api_key_location = "env::Z_AI_API_KEY" + # --- Qwen3-Coder 30B MoE (3.3B active โ€” SWE-Bench SOTA, fits 16GB) --- [models.coding_qwen3_coder_30b_local] routing = ["ollama_local"] @@ -1471,6 +1472,14 @@ type = "chat_completion" model = "chat_zai_glm5_turbo" weight = 0.3 temperature = 0.1 +max_tokens = 8192 + +[functions.coding_kilocode.variants.cloud_zai_glm51] +type = "chat_completion" +model = "chat_zai_glm51" +weight = 0.2 +temperature = 0.2 +max_tokens = 8192 [functions.coding_kilocode.variants.local_qwen35_9b] type = "chat_completion" @@ -1673,31 +1682,6 @@ temperature = 0.7 max_tokens = 8192 weight = 0.0 -# --- KiloCode coding function (GLM-backed with KiloCode direct overflow) --- - -[functions.coding_kilocode] -type = "chat" - -[functions.coding_kilocode.variants.cloud_zai_turbo] -type = "chat_completion" -model = "chat_zai_glm5_turbo" -temperature = 0.2 -max_tokens = 8192 -weight = 0.8 - -[functions.coding_kilocode.variants.cloud_kilocode] -type = "chat_completion" -model = "chat_kilocode" -temperature = 0.2 -max_tokens = 8192 -weight = 0.3 - -[functions.coding_kilocode.variants.cloud_zai_glm51] -type = "chat_completion" -model = "chat_zai_glm51" -temperature = 0.2 -max_tokens = 8192 -weight = 0.2 # --- Worker functions: registry lane aliases ONLY (no-hardcoded-models gate) --- diff --git a/pmoves/tests/test_mcp_config_generator.py b/pmoves/tests/test_mcp_config_generator.py new file mode 100644 index 0000000000..a2d7f98de3 --- /dev/null +++ b/pmoves/tests/test_mcp_config_generator.py @@ -0,0 +1,387 @@ +"""Tests for pmoves.tools.mcp_config_generator.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +import pytest + +from pmoves.tools import mcp_config_generator as gen + + +@pytest.fixture +def sample_inventory(tmp_path: Path) -> Path: + path = tmp_path / "mcp_inventory.json" + path.write_text( + json.dumps( + { + "version": 1, + "defaults": { + "cipher_local_url": "http://localhost:8105/mcp/sse", + "cipher_fleet_url": "http://${TS_Z890}:8105/mcp/sse", + "agent_zero_local_url": "http://localhost:8080/mcp", + "agent_zero_fleet_url": "http://${TS_Z890}:8080/mcp", + }, + "groups": { + "core_pmoves": { + "servers": [ + { + "key": "pmoves-cipher", + "description": "Cipher memory", + "transport": "sse", + "endpoint": "fleet", + "endpoint_prefix": "cipher", + "headers": {"Authorization": "Bearer ${CIPHER_API_TOKEN}"}, + }, + { + "key": "pmoves-cipher-local", + "description": "Local cipher memory", + "transport": "sse", + "endpoint": "local", + "endpoint_prefix": "cipher", + "url": "http://localhost:8105/mcp/sse", + "headers": {"Authorization": "Bearer ${CIPHER_API_TOKEN}"}, + "clients": ["hermes"], + }, + { + "key": "agent-zero", + "description": "Agent Zero", + "transport": "http", + "endpoint": "fleet", + "endpoint_prefix": "agent_zero", + }, + { + "key": "pmoves-nats-fleet", + "description": "NATS", + "transport": "stdio", + "command": "uv", + "args": ["run", "nats_mcp.server"], + "env": {"NATS_URL": "${NATS_URL}"}, + }, + { + "key": "hermes-only", + "description": "Hermes-only server", + "transport": "sse", + "url": "http://localhost:7000/sse", + "clients": ["hermes"], + }, + ] + } + }, + } + ), + encoding="utf-8", + ) + return path + + +@pytest.fixture +def context() -> dict[str, str]: + return { + "TS_Z890": "z890.example.com", + "CIPHER_API_TOKEN": "test-token", + "NATS_URL": "nats://localhost:4222", + } + + +def test_load_inventory_version_guard(tmp_path: Path) -> None: + path = tmp_path / "bad.json" + path.write_text(json.dumps({"version": 99}), encoding="utf-8") + with pytest.raises(gen.MCPConfigError): + gen.load_inventory(path) + + +def test_expand_simple_and_default() -> None: + env = {"EXISTING": "value"} + assert gen._expand("${EXISTING}", env) == "value" + assert gen._expand("${MISSING:-fallback}", env) == "fallback" + assert gen._expand("${MISSING}", env) == "${MISSING}" + + +def test_expand_nested_default() -> None: + env = {"OUTER": "outer-value"} + # inner ${OUTER} should resolve, then outer default uses it + assert gen._expand("${MISSING:-${OUTER}}", env) == "outer-value" + assert gen._expand("${A:-${B:-fallback}}", {}) == "fallback" + + +def test_render_claude_kimi(sample_inventory: Path, context: dict[str, str]) -> None: + inventory = gen.load_inventory(sample_inventory) + rendered = gen.generate_for_client("claude", inventory=inventory, context=context) + servers = rendered["mcpServers"] + assert "pmoves-cipher" in servers + assert servers["pmoves-cipher"]["type"] == "sse" + assert servers["pmoves-cipher"]["url"] == "http://z890.example.com:8105/mcp/sse" + assert servers["pmoves-cipher"]["headers"]["Authorization"] == "Bearer test-token" + assert "hermes-only" not in servers + + +def test_render_kilocode_permission_object(sample_inventory: Path, context: dict[str, str]) -> None: + inventory = gen.load_inventory(sample_inventory) + rendered = gen.generate_for_client("kilocode", inventory=inventory, context=context) + assert "pmoves-cipher_*" in rendered["permission"] + assert rendered["permission"]["pmoves-cipher_*"] == "allow" + assert rendered["mcp"]["pmoves-nats-fleet"]["type"] == "local" + + +def test_render_crush_uses_sse_type(sample_inventory: Path, context: dict[str, str]) -> None: + inventory = gen.load_inventory(sample_inventory) + rendered = gen.generate_for_client("crush", inventory=inventory, context=context) + assert rendered["mcp"]["pmoves-cipher"]["type"] == "sse" + assert rendered["mcp"]["agent-zero"]["type"] == "http" + + +def test_render_hermes_local_cipher(sample_inventory: Path, context: dict[str, str]) -> None: + inventory = gen.load_inventory(sample_inventory) + rendered = gen.generate_for_client("hermes", inventory=inventory, context=context) + servers = rendered["mcp_servers"] + assert "pmoves-cipher" in servers + assert "hermes-only" in servers + assert servers["pmoves-cipher"]["enabled"] is True + + +def test_write_client_config_merges_json(tmp_path: Path, sample_inventory: Path, context: dict[str, str]) -> None: + inventory = gen.load_inventory(sample_inventory) + existing = tmp_path / "mcp.json" + existing.write_text(json.dumps({"mcpServers": {"old": {"type": "sse", "url": "http://old"}}}), encoding="utf-8") + gen.write_client_config("kimi", existing, merge=True, inventory=inventory, context=context) + data = json.loads(existing.read_text(encoding="utf-8")) + assert "old" in data["mcpServers"] + assert "pmoves-cipher" in data["mcpServers"] + assert (tmp_path / "mcp.json.pre-mcp-bootstrap.bak").exists() + + +def test_write_client_config_kilocode_permissions_merge(tmp_path: Path, sample_inventory: Path, context: dict[str, str]) -> None: + inventory = gen.load_inventory(sample_inventory) + existing = tmp_path / "kilo.json" + existing.write_text( + json.dumps( + { + "mcp": {"existing": {"type": "remote", "url": "http://existing"}}, + "permission": {"bash": "allow", "existing_*": "allow"}, + } + ), + encoding="utf-8", + ) + gen.write_client_config("kilocode", existing, merge=True, inventory=inventory, context=context) + data = json.loads(existing.read_text(encoding="utf-8")) + assert "existing" in data["mcp"] + assert "pmoves-cipher" in data["mcp"] + assert data["permission"]["bash"] == "allow" + assert data["permission"]["pmoves-cipher_*"] == "allow" + + +def test_deep_merge_lists_unique() -> None: + merged = gen._deep_merge(["a"], ["a", "b"]) + assert merged == ["a", "b"] + + +def test_deep_merge_args_replaces_list() -> None: + base = {"command": "npx", "args": ["-y", "old-package"]} + overlay = {"args": ["-y", "new-package"]} + merged = gen._deep_merge(base, overlay) + assert merged["args"] == ["-y", "new-package"] + + +def test_endpoint_fleet_resolves_fleet_urls(sample_inventory: Path) -> None: + inventory = gen.load_inventory(sample_inventory) + rendered = gen.generate_for_client("opencode", inventory=inventory, endpoint="fleet") + assert rendered["mcpServers"]["pmoves-cipher"]["url"] == "http://${TS_Z890}:8105/mcp/sse" + assert rendered["mcpServers"]["agent-zero"]["url"] == "http://${TS_Z890}:8080/mcp" + + +def test_endpoint_local_resolves_local_urls(sample_inventory: Path) -> None: + inventory = gen.load_inventory(sample_inventory) + rendered = gen.generate_for_client("opencode", inventory=inventory, endpoint="local") + assert rendered["mcpServers"]["pmoves-cipher"]["url"] == "http://localhost:8105/mcp/sse" + assert rendered["mcpServers"]["agent-zero"]["url"] == "http://localhost:8080/mcp" + + +def test_hermes_local_cipher_keeps_explicit_url(sample_inventory: Path) -> None: + inventory = gen.load_inventory(sample_inventory) + rendered = gen.generate_for_client("hermes", inventory=inventory, endpoint="fleet") + servers = rendered["mcp_servers"] + assert servers["pmoves-cipher-local"]["url"] == "http://localhost:8105/mcp/sse" + + +@pytest.fixture +def scope_inventory(tmp_path: Path) -> Path: + """Full inventory usable by OpenClaw scope tests (full + edge tiers).""" + path = tmp_path / "mcp_inventory.json" + path.write_text( + json.dumps( + { + "version": 1, + "defaults": { + "cipher_local_url": "http://localhost:8105/mcp/sse", + "cipher_fleet_url": "http://${TS_Z890}:8105/mcp/sse", + "agent_zero_local_url": "http://localhost:8080/mcp", + "agent_zero_fleet_url": "http://${TS_Z890}:8080/mcp", + }, + "groups": { + "core_pmoves": { + "servers": [ + { + "key": "pmoves-cipher", + "description": "Cipher memory", + "transport": "sse", + "endpoint": "fleet", + "endpoint_prefix": "cipher", + "headers": {"Authorization": "Bearer ${CIPHER_API_TOKEN}"}, + }, + { + "key": "agent-zero", + "description": "Agent Zero", + "transport": "http", + "endpoint": "fleet", + "endpoint_prefix": "agent_zero", + }, + { + "key": "pmoves-nats-fleet", + "description": "NATS", + "transport": "stdio", + "command": "uv", + "args": ["run", "nats_mcp.server"], + "env": {"NATS_URL": "${NATS_URL}"}, + }, + { + "key": "pmoves-supabase", + "description": "Supabase", + "transport": "stdio", + "command": "npx", + "args": ["-y", "@supabase/mcp-server-postgrest"], + }, + { + "key": "supabase-db", + "description": "Supabase DB", + "transport": "stdio", + "command": "uvx", + "args": ["postgres-mcp"], + "env": {"DATABASE_URI": "${SUPABASE_DB_URI}"}, + }, + { + "key": "huggingface", + "description": "HuggingFace", + "transport": "stdio", + "command": "npx", + "args": ["-y", "@llmindset/hf-mcp-server"], + "env": {"HF_TOKEN": "${HF_TOKEN}"}, + }, + { + "key": "tailscale", + "description": "Tailscale", + "transport": "stdio", + "command": "npx", + "args": ["-y", "tailscale-mcp"], + "env": { + "TAILSCALE_API_KEY": "${TAILSCALE_API_KEY}", + "TAILSCALE_TAILNET": "${TAILSCALE_TAILNET}", + }, + }, + ] + }, + "docker_toolkit": { + "servers": [ + { + "key": "pmoves-docker-gateway", + "description": "Docker gateway", + "transport": "stdio", + "command": "docker", + "args": ["mcp", "gateway", "run", "--profile", "pmoves_5090_web"], + } + ] + }, + }, + } + ), + encoding="utf-8", + ) + return path + + +def test_openclaw_scope_bootstrap_preserves_non_pmoves( + tmp_path: Path, scope_inventory: Path +) -> None: + from pmoves.tools import bootstrap_openclaw_scopes as scopes + + scopes_dir = tmp_path / "scopes" + scopes_dir.mkdir() + scope_path = scopes_dir / "4090.json" + scope_path.write_text( + json.dumps( + { + "identity": {"node": "4090"}, + "mcp_servers": { + "gpu-mesh": {"type": "nats", "url": "nats://example:4222"}, + "pmoves-cipher": {"type": "sse", "url": "http://old/sse"}, + }, + } + ), + encoding="utf-8", + ) + + orig_dir = scopes.SCOPES_DIR + try: + scopes.SCOPES_DIR = scopes_dir + scopes.INVENTORY_PATH = scope_inventory + scopes.main([]) + finally: + scopes.SCOPES_DIR = orig_dir + + data = json.loads(scope_path.read_text(encoding="utf-8")) + servers = data["mcp_servers"] + assert "gpu-mesh" in servers, "scope-specific non-PMOVES MCP should be preserved" + assert servers["pmoves-cipher"]["url"] == "http://${TS_Z890}:8105/mcp/sse" + assert "agent-zero" in servers + assert "pmoves-docker-gateway" in servers + + +def test_openclaw_scope_bootstrap_edge_tier( + tmp_path: Path, scope_inventory: Path +) -> None: + from pmoves.tools import bootstrap_openclaw_scopes as scopes + + scopes_dir = tmp_path / "scopes" + scopes_dir.mkdir() + scope_path = scopes_dir / "nemoclaw.json" + scope_path.write_text( + json.dumps({"identity": {"node": "jetson"}, "mcp_servers": {}}), + encoding="utf-8", + ) + + orig_dir = scopes.SCOPES_DIR + try: + scopes.SCOPES_DIR = scopes_dir + scopes.INVENTORY_PATH = scope_inventory + scopes.main([]) + finally: + scopes.SCOPES_DIR = orig_dir + + data = json.loads(scope_path.read_text(encoding="utf-8")) + servers = data["mcp_servers"] + assert set(servers.keys()) == {"pmoves-cipher", "agent-zero", "tailscale"} + + +def test_openclaw_scope_check_passes_for_canonical( + tmp_path: Path, scope_inventory: Path +) -> None: + from pmoves.tools import bootstrap_openclaw_scopes as scopes + + scopes_dir = tmp_path / "scopes" + scopes_dir.mkdir() + scope_path = scopes_dir / "4090.json" + scope_path.write_text( + json.dumps({"identity": {"node": "4090"}, "mcp_servers": {}}), + encoding="utf-8", + ) + + orig_dir = scopes.SCOPES_DIR + try: + scopes.SCOPES_DIR = scopes_dir + scopes.INVENTORY_PATH = scope_inventory + scopes.main([]) + assert scopes.main(["--check"]) == 0 + finally: + scopes.SCOPES_DIR = orig_dir diff --git a/pmoves/tools/bootstrap_openclaw_scopes.py b/pmoves/tools/bootstrap_openclaw_scopes.py new file mode 100644 index 0000000000..03a9a771f2 --- /dev/null +++ b/pmoves/tools/bootstrap_openclaw_scopes.py @@ -0,0 +1,199 @@ +"""Update OpenClaw scope configs with canonical PMOVES MCP servers. + +Scope configs live in pmoves/configs/claws/scopes/*.json. Each scope is assigned a +tier (full or edge) and an endpoint mode (fleet or local). The script generates a +canonical PMOVES mcp_servers block from pmoves/config/mcp_inventory.json, filters +it by tier, and merges it non-destructively with the existing scope file so that +scope-specific non-PMOVES MCPs (e.g., gpu-mesh, docker, zai-*) are preserved. +""" + +from __future__ import annotations + +import argparse +import json +import shutil +import sys +from pathlib import Path +from typing import Any, Dict, Set, Tuple + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCOPES_DIR = REPO_ROOT / "pmoves" / "configs" / "claws" / "scopes" +INVENTORY_PATH = REPO_ROOT / "pmoves" / "config" / "mcp_inventory.json" + +# Canonical PMOVES MCP keys. These are replaced/added; everything else is preserved. +PMOVES_MCP_KEYS: Set[str] = { + "pmoves-cipher", + "pmoves-cipher-local", + "agent-zero", + "pmoves-nats-fleet", + "pmoves-supabase", + "supabase-db", + "huggingface", + "tailscale", + "pmoves-docker-gateway", + "pmoves-docker-gateway-sse", +} + +# Per-scope policy: (tier, endpoint_mode) +SCOPE_CONFIG: Dict[str, Tuple[str, str]] = { + "4090": ("full", "fleet"), + "5090": ("full", "fleet"), + "z890": ("full", "local"), + "kvm4-1": ("full", "fleet"), + "kvm4-2": ("full", "fleet"), + "nemotron-claw": ("full", "fleet"), + "spark": ("full", "fleet"), + "nemoclaw": ("edge", "fleet"), + "kvm2": ("full", "fleet"), +} + +# MCP keys included per tier. +TIER_KEYS: Dict[str, Set[str]] = { + "full": { + "pmoves-cipher", + "agent-zero", + "pmoves-nats-fleet", + "pmoves-supabase", + "supabase-db", + "huggingface", + "tailscale", + "pmoves-docker-gateway", + }, + "edge": { + "pmoves-cipher", + "agent-zero", + "tailscale", + }, +} + + +def load_inventory() -> Dict[str, Any]: + return json.loads(INVENTORY_PATH.read_text(encoding="utf-8")) + + +def canonical_scope_mcp_servers(tier: str, endpoint: str) -> Dict[str, Any]: + """Return the canonical PMOVES mcp_servers block for an OpenClaw scope.""" + sys.path.insert(0, str(REPO_ROOT / "pmoves" / "tools")) + from mcp_config_generator import generate_for_client + + rendered = generate_for_client( + "opencode", + inventory=load_inventory(), + endpoint=endpoint, + context={}, + ) + allowed = TIER_KEYS.get(tier, TIER_KEYS["edge"]) + return {k: v for k, v in rendered["mcpServers"].items() if k in allowed} + + +def update_scope(path: Path, dry_run: bool = False) -> Tuple[bool, list[str]]: + """Update a single scope file. Returns (changed, list of messages).""" + data = json.loads(path.read_text(encoding="utf-8")) + node = data.get("identity", {}).get("node") + if node is None: + return False, [f"SKIP {path.name}: missing identity.node"] + + tier, endpoint = SCOPE_CONFIG.get(node, ("edge", "fleet")) + canonical = canonical_scope_mcp_servers(tier, endpoint) + + existing = data.get("mcp_servers", {}) + + # Preserve non-PMOVES servers, replace/add PMOVES ones. + preserved = {k: v for k, v in existing.items() if k not in PMOVES_MCP_KEYS} + merged = {**preserved, **canonical} + + missing = sorted(set(canonical.keys()) - set(merged.keys())) + if missing: + # Should not happen because canonical keys are disjoint from preserved keys, + # but guard against scope-specific keys accidentally shadowing PMOVES keys. + return False, [f"SKIP {path.name}: key collision for {missing}"] + + if existing == merged: + return False, [f"SKIP {path.name} (already canonical)"] + + if dry_run: + return True, [f"DRY-RUN {path.name}: would update ({len(merged)} servers)"] + + backup = Path(str(path) + ".pre-mcp-bootstrap.bak") + if not backup.exists(): + shutil.copy2(path, backup) + + data["mcp_servers"] = merged + path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + return True, [f"UPDATED {path.name} (tier={tier}, endpoint={endpoint})"] + + +def check_scopes() -> int: + """Validate that every scope matches its tier/endpoint expectation.""" + errors: list[str] = [] + ok = 0 + + for path in sorted(SCOPES_DIR.glob("*.json")): + data = json.loads(path.read_text(encoding="utf-8")) + node = data.get("identity", {}).get("node") + if node is None: + errors.append(f"{path.name}: missing identity.node") + continue + + tier, endpoint = SCOPE_CONFIG.get(node, ("edge", "fleet")) + expected_keys = set(TIER_KEYS.get(tier, TIER_KEYS["edge"])) + actual_keys = set(data.get("mcp_servers", {}).keys()) + + missing = sorted(expected_keys - actual_keys) + unexpected = sorted( + k for k in (actual_keys - expected_keys) if k in PMOVES_MCP_KEYS + ) + + if missing or unexpected: + errors.append( + f"{path.name} (tier={tier}): missing={missing}, unexpected_pmoves={unexpected}" + ) + else: + ok += 1 + + if errors: + print("OpenClaw scope MCP check FAILED", file=sys.stderr) + for err in errors: + print(f" {err}", file=sys.stderr) + print(f" {ok} scopes OK", file=sys.stderr) + return 1 + + print(f"OpenClaw scope MCP check OK: {ok} scopes canonical") + return 0 + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Update OpenClaw scope configs with canonical PMOVES MCP servers." + ) + parser.add_argument( + "--check", + action="store_true", + help="Validate scopes without modifying files.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print what would change without writing files.", + ) + args = parser.parse_args(argv) + + if args.check: + return check_scopes() + + updated = 0 + skipped = 0 + for path in sorted(SCOPES_DIR.glob("*.json")): + changed, messages = update_scope(path, dry_run=args.dry_run) + for msg in messages: + print(msg) + if changed: + updated += 1 + else: + skipped += 1 + print(f"Done: {updated} updated, {skipped} skipped") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pmoves/tools/bootstrap_opencode.py b/pmoves/tools/bootstrap_opencode.py new file mode 100644 index 0000000000..49479bcd7d --- /dev/null +++ b/pmoves/tools/bootstrap_opencode.py @@ -0,0 +1,84 @@ +"""Update OpenCode node configs with canonical PMOVES MCP servers. + +Preserves non-PMOVES MCP servers (e.g., zai-vision, docker) and replaces or adds +the canonical PMOVES entries from pmoves/config/mcp_inventory.json. +""" + +from __future__ import annotations + +import json +import shutil +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +CLAWS_DIR = REPO_ROOT / "pmoves" / "configs" / "claws" +INVENTORY_PATH = REPO_ROOT / "pmoves" / "config" / "mcp_inventory.json" + +# Keys that the PMOVES inventory owns. These are replaced; others are preserved. +PMOVES_MCP_KEYS = { + "pmoves-cipher", + "pmoves-cipher-local", + "agent-zero", + "pmoves-nats-fleet", + "pmoves-supabase", + "supabase-db", + "huggingface", + "tailscale", + "pmoves-docker-gateway", + "pmoves-docker-gateway-sse", +} + + +def load_inventory() -> dict: + return json.loads(INVENTORY_PATH.read_text(encoding="utf-8")) + + +def canonical_opencode_mcp_servers() -> dict: + """Return the canonical PMOVES mcpServers block for OpenCode/scope format.""" + sys.path.insert(0, str(REPO_ROOT / "pmoves" / "tools")) + from mcp_config_generator import generate_for_client + + rendered = generate_for_client("opencode", inventory=load_inventory(), context={}) + return rendered["mcpServers"] + + +def update_config(path: Path, canonical: dict) -> bool: + data = json.loads(path.read_text(encoding="utf-8")) + existing = data.get("mcpServers", {}) + + # Preserve non-PMOVES servers, replace/add PMOVES ones. + preserved = {k: v for k, v in existing.items() if k not in PMOVES_MCP_KEYS} + merged = {**preserved, **canonical} + + if existing == merged: + return False + + backup = Path(str(path) + ".pre-mcp-bootstrap.bak") + if not backup.exists(): + shutil.copy2(path, backup) + + data["mcpServers"] = merged + path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + return True + + +def main() -> int: + canonical = canonical_opencode_mcp_servers() + updated = 0 + skipped = 0 + + for path in sorted(CLAWS_DIR.glob("opencode-*.json")): + changed = update_config(path, canonical) + if changed: + print(f"UPDATED {path.relative_to(REPO_ROOT)}") + updated += 1 + else: + print(f"SKIPPED {path.relative_to(REPO_ROOT)} (already canonical)") + skipped += 1 + print(f"Done: {updated} updated, {skipped} skipped") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pmoves/tools/crush_configurator.py b/pmoves/tools/crush_configurator.py index d7486c678f..9114012c23 100644 --- a/pmoves/tools/crush_configurator.py +++ b/pmoves/tools/crush_configurator.py @@ -169,6 +169,17 @@ class MCPSpec: config: Dict[str, object] required_commands: List[str] = field(default_factory=list) required_env: Optional[str] = None + required_envs: List[str] = field(default_factory=list) + + def missing_envs(self, env_cache: Dict[Path, Dict[str, str]]) -> List[str]: + keys = list(self.required_envs) + if self.required_env: + keys.append(self.required_env) + return [ + key + for key in keys + if not _lookup_env(key, env_cache) + ] MCP_SPECS: List[MCPSpec] = [ @@ -182,6 +193,129 @@ class MCPSpec: }, required_commands=["pmoves-mini"], ), + MCPSpec( + key="pmoves-cipher", + config={ + "type": "sse", + "url": "http://${TS_Z890}:8105/mcp/sse", + "headers": {"Authorization": "Bearer ${CIPHER_API_TOKEN}"}, + "timeout": 30, + }, + required_env="CIPHER_API_TOKEN", + ), + MCPSpec( + key="pmoves-cipher-local", + config={ + "type": "sse", + "url": "http://localhost:8105/mcp/sse", + "headers": {"Authorization": "Bearer ${CIPHER_API_TOKEN}"}, + "timeout": 30, + }, + required_env="CIPHER_API_TOKEN", + ), + MCPSpec( + key="agent-zero", + config={ + "type": "http", + "url": "http://${TS_Z890}:8080/mcp", + "timeout": 30, + }, + ), + MCPSpec( + key="pmoves-nats-fleet", + config={ + "type": "stdio", + "command": "uv", + "args": [ + "--directory", + "./pmoves-nats-mcp", + "run", + "python", + "-m", + "nats_mcp.server", + ], + "timeout": 60, + }, + required_commands=["uv"], + required_env="NATS_URL", + ), + MCPSpec( + key="pmoves-supabase", + config={ + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@supabase/mcp-server-postgrest@0.1.1", + "--apiUrl", + "${SUPABASE_REST_URL:-http://localhost:8000/rest/v1}", + "--apiKey", + "${SUPABASE_SERVICE_ROLE_KEY:-${SUPABASE_SERVICE_KEY}}", + "--schema", + "public", + ], + "timeout": 60, + }, + required_commands=["npx"], + ), + MCPSpec( + key="supabase-db", + config={ + "type": "stdio", + "command": "uvx", + "args": [ + "postgres-mcp@0.3.0", + "--access-mode=unrestricted", + ], + "timeout": 60, + }, + required_commands=["uvx"], + required_env="SUPABASE_DB_URI", + ), + MCPSpec( + key="huggingface", + config={ + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@llmindset/hf-mcp-server@0.3.30", + ], + "timeout": 60, + }, + required_commands=["npx"], + required_env="HF_TOKEN", + ), + MCPSpec( + key="tailscale", + config={ + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "tailscale-mcp@2026.4.10-1", + ], + "timeout": 60, + }, + required_commands=["npx"], + required_envs=["TAILSCALE_API_KEY", "TAILSCALE_TAILNET"], + ), + MCPSpec( + key="pmoves-docker-gateway", + config={ + "type": "stdio", + "command": "docker", + "args": [ + "mcp", + "gateway", + "run", + "--profile", + "pmoves_5090_web", + ], + "timeout": 60, + }, + required_commands=["docker"], + ), MCPSpec( key="docker", config={ @@ -371,7 +505,7 @@ def build_config() -> Tuple[Dict[str, object], Dict[str, ProviderSpec]]: disabled = False if spec.required_commands and not all(shutil.which(cmd) for cmd in spec.required_commands): disabled = True - if spec.required_env and not _lookup_env(spec.required_env, env_cache): + if spec.missing_envs(env_cache): disabled = True if disabled: config["disabled"] = True diff --git a/pmoves/tools/kilo_parity_mcp_check.py b/pmoves/tools/kilo_parity_mcp_check.py new file mode 100644 index 0000000000..ce62efbece --- /dev/null +++ b/pmoves/tools/kilo_parity_mcp_check.py @@ -0,0 +1,35 @@ +"""Verify kilo.json MCP keys match the canonical PMOVES inventory.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +def main() -> int: + repo_root = Path(__file__).resolve().parents[2] + inventory_path = repo_root / "pmoves" / "config" / "mcp_inventory.json" + kilo_path = repo_root / "kilo.json" + + inventory = json.loads(inventory_path.read_text(encoding="utf-8")) + kilo = json.loads(kilo_path.read_text(encoding="utf-8")) + kilo_keys = set(kilo.get("mcp", {}).keys()) + + expected: set[str] = set() + for group in inventory.get("groups", {}).values(): + for srv in group.get("servers", []): + clients = srv.get("clients") + if clients is None or "kilocode" in clients: + expected.add(srv["key"]) + + missing = sorted(expected - kilo_keys) + for key in missing: + print(f" โŒ kilo.json missing MCP: {key}", file=sys.stderr) + if not missing: + print(" โœ… kilo.json contains all canonical MCP servers", file=sys.stderr) + return len(missing) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pmoves/tools/mcp_config_generator.py b/pmoves/tools/mcp_config_generator.py new file mode 100644 index 0000000000..2b5f142c58 --- /dev/null +++ b/pmoves/tools/mcp_config_generator.py @@ -0,0 +1,509 @@ +"""Generate PMOVES MCP server configs for every supported agent stack. + +Reads pmoves/config/mcp_inventory.json and emits client-native snippets for: + - Claude Code / Kimi Code CLI -> mcp.json (mcpServers) + - KiloCode -> kilo.json fragment (mcp + permission) + - Hermes Agent -> Hermes config.yaml mcp_servers block + - Crush CLI -> crush.json mcp block + +The generator is idempotent and safe to run from any bootstrap path. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional, Tuple + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +PMOVES_DIR = PROJECT_ROOT / "pmoves" +INVENTORY_PATH = PMOVES_DIR / "config" / "mcp_inventory.json" + +# Map of client -> default output path relative to repo root +DEFAULT_OUTPUTS = { + "claude": PROJECT_ROOT / ".claude" / "mcp.json", + "kimi": PROJECT_ROOT / ".kimi" / "mcp.json", + "kilocode": PROJECT_ROOT / "kilo.json", + "opencode": PROJECT_ROOT / ".opencode" / "config.json", + "hermes": Path.home() / ".hermes" / "profiles" / "pmoves-hermes" / "config.yaml", + "crush": Path.home() / ".config" / "crush" / "crush.json", +} + + +class MCPConfigError(Exception): + pass + + +@dataclass +class ServerSpec: + key: str + description: str + transport: str + url: Optional[str] = None + command: Optional[str] = None + args: List[str] = field(default_factory=list) + env: Dict[str, str] = field(default_factory=dict) + headers: Dict[str, str] = field(default_factory=dict) + timeout: Optional[int] = None + clients: Optional[List[str]] = None + endpoint: Optional[str] = None + endpoint_prefix: Optional[str] = None + + def supports_client(self, client: str) -> bool: + if self.clients is None: + return True + return client in self.clients + + +def load_inventory(path: Optional[Path] = None) -> Dict[str, Any]: + path = path or INVENTORY_PATH + if not path.exists(): + raise MCPConfigError(f"MCP inventory not found: {path}") + data = json.loads(path.read_text(encoding="utf-8")) + if data.get("version") != 1: + raise MCPConfigError(f"Unsupported inventory version: {data.get('version')}") + return data + + +def _expand(value: str, env: Dict[str, str], *, allow_os_environ: bool = True) -> str: + """Expand ${VAR} and ${VAR:-default} placeholders. + + Resolution order: explicit ``env`` (from --set) first, then ``os.environ`` + (only when *allow_os_environ* is True). + + When *allow_os_environ* is False (tracked config generation), unresolved + placeholders are preserved as-is so secret values never leak into + committed files. + """ + result: List[str] = [] + i = 0 + while i < len(value): + if value[i] != "$" or i + 1 >= len(value) or value[i + 1] != "{": + result.append(value[i]) + i += 1 + continue + + # Find matching closing brace, honoring nesting. + start = i + depth = 1 + i += 2 + while i < len(value) and depth > 0: + if value[i] == "{": + depth += 1 + elif value[i] == "}": + depth -= 1 + i += 1 + inner = value[start + 2 : i - 1] + + # Split var from :-default. + colon_dash = inner.find(":-") + if colon_dash >= 0: + var = inner[:colon_dash] + default = inner[colon_dash + 2 :] + else: + var = inner + default = "" + + val = env.get(var) + if not val and allow_os_environ: + val = os.environ.get(var) + if val: + result.append(val) + elif default: + # Recursively expand default so nested fallbacks resolve. + result.append(_expand(default, env)) + else: + result.append(value[start:i]) + + return "".join(result) + + +def _split_command_args(command: str, args: List[str]) -> Tuple[str, List[str]]: + """Return executable + args. + + Commands are kept as declared (npx, uv, docker, etc.) so generated configs + remain portable across nodes and merge cleanly with existing hand-written + entries that use the same short names. + """ + return command, list(args) + + +def _collect_servers(inventory: Dict[str, Any], client: str, endpoint: str) -> List[ServerSpec]: + specs: List[ServerSpec] = [] + defaults = inventory.get("defaults", {}) + + for group in inventory.get("groups", {}).values(): + for server in group.get("servers", []): + spec = ServerSpec( + key=server["key"], + description=server.get("description", ""), + transport=server["transport"], + url=server.get("url"), + command=server.get("command"), + args=server.get("args", []), + env=server.get("env", {}), + headers=server.get("headers", {}), + timeout=server.get("timeout"), + clients=server.get("clients"), + endpoint=server.get("endpoint"), + endpoint_prefix=server.get("endpoint_prefix"), + ) + if not spec.supports_client(client): + continue + # Resolve endpoint-specific URL defaults for groups that define them. + # The caller's endpoint preference overrides the server's default so + # the same server key can be rendered for fleet (Tailscale) or local + # (localhost) consumers without duplicating inventory entries. + target_endpoint = endpoint or spec.endpoint + if target_endpoint and spec.url is None: + prefix = spec.endpoint_prefix or spec.key.split("-")[0] + key = f"{prefix}_{target_endpoint}_url" + if key in defaults: + spec.url = defaults[key] + specs.append(spec) + return specs + + +def _render_env(env: Dict[str, str], context: Dict[str, str], **kw: Any) -> Dict[str, str]: + return {k: _expand(v, context, **kw) for k, v in env.items()} + + +def _render_headers(headers: Dict[str, str], context: Dict[str, str], **kw: Any) -> Dict[str, str]: + return {k: _expand(v, context, **kw) for k, v in headers.items()} + + +def _render_args(args: List[str], context: Dict[str, str], **kw: Any) -> List[str]: + return [_expand(a, context, **kw) for a in args] + + +# --------------------------------------------------------------------------- +# Client renderers +# --------------------------------------------------------------------------- + + +def render_claude_kimi(specs: List[ServerSpec], context: Dict[str, str], **kw: Any) -> Dict[str, Any]: + """Render mcpServers block for Claude Code / Kimi Code CLI.""" + servers: Dict[str, Any] = {} + for spec in specs: + entry: Dict[str, Any] = {} + if spec.description: + entry["description"] = spec.description + if spec.transport in ("sse", "http"): + entry["type"] = spec.transport + entry["url"] = _expand(spec.url or "", context, **kw) + if spec.headers: + entry["headers"] = _render_headers(spec.headers, context, **kw) + elif spec.transport == "stdio": + command, args = _split_command_args(spec.command or "", spec.args) + entry["command"] = command + entry["args"] = _render_args(args, context, **kw) + if spec.env: + entry["env"] = _render_env(spec.env, context, **kw) + if spec.timeout: + entry["timeout"] = spec.timeout + else: + continue + servers[spec.key] = entry + return {"mcpServers": servers} + + +def render_kilocode(specs: List[ServerSpec], context: Dict[str, str], **kw: Any) -> Dict[str, Any]: + """Render KiloCode mcp + permission blocks.""" + mcp: Dict[str, Any] = {} + permissions: Dict[str, str] = {} + for spec in specs: + entry: Dict[str, Any] = {} + if spec.transport in ("sse", "http"): + entry["type"] = "remote" + entry["url"] = _expand(spec.url or "", context, **kw) + if spec.headers: + entry["headers"] = _render_headers(spec.headers, context, **kw) + elif spec.transport == "stdio": + command, args = _split_command_args(spec.command or "", spec.args) + entry["type"] = "local" + entry["command"] = [command, *_render_args(args, context, **kw)] + if spec.env: + entry["environment"] = _render_env(spec.env, context, **kw) + else: + continue + mcp[spec.key] = entry + permissions[f"{spec.key}_*"] = "allow" + return {"mcp": mcp, "permission": permissions} + + +def render_hermes(specs: List[ServerSpec], context: Dict[str, str], **kw: Any) -> Dict[str, Any]: + """Render Hermes Agent config.yaml mcp_servers block.""" + servers: Dict[str, Any] = {} + for spec in specs: + entry: Dict[str, Any] = {"enabled": True} + if spec.transport in ("sse", "http"): + entry["type"] = spec.transport + entry["url"] = _expand(spec.url or "", context, **kw) + if spec.headers: + entry["headers"] = _render_headers(spec.headers, context, **kw) + elif spec.transport == "stdio": + command, args = _split_command_args(spec.command or "", spec.args) + entry["type"] = "stdio" + entry["command"] = command + entry["args"] = _render_args(args, context, **kw) + if spec.env: + entry["env"] = _render_env(spec.env, context, **kw) + else: + continue + servers[spec.key] = entry + return {"mcp_servers": servers} + + +def render_crush(specs: List[ServerSpec], context: Dict[str, str], **kw: Any) -> Dict[str, Any]: + """Render Crush CLI crush.json mcp block.""" + mcp: Dict[str, Any] = {} + for spec in specs: + entry: Dict[str, Any] = {} + if spec.transport == "sse": + entry["type"] = "sse" + entry["url"] = _expand(spec.url or "", context, **kw) + if spec.headers: + entry["headers"] = _render_headers(spec.headers, context, **kw) + elif spec.transport == "http": + entry["type"] = "http" + entry["url"] = _expand(spec.url or "", context, **kw) + if spec.headers: + entry["headers"] = _render_headers(spec.headers, context, **kw) + elif spec.transport == "stdio": + command, args = _split_command_args(spec.command or "", spec.args) + entry["type"] = "stdio" + entry["command"] = command + entry["args"] = _render_args(args, context, **kw) + else: + continue + if spec.timeout: + entry["timeout"] = spec.timeout + mcp[spec.key] = entry + return {"mcp": mcp} + + +RENDERERS = { + "claude": render_claude_kimi, + "kimi": render_claude_kimi, + "opencode": render_claude_kimi, + "kilocode": render_kilocode, + "hermes": render_hermes, + "crush": render_crush, +} + + +# --------------------------------------------------------------------------- +# Merge / write helpers +# --------------------------------------------------------------------------- + + +def _deep_merge(base: Any, overlay: Any, *, _key: Optional[str] = None) -> Any: + """Recursively merge overlay into base. + + Dicts are merged. For lists, the merge behavior depends on context: + - 'args' values are ordered command-line arguments: overlay replaces base. + - Other lists are concatenated with duplicates removed. + """ + if isinstance(base, dict) and isinstance(overlay, dict): + result = dict(base) + for key, value in overlay.items(): + result[key] = _deep_merge(result.get(key), value, _key=key) + return result + if isinstance(base, list) and isinstance(overlay, list): + if _key == "args": + return list(overlay) + return list(base) + [item for item in overlay if item not in base] + return overlay + + +def _load_json_or_empty(path: Path) -> Dict[str, Any]: + if not path.exists(): + return {} + try: + return json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise MCPConfigError(f"Invalid JSON in {path}: {exc}") + + +def _backup_once(path: Path) -> None: + backup = Path(str(path) + ".pre-mcp-bootstrap.bak") + if path.exists() and not backup.exists(): + shutil.copy2(path, backup) + + +def generate_for_client( + client: str, + *, + inventory: Optional[Dict[str, Any]] = None, + endpoint: str = "fleet", + context: Optional[Dict[str, str]] = None, + allow_os_environ: bool = True, +) -> Dict[str, Any]: + inventory = inventory or load_inventory() + context = context or {} + if client not in RENDERERS: + raise MCPConfigError(f"Unknown client: {client}") + specs = _collect_servers(inventory, client, endpoint) + return RENDERERS[client](specs, context, allow_os_environ=allow_os_environ) + + +def write_client_config( + client: str, + output_path: Path, + *, + merge: bool = True, + inventory: Optional[Dict[str, Any]] = None, + endpoint: str = "fleet", + context: Optional[Dict[str, str]] = None, + allow_os_environ: bool = True, +) -> Path: + """Generate and write config for a single client. + + For JSON clients (Claude, Kimi, KiloCode, Crush) the output is merged with + the existing file when merge=True. For Hermes, a YAML mcp_servers snippet + is printed to stdout; callers are responsible for injecting it into the + Hermes config.yaml because YAML round-tripping is left to the Hermes + bootstrap script. + """ + rendered = generate_for_client(client, inventory=inventory, endpoint=endpoint, context=context, + allow_os_environ=allow_os_environ) + + if client == "hermes": + # Hermes is YAML; write a snippet file for the bootstrap script to merge. + # NEVER overwrite the real config.yaml directly. + snippet_path = output_path.with_suffix(".mcp_snippet.json") if output_path.suffix == ".yaml" else output_path + snippet_path.parent.mkdir(parents=True, exist_ok=True) + snippet_path.write_text( + f"# PMOVES MCP servers (auto-generated by mcp_config_generator.py)\n" + f"# Merge this into your Hermes config.yaml under the top-level `mcp_servers:` key.\n" + + json.dumps(rendered, indent=2), + encoding="utf-8", + ) + return snippet_path + + if merge: + _backup_once(output_path) + existing = _load_json_or_empty(output_path) + final = _deep_merge(existing, rendered) + else: + final = rendered + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(final, indent=2) + "\n", encoding="utf-8") + return output_path + + +def main(argv: Optional[List[str]] = None) -> int: + parser = argparse.ArgumentParser( + description="Generate PMOVES MCP configs for agent stacks." + ) + parser.add_argument( + "--client", + choices=list(RENDERERS.keys()) + ["all"], + required=True, + help="Target agent stack (or 'all').", + ) + parser.add_argument( + "--output", + type=Path, + help="Output file path (default: client-specific known path).", + ) + parser.add_argument( + "--output-dir", + type=Path, + help="When --client=all, write each client config into this directory.", + ) + parser.add_argument( + "--endpoint", + choices=["local", "fleet"], + default="fleet", + help="Prefer local URLs (localhost) or fleet URLs (Tailscale) where applicable.", + ) + parser.add_argument( + "--no-merge", + action="store_true", + help="Overwrite the output file instead of merging with existing config.", + ) + parser.add_argument( + "--inventory", + type=Path, + default=INVENTORY_PATH, + help="Path to mcp_inventory.json.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print rendered config to stdout instead of writing files.", + ) + parser.add_argument( + "--set", + action="append", + metavar="KEY=VALUE", + default=[], + help="Override context variable for ${...} expansion (can be repeated).", + ) + args = parser.parse_args(argv) + + inventory = load_inventory(args.inventory) + context: Dict[str, str] = {} + for item in args.set: + if "=" not in item: + print(f"ERROR: --set expects KEY=VALUE, got: {item}", file=sys.stderr) + return 2 + k, v = item.split("=", 1) + context[k] = v + + clients = list(RENDERERS.keys()) if args.client == "all" else [args.client] + + # Reject --client all with a single --output: each client would overwrite + # the same file, leaving only the last client's config. + if args.client == "all" and args.output and not args.output_dir: + print("ERROR: --client all requires --output-dir, not --output " + "(clients would overwrite each other)", file=sys.stderr) + return 2 + + # Determine whether this is a tracked-config write (repo-relative paths). + # Tracked configs must preserve ${VAR} placeholders, never expand secrets. + tracked_clients = {"claude", "kimi", "kilocode", "opencode"} + + for client in clients: + output = args.output + if output is None: + if args.client == "all" and args.output_dir: + output = args.output_dir / f"mcp.{client}.json" + else: + output = DEFAULT_OUTPUTS[client] + + is_tracked = client in tracked_clients and output.is_relative_to(PROJECT_ROOT) + rendered = generate_for_client( + client, inventory=inventory, endpoint=args.endpoint, context=context, + allow_os_environ=not is_tracked, + ) + + if args.dry_run: + print(f"# --- {client} -> {output} ---") + print(json.dumps(rendered, indent=2)) + continue + + written = write_client_config( + client, + output, + merge=not args.no_merge, + inventory=inventory, + endpoint=args.endpoint, + context=context, + allow_os_environ=not is_tracked, + ) + print(f"OK Wrote {client} MCP config to {written}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main())