@sammorrowdrums/mcpi-ext 0.2.0 → 0.2.1
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.
- package/dist/code-mode/code-mode-integration.test.d.ts +2 -0
- package/dist/code-mode/code-mode-integration.test.d.ts.map +1 -0
- package/dist/code-mode/code-mode-integration.test.js +116 -0
- package/dist/code-mode/code-mode-integration.test.js.map +1 -0
- package/dist/code-mode/eligibility.d.ts +10 -0
- package/dist/code-mode/eligibility.d.ts.map +1 -0
- package/dist/code-mode/eligibility.js +14 -0
- package/dist/code-mode/eligibility.js.map +1 -0
- package/dist/code-mode/eligibility.test.d.ts +2 -0
- package/dist/code-mode/eligibility.test.d.ts.map +1 -0
- package/dist/code-mode/eligibility.test.js +53 -0
- package/dist/code-mode/eligibility.test.js.map +1 -0
- package/dist/code-mode/executor.d.ts +35 -0
- package/dist/code-mode/executor.d.ts.map +1 -0
- package/dist/code-mode/executor.js +120 -0
- package/dist/code-mode/executor.js.map +1 -0
- package/dist/code-mode/executor.test.d.ts +2 -0
- package/dist/code-mode/executor.test.d.ts.map +1 -0
- package/dist/code-mode/executor.test.js +130 -0
- package/dist/code-mode/executor.test.js.map +1 -0
- package/dist/code-mode/index.d.ts +65 -0
- package/dist/code-mode/index.d.ts.map +1 -0
- package/dist/code-mode/index.js +175 -0
- package/dist/code-mode/index.js.map +1 -0
- package/dist/code-mode/tools.d.ts +45 -0
- package/dist/code-mode/tools.d.ts.map +1 -0
- package/dist/code-mode/tools.js +70 -0
- package/dist/code-mode/tools.js.map +1 -0
- package/dist/code-mode/type-hints.d.ts +20 -0
- package/dist/code-mode/type-hints.d.ts.map +1 -0
- package/dist/code-mode/type-hints.js +189 -0
- package/dist/code-mode/type-hints.js.map +1 -0
- package/dist/code-mode/type-hints.test.d.ts +2 -0
- package/dist/code-mode/type-hints.test.d.ts.map +1 -0
- package/dist/code-mode/type-hints.test.js +157 -0
- package/dist/code-mode/type-hints.test.js.map +1 -0
- package/dist/docker-e2e.d.ts +55 -0
- package/dist/docker-e2e.d.ts.map +1 -0
- package/dist/docker-e2e.js +235 -0
- package/dist/docker-e2e.js.map +1 -0
- package/dist/docker-e2e.test.d.ts +2 -0
- package/dist/docker-e2e.test.d.ts.map +1 -0
- package/dist/docker-e2e.test.js +176 -0
- package/dist/docker-e2e.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +8 -0
- package/dist/index.test.js.map +1 -0
- package/dist/mcp/client-manager.d.ts +46 -0
- package/dist/mcp/client-manager.d.ts.map +1 -0
- package/dist/mcp/client-manager.js +148 -0
- package/dist/mcp/client-manager.js.map +1 -0
- package/dist/mcp/client-manager.test.d.ts +2 -0
- package/dist/mcp/client-manager.test.d.ts.map +1 -0
- package/dist/mcp/client-manager.test.js +204 -0
- package/dist/mcp/client-manager.test.js.map +1 -0
- package/dist/mcp/config-loader.d.ts +8 -0
- package/dist/mcp/config-loader.d.ts.map +1 -0
- package/dist/mcp/config-loader.js +43 -0
- package/dist/mcp/config-loader.js.map +1 -0
- package/dist/mcp/config-loader.test.d.ts +2 -0
- package/dist/mcp/config-loader.test.d.ts.map +1 -0
- package/dist/mcp/config-loader.test.js +93 -0
- package/dist/mcp/config-loader.test.js.map +1 -0
- package/dist/mcp/config.d.ts +46 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +28 -0
- package/dist/mcp/config.js.map +1 -0
- package/{src/mcp/index.ts → dist/mcp/index.d.ts} +1 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/skills/discover.d.ts +10 -0
- package/dist/skills/discover.d.ts.map +1 -0
- package/dist/skills/discover.js +60 -0
- package/dist/skills/discover.js.map +1 -0
- package/dist/skills/format.d.ts +10 -0
- package/dist/skills/format.d.ts.map +1 -0
- package/dist/skills/format.js +38 -0
- package/dist/skills/format.js.map +1 -0
- package/dist/skills/format.test.d.ts +2 -0
- package/dist/skills/format.test.d.ts.map +1 -0
- package/dist/skills/format.test.js +63 -0
- package/dist/skills/format.test.js.map +1 -0
- package/{src/skills/index.ts → dist/skills/index.d.ts} +2 -5
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/load-skill-tool.d.ts +40 -0
- package/dist/skills/load-skill-tool.d.ts.map +1 -0
- package/dist/skills/load-skill-tool.js +104 -0
- package/dist/skills/load-skill-tool.js.map +1 -0
- package/dist/skills/mcp-tool-proxy.d.ts +11 -0
- package/dist/skills/mcp-tool-proxy.d.ts.map +1 -0
- package/dist/skills/mcp-tool-proxy.js +116 -0
- package/dist/skills/mcp-tool-proxy.js.map +1 -0
- package/dist/skills/skill-integration.test.d.ts +2 -0
- package/dist/skills/skill-integration.test.d.ts.map +1 -0
- package/dist/skills/skill-integration.test.js +119 -0
- package/dist/skills/skill-integration.test.js.map +1 -0
- package/dist/skills/skill-registry.d.ts +39 -0
- package/dist/skills/skill-registry.d.ts.map +1 -0
- package/dist/skills/skill-registry.js +48 -0
- package/dist/skills/skill-registry.js.map +1 -0
- package/dist/skills/skill-registry.test.d.ts +2 -0
- package/dist/skills/skill-registry.test.d.ts.map +1 -0
- package/dist/skills/skill-registry.test.js +67 -0
- package/dist/skills/skill-registry.test.js.map +1 -0
- package/dist/test-servers/weather-server.d.ts +12 -0
- package/dist/test-servers/weather-server.d.ts.map +1 -0
- package/dist/test-servers/weather-server.js +139 -0
- package/dist/test-servers/weather-server.js.map +1 -0
- package/dist/test-servers/weather-stdio.d.ts +2 -0
- package/dist/test-servers/weather-stdio.d.ts.map +1 -0
- package/{src/test-servers/weather-stdio.ts → dist/test-servers/weather-stdio.js} +1 -1
- package/dist/test-servers/weather-stdio.js.map +1 -0
- package/dist/tool-cli/cli.d.ts +3 -0
- package/dist/tool-cli/cli.d.ts.map +1 -0
- package/dist/tool-cli/cli.js +179 -0
- package/dist/tool-cli/cli.js.map +1 -0
- package/dist/tool-cli/constants.d.ts +7 -0
- package/dist/tool-cli/constants.d.ts.map +1 -0
- package/dist/tool-cli/constants.js +15 -0
- package/dist/tool-cli/constants.js.map +1 -0
- package/dist/tool-cli/format.d.ts +8 -0
- package/dist/tool-cli/format.d.ts.map +1 -0
- package/{src/tool-cli/format.ts → dist/tool-cli/format.js} +5 -4
- package/dist/tool-cli/format.js.map +1 -0
- package/dist/tool-cli/format.test.d.ts +2 -0
- package/dist/tool-cli/format.test.d.ts.map +1 -0
- package/dist/tool-cli/format.test.js +30 -0
- package/dist/tool-cli/format.test.js.map +1 -0
- package/{src/tool-cli/index.ts → dist/tool-cli/index.d.ts} +1 -0
- package/dist/tool-cli/index.d.ts.map +1 -0
- package/dist/tool-cli/index.js +3 -0
- package/dist/tool-cli/index.js.map +1 -0
- package/dist/tool-cli/rpc-client.d.ts +6 -0
- package/dist/tool-cli/rpc-client.d.ts.map +1 -0
- package/dist/tool-cli/rpc-client.js +25 -0
- package/dist/tool-cli/rpc-client.js.map +1 -0
- package/dist/tool-cli/rpc-server.d.ts +48 -0
- package/dist/tool-cli/rpc-server.d.ts.map +1 -0
- package/dist/tool-cli/rpc-server.js +220 -0
- package/dist/tool-cli/rpc-server.js.map +1 -0
- package/dist/tool-cli/rpc-server.test.d.ts +2 -0
- package/dist/tool-cli/rpc-server.test.d.ts.map +1 -0
- package/dist/tool-cli/rpc-server.test.js +154 -0
- package/dist/tool-cli/rpc-server.test.js.map +1 -0
- package/package.json +5 -1
- package/.dockerignore +0 -6
- package/.github/dependabot.yml +0 -25
- package/.github/workflows/ci.yml +0 -50
- package/.hadolint.yaml +0 -6
- package/.prettierignore +0 -4
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -118
- package/DECISIONS.md +0 -99
- package/Dockerfile +0 -39
- package/aube-lock.yaml +0 -4146
- package/docs/code-mode.md +0 -38
- package/docs/skills.md +0 -57
- package/docs/tool-cli.md +0 -54
- package/eslint.config.js +0 -27
- package/images/banner.webp +0 -0
- package/images/code-c-maude.webp +0 -0
- package/images/code-mode-histogram.png +0 -0
- package/images/nuclear-mcp-football.webp +0 -0
- package/images/skills-enabling-mcp-tools.png +0 -0
- package/images/the-skill-dealer.webp +0 -0
- package/images/tool-cli-grep.png +0 -0
- package/mise.toml +0 -53
- package/scripts/docker-entrypoint.sh +0 -29
- package/scripts/test-echo-server.mjs +0 -28
- package/scripts/test-mcp-integration.mjs +0 -59
- package/scripts/test-tool-cli-smoke.mjs +0 -93
- package/src/code-mode/code-mode-integration.test.ts +0 -135
- package/src/code-mode/eligibility.test.ts +0 -60
- package/src/code-mode/eligibility.ts +0 -16
- package/src/code-mode/executor.test.ts +0 -168
- package/src/code-mode/executor.ts +0 -163
- package/src/code-mode/index.ts +0 -208
- package/src/code-mode/tools.ts +0 -110
- package/src/code-mode/type-hints.test.ts +0 -187
- package/src/code-mode/type-hints.ts +0 -249
- package/src/docker-e2e.test.ts +0 -195
- package/src/docker-e2e.ts +0 -277
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -179
- package/src/mcp/client-manager.test.ts +0 -250
- package/src/mcp/client-manager.ts +0 -199
- package/src/mcp/config-loader.test.ts +0 -115
- package/src/mcp/config-loader.ts +0 -46
- package/src/mcp/config.ts +0 -40
- package/src/skills/discover.ts +0 -81
- package/src/skills/format.test.ts +0 -70
- package/src/skills/format.ts +0 -42
- package/src/skills/load-skill-tool.ts +0 -136
- package/src/skills/mcp-tool-proxy.ts +0 -143
- package/src/skills/skill-integration.test.ts +0 -139
- package/src/skills/skill-registry.test.ts +0 -83
- package/src/skills/skill-registry.ts +0 -69
- package/src/test-servers/weather-server.ts +0 -171
- package/src/tool-cli/format.test.ts +0 -33
- package/tsconfig.json +0 -21
package/docs/code-mode.md
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
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
|
package/docs/tool-cli.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
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 <server> <tool> '{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).
|
package/eslint.config.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import eslint from "@eslint/js";
|
|
2
|
-
import tseslint from "typescript-eslint";
|
|
3
|
-
import eslintConfigPrettier from "eslint-config-prettier";
|
|
4
|
-
|
|
5
|
-
export default tseslint.config(
|
|
6
|
-
eslint.configs.recommended,
|
|
7
|
-
...tseslint.configs.strict,
|
|
8
|
-
...tseslint.configs.stylistic,
|
|
9
|
-
eslintConfigPrettier,
|
|
10
|
-
{
|
|
11
|
-
languageOptions: {
|
|
12
|
-
parserOptions: {
|
|
13
|
-
projectService: true,
|
|
14
|
-
tsconfigRootDir: import.meta.dirname,
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
rules: {
|
|
18
|
-
"@typescript-eslint/no-unused-vars": [
|
|
19
|
-
"error",
|
|
20
|
-
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
21
|
-
],
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
ignores: ["dist/", "node_modules/", "*.js", "scripts/"],
|
|
26
|
-
},
|
|
27
|
-
);
|
package/images/banner.webp
DELETED
|
Binary file
|
package/images/code-c-maude.webp
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/images/tool-cli-grep.png
DELETED
|
Binary file
|
package/mise.toml
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
[tools]
|
|
2
|
-
node = "22"
|
|
3
|
-
|
|
4
|
-
[tasks.build]
|
|
5
|
-
description = "Build TypeScript"
|
|
6
|
-
run = "npx tsc"
|
|
7
|
-
|
|
8
|
-
[tasks.dev]
|
|
9
|
-
description = "Watch mode"
|
|
10
|
-
run = "npx tsc --watch"
|
|
11
|
-
|
|
12
|
-
[tasks.test]
|
|
13
|
-
description = "Run tests"
|
|
14
|
-
run = "npx vitest --run"
|
|
15
|
-
|
|
16
|
-
[tasks.check]
|
|
17
|
-
description = "Type-check without emitting"
|
|
18
|
-
run = "npx tsc --noEmit"
|
|
19
|
-
|
|
20
|
-
[tasks.lint]
|
|
21
|
-
description = "Lint TypeScript files"
|
|
22
|
-
run = "npx eslint ."
|
|
23
|
-
|
|
24
|
-
[tasks.format]
|
|
25
|
-
description = "Format code with Prettier"
|
|
26
|
-
run = "npx prettier --write ."
|
|
27
|
-
|
|
28
|
-
[tasks."format:check"]
|
|
29
|
-
description = "Check code formatting"
|
|
30
|
-
run = "npx prettier --check ."
|
|
31
|
-
|
|
32
|
-
[tasks.start]
|
|
33
|
-
description = "Run the built project"
|
|
34
|
-
run = "node dist/index.js"
|
|
35
|
-
|
|
36
|
-
[tasks.docker-build]
|
|
37
|
-
description = "Build the Docker e2e image"
|
|
38
|
-
run = "docker build -t mcpi-ext-e2e ."
|
|
39
|
-
|
|
40
|
-
[tasks."test:integration"]
|
|
41
|
-
description = "Run MCP integration smoke test"
|
|
42
|
-
depends = ["build"]
|
|
43
|
-
run = "node scripts/test-mcp-integration.mjs"
|
|
44
|
-
|
|
45
|
-
[tasks."test:tool-cli"]
|
|
46
|
-
description = "Run tool-cli smoke test (requires build)"
|
|
47
|
-
depends = ["build"]
|
|
48
|
-
run = "node scripts/test-tool-cli-smoke.mjs"
|
|
49
|
-
|
|
50
|
-
[tasks."test:skill-integration"]
|
|
51
|
-
description = "Run skill discovery integration test (requires build)"
|
|
52
|
-
depends = ["build"]
|
|
53
|
-
run = "npx vitest run src/skills/skill-integration.test.ts"
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
if [ -z "${GITHUB_TOKEN:-}" ]; then
|
|
5
|
-
echo "ERROR: GITHUB_TOKEN environment variable is required" >&2
|
|
6
|
-
exit 1
|
|
7
|
-
fi
|
|
8
|
-
|
|
9
|
-
if [ $# -eq 0 ]; then
|
|
10
|
-
echo "ERROR: No task provided. Pass the task as arguments." >&2
|
|
11
|
-
echo "Usage: docker run -e GITHUB_TOKEN=... IMAGE \"your task here\"" >&2
|
|
12
|
-
exit 1
|
|
13
|
-
fi
|
|
14
|
-
|
|
15
|
-
TASK="$*"
|
|
16
|
-
|
|
17
|
-
PI_ARGS=(
|
|
18
|
-
-e /app/dist/index.js
|
|
19
|
-
--provider github-copilot
|
|
20
|
-
--mode json
|
|
21
|
-
-p
|
|
22
|
-
--no-session
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
if [ -n "${PI_MODEL:-}" ]; then
|
|
26
|
-
PI_ARGS+=(--model "$PI_MODEL")
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
exec npx pi "${PI_ARGS[@]}" "$TASK"
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Minimal MCP server for integration testing.
|
|
4
|
-
* Exposes a single "echo" tool that returns its input.
|
|
5
|
-
*/
|
|
6
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
|
|
10
|
-
const server = new McpServer({
|
|
11
|
-
name: "test-echo-server",
|
|
12
|
-
version: "0.1.0",
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
server.registerTool(
|
|
16
|
-
"echo",
|
|
17
|
-
{
|
|
18
|
-
description: "Echoes back the input message",
|
|
19
|
-
inputSchema: { message: z.string() },
|
|
20
|
-
annotations: { readOnlyHint: true },
|
|
21
|
-
},
|
|
22
|
-
async ({ message }) => ({
|
|
23
|
-
content: [{ type: "text", text: message }],
|
|
24
|
-
}),
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
const transport = new StdioServerTransport();
|
|
28
|
-
await server.connect(transport);
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Integration smoke test: connect to the test-echo-server via stdio,
|
|
4
|
-
* discover tools, call the echo tool, disconnect, exit 0.
|
|
5
|
-
*/
|
|
6
|
-
import { McpClientManager } from "../dist/mcp/client-manager.js";
|
|
7
|
-
|
|
8
|
-
const manager = new McpClientManager();
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
await manager.connectAll(
|
|
12
|
-
{
|
|
13
|
-
mcpServers: {
|
|
14
|
-
"test-echo": {
|
|
15
|
-
type: "stdio",
|
|
16
|
-
command: "node",
|
|
17
|
-
args: ["scripts/test-echo-server.mjs"],
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
(msg) => console.log(msg),
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
const servers = manager.getConnectedServers();
|
|
25
|
-
if (servers.length !== 1) {
|
|
26
|
-
throw new Error(`Expected 1 server, got ${servers.length}`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const tools = manager.getTools();
|
|
30
|
-
console.log(`Discovered ${tools.length} tool(s): ${tools.map((t) => t.name).join(", ")}`);
|
|
31
|
-
|
|
32
|
-
const echoTool = tools.find((t) => t.name === "echo");
|
|
33
|
-
if (!echoTool) {
|
|
34
|
-
throw new Error('Expected to find "echo" tool');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Call the tool via the client
|
|
38
|
-
const client = manager.getClient("test-echo");
|
|
39
|
-
if (!client) throw new Error("No client for test-echo");
|
|
40
|
-
|
|
41
|
-
const result = await client.callTool({
|
|
42
|
-
name: "echo",
|
|
43
|
-
arguments: { message: "hello from integration test" },
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const text = result.content?.[0]?.text;
|
|
47
|
-
if (text !== "hello from integration test") {
|
|
48
|
-
throw new Error(`Unexpected echo result: ${JSON.stringify(result)}`);
|
|
49
|
-
}
|
|
50
|
-
console.log(`Echo tool returned: "${text}"`);
|
|
51
|
-
|
|
52
|
-
await manager.disconnectAll();
|
|
53
|
-
console.log("Integration test passed ✓");
|
|
54
|
-
process.exit(0);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
console.error("Integration test failed:", err);
|
|
57
|
-
await manager.disconnectAll().catch(() => {});
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Smoke test: start RPC server with the weather test server,
|
|
4
|
-
* exercise every tool-cli command via the CLI binary, then shut down.
|
|
5
|
-
*
|
|
6
|
-
* Tests the full wiring: ToolProvider → ToolCliServer → tool-cli CLI.
|
|
7
|
-
*/
|
|
8
|
-
import { McpClientManager } from "../dist/mcp/client-manager.js";
|
|
9
|
-
import { ToolCliServer } from "@sammorrowdrums/tool-cli/server";
|
|
10
|
-
import { execFile } from "node:child_process";
|
|
11
|
-
import { promisify } from "node:util";
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
|
|
15
|
-
const execFileAsync = promisify(execFile);
|
|
16
|
-
|
|
17
|
-
// Resolve the tool-cli binary from node_modules/.bin
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
const CLI = join(__dirname, "..", "node_modules", ".bin", "tool-cli");
|
|
20
|
-
|
|
21
|
-
const manager = new McpClientManager();
|
|
22
|
-
let server;
|
|
23
|
-
|
|
24
|
-
async function run(...args) {
|
|
25
|
-
const { stdout } = await execFileAsync("node", [CLI, ...args], {
|
|
26
|
-
encoding: "utf-8",
|
|
27
|
-
env: {
|
|
28
|
-
...process.env,
|
|
29
|
-
TOOL_CLI_PORT: String(server.getPort()),
|
|
30
|
-
TOOL_CLI_TOKEN: token,
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
console.log(`$ tool-cli ${args.join(" ")}`);
|
|
34
|
-
console.log(stdout);
|
|
35
|
-
return stdout;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let token;
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
await manager.connectOne("weather", {
|
|
42
|
-
type: "stdio",
|
|
43
|
-
command: "node",
|
|
44
|
-
args: ["dist/test-servers/weather-stdio.js"],
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Bridge McpClientManager to ToolProvider
|
|
48
|
-
const provider = {
|
|
49
|
-
getServerNames: () => manager.getConnectedServers(),
|
|
50
|
-
getTools: (s) => manager.getToolsForServer(s),
|
|
51
|
-
async callTool(s, tool, args) {
|
|
52
|
-
const client = manager.getClient(s);
|
|
53
|
-
if (!client) throw new Error(`No client for "${s}"`);
|
|
54
|
-
const result = await client.callTool({ name: tool, arguments: args });
|
|
55
|
-
return {
|
|
56
|
-
content: result.content,
|
|
57
|
-
isError: result.isError === true ? true : undefined,
|
|
58
|
-
structuredContent: result.structuredContent,
|
|
59
|
-
};
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
server = new ToolCliServer(provider);
|
|
64
|
-
const startResult = await server.start(console.log);
|
|
65
|
-
token = startResult.token;
|
|
66
|
-
|
|
67
|
-
// --help: list servers
|
|
68
|
-
const help = await run("--help");
|
|
69
|
-
if (!help.includes("weather")) throw new Error("--help missing weather server");
|
|
70
|
-
|
|
71
|
-
// <server>: list tools
|
|
72
|
-
const tools = await run("weather");
|
|
73
|
-
if (!tools.includes("check_weather_for_city"))
|
|
74
|
-
throw new Error("tool list missing check_weather_for_city");
|
|
75
|
-
|
|
76
|
-
// <server> <tool>: describe
|
|
77
|
-
const desc = await run("weather", "check_weather_for_city");
|
|
78
|
-
if (!desc.includes("city")) throw new Error("describe missing city param");
|
|
79
|
-
|
|
80
|
-
// <server> <tool> <args>: call
|
|
81
|
-
const result = await run("weather", "check_weather_for_city", '{"city":"London"}');
|
|
82
|
-
if (!result.includes("London")) throw new Error("call result missing London");
|
|
83
|
-
|
|
84
|
-
await server.stop();
|
|
85
|
-
await manager.disconnectAll();
|
|
86
|
-
console.log("Smoke test passed ✓");
|
|
87
|
-
process.exit(0);
|
|
88
|
-
} catch (err) {
|
|
89
|
-
console.error("Smoke test failed:", err);
|
|
90
|
-
await server?.stop().catch(() => {});
|
|
91
|
-
await manager.disconnectAll().catch(() => {});
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import { McpClientManager } from "../mcp/client-manager.js";
|
|
3
|
-
import { CodeModeManager } from "./index.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Integration test: connect to the test weather server, verify code mode
|
|
7
|
-
* eligibility, type hint generation, and sandboxed execution with real
|
|
8
|
-
* MCP tool calls returning structuredContent.
|
|
9
|
-
*/
|
|
10
|
-
describe("code mode integration (weather server)", () => {
|
|
11
|
-
const manager = new McpClientManager();
|
|
12
|
-
const codeMode = new CodeModeManager();
|
|
13
|
-
|
|
14
|
-
beforeAll(async () => {
|
|
15
|
-
await manager.connectAll({
|
|
16
|
-
mcpServers: {
|
|
17
|
-
"test-weather": {
|
|
18
|
-
type: "stdio",
|
|
19
|
-
command: "node",
|
|
20
|
-
args: ["dist/test-servers/weather-stdio.js"],
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
codeMode.initialize(manager);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterAll(async () => {
|
|
29
|
-
await manager.disconnectAll();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("discovers eligible tools with readOnlyHint + outputSchema", () => {
|
|
33
|
-
const eligible = codeMode.getEligibleTools();
|
|
34
|
-
expect(eligible.length).toBeGreaterThanOrEqual(2);
|
|
35
|
-
const names = eligible.map((t) => t.name).sort();
|
|
36
|
-
expect(names).toContain("check_weather_for_city");
|
|
37
|
-
expect(names).toContain("check_weekly_forecast_for_city");
|
|
38
|
-
expect(names).toContain("echo");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("generates type hints for eligible tools", () => {
|
|
42
|
-
const hints = codeMode.getTypeHints();
|
|
43
|
-
expect(hints).toContain("declare const codemode");
|
|
44
|
-
expect(hints).toContain("check_weather_for_city");
|
|
45
|
-
expect(hints).toContain("check_weekly_forecast_for_city");
|
|
46
|
-
expect(hints).toContain("echo");
|
|
47
|
-
expect(hints).toContain("listTools");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("reports as active", () => {
|
|
51
|
-
expect(codeMode.isActive).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("generates system prompt section", () => {
|
|
55
|
-
const section = codeMode.formatSystemPromptSection();
|
|
56
|
-
expect(section).toContain("<code_mode>");
|
|
57
|
-
expect(section).toContain("code_search");
|
|
58
|
-
expect(section).toContain("code_execute");
|
|
59
|
-
expect(section).toContain("declare const codemode");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("executes code that calls a single tool", async () => {
|
|
63
|
-
const result = await codeMode.executeCode(`
|
|
64
|
-
const weather = await codemode.check_weather_for_city({ city: "Tokyo" });
|
|
65
|
-
return weather;
|
|
66
|
-
`);
|
|
67
|
-
|
|
68
|
-
expect(result.error).toBeUndefined();
|
|
69
|
-
expect(result.result).toHaveProperty("temperature", 26);
|
|
70
|
-
expect(result.result).toHaveProperty("conditions", "Sunny");
|
|
71
|
-
expect(result.result).toHaveProperty("humidity", 55);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("executes code that chains multiple tool calls", async () => {
|
|
75
|
-
const result = await codeMode.executeCode(`
|
|
76
|
-
const cities = ["London", "Tokyo"];
|
|
77
|
-
const results = [];
|
|
78
|
-
for (const city of cities) {
|
|
79
|
-
const w = await codemode.check_weather_for_city({ city });
|
|
80
|
-
results.push({ city, temp: w.temperature });
|
|
81
|
-
}
|
|
82
|
-
return results;
|
|
83
|
-
`);
|
|
84
|
-
|
|
85
|
-
expect(result.error).toBeUndefined();
|
|
86
|
-
expect(result.result).toEqual([
|
|
87
|
-
{ city: "London", temp: 14 },
|
|
88
|
-
{ city: "Tokyo", temp: 26 },
|
|
89
|
-
]);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("executes code that uses listTools", async () => {
|
|
93
|
-
const result = await codeMode.executeCode(`
|
|
94
|
-
const tools = await codemode.listTools();
|
|
95
|
-
return tools.sort();
|
|
96
|
-
`);
|
|
97
|
-
|
|
98
|
-
expect(result.error).toBeUndefined();
|
|
99
|
-
const names = result.result as string[];
|
|
100
|
-
expect(names).toContain("check_weather_for_city");
|
|
101
|
-
expect(names).toContain("echo");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("executes code that calls echo tool", async () => {
|
|
105
|
-
const result = await codeMode.executeCode(`
|
|
106
|
-
const r = await codemode.echo({ message: "hello code mode" });
|
|
107
|
-
return r;
|
|
108
|
-
`);
|
|
109
|
-
|
|
110
|
-
expect(result.error).toBeUndefined();
|
|
111
|
-
expect(result.result).toEqual({ echo: "hello code mode" });
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("handles tool errors gracefully", async () => {
|
|
115
|
-
const result = await codeMode.executeCode(`
|
|
116
|
-
const w = await codemode.check_weather_for_city({ city: "Atlantis" });
|
|
117
|
-
return w;
|
|
118
|
-
`);
|
|
119
|
-
|
|
120
|
-
// Atlantis isn't in the data — server returns text-only content (no structuredContent)
|
|
121
|
-
// The executor should still return something (parsed text or raw string)
|
|
122
|
-
expect(result.error).toBeUndefined();
|
|
123
|
-
expect(result.result).toBeDefined();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("search mode works for tool discovery", async () => {
|
|
127
|
-
const result = await codeMode.searchTools(`
|
|
128
|
-
const tools = await codemode.listTools();
|
|
129
|
-
return tools;
|
|
130
|
-
`);
|
|
131
|
-
|
|
132
|
-
expect(result.error).toBeUndefined();
|
|
133
|
-
expect(Array.isArray(result.result)).toBe(true);
|
|
134
|
-
});
|
|
135
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type { McpTool } from "../mcp/index.js";
|
|
3
|
-
import { isEligibleForCodeMode } from "./eligibility.js";
|
|
4
|
-
|
|
5
|
-
function makeTool(overrides: Partial<McpTool> = {}): McpTool {
|
|
6
|
-
return {
|
|
7
|
-
name: "test_tool",
|
|
8
|
-
description: "A test tool",
|
|
9
|
-
inputSchema: { type: "object", properties: {} },
|
|
10
|
-
serverName: "test-server",
|
|
11
|
-
...overrides,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe("isEligibleForCodeMode", () => {
|
|
16
|
-
it("returns true when readOnlyHint is true and outputSchema is defined", () => {
|
|
17
|
-
const tool = makeTool({
|
|
18
|
-
annotations: { readOnlyHint: true },
|
|
19
|
-
outputSchema: { type: "object", properties: { result: { type: "string" } } },
|
|
20
|
-
});
|
|
21
|
-
expect(isEligibleForCodeMode(tool)).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("returns false when readOnlyHint is false", () => {
|
|
25
|
-
const tool = makeTool({
|
|
26
|
-
annotations: { readOnlyHint: false },
|
|
27
|
-
outputSchema: { type: "object", properties: {} },
|
|
28
|
-
});
|
|
29
|
-
expect(isEligibleForCodeMode(tool)).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("returns false when readOnlyHint is missing", () => {
|
|
33
|
-
const tool = makeTool({
|
|
34
|
-
annotations: {},
|
|
35
|
-
outputSchema: { type: "object", properties: {} },
|
|
36
|
-
});
|
|
37
|
-
expect(isEligibleForCodeMode(tool)).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("returns false when annotations is undefined", () => {
|
|
41
|
-
const tool = makeTool({
|
|
42
|
-
annotations: undefined,
|
|
43
|
-
outputSchema: { type: "object", properties: {} },
|
|
44
|
-
});
|
|
45
|
-
expect(isEligibleForCodeMode(tool)).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("returns false when outputSchema is missing", () => {
|
|
49
|
-
const tool = makeTool({
|
|
50
|
-
annotations: { readOnlyHint: true },
|
|
51
|
-
outputSchema: undefined,
|
|
52
|
-
});
|
|
53
|
-
expect(isEligibleForCodeMode(tool)).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("returns false when both readOnlyHint and outputSchema are missing", () => {
|
|
57
|
-
const tool = makeTool();
|
|
58
|
-
expect(isEligibleForCodeMode(tool)).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { McpClientManager, McpTool } from "../mcp/index.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A tool is eligible for code mode when:
|
|
5
|
-
* 1. annotations.readOnlyHint === true — does not modify its environment
|
|
6
|
-
* 2. outputSchema is defined — results are typed and parseable
|
|
7
|
-
*/
|
|
8
|
-
export function isEligibleForCodeMode(tool: McpTool): boolean {
|
|
9
|
-
const annotations = tool.annotations as { readOnlyHint?: boolean } | undefined;
|
|
10
|
-
return annotations?.readOnlyHint === true && tool.outputSchema != null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Get all tools eligible for code mode across all connected MCP servers. */
|
|
14
|
-
export function getEligibleTools(mcpManager: McpClientManager): McpTool[] {
|
|
15
|
-
return mcpManager.getTools().filter(isEligibleForCodeMode);
|
|
16
|
-
}
|