Skip to content

Add ix mcp subcommand: expose ix as a local MCP server #219

@josephismikhail

Description

@josephismikhail

Summary

Add an ix mcp subcommand that runs ix as a local Model Context Protocol (MCP) server. Exposes a curated set of read + write tools to MCP-aware AI clients (Claude Code, Cursor, OpenCode, etc.) so the model can invoke ix operations directly without going through shell subprocess invocations.

This is orthogonal to and complementary with --format llm (#206), not a replacement. --format llm shrinks the output payload; MCP changes how the call is made and discovered. Tool handlers internally render with the same compact format defined in #206.

Motivation

Today, AI agents invoke ix via shell:

  • ix-claude-plugin hooks shell out to ix text, ix overview, etc. and inject results as additionalContext.
  • Skills under skills/ instruct the model to run ix <cmd> --format json in its reasoning, which Claude Code executes as a Bash tool call.

This is fine but has friction:

  • Model has to know about ix. The CLAUDE.md routing table is ~3-4 KB of context loaded into every session teaching the model what commands exist.
  • Shell escaping is fragile. See [IX Error][ix] ix-text: text search failed #196 — multi-line patterns and backslash escapes blew up because the hook concatenated shell strings. MCP passes args as structured JSON; this class of bug disappears.
  • Per-invocation overhead. Each shell call spawns a Node process (~50-200 ms before any actual work). For fast commands (ix locate, ix entity) startup dominates. A long-lived MCP server amortizes this.
  • No tool discovery. Adding a new command requires updating CLAUDE.md prose. With MCP, tool schemas are auto-discovered at session start.

MCP solves the transport + discovery problem. --format llm solves the payload size problem. Both stack: a session that uses both gets the byte savings of --format llm on tool responses plus the discovery and shell-escape wins of MCP.

Where the code lives

ix mcp subcommand inside ix-cli. Considered alternatives:

Approach Verdict
ix-cli subcommand (this proposal) New ix mcp in this repo OK. Single source of truth, reuses existing handlers + renderers, no version skew, works for every MCP client
Plugin-side wrapper in ix-claude-plugin Plugin ships an MCP server that shells out to ix per call Rejected. Reimplements logic, doesn't help other AI tools, per-call subprocess cost
Standalone ix-mcp repo New repo, depends on the @ix/client package Rejected. Three-repo coordination burden, separate release cadence

Files (new):

  • ix-cli/src/cli/commands/mcp.ts — registers ix mcp subcommand
  • ix-cli/src/mcp/server.ts — MCP server boilerplate (uses @modelcontextprotocol/sdk)
  • ix-cli/src/mcp/tools/index.ts — tool registry
  • ix-cli/src/mcp/tools/<command>.ts — one file per tool, exposes the tool's JSON schema and wraps the existing CLI handler
  • ix-cli/src/mcp/format.ts — shared response formatter, delegates to the --format llm renderers
  • docs/mcp.md — user-facing setup guide

Transport

stdio only in v1. Client spawns ix mcp as a subprocess; JSON-RPC over stdin/stdout. SSE/HTTP transports add lifecycle and security surface we don't need. If a remote use case appears, that is a v3 addition with explicit auth design.

Tool catalog (v1 — 28 tools)

Tools map roughly 1:1 to CLI commands the model already invokes per CLAUDE.md routing. Naming is flat (search, locate, etc.); MCP clients namespace by server name (e.g., ix:search in Claude Code).

Read tools — navigate (10)

search, locate, explain, read, entity, overview, inventory, rank, stats, text

Read tools — relationships (6)

callers, callees, contains, imports, imported_by, depends

Read tools — structural analysis (3)

smells, subsystems, impact

Read tools — history & decisions (4)

history, decisions, conflicts, briefing

Write tools (5, Pro-gated)

decide, bug_create, bug_update, goal_create, plan_create

Write tools are annotated as destructive in their MCP descriptions so clients (Claude Code, Cursor) can apply per-call confirmation policies.

Deliberately not in v1

  • map, ingest — expensive (seconds to minutes); defer to v2 with explicit time-cost annotations
  • docker, instance, status, doctor, upgrade, view, reset, config, savings — ops/admin, not model-invoked
  • query — deprecated per CLAUDE.md
  • diff, patches — niche; add when explicit demand appears
  • Workflow-aggregated tools (investigate, audit, discover) — see v2 below

User controls (toggling tools and the server)

Users should be able to constrain what the MCP server exposes without editing every plugin manifest that registers it. v1 ships four layers of control, each addressing a different threat model:

1. Server kill switch

  • Env: IX_MCP_ENABLED=false
  • Config: mcp.enabled: false in ~/.ix/config
  • When disabled, ix mcp exits immediately with a clear stderr message. The MCP client treats this as server-not-available and falls back to whatever the model would do without ix tools.
  • Use case: user doesn't want MCP at all, even if a plugin registers it.

2. Read-only mode

  • Flag: ix mcp --read-only
  • Env: IX_MCP_READ_ONLY=1
  • Disables all 5 write tools (decide, bug_create, bug_update, goal_create, plan_create). Server still advertises the 23 read tools.
  • Use case: CI runs, sandboxed contexts, demos, untrusted-model paths, "I want navigation but no state mutations."

3. Per-tool allowlist / blocklist

  • ix mcp --enable-tools search,locate,overview — explicit allowlist (most restrictive)
  • ix mcp --disable-tools decide,read — blocklist (more common)
  • Env equivalents: IX_MCP_ENABLED_TOOLS, IX_MCP_DISABLED_TOOLS
  • Same values readable from ~/.ix/config so users don't repeat them per session
  • Allowlist wins if both are set (warn at startup).
  • Use case: lock down to a known-good subset; reduce session-context cost from tool descriptions.

4. Per-call confirmation (client-side)

Write tools are annotated as destructive in MCP tool metadata. Compatible clients (Claude Code, Cursor) honor the annotation and prompt the user before each call. The server just emits the annotation; the actual prompt policy lives with the client.

  • Use case: "I'm OK with this tool existing but want explicit consent each time."

Defaults

All four default to permissive: server enabled, full toolset, no filter, destructive annotation present (so client-side confirmation kicks in for writes). Users opt into restriction.

Tool response format

Each tool returns --format llm-style text by default, optionally JSON if the caller passes format: "json" in its args. Internally, tool handlers call the same renderers Ix#206 ships. The MCP layer adds no per-call rendering; it just selects which renderer to invoke.

This is why Ix#206 (--format llm) must land first. The MCP server's reason for existing token-wise is to deliver compact responses; if --format llm isn't ready, the MCP server has nothing to render with.

Config and auth

Reuse the existing CLI mechanisms with no new surface:

  • Env: IX_ENDPOINT, IX_ORG_ID, IX_HOME, IX_DEBUG
  • Files: ~/.ix/config, workspace .ix/config
  • The MCP server is the same logical client as ix CLI; same IxClient, same getEndpoint.

The server probes backend health and schema version at startup (same as today's CLI commands).

State and caching

V1: stateless. Each tool call hits the backend. No in-process cache.

V2 follow-up: TTL cache for read-heavy reads (entity, overview, read) that don't change mid-session. Per-tool TTLs.

Build / install impact

  • New dependency: @modelcontextprotocol/sdk (~50 KB minified, no native bindings)
  • Bundle size of ix increases marginally; no install-script changes
  • ix mcp is discoverable via ix --help so users find it the same way they find other subcommands

Test strategy

  • Unit: per-tool handler tests with mocked IxClient, assert schema validation and response shape
  • Integration: spin up ix mcp as a subprocess in a vitest test, drive it with the MCP client SDK, assert tool list + call responses against running memory-layer
  • Manual: Claude Code and Cursor sessions with the server registered; verify auto-discovery, tool execution, error paths

Adopter-side work

Tracked in ix-infrastructure/ix-claude-plugin#22 (analogous to plugin#21 for --format llm). Scope on the plugin side:

  • .claude-plugin/plugin.json gets an mcpServers.ix entry pointing at ix mcp
  • Skills under skills/ migrate from ix <cmd> --format json invocations to ix:<tool> MCP tool calls where the call is model-invoked
  • Hooks under hooks/ stay on CLI; they fire on shell events, not model events, so MCP doesn't help them

Other adopters (ix-cursor-plugin, ix-codex-plugin, ix-opencode-plugin, ix-gemini-plugin) get analogous adoption issues filed in their respective repos.

Decisions locked in

  • Subcommand name: ix mcp
  • Transport: stdio only in v1
  • Code home: ix-cli subcommand (Option A from the alternatives table)
  • Tool naming: flat names (search, subsystems, ...); MCP clients namespace by server
  • Response format: delegates to --format llm renderers from Ix#206; not a separate format
  • Config: reuses existing IxClient/env/config files, no new mechanism
  • State: stateless v1; caching deferred to v2
  • User controls: four-layer model (server kill switch, read-only mode, per-tool allow/blocklist, MCP-annotated destructive flag for client-side confirmation). All default to permissive.
  • Per-call confirmation: server emits the destructive annotation on write tools; the actual per-call prompt policy lives with the MCP client.

Out of scope for v1 (named explicitly so they don't sneak in)

  • Workflow-aggregated tools (investigate, audit, etc.); design once we have v1 usage data
  • Streaming responses
  • Result caching
  • map, ingest, or any expensive/destructive operation
  • SSE / remote transport
  • Server-side rate limiting or per-session metrics
  • Auth beyond the existing IxClient mechanism

Open questions

  1. Write tools in v1? Currently planned to ship 5 write tools (decide, bug_create, etc.) annotated as destructive so clients can prompt; the new --read-only flag lets users disable them per-session. Alternative: defer all write tools to v2 and keep v1 purely read-only. Trade-off: read-only v1 ships sooner with smaller surface; write-included v1 means skills don't need to fall back to CLI for the write paths.
  2. Tool description length budget. Every tool description is permanent session context. Cap at ~150 chars/tool? At 28 tools that is a 4 KB ceiling.
  3. Subcommand visibility: should ix mcp show up in ix --help? Lean yes; discoverability beats hiding.

Dependencies and sequencing

Hard prerequisite: Ix#206 (--format llm) must land first, at least Tier 1 renderers. MCP tool responses depend on having compact renderers to call into. Once --format llm PR 1 ships, MCP work can begin.

v2 / v3 follow-ups (not this issue)

  • v2: workflow-aggregated tools, result caching, streaming for large responses, map/ingest exposure
  • v3: SSE/HTTP transport for remote scenarios, MCP "resources" exposure (graph nodes as resources), subscribe-style updates

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions