@mks2508/axon-mcp
v0.2.2
Published
MCP server axon: expone read+write del roadmap v2 a clientes MCP (Claude Code, opencode). TERCER consumer del core (mismo modelo que CLI), sin BFF. Spec MCP 2026-07-28 + SDK v2 dual-era + stdio (local) + HTTP streamable (deploy).
Readme
@mks2508/axon-mcp
MCP server para el modelo de roadmap axon v2. Tercer consumer del core (mismo modelo que la CLI), sin BFF ni SDK de cliente. Expone read + write del roadmap a clientes MCP (Claude Code, opencode) sobre JSON-RPC stdio y HTTP streamable (Stateless).
- Spec: MCP 2026-07-28 (dual-era: 2025 + 2026 negotiation).
- Runtime: Bun-first (
engines.bun >=1.1). El tarball shippea TS directo (mismo patrón que@mks2508/axon). Sin build step. - Stack:
@modelcontextprotocol/server@^2+zod@^4(v4z.object({...})schema, no raw shape). SDK v1 solo en devDeps para tests.
Install & wire-up
Claude Code
# Stdio (local, sin auth — el cliente y el server viven en el mismo trust zone).
claude mcp add axon-mcp -- bunx @mks2508/axon-mcp
# O si lo instalaste como dev dep en tu repo:
claude mcp add axon-mcp -- bun ./node_modules/@mks2508/axon-mcp/src/index.tsopencode
opencode no tiene formato de plugin nativo — el bin trae su propio adapter:
# ./opencode.json (cwd)
bunx @mks2508/axon-mcp init --opencode
# ~/.config/opencode/opencode.json (global)
bunx @mks2508/axon-mcp init --opencode --global
# con modelo autoridad explícito (se resuelve a absoluto)
bunx @mks2508/axon-mcp init --opencode --model /abs/path/to/your/roadmap.model.ymlPrimera conexión puede tardar: bunx -y resuelve+descarga el tarball la primera vez
y eso puede exceder el timeout de conexión MCP del cliente (opencode mcp list reportó
Connection closed en cold y connected con el paquete cacheado). Un retry lo arregla.
Merge NO destructivo e idempotente: si opencode.json ya existe, solo se toca la key
mcp.axon — el resto del fichero (otras keys, otros mcp.*) se preserva. Re-run sin
--model reescribe mcp.axon con la shape canónica (si antes tenía environment
por un --model previo, se pierde — es determinista, no accidental). JSON inválido en
disco → error, fichero intacto.
Shape que escribe (schema mcp de opencode, verificado contra source):
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"axon": {
"type": "local",
"command": ["bunx", "-y", "@mks2508/axon-mcp@latest"],
"enabled": true
// "environment": { "AXON_MODEL": "<abs path>" } — solo si se pasó --model
}
}
}Skills/CLAUDE.md: opencode lee nativamente ~/.claude/skills/<name>/SKILL.md y
~/.claude/CLAUDE.md (compat directa, no hace falta nada adicional aquí) — depende de
que esas copias existan en disco; si algún tombstone de dotfiles las borra, esta compat
se rompe (data point, no resuelto por esta unidad).
HTTP mode (deploy interno)
AXON_MCP_TOKEN=$(openssl rand -hex 32) \
AXON_MODEL=/abs/path/to/your/roadmap.model.yml \
bunx @mks2508/axon-mcp serve --port 7361Auth: Authorization: Bearer $AXON_MCP_TOKEN (401 si falta o no coincide). ADR-0029
bearer+tailnet v1; OAuth RS cuando haya deploy público.
Health sin auth: GET /healthz → {"ok": true, "server": "axon-mcp"}.
Tools (6)
| Tool | Tipo | Mutates | Descripción |
|---------------|--------|---------|-------------|
| query | read | no | status (dashboard) · show <id> · now (actionable) · graph (DAG SVG) · roadmap. |
| set_status | write | sí | Cambia status de un nodo (done estampa completedDate). |
| set_gate | write | sí | Mutar verdict (pass/provisional/partial/open) + evidencia de un item existente; con create: true (+ class opcional) crea uno nuevo en vez de una 7ª tool — sin create, un itemId con typo sigue fallando (nunca upsert). |
| add_node | write | sí | Añade nodo top-level o milestone (parent). |
| rm_node | write | sí | Borra nodo. Bloquea si tiene dependents (force=true los rompe). |
| set_node | write | sí | Patch de title/zone/dependsOn/refs/docs. |
format: 'human' | 'json' — human devuelve content[].text (1-3 líneas); json
devuelve structuredContent con campo summary (workaround bug #55677 — Claude Code
descarta text cuando ambos canales están presentes, así que duplicamos la prosa en
structuredContent.summary).
Prompts (2)
Aparecen en Claude Code como /mcp__axon__<nombre>:
estado— Llamaquery status+query nowy devuelve resumen ejecutivo.cerrar-milestone <nodeId>— Guía paso a paso: set_gate → confirmar → set_status done → verificar.
Apps (MCP Apps / SEP-1865, widgets ui:// interactivos)
Baseline (format: human, sección "Tools" arriba) queda 100% intacto en TODOS los
clients — MCP Apps es una capa adicional, opt-in por el host, no un reemplazo.
query declara _meta.ui.resourceUri: "ui://axon/app" — un único resource router
(vanilla JS + SVG, sin react, sin frameworks) que monta la vista dedicada según
structuredContent.command:
| command | vista |
|---|---|
| status | status-card (counts, gates, violations) |
| graph | dag-viewer (pan/zoom, click nodo → detalle + dropdown status → write) |
| roadmap | roadmap-table (tabla de nodos top-level) |
| show / now | fallback de texto (sin widget dedicado, lock A1) |
Por qué un solo resource y no uno por vista: SEP-1865 declara _meta.ui.resourceUri
de forma ESTÁTICA en la definición de la tool (tools/list) — un CallToolResult no
puede seleccionar un resource distinto por llamada (spec, sección "Predeclared Resources
vs. Inline Embedding": "Require UI resources to be registered and referenced in tool
metadata"; alternativa de "embedded resources" — resource per-call — evaluada y
descartada). Como query es una sola tool MCP (lock L1, consolidada), solo puede
declarar UN resourceUri — de ahí el router.
Escribir desde el DAG (lock A2): click nodo → dropdown status → Apply dispara
tools/call set_status desde el iframe, y tras el ok re-lanza query graph para
refrescar la vista. Cada call incluye model: structuredContent.modelPath — en Desktop
el cwd del server MCP no es el repo, así que la resolución por .claude/axon.config.json
NO dispara; sin este threading los writes fallarían justo en el client al que apunta la
feature. Errores del tools/call se muestran en un banner en la UI (no se silencian); el
permission prompt del host antes de ejecutar el write es flujo normal, no un error.
Clients que la renderizan
Matriz oficial (modelcontextprotocol.io/extensions/client-matrix): Claude Desktop y
claude.ai (web) SÍ la soportan; también VS Code Copilot, ChatGPT, Cursor, Goose,
Postman, MCPJam. Claude Code NO la soporta (ausente de la matriz + sin mención en su
changelog) — en Claude Code, query sigue funcionando exactamente igual que antes
(content[].text compacto), el host simplemente ignora _meta.ui porque no negoció la
extension io.modelcontextprotocol/ui en el initialize.
Probarla en Claude Desktop (config manual)
Claude Desktop no lee .claude/axon.config.json — pasa AXON_MODEL explícito:
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"axon-mcp": {
"command": "bunx",
"args": ["-y", "@mks2508/axon-mcp"],
"env": { "AXON_MODEL": "/abs/path/to/your/roadmap.model.yml" }
}
}
}Reinicia Claude Desktop, invoca query status (o graph/roadmap) desde el chat — el
widget se renderiza inline. format: 'json' NO es necesario para que el widget aparezca
(el widget lee structuredContent, que el server ya adjunta siempre).
Build del widget (bun run build:apps)
El HTML del widget es un artefacto build-time checked-in
(src/apps/app.generated.html, convención *.generated.* del repo) — el runtime del
server NO ejecuta ningún build step, solo lee el fichero. Regenerar tras tocar
src/apps/router.ts o src/apps/views/*.ts:
bun run build:apps # bundlea con `bun build` (target browser, minify) e inyecta
# el bundle inline en src/apps/template.html
bun run typecheck:apps # tsc --noEmit con lib DOM (separado del tsconfig del server:
# el server corre en Bun/Node, sin document/window)@modelcontextprotocol/ext-apps (App Bridge, npm ^1.7.5) es devDependency SOLO — se
bundlea, no se shipea como dependency runtime. Guard automático en el build: si el bundle
contiene la cadena "react" en cualquier forma, el script aborta (lock: nunca shipear
react en el HTML final).
Resolución del modelo (en cada tool call)
modelparameter (si el cliente lo pasa).AXON_MODELenv var.- Campo
roadmapModelde.claude/axon.config.jsonen cwd. - Error con hint accionable (
AXON_MCP_NO_MODEL_RESOLVED).
Sin BFF, sin estado en memoria
Stateless per-request (HTTP) y sin sesiones persistentes (stdio). Cada tool call resuelve modelo, carga DAG, ejecuta handler de core, devuelve resultado. Sin caches, sin locks compartidos — el modelo es el state, los write handlers de core son atómicos.
Development
# Tests (bun:test, 53 tests, 5 files)
bun test
# Typecheck
bunx tsc --noEmit -p tsconfig.json
# Smoke local (stdio)
bun ./src/index.ts
# En otro terminal:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' | bun ./src/index.tsADR-0029
axon-mcp-distribution: bearer+tailnet v1 con HTTP server local. Stateless streamable,
auth via env token. Mismo modelo de distribución que el resto del monorepo.
License
MIT
