@sammorrowdrums/mcpi-ext 0.1.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/README.md +109 -139
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/tool-cli/index.d.ts +2 -2
- package/dist/tool-cli/index.d.ts.map +1 -1
- package/dist/tool-cli/index.js +1 -2
- package/dist/tool-cli/index.js.map +1 -1
- package/package.json +9 -7
- 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/eslint.config.js +0 -27
- package/images/banner.webp +0 -0
- package/images/code-c-maude.webp +0 -0
- package/images/nuclear-mcp-football.webp +0 -0
- package/images/the-skill-dealer.webp +0 -0
- package/mise.toml +0 -65
- 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 -62
- 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 -158
- 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/mcp/index.ts +0 -4
- 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/index.ts +0 -9
- 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/test-servers/weather-stdio.ts +0 -11
- package/src/tool-cli/cli.ts +0 -232
- package/src/tool-cli/constants.ts +0 -15
- package/src/tool-cli/format.test.ts +0 -33
- package/src/tool-cli/format.ts +0 -56
- package/src/tool-cli/index.ts +0 -3
- package/src/tool-cli/rpc-client.ts +0 -39
- package/src/tool-cli/rpc-server.test.ts +0 -193
- package/src/tool-cli/rpc-server.ts +0 -287
- package/tsconfig.json +0 -21
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
|
package/mise.toml
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
[tools]
|
|
2
|
-
node = "22"
|
|
3
|
-
|
|
4
|
-
[tasks.build]
|
|
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
|
-
]
|
|
19
|
-
|
|
20
|
-
[tasks.dev]
|
|
21
|
-
description = "Watch mode"
|
|
22
|
-
run = "npx tsc --watch"
|
|
23
|
-
|
|
24
|
-
[tasks.test]
|
|
25
|
-
description = "Run tests"
|
|
26
|
-
run = "npx vitest --run"
|
|
27
|
-
|
|
28
|
-
[tasks.check]
|
|
29
|
-
description = "Type-check without emitting"
|
|
30
|
-
run = "npx tsc --noEmit"
|
|
31
|
-
|
|
32
|
-
[tasks.lint]
|
|
33
|
-
description = "Lint TypeScript files"
|
|
34
|
-
run = "npx eslint ."
|
|
35
|
-
|
|
36
|
-
[tasks.format]
|
|
37
|
-
description = "Format code with Prettier"
|
|
38
|
-
run = "npx prettier --write ."
|
|
39
|
-
|
|
40
|
-
[tasks."format:check"]
|
|
41
|
-
description = "Check code formatting"
|
|
42
|
-
run = "npx prettier --check ."
|
|
43
|
-
|
|
44
|
-
[tasks.start]
|
|
45
|
-
description = "Run the built project"
|
|
46
|
-
run = "node dist/index.js"
|
|
47
|
-
|
|
48
|
-
[tasks.docker-build]
|
|
49
|
-
description = "Build the Docker e2e image"
|
|
50
|
-
run = "docker build -t mcpi-ext-e2e ."
|
|
51
|
-
|
|
52
|
-
[tasks."test:integration"]
|
|
53
|
-
description = "Run MCP integration smoke test"
|
|
54
|
-
depends = ["build"]
|
|
55
|
-
run = "node scripts/test-mcp-integration.mjs"
|
|
56
|
-
|
|
57
|
-
[tasks."test:tool-cli"]
|
|
58
|
-
description = "Run tool-cli smoke test (requires build)"
|
|
59
|
-
depends = ["build"]
|
|
60
|
-
run = "node scripts/test-tool-cli-smoke.mjs"
|
|
61
|
-
|
|
62
|
-
[tasks."test:skill-integration"]
|
|
63
|
-
description = "Run skill discovery integration test (requires build)"
|
|
64
|
-
depends = ["build"]
|
|
65
|
-
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,62 +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, then shut down.
|
|
5
|
-
*/
|
|
6
|
-
import { McpClientManager } from "../dist/mcp/client-manager.js";
|
|
7
|
-
import { ToolCliRpcServer } from "../dist/tool-cli/rpc-server.js";
|
|
8
|
-
import { execFile } from "node:child_process";
|
|
9
|
-
import { promisify } from "node:util";
|
|
10
|
-
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
|
-
const CLI = "dist/tool-cli/cli.js";
|
|
13
|
-
const manager = new McpClientManager();
|
|
14
|
-
let rpc;
|
|
15
|
-
|
|
16
|
-
async function run(...args) {
|
|
17
|
-
const { stdout } = await execFileAsync("node", [CLI, ...args], {
|
|
18
|
-
encoding: "utf-8",
|
|
19
|
-
env: { ...process.env, TOOL_CLI_PORT: String(rpc.getPort()) },
|
|
20
|
-
});
|
|
21
|
-
console.log(`$ tool-cli ${args.join(" ")}`);
|
|
22
|
-
console.log(stdout);
|
|
23
|
-
return stdout;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
await manager.connectOne("weather", {
|
|
28
|
-
type: "stdio",
|
|
29
|
-
command: "node",
|
|
30
|
-
args: ["dist/test-servers/weather-stdio.js"],
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
rpc = new ToolCliRpcServer(manager, 0);
|
|
34
|
-
await rpc.start(console.log);
|
|
35
|
-
|
|
36
|
-
// --help: list servers
|
|
37
|
-
const help = await run("--help");
|
|
38
|
-
if (!help.includes("weather")) throw new Error("--help missing weather server");
|
|
39
|
-
|
|
40
|
-
// <server>: list tools
|
|
41
|
-
const tools = await run("weather");
|
|
42
|
-
if (!tools.includes("check_weather_for_city"))
|
|
43
|
-
throw new Error("tool list missing check_weather_for_city");
|
|
44
|
-
|
|
45
|
-
// <server> <tool>: describe
|
|
46
|
-
const desc = await run("weather", "check_weather_for_city");
|
|
47
|
-
if (!desc.includes("city")) throw new Error("describe missing city param");
|
|
48
|
-
|
|
49
|
-
// <server> <tool> <args>: call
|
|
50
|
-
const result = await run("weather", "check_weather_for_city", '{"city":"London"}');
|
|
51
|
-
if (!result.includes("London")) throw new Error("call result missing London");
|
|
52
|
-
|
|
53
|
-
await rpc.stop();
|
|
54
|
-
await manager.disconnectAll();
|
|
55
|
-
console.log("Smoke test passed ✓");
|
|
56
|
-
process.exit(0);
|
|
57
|
-
} catch (err) {
|
|
58
|
-
console.error("Smoke test failed:", err);
|
|
59
|
-
await rpc?.stop().catch(() => {});
|
|
60
|
-
await manager.disconnectAll().catch(() => {});
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
@@ -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
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { executeInSandbox, normalizeCode } from "./executor.js";
|
|
3
|
-
|
|
4
|
-
describe("normalizeCode", () => {
|
|
5
|
-
it("strips markdown code fences", () => {
|
|
6
|
-
expect(normalizeCode("```js\nconst x = 1;\n```")).toBe("const x = 1;");
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("strips typescript fences", () => {
|
|
10
|
-
expect(normalizeCode("```typescript\nconst x = 1;\n```")).toBe("const x = 1;");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("wraps arrow function in invocation", () => {
|
|
14
|
-
expect(normalizeCode("async () => { return 42; }")).toBe(
|
|
15
|
-
"return (async () => { return 42; })();",
|
|
16
|
-
);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("wraps function declaration in invocation", () => {
|
|
20
|
-
const code = "async function main() { return 42; }";
|
|
21
|
-
const result = normalizeCode(code);
|
|
22
|
-
expect(result).toContain("async function main()");
|
|
23
|
-
expect(result).toContain("return main();");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("strips export default", () => {
|
|
27
|
-
const result = normalizeCode("export default async () => { return 42; }");
|
|
28
|
-
expect(result).toContain("return (async () => { return 42; })();");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("leaves bare code as-is", () => {
|
|
32
|
-
expect(normalizeCode("const x = 1;\nreturn x;")).toBe("const x = 1;\nreturn x;");
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe("executeInSandbox", () => {
|
|
37
|
-
it("executes simple code and returns result", async () => {
|
|
38
|
-
const result = await executeInSandbox("return 2 + 2;", [], async () => ({}), {
|
|
39
|
-
timeoutMs: 5000,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
expect(result.result).toBe(4);
|
|
43
|
-
expect(result.error).toBeUndefined();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("captures console.log output", async () => {
|
|
47
|
-
const result = await executeInSandbox(
|
|
48
|
-
'console.log("hello", "world"); return 1;',
|
|
49
|
-
[],
|
|
50
|
-
async () => ({}),
|
|
51
|
-
{ timeoutMs: 5000 },
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
expect(result.result).toBe(1);
|
|
55
|
-
expect(result.logs).toContain("hello world");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("dispatches tool calls through the callback", async () => {
|
|
59
|
-
const dispatched: { name: string; args: Record<string, unknown> }[] = [];
|
|
60
|
-
|
|
61
|
-
const dispatch = async (name: string, args: Record<string, unknown>) => {
|
|
62
|
-
dispatched.push({ name, args });
|
|
63
|
-
return { results: ["doc1", "doc2"] };
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const code = `
|
|
67
|
-
const result = await codemode.search_docs({ query: "test" });
|
|
68
|
-
return result;
|
|
69
|
-
`;
|
|
70
|
-
|
|
71
|
-
const result = await executeInSandbox(code, ["search_docs"], dispatch, { timeoutMs: 5000 });
|
|
72
|
-
|
|
73
|
-
expect(result.error).toBeUndefined();
|
|
74
|
-
expect(result.result).toEqual({ results: ["doc1", "doc2"] });
|
|
75
|
-
expect(dispatched).toHaveLength(1);
|
|
76
|
-
expect(dispatched[0].name).toBe("search_docs");
|
|
77
|
-
expect(dispatched[0].args).toEqual({ query: "test" });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("handles tools with special characters in names", async () => {
|
|
81
|
-
const dispatched: string[] = [];
|
|
82
|
-
|
|
83
|
-
const dispatch = async (name: string) => {
|
|
84
|
-
dispatched.push(name);
|
|
85
|
-
return { ok: true };
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const code = `
|
|
89
|
-
const result = await codemode.github_list_repos({});
|
|
90
|
-
return result;
|
|
91
|
-
`;
|
|
92
|
-
|
|
93
|
-
const result = await executeInSandbox(code, ["github.list-repos"], dispatch, {
|
|
94
|
-
timeoutMs: 5000,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
expect(result.error).toBeUndefined();
|
|
98
|
-
expect(dispatched).toContain("github.list-repos");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("returns error for invalid code", async () => {
|
|
102
|
-
const result = await executeInSandbox("throw new Error('boom');", [], async () => ({}), {
|
|
103
|
-
timeoutMs: 5000,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
expect(result.error).toContain("boom");
|
|
107
|
-
expect(result.result).toBeUndefined();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("enforces timeout", async () => {
|
|
111
|
-
const result = await executeInSandbox("while(true) {}", [], async () => ({}), {
|
|
112
|
-
timeoutMs: 100,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
expect(result.error).toBeDefined();
|
|
116
|
-
expect(result.result).toBeUndefined();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("chains multiple tool calls", async () => {
|
|
120
|
-
const calls: string[] = [];
|
|
121
|
-
|
|
122
|
-
const dispatch = async (name: string, args: Record<string, unknown>) => {
|
|
123
|
-
calls.push(name);
|
|
124
|
-
if (name === "list_items") return { items: ["a", "b", "c"] };
|
|
125
|
-
if (name === "get_details") return { detail: `info for ${args.id}` };
|
|
126
|
-
return {};
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const code = `
|
|
130
|
-
const { items } = await codemode.list_items({});
|
|
131
|
-
const details = [];
|
|
132
|
-
for (const id of items) {
|
|
133
|
-
const d = await codemode.get_details({ id });
|
|
134
|
-
details.push(d.detail);
|
|
135
|
-
}
|
|
136
|
-
return details;
|
|
137
|
-
`;
|
|
138
|
-
|
|
139
|
-
const result = await executeInSandbox(code, ["list_items", "get_details"], dispatch, {
|
|
140
|
-
timeoutMs: 5000,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
expect(result.error).toBeUndefined();
|
|
144
|
-
expect(result.result).toEqual(["info for a", "info for b", "info for c"]);
|
|
145
|
-
expect(calls).toEqual(["list_items", "get_details", "get_details", "get_details"]);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("prevents imports", async () => {
|
|
149
|
-
const code = 'import("fs")';
|
|
150
|
-
const result = await executeInSandbox(code, [], async () => ({}), { timeoutMs: 5000 });
|
|
151
|
-
|
|
152
|
-
expect(result.error).toBeDefined();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("listTools returns available tools", async () => {
|
|
156
|
-
const code = `
|
|
157
|
-
const tools = await codemode.listTools();
|
|
158
|
-
return tools;
|
|
159
|
-
`;
|
|
160
|
-
|
|
161
|
-
const result = await executeInSandbox(code, ["search", "execute"], async () => ({}), {
|
|
162
|
-
timeoutMs: 5000,
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
expect(result.error).toBeUndefined();
|
|
166
|
-
expect(result.result).toEqual(["search", "execute"]);
|
|
167
|
-
});
|
|
168
|
-
});
|