@sammorrowdrums/mcpi-ext 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/README.md +109 -139
  3. package/docs/code-mode.md +38 -0
  4. package/docs/skills.md +57 -0
  5. package/docs/tool-cli.md +54 -0
  6. package/images/code-mode-histogram.png +0 -0
  7. package/images/skills-enabling-mcp-tools.png +0 -0
  8. package/images/tool-cli-grep.png +0 -0
  9. package/mise.toml +1 -13
  10. package/package.json +5 -7
  11. package/scripts/test-tool-cli-smoke.mjs +40 -9
  12. package/src/index.ts +24 -3
  13. package/src/tool-cli/index.ts +2 -2
  14. package/dist/code-mode/code-mode-integration.test.d.ts +0 -2
  15. package/dist/code-mode/code-mode-integration.test.d.ts.map +0 -1
  16. package/dist/code-mode/code-mode-integration.test.js +0 -116
  17. package/dist/code-mode/code-mode-integration.test.js.map +0 -1
  18. package/dist/code-mode/eligibility.d.ts +0 -10
  19. package/dist/code-mode/eligibility.d.ts.map +0 -1
  20. package/dist/code-mode/eligibility.js +0 -14
  21. package/dist/code-mode/eligibility.js.map +0 -1
  22. package/dist/code-mode/eligibility.test.d.ts +0 -2
  23. package/dist/code-mode/eligibility.test.d.ts.map +0 -1
  24. package/dist/code-mode/eligibility.test.js +0 -53
  25. package/dist/code-mode/eligibility.test.js.map +0 -1
  26. package/dist/code-mode/executor.d.ts +0 -35
  27. package/dist/code-mode/executor.d.ts.map +0 -1
  28. package/dist/code-mode/executor.js +0 -120
  29. package/dist/code-mode/executor.js.map +0 -1
  30. package/dist/code-mode/executor.test.d.ts +0 -2
  31. package/dist/code-mode/executor.test.d.ts.map +0 -1
  32. package/dist/code-mode/executor.test.js +0 -130
  33. package/dist/code-mode/executor.test.js.map +0 -1
  34. package/dist/code-mode/index.d.ts +0 -65
  35. package/dist/code-mode/index.d.ts.map +0 -1
  36. package/dist/code-mode/index.js +0 -175
  37. package/dist/code-mode/index.js.map +0 -1
  38. package/dist/code-mode/tools.d.ts +0 -45
  39. package/dist/code-mode/tools.d.ts.map +0 -1
  40. package/dist/code-mode/tools.js +0 -70
  41. package/dist/code-mode/tools.js.map +0 -1
  42. package/dist/code-mode/type-hints.d.ts +0 -20
  43. package/dist/code-mode/type-hints.d.ts.map +0 -1
  44. package/dist/code-mode/type-hints.js +0 -189
  45. package/dist/code-mode/type-hints.js.map +0 -1
  46. package/dist/code-mode/type-hints.test.d.ts +0 -2
  47. package/dist/code-mode/type-hints.test.d.ts.map +0 -1
  48. package/dist/code-mode/type-hints.test.js +0 -157
  49. package/dist/code-mode/type-hints.test.js.map +0 -1
  50. package/dist/docker-e2e.d.ts +0 -55
  51. package/dist/docker-e2e.d.ts.map +0 -1
  52. package/dist/docker-e2e.js +0 -235
  53. package/dist/docker-e2e.js.map +0 -1
  54. package/dist/docker-e2e.test.d.ts +0 -2
  55. package/dist/docker-e2e.test.d.ts.map +0 -1
  56. package/dist/docker-e2e.test.js +0 -176
  57. package/dist/docker-e2e.test.js.map +0 -1
  58. package/dist/index.d.ts +0 -3
  59. package/dist/index.d.ts.map +0 -1
  60. package/dist/index.js +0 -128
  61. package/dist/index.js.map +0 -1
  62. package/dist/index.test.d.ts +0 -2
  63. package/dist/index.test.d.ts.map +0 -1
  64. package/dist/index.test.js +0 -8
  65. package/dist/index.test.js.map +0 -1
  66. package/dist/mcp/client-manager.d.ts +0 -46
  67. package/dist/mcp/client-manager.d.ts.map +0 -1
  68. package/dist/mcp/client-manager.js +0 -148
  69. package/dist/mcp/client-manager.js.map +0 -1
  70. package/dist/mcp/client-manager.test.d.ts +0 -2
  71. package/dist/mcp/client-manager.test.d.ts.map +0 -1
  72. package/dist/mcp/client-manager.test.js +0 -204
  73. package/dist/mcp/client-manager.test.js.map +0 -1
  74. package/dist/mcp/config-loader.d.ts +0 -8
  75. package/dist/mcp/config-loader.d.ts.map +0 -1
  76. package/dist/mcp/config-loader.js +0 -43
  77. package/dist/mcp/config-loader.js.map +0 -1
  78. package/dist/mcp/config-loader.test.d.ts +0 -2
  79. package/dist/mcp/config-loader.test.d.ts.map +0 -1
  80. package/dist/mcp/config-loader.test.js +0 -93
  81. package/dist/mcp/config-loader.test.js.map +0 -1
  82. package/dist/mcp/config.d.ts +0 -46
  83. package/dist/mcp/config.d.ts.map +0 -1
  84. package/dist/mcp/config.js +0 -28
  85. package/dist/mcp/config.js.map +0 -1
  86. package/dist/mcp/index.d.ts +0 -5
  87. package/dist/mcp/index.d.ts.map +0 -1
  88. package/dist/mcp/index.js +0 -4
  89. package/dist/mcp/index.js.map +0 -1
  90. package/dist/skills/discover.d.ts +0 -10
  91. package/dist/skills/discover.d.ts.map +0 -1
  92. package/dist/skills/discover.js +0 -60
  93. package/dist/skills/discover.js.map +0 -1
  94. package/dist/skills/format.d.ts +0 -10
  95. package/dist/skills/format.d.ts.map +0 -1
  96. package/dist/skills/format.js +0 -38
  97. package/dist/skills/format.js.map +0 -1
  98. package/dist/skills/format.test.d.ts +0 -2
  99. package/dist/skills/format.test.d.ts.map +0 -1
  100. package/dist/skills/format.test.js +0 -63
  101. package/dist/skills/format.test.js.map +0 -1
  102. package/dist/skills/index.d.ts +0 -6
  103. package/dist/skills/index.d.ts.map +0 -1
  104. package/dist/skills/index.js +0 -6
  105. package/dist/skills/index.js.map +0 -1
  106. package/dist/skills/load-skill-tool.d.ts +0 -40
  107. package/dist/skills/load-skill-tool.d.ts.map +0 -1
  108. package/dist/skills/load-skill-tool.js +0 -104
  109. package/dist/skills/load-skill-tool.js.map +0 -1
  110. package/dist/skills/mcp-tool-proxy.d.ts +0 -11
  111. package/dist/skills/mcp-tool-proxy.d.ts.map +0 -1
  112. package/dist/skills/mcp-tool-proxy.js +0 -116
  113. package/dist/skills/mcp-tool-proxy.js.map +0 -1
  114. package/dist/skills/skill-integration.test.d.ts +0 -2
  115. package/dist/skills/skill-integration.test.d.ts.map +0 -1
  116. package/dist/skills/skill-integration.test.js +0 -119
  117. package/dist/skills/skill-integration.test.js.map +0 -1
  118. package/dist/skills/skill-registry.d.ts +0 -39
  119. package/dist/skills/skill-registry.d.ts.map +0 -1
  120. package/dist/skills/skill-registry.js +0 -48
  121. package/dist/skills/skill-registry.js.map +0 -1
  122. package/dist/skills/skill-registry.test.d.ts +0 -2
  123. package/dist/skills/skill-registry.test.d.ts.map +0 -1
  124. package/dist/skills/skill-registry.test.js +0 -67
  125. package/dist/skills/skill-registry.test.js.map +0 -1
  126. package/dist/test-servers/weather-server.d.ts +0 -12
  127. package/dist/test-servers/weather-server.d.ts.map +0 -1
  128. package/dist/test-servers/weather-server.js +0 -139
  129. package/dist/test-servers/weather-server.js.map +0 -1
  130. package/dist/test-servers/weather-stdio.d.ts +0 -2
  131. package/dist/test-servers/weather-stdio.d.ts.map +0 -1
  132. package/dist/test-servers/weather-stdio.js +0 -11
  133. package/dist/test-servers/weather-stdio.js.map +0 -1
  134. package/dist/tool-cli/cli.d.ts +0 -3
  135. package/dist/tool-cli/cli.d.ts.map +0 -1
  136. package/dist/tool-cli/cli.js +0 -179
  137. package/dist/tool-cli/cli.js.map +0 -1
  138. package/dist/tool-cli/constants.d.ts +0 -7
  139. package/dist/tool-cli/constants.d.ts.map +0 -1
  140. package/dist/tool-cli/constants.js +0 -15
  141. package/dist/tool-cli/constants.js.map +0 -1
  142. package/dist/tool-cli/format.d.ts +0 -8
  143. package/dist/tool-cli/format.d.ts.map +0 -1
  144. package/dist/tool-cli/format.js +0 -57
  145. package/dist/tool-cli/format.js.map +0 -1
  146. package/dist/tool-cli/format.test.d.ts +0 -2
  147. package/dist/tool-cli/format.test.d.ts.map +0 -1
  148. package/dist/tool-cli/format.test.js +0 -30
  149. package/dist/tool-cli/format.test.js.map +0 -1
  150. package/dist/tool-cli/index.d.ts +0 -4
  151. package/dist/tool-cli/index.d.ts.map +0 -1
  152. package/dist/tool-cli/index.js +0 -4
  153. package/dist/tool-cli/index.js.map +0 -1
  154. package/dist/tool-cli/rpc-client.d.ts +0 -6
  155. package/dist/tool-cli/rpc-client.d.ts.map +0 -1
  156. package/dist/tool-cli/rpc-client.js +0 -25
  157. package/dist/tool-cli/rpc-client.js.map +0 -1
  158. package/dist/tool-cli/rpc-server.d.ts +0 -48
  159. package/dist/tool-cli/rpc-server.d.ts.map +0 -1
  160. package/dist/tool-cli/rpc-server.js +0 -220
  161. package/dist/tool-cli/rpc-server.js.map +0 -1
  162. package/dist/tool-cli/rpc-server.test.d.ts +0 -2
  163. package/dist/tool-cli/rpc-server.test.d.ts.map +0 -1
  164. package/dist/tool-cli/rpc-server.test.js +0 -154
  165. package/dist/tool-cli/rpc-server.test.js.map +0 -1
  166. package/src/tool-cli/cli.ts +0 -232
  167. package/src/tool-cli/constants.ts +0 -15
  168. package/src/tool-cli/rpc-client.ts +0 -39
  169. package/src/tool-cli/rpc-server.test.ts +0 -193
  170. package/src/tool-cli/rpc-server.ts +0 -287
