Skip to content

feat(mcp): serve tools from the Ix CLI instead of shipping a server - #24

Open
KageBinary wants to merge 3 commits into
mainfrom
feat/mcp-delegate-to-ix-cli
Open

feat(mcp): serve tools from the Ix CLI instead of shipping a server#24
KageBinary wants to merge 3 commits into
mainfrom
feat/mcp-delegate-to-ix-cli

Conversation

@KageBinary

Copy link
Copy Markdown
Collaborator

Replaces this plugin's own MCP server with the one the Ix CLI now ships.

What changed

mcp/server.ts plus 23 tools in mcp/tools/ each wrapped an ix CLI call. The CLI serves the same 23 tools itself as ix mcp, so this was a second implementation of one surface — maintained here, and drifting independently of the other five plugins' servers.

mcp.json now points Cursor at ix mcp.

Tool parity is exact. ix mcp serves the same 23 names, verified by diffing the two surfaces. Nothing an agent could call before is gone.

What stays, and why

mcp/hooks/, mcp/lib/ and mcp/shared/ are untouched. hooks/hooks.json runs node mcp/dist/hooks/*.js, and MCP has no concept of a hook — this is the part ix mcp cannot absorb. The npm build stays with it; the install messages just stop calling it "the MCP server". All seven hooks.json entrypoints still emit to mcp/dist/hooks/ after a clean rebuild.

Also removed, as unreachable once tools/ is gone

  • lib/llm.tstools/ was its only importer. Redaction on the hook path is unaffected: that lives in shared/secrets.ts, which lib/parser.ts and lib/errors.ts use directly.
  • tests/integration/llm-passthrough.test.ts (4 tests) — its subject was the tool layer's llm-vs-JSON dispatch, which is now the CLI's business.
  • tests/unit/llm.test.ts (8 tests) — covered the module above.
  • @modelcontextprotocol/sdk and zod from mcp/package.json — the server and tools were their only consumers.

Flag this one on review if you would rather keep lib/llm.ts: it is dead after this change, but it is recent deliberate work and reverting that part is a one-file change.

Tests

Test count drops 15 → 3; all three pass, plus typecheck and a clean rebuild. The 12 removed tests covered removed code. plugin-success-loop.test.ts kept its coverage — it drove the hook loop through tools/subsystems, and now reads the graph through lib/cli, asserting against ix subsystems --format json as the CLI actually emits it rather than the envelope the tool used to build.

Note the baseline: a fresh clone fails 6/15 until npm ci runs in mcp/. That is pre-existing and unrelated.

Version floor

ix mcp needs ix >= 0.9.3, the first release carrying the subcommand. This PR cannot land before that release exists. It is opened now so the migration is reviewable, not because it is ready to merge.

Not in scope

Hooks, rules, skills and subagents stay exactly as they are — see docs/mcp-plugin-consolidation.md in the Ix repo for the full matrix.

This plugin shipped its own MCP server: `mcp/server.ts` plus 23 tools in
`mcp/tools/`, each wrapping an `ix` CLI call. The CLI now serves the same
23 tools as `ix mcp`, so this was a second implementation of one surface,
maintained separately from the other five plugins' servers.

mcp.json points Cursor at `ix mcp`. Tool parity is exact — the CLI serves
the same 23 names — so nothing an agent could call before is gone.

What stays: `mcp/hooks/`, `mcp/lib/` and `mcp/shared/`. hooks.json runs
`node mcp/dist/hooks/*.js`, and MCP has no concept of a hook, so this is
the part `ix mcp` cannot absorb. The npm build stays with it; the install
messages just no longer call it "the MCP server".

Also removed as unreachable once tools/ is gone:

- `lib/llm.ts` — only tools/ ever imported it. Redaction on the hook path
  is unaffected: that lives in shared/secrets.ts, which lib/parser.ts and
  lib/errors.ts use directly.
- `tests/integration/llm-passthrough.test.ts` (4 tests) — its subject was
  the tool layer's llm-vs-JSON dispatch, which is now the CLI's.
- `tests/unit/llm.test.ts` (8 tests) — covered the module above.

Test count therefore drops 15 -> 3; all three pass, as do typecheck and a
clean rebuild, and all seven hooks.json entrypoints still emit to
mcp/dist/hooks/. The 12 removed tests covered removed code.

@modelcontextprotocol/sdk and zod are dropped from mcp/package.json — the
server and tools were their only consumers.
@KageBinary
KageBinary force-pushed the feat/mcp-delegate-to-ix-cli branch from d17ba12 to 05340b5 Compare August 12, 2026 00:29
mcp.json points Cursor straight at `ix mcp`, which only exists from
0.9.3. On an older CLI nothing here objects: the registration is written,
accepted, and then fails at spawn with "unknown command 'mcp'", which
Cursor reports as a generic connection failure. Nothing tells the user to
upgrade.

ix-codex-plugin gates this at runtime through MIN_IX_VERSION_FOR_MCP.
Cursor has no equivalent hook — mcp.json names the binary directly, and
adding a shim to hold a version check would put back the wrapper this PR
removes. So the check goes where there is still somewhere to print: the
installer.

An unreadable version warns rather than blocks. A version this script
cannot parse is not grounds for refusing an install that may well work,
and on Windows the likeliest cause of an unreadable one is a broken
launcher, which is its own message.

The PowerShell path reads the version inside try/catch. $ErrorActionPreference
is Stop and from PowerShell 7.4 $PSNativeCommandUseErrorActionPreference
makes a non-zero exit terminating, so a broken `ix` — the exact case the
warning is for — would otherwise abort with a native-command error instead.

shellcheck clean. Gate verified against 0.8.1/0.9.2 (block), 0.9.3/0.9.4/
0.10.0/1.0.0 (pass, including the 0.10-over-0.9 ordering), and empty or
unparseable output (warn).
@KageBinary

Copy link
Copy Markdown
Collaborator Author

The version gate is satisfied — ix v0.9.3 is released.

ix mcp is in the published artifact; verified by extracting ix-0.9.3-linux-amd64.tar.gz and running ix mcp --help against it. The Homebrew formula is on v0.9.3 too (it had been stuck on v0.9.1), so brew install ix gets a CLI with the command as well.

Nothing further is needed here — the floor this PR gates on now exists. Ready to merge; the repo ruleset means it needs someone with bypass rights.

…spawn

mcp.json now names the bare `ix`, and Cursor spawns that itself through
CreateProcess -- which consults no PATHEXT, while npm ships no ix.exe, only
ix.CMD. So the registration this PR introduces could never start on Windows.
The old entry ran `node ./mcp/dist/server.js` and was unaffected, making this a
regression rather than a pre-existing gap.

The bare name is correct on macOS and Linux and survives an upgrade that moves
the install, so this is fixed where the file is copied rather than in the file
itself: install.ps1 resolves a launcher and rewrites the copied mcp.json.

Resolution mirrors ix-cli/src/mcp/hosts.ts, including the part that is not
obvious -- `where ix` lists the exact-name match FIRST, and npm's is an
extensionless `#!/bin/sh` shim that CreateProcess cannot launch, so taking the
first line records the one entry that does not work. Only an entry carrying a
PATHEXT extension is accepted. Failing to resolve one warns rather than throws:
the hooks and skills do not depend on it, and leaving the bare name is no worse
than not trying.

Written with WriteAllText and a BOM-less UTF8Encoding, because Set-Content
-Encoding UTF8 emits a BOM on Windows PowerShell 5.1 and a BOM ahead of `{`
breaks a strict JSON parser -- which would corrupt the file being repaired.
Entries are matched by command rather than by server name, since the name
follows $PluginName.

Also carries over two edits install.sh got in this PR and install.ps1 did not:
the build gate still tested for mcp\dist\server.js, the file this PR deletes,
so every Windows install re-ran `npm ci && npm run build` unconditionally and
announced it as "Building MCP server..." for a server that no longer exists. It
now gates on the hooks entry point, as install.sh does.

Not executed: no PowerShell on the machine this was written on, and this repo's
CI has no Windows job.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant