|
| 1 | +# @korg/introspect-mcp |
| 2 | + |
| 3 | +> **Bridge any `--introspect`-aware CLI to Claude Code via MCP. TypeScript edition.** |
| 4 | +
|
| 5 | +One MCP server, any binary in the ecosystem. After installing once, |
| 6 | +adding a new tool surface to Claude Code's agent is **just one line of |
| 7 | +config per binary**. The TS port of `korg-introspect-mcp` — same wire |
| 8 | +contract, same safety gating, same `command_id` conventions as the |
| 9 | +Python reference, distributed via npm for `npx` install. |
| 10 | + |
| 11 | +```bash |
| 12 | +npx -y @korg/introspect-mcp thump --list-tools |
| 13 | +# [korg-introspect-mcp] thump v0.2.0 — 9 tool(s): |
| 14 | +# thump.tui side_effects=fs_read ... |
| 15 | +# thump.generate side_effects=fs_write ... (would be denied) |
| 16 | +# ... |
| 17 | +``` |
| 18 | + |
| 19 | +## Why a TypeScript port |
| 20 | + |
| 21 | +The Python `korg-introspect-mcp` works perfectly — but it requires |
| 22 | +`pipx install` + a Python venv. For Claude Code (a TypeScript app) |
| 23 | +users on Mac/Linux, the canonical "install a new MCP server" idiom is |
| 24 | +`npx -y @some/package`. This TS port closes that distribution gap. |
| 25 | + |
| 26 | +The Python version stays as the reference implementation. Both ports |
| 27 | +emit the same MCP wire format, accept the same args, and produce the |
| 28 | +same tool results. |
| 29 | + |
| 30 | +## Register with Claude Code |
| 31 | + |
| 32 | +Add one entry per binary you want to expose: |
| 33 | + |
| 34 | +```json |
| 35 | +{ |
| 36 | + "mcpServers": { |
| 37 | + "korg-thump": { |
| 38 | + "command": "npx", |
| 39 | + "args": ["-y", "@korg/introspect-mcp", "thump"], |
| 40 | + "env": { "KORG_INTROSPECT_MCP_ALLOW": "fs_write" } |
| 41 | + }, |
| 42 | + "korg": { |
| 43 | + "command": "npx", |
| 44 | + "args": ["-y", "@korg/introspect-mcp", "/usr/local/bin/korg"], |
| 45 | + "env": { "KORG_INTROSPECT_MCP_ALLOW": "all" } |
| 46 | + }, |
| 47 | + "korg-korgex": { |
| 48 | + "command": "npx", |
| 49 | + "args": ["-y", "@korg/introspect-mcp", "korgex"] |
| 50 | + } |
| 51 | + } |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +Restart Claude Code. **30+ tools across the entire korg ecosystem** |
| 56 | +land in the agent's toolset. Tool names match each binary's |
| 57 | +`command_id` from its `--introspect` document, so the |
| 58 | +recall→re-invoke loop (when paired with `@korg/recall-mcp`) is |
| 59 | +deterministic. |
| 60 | + |
| 61 | +## What it does |
| 62 | + |
| 63 | +1. At startup, runs `<binary> --introspect` and parses the |
| 64 | + `korg:introspect@v1` document. |
| 65 | +2. Registers one MCP tool per `Callable`, using the introspect |
| 66 | + `input_schema` directly as the MCP `inputSchema` (no re-encoding). |
| 67 | +3. When the agent calls a tool, the bridge: |
| 68 | + - maps MCP arguments to CLI argv (kebab-case long flags, |
| 69 | + bool flag-on-true, arrays repeat the flag, command_id segments |
| 70 | + become subcommand path), |
| 71 | + - execs the binary as a subprocess, |
| 72 | + - formats stdout per the declared `output_mode` (envelope JSON |
| 73 | + parsed + pretty-printed, stream returned raw, session refused |
| 74 | + as unsupported in v1, none → "ok"), |
| 75 | + - honors `capabilities.side_effects` — refuses |
| 76 | + `fs_write` / `network` / `ledger_write` invocations unless |
| 77 | + explicitly allowed. |
| 78 | + |
| 79 | +## Safety gate |
| 80 | + |
| 81 | +By default only `none` and `fs_read` callables are invocable. |
| 82 | +Everything else requires explicit opt-in via the |
| 83 | +`KORG_INTROSPECT_MCP_ALLOW` env var (set in the MCP server's |
| 84 | +`env` block): |
| 85 | + |
| 86 | +| Value | Allows | |
| 87 | +|---|---| |
| 88 | +| (unset) | `none`, `fs_read` (safe default) | |
| 89 | +| `fs_write` | + file writes | |
| 90 | +| `network` | + network access | |
| 91 | +| `ledger_write` | + writing to the korg ledger | |
| 92 | +| `fs_write,network,ledger_write` | full access (combine) | |
| 93 | +| `all` or `*` | everything | |
| 94 | + |
| 95 | +When the agent calls a denied tool, the response explains exactly |
| 96 | +which env var to set. **The default is stricter than running the |
| 97 | +binary directly via Bash** — defense in depth for MCP-driven |
| 98 | +invocations. |
| 99 | + |
| 100 | +## Argv mapping convention |
| 101 | + |
| 102 | +| MCP argument | CLI argv | |
| 103 | +|---|---| |
| 104 | +| `"query": "x"` | `--query x` | |
| 105 | +| `"top_n": 5` | `--top-n 5` | |
| 106 | +| `"quiet": true` | `--quiet` | |
| 107 | +| `"quiet": false` | *(omitted)* | |
| 108 | +| `"tags": ["a", "b"]` | `--tags a --tags b` | |
| 109 | +| `command_id: "thump.bun.script.run"` | `bun script run` as subcommand path | |
| 110 | + |
| 111 | +This convention matches clap + argparse with kebab-case long flags, |
| 112 | +which the entire korg ecosystem uses. If a binary deviates, the right |
| 113 | +fix is on the binary side — keep this mapper boring. |
| 114 | + |
| 115 | +## CLI usage |
| 116 | + |
| 117 | +```bash |
| 118 | +# Sanity-check what an agent will see (no MCP server launched): |
| 119 | +npx -y @korg/introspect-mcp thump --list-tools |
| 120 | + |
| 121 | +# Run as MCP server on stdio (default — what Claude Code calls): |
| 122 | +npx -y @korg/introspect-mcp thump --allow fs_write |
| 123 | + |
| 124 | +# Pass an absolute path or PATH name: |
| 125 | +npx -y @korg/introspect-mcp /usr/local/bin/korgex --allow all |
| 126 | +``` |
| 127 | + |
| 128 | +## What's deliberately NOT supported in v1 |
| 129 | + |
| 130 | +- **`output_mode: session`** — long-lived stateful sessions over stdio |
| 131 | + MCP need persistent bidirectional I/O. Agents that try to call a |
| 132 | + session-mode callable get a clear error explaining why. |
| 133 | +- **Streaming progress mid-call.** Stdout is buffered and returned at |
| 134 | + completion. A v2 could stream chunks back via MCP notifications. |
| 135 | +- **Auto-rerun on tool drift.** If `<binary> --introspect` changes |
| 136 | + between bridge invocations, restart the MCP server to pick up |
| 137 | + the new schema. |
| 138 | + |
| 139 | +## Tests |
| 140 | + |
| 141 | +56 tests covering: |
| 142 | +- **args.test.ts** (15) — kebab conversion, bool/array/null handling, |
| 143 | + command_id subcommand-path splitting, naked binary, nested |
| 144 | + subcommand paths, dashed names not split, fallback for |
| 145 | + missing-prefix command_ids. |
| 146 | +- **safety.test.ts** (12) — default deny, env-var parsing |
| 147 | + (single/comma/`all`/`*`/whitespace), case-insensitivity, factories, |
| 148 | + denial-message contents. |
| 149 | +- **discovery.test.ts** (10) — binary lookup (absolute/PATH), |
| 150 | + `--introspect` invocation, malformed JSON, schema mismatch, |
| 151 | + duplicate IDs, missing required fields, valid minimal document, |
| 152 | + end-to-end with a fixture binary. |
| 153 | +- **invoker.test.ts** (6) — envelope JSON pretty-print, kebab-case |
| 154 | + flag flow-through, array flag repetition, session-mode refused, |
| 155 | + non-zero exit reports stderr+code, timeout. |
| 156 | +- **server.test.ts** (6) — `buildToolsList` produces correct MCP |
| 157 | + shape, tags capabilities in description, passes input_schema |
| 158 | + verbatim, end-to-end SDK client → compiled CLI → fixture binary |
| 159 | + roundtrip (init + list + call), default policy refuses fs_write |
| 160 | + with a helpful env-var hint. |
| 161 | +- **_sanity.test.ts** (1) — fixture binary spawns correctly. |
| 162 | + |
| 163 | +Run them: |
| 164 | +```bash |
| 165 | +npm test |
| 166 | +``` |
| 167 | + |
| 168 | +## Architecture |
| 169 | + |
| 170 | +``` |
| 171 | +src/ |
| 172 | +├── discovery.ts # exec <binary> --introspect, validate doc |
| 173 | +├── args.ts # MCP arguments → CLI argv (uniform convention) |
| 174 | +├── safety.ts # Policy: side_effects allow-list |
| 175 | +├── invoker.ts # exec the binary, format by output_mode |
| 176 | +├── server.ts # MCP wiring via @modelcontextprotocol/sdk |
| 177 | +├── cli.ts # CLI entry: <binary> [--allow] [--list-tools] |
| 178 | +└── index.ts # public API for programmatic use |
| 179 | +``` |
| 180 | + |
| 181 | +Zero required runtime dependencies beyond `@modelcontextprotocol/sdk`. |
| 182 | +Pure stdlib for everything else. |
| 183 | + |
| 184 | +## License |
| 185 | + |
| 186 | +MIT. |
0 commit comments