Developer Docs MCP Tooling
PHASE 07

MCP Tooling

MuseHub exposes the full Muse platform as a Model Context Protocol server — protocol version 2025-11-25. Full elicitation (form + URL), a persistent SSE push channel for progress notifications, and no external MCP SDK — pure async Python, hand-rolled JSON-RPC 2.0. The live tool and resource reference is at /mcp/docs.

PropertyValue
Server namemusehub-mcp
Server version0.2.0
Protocol version2025-11-25
Transportsstdio (local), HTTP Streamable (production)
Tools/mcp/docs — live reference with parameter schemas
Resources/mcp/docs#resources — live reference
Prompts/mcp/docs#prompts — live reference

Transports

Stdio — local development

Newline-delimited JSON-RPC 2.0 over stdin/stdout. No authentication required — the process is trusted. Logs go to stderr. User identity defaults to "stdio-user".

python -m musehub.mcp.stdio_server

HTTP Streamable — production

Full MCP 2025-11-25 Streamable HTTP implementation. Three endpoints:

MethodPathPurpose
POST /mcp All client→server JSON-RPC messages (requests, notifications, batches). Returns application/json or text/event-stream for elicitation-powered tools.
GET /mcp Persistent SSE push channel. Carries elicitation requests, progress notifications, server messages.
DELETE /mcp Client-initiated session termination. Closes SSE queues, cancels pending elicitations.

Launching the server

MuseHub ships the MCP server as a built-in service. Use muse hub mcp to start it without standing up the full web stack — useful for local development, scripted agent pipelines, and IDE integrations.

# HTTP Streamable — listens on http://127.0.0.1:1337/mcp by default
muse hub mcp

# Custom port
muse hub mcp --port 8080

# Target a specific hub instead of localhost
muse hub mcp --hub http://staging.musehub.ai

# Stdio mode (newline-delimited JSON-RPC on stdin/stdout — no HTTP layer)
muse hub mcp --stdio
# muse hub mcp output
musehub-mcp v0.2.0 (protocol 2025-11-25)
  transport : HTTP Streamable
  endpoint  : http://127.0.0.1:1337/mcp
  tools     : 119  (45 read · 59 write · 2 elicitation · 11 coord · 2 workspace)
  resources : 28
  prompts   : 11
  auth      : MSign ed25519 — write tools require a fresh signature per request
Ready.
FlagDefaultPurpose
--port N1337HTTP listen port
--host ADDR127.0.0.1HTTP bind address
--hub URLhub from repo configOverride hub target
--stdiooffStdio transport instead of HTTP
--no-authoffDisable MSign check (dev only)
--log-level LEVELinfoLogging verbosity

Connecting

Claude Desktop

Add the MCP server to Claude Desktop's config file at ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows).

// ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "musehub": {
      "command": "muse",
      "args": ["hub", "mcp", "--stdio"]
    }
  }
}

Claude Desktop uses the stdio transport. The server process inherits your shell environment — ~/.muse/identity.toml provides the signing key automatically. Restart Claude Desktop after editing the config.

Cursor

// .cursor/mcp.json
{
  "mcpServers": {
    "musehub": {
      "command": "muse",
      "args": ["hub", "mcp", "--stdio"]
    }
  }
}

HTTP Streamable (generic client)

# 1. Initialize — get session ID
POST http://staging.musehub.ai/mcp
Content-Type: application/json

{
  "jsonrpc": "2.0", "id": 1, "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": { "elicitation": { "form": {}, "url": {} } },
    "clientInfo": { "name": "my-agent", "version": "1.0" }
  }
}
# Response header: Mcp-Session-Id: <session-id>

# 2. Open SSE push channel
GET http://staging.musehub.ai/mcp
Mcp-Session-Id: <session-id>
Accept: text/event-stream

# 3. Call tools (include session ID on every request)
POST http://staging.musehub.ai/mcp
Content-Type: application/json
Mcp-Session-Id: <session-id>
Authorization: MSign handle="gabriel" ts=1745280000 sig="<base64url>"