@@ -30,7 +30,7 @@ jobs:
30
30
  run: mise run lint
31
31
 
32
32
  - name: Lint Dockerfile
33
- uses: hadolint/hadolint-action@v3.1.0
33
+ uses: hadolint/hadolint-action@v3.3.0
34
34
  with:
35
35
  dockerfile: Dockerfile
36
36
 
package/README.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # mcpi-ext
2
2
 
3
- > **Experimental.** This extension implements progressive MCP tool discovery via skills for [mcpi](https://github.com/SamMorrowDrums/mcpi) (an experimental pi fork). Please only use this to try out the experiment on skills over MCP. See [progressive tool discovery docs](https://github.com/SamMorrowDrums/mcpi/blob/main/docs/progressive-tool-discovery.md) for details.
3
+ > **Experimental.** This extension implements progressive MCP tool discovery via skills for [mcpi](https://github.com/SamMorrowDrums/mcpi) (an experimental pi fork). See the [skills-as-groups proposal](https://github.com/modelcontextprotocol/experimental-ext-grouping/pull/13) for the proposed MCP spec addition, and the [progressive tool discovery docs](https://github.com/SamMorrowDrums/mcpi/blob/main/docs/progressive-tool-discovery.md) for implementation details.
4
+
5
+ ```sh
6
+ npm install -g @sammorrowdrums/mcpi@latest @sammorrowdrums/mcpi-ext@latest @sammorrowdrums/tool-cli@latest
7
+ mcpi --extension $(npm root -g)/@sammorrowdrums/mcpi-ext/dist/index.js \
8
+ --mcp-config ~/.config/mcpi-ext/mcp.json
9
+ ```
10
+
11
+ See [Quick Start](#quick-start) for MCP server configuration.
12
+
13
+ ---
4
14
 
5
15
  ![Three figures in a dark, Sandman-esque realm — The Skill Dealer, The Nuclear Football, and Codey C. Maude — standing before swirling constellations of MCP tool connections](images/banner.webp)
6
16
 
@@ -30,143 +40,85 @@ Building custom [MCP](https://modelcontextprotocol.io/) support as [mcpi](https:
30
40
 
31
41
  > _The Skill Dealer does not give you what you ask for. The Skill Dealer gives you what you need — and nothing more._
32
42
 
33
- MCP servers can ship `skill://` resources: SKILL.md files with frontmatter declaring which tools a skill gates. On connection, the extension discovers all skills and registers their tools with `deferred: true` present in pi's tool registry but excluded from both the tools array sent to the model and the system prompt.
34
-
35
- This approach is **cache-preserving**: the tools array and system prompt stay constant throughout the conversation, so prompt cache is never invalidated by skill activation.
43
+ MCP servers ship `skill://` resources SKILL.md files declaring which tools a skill gates. The extension discovers skills on connection and registers their tools with `deferred: true`: present in the registry for dispatch but hidden from the model and the prompt. **Cache is preserved** — neither the tools array nor the system prompt ever changes.
36
44
 
37
- ### How deferred tool gating works
45
+ When the model calls `load_skill`, the skill's instructions arrive and its tools are unblocked. The model discovers tools from the skill body and can call them immediately. The MCP server itself declares how its tools should be discovered.
38
46
 
39
- Three mechanisms work together:
47
+ 📖 [**How it works →**](docs/skills.md) — deferred gating, `defer_loading` provider support, `tool_call` hook enforcement.
40
48
 
41
- 1. **`deferred: true`** MCP tool proxies are registered with this flag. Pi's runtime keeps them in the internal registry for execution dispatch (via `resolveTool`) but excludes them from the tools array and system prompt sent to the model.
49
+ > _"What you do not need to know," said the Skill Dealer, shuffling the deck, "you will not be burdened with knowing."_
42
50
 
43
- 2. **Provider-native `defer_loading`**Pi's providers map `deferred: true` to the native API parameter. Both Anthropic and OpenAI support this (tested with Claude Opus 4.7 and GPT-5.4). On Anthropic, `defer_loading` keeps the tool in the grammar but hidden from the model's view the skill body naming the tools is sufficient for the model to call them. On OpenAI Responses, pi-mono auto-injects `{"type": "tool_search"}` and the model searches/loads deferred tools server-side.
51
+ ![Skills enabling MCP tools — the model loads a skill and gains access to gated tools](images/skills-enabling-mcp-tools.png)
44
52
 
45
- 3. **`tool_call` hook gating** — The extension registers a `tool_call` event handler that blocks premature calls to skill-gated tools. If the model tries to call a gated tool before loading its skill, the handler returns an error: _"Tool X requires loading a skill first. Call load_skill with: Y"_. This creates a natural feedback loop and serves as the provider-agnostic enforcement layer.
53
+ ---
46
54
 
47
- When the model invokes `load_skill`:
55
+ ## II. The Nuclear Football
48
56
 
49
- 1. The skill's SKILL.md is read from the MCP server and returned as workflow instructions
50
- 2. The skill's `allowedTools` are added to the `enabledTools` set, unblocking the `tool_call` gate
51
- 3. The model can now call the tools — it discovers them from the skill body (which names them) and the provider's grammar
57
+ ![A glowing briefcase marked 'tool-cli' being passed between hands in a dark corridor, trailing sparks of shell commands](images/nuclear-mcp-football.webp)
52
58
 
53
- ```mermaid
54
- sequenceDiagram
55
- participant Model
56
- participant load_skill
57
- participant SkillRegistry
58
- participant MCP Server
59
- participant tool_call gate
60
-
61
- Model->>load_skill: load_skill("github-pr")
62
- load_skill->>SkillRegistry: Look up skill
63
- SkillRegistry-->>load_skill: skill metadata + allowed-tools
64
- load_skill->>MCP Server: Read skill://github-pr
65
- MCP Server-->>load_skill: SKILL.md body
66
- load_skill->>tool_call gate: Enable allowed-tools
67
- load_skill-->>Model: Return workflow instructions
68
- Note over Model: Skill body names the tools.<br/>Model calls them via tool_use.
69
- Model->>tool_call gate: create_pull_request(...)
70
- tool_call gate-->>Model: ✓ Allowed (skill loaded)
71
- ```
59
+ > _The Football is not a weapon. The Football is the authority to use weapons. Whoever holds it can reach any server, call any tool, chain any result — but they must do so deliberately, one command at a time._
72
60
 
73
- This is self-referential enablement: **the MCP server itself declares how its tools should be discovered**. The harness holds all the tools as deferred. The skill decides which ones the model can access. The model gets instructions in one atomic operation, paying only the tokens for the skills it actually loads — and the prompt cache stays intact.
61
+ [`tool-cli`](https://github.com/SamMorrowDrums/tool-cli) is a thin CLI binary that speaks JSON-RPC to the extension. The agent uses it like any shell command composable with pipes, grep, jq, loops. Discovery is progressive: server list tool list schema call. Each step pays only the tokens it needs.
74
62
 
75
- The context window stays clean. The tools appear exactly when the model has the context to use them well. And prompt cache is preserved because neither the tools array nor the system prompt changes.
63
+ 📖 [**How it works →**](docs/tool-cli.md) architecture, progressive discovery, shell composability.
64
+ 📦 [**Standalone package →**](https://github.com/SamMorrowDrums/tool-cli) — `ToolProvider` interface, server, and implementor guidance for other languages.
76
65
 
77
- Anthropic's [tool search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) solves a similar problem from the model side — deferring tool loading to avoid cache invalidation from large tool lists. Our approach uses Anthropic's native `defer_loading` parameter (when available) combined with extension-level `tool_call` gating for provider-agnostic safety. Where tool search has the model _pull_ tools on demand, skill invocation _pushes_ them: when `load_skill` fires, the skill's tools are unblocked and the model gets workflow instructions. The model doesn't search for tools the right tools arrive because the skill declared them.
66
+ > _They pass the Football from hand to hand. It is heavy with potential. Every tool on every server is one command awaybut you must type the command yourself._
78
67
 
79
- > _"What you do not need to know," said the Skill Dealer, shuffling the deck, "you will not be burdened with knowing."_
68
+ ![tool-cli in action progressive discovery piped through grep](images/tool-cli-grep.png)
80
69
 
81
70
  ---
82
71
 
83
- ## II. The Nuclear Football
84
-
85
- ![A glowing briefcase marked 'tool-cli' being passed between hands in a dark corridor, trailing sparks of shell commands](images/nuclear-mcp-football.webp)
86
-
87
- > _The Football is not a weapon. The Football is the authority to use weapons. Whoever holds it can reach any server, call any tool, chain any result — but they must do so deliberately, one command at a time._
88
-
89
- `tool-cli` is a thin CLI binary that speaks JSON-RPC 2.0 to the extension over HTTP. The agent uses it like any shell command — composable with pipes, grep, jq, loops, and all the bash idioms it already knows.
90
-
91
- ```mermaid
92
- flowchart TD
93
- A["Agent (mcpi)"] -->|shell exec| B["tool-cli &lt;server&gt; &lt;tool&gt; '{args}'"]
94
- B -->|"HTTP JSON-RPC (localhost:7179)"| C["ToolCliRpcServer (in extension)"]
95
- C -->|"MCP protocol (stdio/HTTP)"| D["MCP Server(s)"]
96
- ```
72
+ ## III. Codey C. Maude
97
73
 
98
- Discovery is **progressive** the agent pays only the tokens it needs:
74
+ ![A luminous figure composed of flowing code, sitting cross-legged in a V8 isolate bubble, reading structured data from floating JSON schemas](images/code-c-maude.webp)
99
75
 
100
- ```sh
101
- tool-cli --help # What servers exist?
102
- tool-cli github # What tools does this server have?
103
- tool-cli github search_code # What's the schema for this tool?
104
- tool-cli github search_code '{"query":"auth"}' # Call it
105
- ```
76
+ > _Codey does not ask permission. Codey does not need to. Everything Codey touches is read-only, every result is typed, and the sandbox cannot be escaped. Codey is safe by construction._
106
77
 
107
- And because it's shell-native, the agent gets bash superpowers for free:
78
+ Code Mode targets **read-only** tools with **structured output**. The model writes JavaScript that chains MCP tool calls inside a V8 isolate — memory-limited, time-limited, no filesystem or network access. Perfect for pagination loops, aggregation, and joins across many calls.
108
79
 
109
- ```sh
110
- # Chain tool calls
111
- tool-cli myserver list_items '{}' | jq -r '.[0].id' | \
112
- xargs -I{} tool-cli myserver get_item '{"id":"{}"}'
113
-
114
- # Process collections
115
- for city in London Tokyo Paris; do
116
- echo "=== $city ==="
117
- tool-cli weather check_weather '{"city":"'"$city"'"}'
118
- done
119
-
120
- # Combine with the Unix toolbox
121
- tool-cli myserver export_csv '{"table":"users"}' | sort -t, -k2 | head -20
122
- ```
80
+ 📖 [**How it works →**](docs/code-mode.md) — sandbox isolation, eligibility, tool dispatch.
123
81
 
124
- The RPC server is the single choke point for all tool execution the natural interception point for human-in-the-loop confirmation on destructive operations.
82
+ > _"I can see everything," Codey said, eyes reflecting infinite JSON. "I just can't touch it. That's the point. That's why they trust me."_
125
83
 
126
- > _They pass the Football from hand to hand. It is heavy with potential. Every tool on every server is one command away — but you must type the command yourself._
84
+ ![Code Mode in action chaining MCP tools in a V8 sandbox to build a histogram](images/code-mode-histogram.png)
127
85
 
128
86
  ---
129
87
 
130
- ## III. Codey C. Maude
88
+ ## When to Use Each Tier
131
89
 
132
- ![A luminous figure composed of flowing code, sitting cross-legged in a V8 isolate bubble, reading structured data from floating JSON schemas](images/code-c-maude.webp)
90
+ > _They asked the three: "Why are there three of you? Isn't one enough?"_
91
+ >
92
+ > _The Skill Dealer laid down a card. "When you know the ritual — the steps, the order, the tools that belong together — you come to me. I give you the ceremony whole."_
93
+ >
94
+ > _The Football's briefcase clicked open. "When you need one answer, quickly, and you know what you're looking for — you reach for me. I'm a shell command. I compose."_
95
+ >
96
+ > _Codey smiled, cross-legged in the isolate. "And when the answer is buried in nine pages of data, when you need loops and math and joins across a thousand records — you write the code, and I run it. Safely."_
97
+ >
98
+ > _"Three is not redundancy," said the Skill Dealer. "Three is completeness."_
133
99
 
134
- > _Codey does not ask permission. Codey does not need to. Everything Codey touches is read-only, every result is typed, and the sandbox cannot be escaped. Codey is safe by construction._
100
+ **The Skill Dealer** when there's a curated workflow for the domain task. "Triage these 20 issues" means loading the triage skill, which gives you the right tools _plus_ the workflow instructions (dedup checks, labeling conventions, close criteria). Re-deriving that from raw tool calls is wasteful and error-prone.
135
101
 
136
- Code Mode is for the tools that are **read-only** (`annotations.readOnlyHint === true`) and return **structured output** (`outputSchema` defined). These two properties together make a tool safe for autonomous use — it can't modify anything, and its results are machine-parseable.
102
+ **The Nuclear Football** one-shot or exploratory calls, especially when piping through Unix tools. `tool-cli github search_code '{"query":"auth"}' | jq '.items[].path'` one call, pipe to jq, done. Also perfect for discovering what's on a server you haven't used before.
137
103
 
138
- The model writes JavaScript that chains these tools:
104
+ **Codey C. Maude** when you need real computation across many calls: pagination loops, aggregation, joining results, math. 876 issues across 9 pages, counting labels per issue, summing into a histogram — that's a loop with state. Doing it via tool-cli would mean 9 separate calls plus shell-side aggregation. Fragile. Codey does it in one sandbox execution.
139
105
 
140
- ```javascript
141
- // Executed in a V8 isolate via isolated-vm
142
- const issues = await codemode.list_issues({ repo: "owner/repo", state: "open" });
143
- const critical = issues.filter((i) => i.labels.includes("critical"));
144
- const details = await Promise.all(critical.map((i) => codemode.get_issue({ number: i.number })));
145
- return details.map((d) => ({ title: d.title, assignee: d.assignee }));
146
- ```
106
+ ### A single task using all three
147
107
 
148
- The sandbox runs in `isolated-vm` genuine V8-level isolation:
108
+ > _"Triage the backlog of github/github-mcp-server: find stale bugs older than 90 days with no recent activity, summarize patterns, and close obvious duplicates."_
149
109
 
150
- - **128MB memory limit**, 30-second timeout
151
- - **No access** to filesystem, network, or Node.js APIs
152
- - Tool calls dispatch to the host via `Reference` callbacks — MCP execution happens outside the sandbox
153
- - ~15ms overhead, negligible vs network I/O
110
+ 1. **Codey** paginated all open bug issues, filtered by `updated < 90d ago`, grouped by label and keyword to find clusters. Computation across many pages — this is what sandboxes are for.
154
111
 
155
- Two tools expose this to the model:
112
+ 2. **The Football** spot-checked suspect issues. `tool-cli github get_issue '{"number":42}'` piped through `jq` to eyeball specific fields. Quick, ad-hoc, composable.
156
113
 
157
- | Tool | Purpose |
158
- | -------------- | ---------------------------------------------------------------------------------- |
159
- | `code_search` | Discover available tools — `codemode.listTools()`, `codemode.describeTools(names)` |
160
- | `code_execute` | Chain tool calls — write JS that calls `codemode.toolName(args)` |
114
+ 3. **The Skill Dealer** loaded `triage-issues` to actually close the duplicates — following the project's triage workflow with correct labels, comment templates, and close reasons. The ceremony, performed correctly.
161
115
 
162
- > _"I can see everything," Codey said, eyes reflecting infinite JSON. "I just can't touch it. That's the point. That's why they trust me."_
116
+ > _The rule of thumb is simple: skill for workflows, tool-cli for one-shots, code_execute for computation. The three are not competing. They are collaborating._
163
117
 
164
118
  ---
165
119
 
166
120
  ## The Architecture
167
121
 
168
- The three tiers are complementary. Skills give curated access with workflow knowledge. The Football gives interactive access with safety. Code Mode gives autonomous access to safe operations at scale.
169
-
170
122
  ```mermaid
171
123
  flowchart TD
172
124
  subgraph mcpi["mcpi (agent)"]
@@ -185,66 +137,62 @@ flowchart TD
185
137
 
186
138
  The harness controls what the model sees. MCP servers just expose their tools and skills. The extension decides _when_ and _how_ to reveal them.
187
139
 
140
+ ### Every call flows through the harness
141
+
142
+ All three tiers route MCP tool calls back through the extension process. This is a subtle but important property: even when the model writes sandboxed JavaScript (Code Mode) or shells out to `tool-cli`, the actual MCP call happens in the harness. This means:
143
+
144
+ - **Every tool invocation appears in the agent log** — skills, tool-cli one-shots, and Code Mode sandbox calls alike. Full observability without instrumentation.
145
+ - **Human-in-the-loop can be added at one point** — the `McpClientManager` is the single choke point. Future work can check tool annotations (`readOnlyHint`, `destructiveHint`) and gate destructive calls through user confirmation, regardless of which tier initiated them.
146
+
188
147
  > _MCP doesn't have a context problem. It never did. It was just waiting for someone to imagine the right way to read the runes._
189
148
 
190
149
  ---
191
150
 
192
151
  ## Quick Start
193
152
 
194
- ### Install from npm
195
-
196
- ```sh
197
- npm install @sammorrowdrums/mcpi @sammorrowdrums/mcpi-ext
198
- ```
199
-
200
- Run mcpi with the extension:
153
+ ### 1. Install
201
154
 
202
155
  ```sh
203
- npx mcpi --extension node_modules/@sammorrowdrums/mcpi-ext/dist/index.js \
204
- --mcp-config ~/.config/mcpi-ext/mcp.json
156
+ npm install -g @sammorrowdrums/mcpi@latest @sammorrowdrums/mcpi-ext@latest @sammorrowdrums/tool-cli@latest
205
157
  ```
206
158
 
207
- ### Local development
159
+ ### 2. Configure MCP servers
208
160
 
209
- ```sh
210
- curl https://mise.run | sh # install mise
211
- eval "$(~/.local/bin/mise activate bash)" # activate
212
- mise install # install node
213
- npm install # install dependencies
214
- mise run build # build
215
- mise run test # test
216
- ```
217
-
218
- For local dev, override the `@sammorrowdrums/mcpi` dependency to point at your local mcpi clone:
161
+ Create `~/.config/mcpi-ext/mcp.json`:
219
162
 
220
163
  ```json
221
- // package.json overrides (not committed)
222
- "devDependencies": {
223
- "@sammorrowdrums/mcpi": "file:../path/to/mcpi/packages/coding-agent"
164
+ {
165
+ "mcpServers": {
166
+ "github": {
167
+ "type": "stdio",
168
+ "command": "docker",
169
+ "args": [
170
+ "run",
171
+ "--rm",
172
+ "-i",
173
+ "-e",
174
+ "GITHUB_PERSONAL_ACCESS_TOKEN",
175
+ "ghcr.io/github/github-mcp-server:skill-discovery",
176
+ "stdio"
177
+ ],
178
+ "env": {
179
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "xxx"
180
+ }
181
+ }
182
+ }
224
183
  }
225
184
  ```
226
185
 
227
- Then load the extension from the built output:
186
+ Replace `xxx` with your [GitHub personal access token](https://github.com/settings/tokens). See [github/github-mcp-server](https://github.com/github/github-mcp-server) for the standard server.
228
187
 
229
- ```sh
230
- mcpi --extension ./dist/index.js --mcp-config ~/.config/mcpi-ext/mcp.json
231
- ```
232
-
233
- ### MCP server configuration
188
+ > **Note:** The `skill-discovery` tag includes experimental `skill://` resources that enable Tier 1 progressive discovery. The standard `ghcr.io/github/github-mcp-server` image works too — tool-cli (Tier 2) and Code Mode (Tier 3) function with any MCP server, but skill-gated tool activation requires `skill://` resources.
234
189
 
235
- Create `~/.config/mcpi-ext/mcp.json` (or pass `--mcp-config /path/to/config.json`):
190
+ You can add more servers — both `stdio` (spawns a process) and `remote` (Streamable HTTP) are supported:
236
191
 
237
192
  ```json
238
193
  {
239
194
  "mcpServers": {
240
- "github": {
241
- "type": "stdio",
242
- "command": "npx",
243
- "args": ["-y", "@modelcontextprotocol/server-github"],
244
- "env": {
245
- "GITHUB_PERSONAL_ACCESS_TOKEN": "xxx"
246
- }
247
- },
195
+ "github": { "...": "..." },
248
196
  "my-remote-server": {
249
197
  "type": "remote",
250
198
  "url": "https://my-mcp-server.example.com/mcp",
@@ -256,7 +204,28 @@ Create `~/.config/mcpi-ext/mcp.json` (or pass `--mcp-config /path/to/config.json
256
204
  }
257
205
  ```
258
206
 
259
- Each server can be `stdio` (spawns a child process) or `remote` (Streamable HTTP). The extension connects to all configured servers on startup, discovers their tools, and gates them via skills.
207
+ ### 3. Run
208
+
209
+ ```sh
210
+ mcpi --extension $(npm root -g)/@sammorrowdrums/mcpi-ext/dist/index.js \
211
+ --mcp-config ~/.config/mcpi-ext/mcp.json
212
+ ```
213
+
214
+ ### Local development
215
+
216
+ ```sh
217
+ git clone https://github.com/SamMorrowDrums/mcpi-ext.git
218
+ cd mcpi-ext
219
+ npm install
220
+ npm run build
221
+ npm test
222
+ ```
223
+
224
+ Then run with your local build:
225
+
226
+ ```sh
227
+ mcpi --extension ./dist/index.js --mcp-config ~/.config/mcpi-ext/mcp.json
228
+ ```
260
229
 
261
230
  See [AGENTS.md](AGENTS.md) for full tooling docs, dev loop, and architecture details.
262
231
 
@@ -270,7 +239,8 @@ src/
270
239
  tool-cli/ tool-cli RPC server, client, CLI binary, prompt
271
240
  code-mode/ V8 sandbox executor, eligibility, type hints
272
241
  test-servers/ Test MCP servers (weather, echo)
273
- images/ Banner and character art
242
+ docs/ Detailed mechanism documentation
243
+ images/ Banner, character art, and screenshots
274
244
  ```
275
245
 
276
246
  ## License
@@ -0,0 +1,38 @@
1
+ # Tier 3 — Codey C. Maude (Code Mode)
2
+
3
+ Code Mode targets tools that are **read-only** (`annotations.readOnlyHint === true`) and return **structured output** (`outputSchema` defined). These two properties together make a tool safe for autonomous use — it can't modify anything, and its results are machine-parseable.
4
+
5
+ ## How it works
6
+
7
+ The model writes JavaScript that chains MCP tool calls. The code runs in a V8 isolate via `isolated-vm`:
8
+
9
+ ```javascript
10
+ const issues = await codemode.list_issues({ repo: "owner/repo", state: "open" });
11
+ const critical = issues.filter((i) => i.labels.includes("critical"));
12
+ const details = await Promise.all(critical.map((i) => codemode.get_issue({ number: i.number })));
13
+ return details.map((d) => ({ title: d.title, assignee: d.assignee }));
14
+ ```
15
+
16
+ ## Sandbox isolation
17
+
18
+ - **128MB memory limit**, 30-second timeout
19
+ - **No access** to filesystem, network, or Node.js APIs
20
+ - Tool calls dispatch to the host via `Reference` callbacks — MCP execution happens outside the sandbox
21
+ - ~15ms overhead, negligible vs network I/O
22
+
23
+ ## Tools
24
+
25
+ | Tool | Purpose |
26
+ | -------------- | ---------------------------------------------------------------------------------- |
27
+ | `code_search` | Discover available tools — `codemode.listTools()`, `codemode.describeTools(names)` |
28
+ | `code_execute` | Chain tool calls — write JS that calls `codemode.toolName(args)` |
29
+
30
+ ## When to use
31
+
32
+ Code Mode shines when you need real computation across many calls: pagination loops, aggregation, joining results, math. For example:
33
+
34
+ - 876 issues across 9 pages, counting labels per issue, building a histogram
35
+ - For each open PR, fetch reviews and compute average time-to-first-review
36
+ - Paginate all items, filter, group, and summarize
37
+
38
+ See [DECISIONS.md #011–012](../DECISIONS.md) for implementation decisions.
package/docs/skills.md ADDED
@@ -0,0 +1,57 @@
1
+ # Tier 1 — The Skill Dealer
2
+
3
+ MCP servers can ship `skill://` resources: SKILL.md files with frontmatter declaring which tools a skill gates. On connection, the extension discovers all skills and registers their tools with `deferred: true`.
4
+
5
+ ## How deferred tool gating works
6
+
7
+ Three mechanisms work together to keep tools hidden until the right moment — while preserving prompt cache:
8
+
9
+ ### 1. `deferred: true`
10
+
11
+ MCP tool proxies are registered with this flag. mcpi keeps them in the tools array (so providers can include them in grammar/dispatch) but excludes them from the system prompt. The tools array stays static throughout the conversation — prompt cache is never invalidated.
12
+
13
+ ### 2. Provider-native `defer_loading`
14
+
15
+ mcpi's providers map `deferred: true` to their native deferred loading mechanism:
16
+
17
+ - **Anthropic** — `defer_loading: true` hides the tool from the model's view while keeping it in the grammar.
18
+ - **OpenAI Responses** — `defer_loading: true` with auto-injected `{"type": "tool_search"}` enables server-side tool discovery.
19
+
20
+ Both tested with Claude Opus 4.7 and GPT-5.4. Since the tools array never changes, prompt cache is preserved on both providers.
21
+
22
+ ### 3. `tool_call` hook gating
23
+
24
+ The extension registers a `tool_call` event handler that blocks premature calls to skill-gated tools. If the model tries to call a gated tool before loading its skill, the handler returns an error:
25
+
26
+ > _"Tool X requires loading a skill first. Call load_skill with: Y"_
27
+
28
+ This creates a natural feedback loop and serves as the enforcement layer across all providers — including those without native `defer_loading` support.
29
+
30
+ ## Flow
31
+
32
+ ```mermaid
33
+ sequenceDiagram
34
+ participant Model
35
+ participant load_skill
36
+ participant SkillRegistry
37
+ participant MCP Server
38
+ participant tool_call gate
39
+
40
+ Model->>load_skill: load_skill("github-pr")
41
+ load_skill->>SkillRegistry: Look up skill
42
+ SkillRegistry-->>load_skill: skill metadata + allowed-tools
43
+ load_skill->>MCP Server: Read skill://github-pr
44
+ MCP Server-->>load_skill: SKILL.md body
45
+ load_skill->>tool_call gate: Enable allowed-tools
46
+ load_skill-->>Model: Return workflow instructions
47
+ Note over Model: Skill body names the tools.<br/>Model calls them via tool_use.
48
+ Model->>tool_call gate: create_pull_request(...)
49
+ tool_call gate-->>Model: ✓ Allowed (skill loaded)
50
+ ```
51
+
52
+ The MCP server itself declares how its tools should be discovered. The harness holds all the tools as deferred. The skill decides which ones the model can access. The model gets instructions in one atomic operation, paying only the tokens for the skills it actually loads — and the prompt cache stays intact.
53
+
54
+ ## See also
55
+
56
+ - [skills-as-groups MCP spec proposal](https://github.com/modelcontextprotocol/experimental-ext-grouping/pull/13)
57
+ - [DECISIONS.md #008](../DECISIONS.md) — cache-safe progressive tool disclosure
@@ -0,0 +1,54 @@
1
+ # Tier 2 — The Nuclear Football (tool-cli)
2
+
3
+ `tool-cli` is a thin CLI binary that speaks JSON-RPC 2.0 to the extension over HTTP. The agent uses it like any shell command — composable with pipes, grep, jq, loops.
4
+
5
+ ## Architecture
6
+
7
+ ```mermaid
8
+ flowchart TD
9
+ A["Agent (mcpi)"] -->|shell exec| B["tool-cli &lt;server&gt; &lt;tool&gt; '{args}'"]
10
+ B -->|"HTTP JSON-RPC (random port, token auth)"| C["ToolCliServer (from @sammorrowdrums/tool-cli)"]
11
+ C -->|"MCP protocol (stdio/HTTP)"| D["MCP Server(s)"]
12
+ ```
13
+
14
+ The RPC server lives in the extension process, started on `session_start` and stopped on `session_shutdown`. The CLI binary uses `fetch` to call it.
15
+
16
+ ## Progressive discovery
17
+
18
+ The agent pays only the tokens it needs:
19
+
20
+ ```sh
21
+ tool-cli --help # What servers exist?
22
+ tool-cli github # What tools does this server have?
23
+ tool-cli github search_code # What's the schema for this tool?
24
+ tool-cli github search_code '{"query":"auth"}' # Call it
25
+ ```
26
+
27
+ ## Shell composability
28
+
29
+ ```sh
30
+ # Chain tool calls
31
+ tool-cli myserver list_items '{}' | jq -r '.[0].id' | \
32
+ xargs -I{} tool-cli myserver get_item '{"id":"{}"}'
33
+
34
+ # Process collections
35
+ for city in London Tokyo Paris; do
36
+ echo "=== $city ==="
37
+ tool-cli weather check_weather '{"city":"'"$city"'"}'
38
+ done
39
+
40
+ # Combine with the Unix toolbox
41
+ tool-cli myserver export_csv '{"table":"users"}' | sort -t, -k2 | head -20
42
+ ```
43
+
44
+ ## Security
45
+
46
+ The server uses token-based auth and dynamic port allocation (provided by [`@sammorrowdrums/tool-cli`](https://github.com/SamMorrowDrums/tool-cli)):
47
+
48
+ 1. `start()` binds to a random port and generates a 32-byte session token
49
+ 2. Returns `{ port, token }` — the extension sets these as env vars via `pi.setEnv()`
50
+ 3. Every request must include `Authorization: Bearer <token>` — rejected with 401 otherwise
51
+
52
+ This enables concurrent sessions and prevents random processes from calling MCP tools. The RPC server is the single choke point for all tool execution — the natural interception point for future human-in-the-loop confirmation on destructive operations.
53
+
54
+ See [tool-cli security docs](https://github.com/SamMorrowDrums/tool-cli#security) and [DECISIONS.md #010](../DECISIONS.md).
Binary file
Binary file
package/mise.toml CHANGED
@@ -3,19 +3,7 @@ node = "22"
3
3
 
4
4
  [tasks.build]
5
5
  description = "Build TypeScript"
6
- run = [
7
- "npx tsc",
8
- "chmod +x dist/tool-cli/cli.js",
9
- ]
10
-
11
- [tasks."install:tool-cli"]
12
- description = "Symlink tool-cli binary for local use"
13
- depends = ["build"]
14
- run = [
15
- "ln -sf ../../dist/tool-cli/cli.js node_modules/.bin/tool-cli",
16
- "mkdir -p ~/.local/bin",
17
- "ln -sf $PWD/dist/tool-cli/cli.js ~/.local/bin/tool-cli",
18
- ]
6
+ run = "npx tsc"
19
7
 
20
8
  [tasks.dev]
21
9
  description = "Watch mode"
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@sammorrowdrums/mcpi-ext",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "dependencies": {
5
5
  "@modelcontextprotocol/sdk": "^1.29.0",
6
+ "@sammorrowdrums/tool-cli": "^0.3.0",
6
7
  "isolated-vm": "^6.1.2",
7
8
  "typebox": "^1.1.31",
8
9
  "zod": "^4.3.6"
9
10
  },
10
11
  "peerDependencies": {
11
- "@sammorrowdrums/mcpi": ">=0.70.0"
12
+ "@sammorrowdrums/mcpi": ">=0.70.9"
12
13
  },
13
14
  "devDependencies": {
14
- "@sammorrowdrums/mcpi": "file:../../../repos/pi-mono/packages/coding-agent",
15
15
  "@eslint/js": "^10.0.1",
16
+ "@sammorrowdrums/mcpi": "^0.70.9",
16
17
  "@types/node": "^25.6.0",
17
18
  "eslint": "^10.2.1",
18
19
  "eslint-config-prettier": "^10.1.8",
@@ -39,8 +40,5 @@
39
40
  "license": "MIT",
40
41
  "main": "./dist/index.js",
41
42
  "type": "module",
42
- "types": "./dist/index.d.ts",
43
- "bin": {
44
- "tool-cli": "./dist/tool-cli/cli.js"
45
- }
43
+ "types": "./dist/index.d.ts"
46
44
  }