{
  "jsonrpc": "2.0", "id": 2, "method": "tools/call",
  "params": { "name": "musehub_set_context", "arguments": { "owner": "gabriel", "slug": "muse" } }
}

# 4. Terminate session
DELETE http://staging.musehub.ai/mcp
Mcp-Session-Id: <session-id>

Session lifecycle

Sessions are created at initialize and identified by a 32-byte URL-safe token returned in the Mcp-Session-Id response header. Every subsequent request must include this header.

PropertyValue
Session TTL15 minutes from last activity; each request resets the timer
Background GCStale sessions expired every 5 minutes
Max sessionsMUSEHUB_MAX_MCP_SESSIONS env var (default 10,000); returns 503 if exceeded
Event bufferRing buffer of 50 most recent SSE events per session — replayed on Last-Event-ID reconnect
Pending elicitationsStored as asyncio.Future objects; cancelled on DELETE /mcp

The initialize response instructions field embeds a 4-step quick-start guide for agents: focus session → orient → branch on error_code not message text → use coordination APIs for parallel work.

Server capabilities

{
  "capabilities": {
    "tools":     { "listChanged": false },
    "resources": { "subscribe": false, "listChanged": false },
    "prompts":   { "listChanged": false },
    "elicitation": { "form": {}, "url": {} },
    "logging":   {}
  }
}

Tools, resources & prompts

The live reference — with expandable parameter schemas, category filtering, and always-current counts — is at /mcp/docs.

All tools follow JSON Schema inputSchema. Repo-scoped tools accept either repo_id (sha256 genesis hash) or owner + slug. If musehub_set_context was called first, all three can be omitted — the session context supplies them automatically.

Tool annotations (MCP 2025-11-25)

CategoryreadOnlyHintdestructiveHintopenWorldHint
Read toolstruefalsefalse
Write toolsfalsefalsefalse
Elicitation toolsfalsefalsetrue

Input schema pattern

{
  "name": "musehub_list_commits",
  "description": "List commits on a MuseHub repository (newest first).",
  "inputSchema": {
    "type": "object",
    "properties": {
      "repo_id":  { "type": "string", "description": "sha256 genesis hash (alt: owner+slug)" },
      "owner":   { "type": "string" },
      "slug":    { "type": "string" },
      "branch":  { "type": "string", "description": "Branch name filter" },
      "limit":   { "type": "integer", "default": 20, "minimum": 1 }
    },
    "required": []  // context from musehub_set_context satisfies repo args
  }
}

Elicitation

Elicitation lets a tool pause mid-execution to ask the user (or agent) for input before continuing. MuseHub supports both modes from MCP 2025-11-25: form (JSON Schema input) and URL (open browser, then confirm).

Form elicitation flow

  1. Tool calls await ctx.elicit_form(schema, message).
  2. Server pushes an elicitation/create SSE event to the client with mode "form", a human-readable message, and a requestedSchema (JSON Schema).
  3. Client renders the form; user fills it in.
  4. Client POSTs a JSON-RPC response (no method) with action: "accept" and content: {…}.
  5. Server resolves the pending asyncio.Future; tool receives the user's data and continues.
// SSE event pushed by server
{
  "jsonrpc": "2.0", "id": "<request_id>", "method": "elicitation/create",
  "params": {
    "mode": "form",
    "message": "Configure your review",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "dimension_focus": { "type": "string", "enum": ["security", "performance", "all"] },
        "review_depth":    { "type": "string", "enum": ["quick", "thorough"] }
      },
      "required": ["dimension_focus"]
    }
  }
}

// Client response (POST /mcp)
{
  "jsonrpc": "2.0", "id": "<request_id>",
  "result": { "action": "accept", "content": { "dimension_focus": "security", "review_depth": "thorough" } }
}
// or: { "result": { "action": "decline" } }

Worked example — musehub_review_proposal_interactive

This is a four-step round-trip. The tool call arrives on POST /mcp, the server pauses mid-execution and pushes an elicitation to the SSE channel, the client responds, and the tool completes with a full review.

Step 1 — client calls the tool

POST /mcp
Authorization: MSign handle="gabriel" alg="ed25519" ts=1746576000 sig="Zj8…"
Mcp-Session-Id: ses_4f2a9b3d
Content-Type: application/json

{
  "jsonrpc": "2.0", "id": "call-77", "method": "tools/call",
  "params": {
    "name": "musehub_review_proposal_interactive",
    "arguments": { "owner": "gabriel", "slug": "muse", "proposal_id": "af54753d" }
  }
}

# Server responds: 200 OK with Content-Type: text/event-stream
# (the response body is an SSE stream because the tool needs to elicit)

Step 2 — server pushes elicitation request to SSE channel

id: 58
event: elicitation/create
data: {
  "jsonrpc": "2.0",
  "id": "call-77",
  "method": "elicitation/create",
  "params": {
    "mode": "form",
    "message": "How should I focus this review?",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "dimension_focus": {
          "type": "string",
          "enum": ["security", "performance", "correctness", "all"],
          "description": "Area to prioritize"
        },
        "review_depth": {
          "type": "string",
          "enum": ["quick", "thorough"],
          "default": "thorough"
        },
        "reviewer_note": {
          "type": "string",
          "description": "Optional context for the reviewer"
        }
      },
      "required": ["dimension_focus"]
    }
  }
}

Step 3 — client submits the form

POST /mcp
Mcp-Session-Id: ses_4f2a9b3d
Content-Type: application/json

{
  "jsonrpc": "2.0", "id": "call-77",
  "result": {
    "action": "accept",
    "content": {
      "dimension_focus": "security",
      "review_depth": "thorough",
      "reviewer_note": "Pay attention to the new auth middleware"
    }
  }
}

Step 4 — tool completes and returns the review

{
  "jsonrpc": "2.0",
  "id": "call-77",
  "result": {
    "ok": true,
    "content": [{
      "type": "text",
      "text": "## Security Review — Proposal #47 (af54753d)\n\n**Risk: MEDIUM**\n\n### Findings\n- `validate_token()` (src/auth/tokens.py:82) — timing-safe comparison not used; switch to `hmac.compare_digest`.\n- `SESSION_SECRET` loaded from env without fallback guard — fails silently in misconfigured deploys.\n\n### Harmony\n- 0 prior conflict patterns for this proposal's changed paths.\n\n### Verdict\nApprove with changes requested."
    }]
  }
}
If the client sends "action": "decline" at step 3, the tool falls back to a quick non-interactive review with default settings and returns "elicitation_declined": true in the result.

URL elicitation flow

Tool calls await ctx.elicit_url(url, message). Server sends an elicitation/create SSE event with mode: "url" and a stable elicitationId. Client opens the URL in a browser. When the user returns, client sends a notifications/elicitation/complete notification.

Timeout and degradation

Elicitation times out after 300 seconds. The tool receives None on timeout and should degrade gracefully (e.g. fall back to defaults or return a partial result). Never block indefinitely.

ToolCallContext API

class ToolCallContext:
    user_id: str | None
    is_agent: bool
    agent_name: str | None
    has_session: bool
    has_elicitation: bool   # true if client advertised elicitation capability

    async def elicit_form(schema, message) -> JSONObject | None
    async def elicit_url(url, message, elicitation_id=None) -> bool
    async def progress(token, value, total=None, label=None) -> None

SSE streaming

The GET /mcp channel is a persistent text/event-stream. It carries three categories of server-push events: elicitation requests, progress notifications, and log messages.

Event wire format (HTML Living Standard)

id: 42
event: notifications/progress
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"t1","progress":3,"total":10,"message":"Analyzing symbols"}}

Event types

Event typeWhenKey payload fields
elicitation/createTool calls elicit_form() or elicit_url()mode, message, requestedSchema or url
notifications/progressTool calls ctx.progress()progressToken, progress, total, message
notifications/messageServer emits info/warn/error loglevel, logger, data
notifications/cancelledClient cancels in-flight tool callprogressToken

curl — open the SSE channel

# 1. Initialize and capture the session ID
SESSION=$(curl -s -X POST http://staging.musehub.ai/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{"elicitation":{"form":{},"url":{}}},"clientInfo":{"name":"curl-test","version":"1.0"}}}' \
  -D - | grep -i mcp-session-id | awk '{print $2}' | tr -d '\r')

# 2. Open persistent SSE channel (blocks; -N disables buffering)
curl -N \
  -H "Accept: text/event-stream" \
  -H "Mcp-Session-Id: $SESSION" \
  http://staging.musehub.ai/mcp
: heartbeat

: heartbeat

id: 1
event: notifications/message
data: {"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","logger":"musehub.mcp","data":"Session ses_4f2a9b3d ready."}}

id: 2
event: notifications/progress
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"t7","progress":5,"total":10,"message":"Analyzing symbols"}}

Heartbeat and reconnection

The server injects a : heartbeat SSE comment every 15 seconds to keep proxies alive. On reconnect, clients pass Last-Event-ID to replay up to the 50 most recent events from the ring buffer.

# reconnect with event replay from event 37 onward
curl -N \
  -H "Accept: text/event-stream" \
  -H "Mcp-Session-Id: $SESSION" \
  -H "Last-Event-ID: 37" \
  http://staging.musehub.ai/mcp

Authentication

Read tools and all resource/prompt reads work without authentication. Write tools require a fresh MSign signature on every tools/call request — not just at initialize.

# Authorization header — required on write tool calls
Authorization: MSign handle="gabriel" alg="ed25519" ts=1745280000 sig="<base64url>"

# Generate with muse sign header
muse sign header \
  --method POST \
  --path /mcp \
  --hub http://staging.musehub.ai --json
ScenarioAuth required
Read tools, resources, promptsNo — anonymous access allowed
Write tools (tools/call)Yes — fresh MSign per request; error code -32001 if missing
Stdio transportNo — trusted local process

Origin validation guards against DNS-rebinding attacks. Allowed origins are configured via MUSEHUB_ALLOWED_ORIGINS (comma-separated). Defaults: http://localhost, http://127.0.0.1, and the hub's own origin. Requests without an Origin header (curl, programmatic clients) are always allowed.

Error handling

JSON-RPC protocol errors are returned as error envelopes. Tool-level errors always return HTTP 200 with a success envelope — branch on the machine-readable error_code, never parse error_message text.

JSON-RPC error codes

CodeConstantMeaning
-32700PARSE_ERRORJSON decode failure
-32600INVALID_REQUESTMissing or invalid method, bad batch format
-32601METHOD_NOT_FOUNDUnknown method name
-32602INVALID_PARAMSParameter type or constraint mismatch
-32603INTERNAL_ERRORUnhandled server exception
-32000UNAUTHORIZEDAuthentication required
-32001WRITE_TOOL_UNAUTHORIZEDFresh MSign signature required for write tool
-32042URL_ELICITATION_REQUIREDClient must advertise URL elicitation capability

Tool-level error envelope

{
  "result": {
    "ok": false,
    "isError": true,
    "error_code": "proposal_not_found",
    "error_message": "Proposal 123 not found.",
    "hint": "Call musehub_list_proposals() to see open proposals."
  }
}

Common tool error codes

error_codeMeaning
repo_not_foundRepo doesn't exist or access denied
branch_not_foundBranch doesn't exist on remote
issue_not_found / proposal_not_foundIssue or proposal number not found
symbol_not_foundSymbol address doesn't exist in the snapshot
unauthenticatedAnonymous access, but write operation attempted
forbiddenAuthenticated but lacks permission
missing_argsRequired arguments absent (and no session context set)
task_not_found / reservation_conflictCoordination errors
elicitation_declinedUser declined the elicitation form
db_unavailableDatabase unreachable — transient; retry