@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
|
@@ -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
|
-
});
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import ivm from "isolated-vm";
|
|
2
|
-
import { sanitizeToolName } from "./type-hints.js";
|
|
3
|
-
|
|
4
|
-
/** Result of code execution. */
|
|
5
|
-
export interface ExecuteResult {
|
|
6
|
-
result: unknown;
|
|
7
|
-
error?: string;
|
|
8
|
-
logs: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** A function the sandbox can call to invoke an MCP tool. */
|
|
12
|
-
export type ToolDispatchFn = (toolName: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
13
|
-
|
|
14
|
-
export interface ExecutorOptions {
|
|
15
|
-
/** Memory limit in MB for the V8 isolate. Default: 128. */
|
|
16
|
-
memoryLimit?: number;
|
|
17
|
-
/** Execution timeout in milliseconds. Default: 30000. */
|
|
18
|
-
timeoutMs?: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const DEFAULT_MEMORY_LIMIT = 128;
|
|
22
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Execute model-generated JavaScript code in an isolated V8 sandbox.
|
|
26
|
-
*
|
|
27
|
-
* The code runs in a fresh `isolated-vm` isolate with:
|
|
28
|
-
* - Enforced memory limit (default 128MB)
|
|
29
|
-
* - Execution timeout (default 30s)
|
|
30
|
-
* - No access to Node.js APIs, filesystem, or network
|
|
31
|
-
* - Only access to provided tool dispatch functions via `codemode.*` proxy
|
|
32
|
-
*
|
|
33
|
-
* Tool calls are dispatched to the host via `Reference` callbacks —
|
|
34
|
-
* actual MCP tool execution happens outside the sandbox.
|
|
35
|
-
*/
|
|
36
|
-
export async function executeInSandbox(
|
|
37
|
-
code: string,
|
|
38
|
-
toolNames: string[],
|
|
39
|
-
dispatch: ToolDispatchFn,
|
|
40
|
-
options: ExecutorOptions = {},
|
|
41
|
-
): Promise<ExecuteResult> {
|
|
42
|
-
const memoryLimit = options.memoryLimit ?? DEFAULT_MEMORY_LIMIT;
|
|
43
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
44
|
-
|
|
45
|
-
const isolate = new ivm.Isolate({ memoryLimit });
|
|
46
|
-
try {
|
|
47
|
-
return await runInIsolate(isolate, code, toolNames, dispatch, timeoutMs);
|
|
48
|
-
} finally {
|
|
49
|
-
isolate.dispose();
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function runInIsolate(
|
|
54
|
-
isolate: ivm.Isolate,
|
|
55
|
-
code: string,
|
|
56
|
-
toolNames: string[],
|
|
57
|
-
dispatch: ToolDispatchFn,
|
|
58
|
-
timeoutMs: number,
|
|
59
|
-
): Promise<ExecuteResult> {
|
|
60
|
-
const ctx = await isolate.createContext();
|
|
61
|
-
const jail = ctx.global;
|
|
62
|
-
|
|
63
|
-
const logs: string[] = [];
|
|
64
|
-
|
|
65
|
-
// Inject console.log that captures to logs array
|
|
66
|
-
const logCallback = new ivm.Callback((...args: unknown[]) => {
|
|
67
|
-
logs.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
|
68
|
-
});
|
|
69
|
-
await jail.set("__log", logCallback);
|
|
70
|
-
|
|
71
|
-
// Inject tool dispatcher Reference (async callback)
|
|
72
|
-
const dispatchRef = new ivm.Reference(async (toolName: string, argsJson: string) => {
|
|
73
|
-
const args = JSON.parse(argsJson) as Record<string, unknown>;
|
|
74
|
-
const result = await dispatch(toolName, args);
|
|
75
|
-
return JSON.stringify(result === undefined ? null : result);
|
|
76
|
-
});
|
|
77
|
-
await jail.set("__dispatch", dispatchRef);
|
|
78
|
-
|
|
79
|
-
// Build the tool call helper and codemode proxy as setup code
|
|
80
|
-
const toolProxyEntries = toolNames
|
|
81
|
-
.map((name) => {
|
|
82
|
-
const safe = sanitizeToolName(name);
|
|
83
|
-
return ` ${safe}: async (args) => __callTool(${JSON.stringify(name)}, args)`;
|
|
84
|
-
})
|
|
85
|
-
.join(",\n");
|
|
86
|
-
|
|
87
|
-
const normalized = normalizeCode(code);
|
|
88
|
-
|
|
89
|
-
// Use ctx.eval with { promise: true } instead of compileModule,
|
|
90
|
-
// because module.evaluate() can resolve prematurely with multiple
|
|
91
|
-
// sequential async Reference.apply calls.
|
|
92
|
-
const wrappedCode = `
|
|
93
|
-
(async () => {
|
|
94
|
-
const console = { log: (...args) => __log(args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')) };
|
|
95
|
-
const __callTool = async (name, args) => {
|
|
96
|
-
const r = await __dispatch.apply(undefined, [name, JSON.stringify(args ?? {})], { arguments: { copy: true }, result: { promise: true, copy: true } });
|
|
97
|
-
return JSON.parse(r);
|
|
98
|
-
};
|
|
99
|
-
const codemode = {
|
|
100
|
-
listTools: async () => ${JSON.stringify(toolNames)},
|
|
101
|
-
describeTools: async (names) => "Use the typed codemode.toolName(args) methods instead.",
|
|
102
|
-
${toolProxyEntries}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const __userFn = async () => {
|
|
106
|
-
${normalized}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
return JSON.stringify({ value: await __userFn() });
|
|
110
|
-
})()
|
|
111
|
-
`;
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const rawResult = (await ctx.eval(wrappedCode, {
|
|
115
|
-
promise: true,
|
|
116
|
-
copy: true,
|
|
117
|
-
timeout: timeoutMs,
|
|
118
|
-
})) as string;
|
|
119
|
-
|
|
120
|
-
const parsed = JSON.parse(rawResult) as { value: unknown };
|
|
121
|
-
return { result: parsed.value, logs };
|
|
122
|
-
} catch (err) {
|
|
123
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
-
return { result: undefined, error: message, logs };
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Normalize model-generated code:
|
|
130
|
-
* - Strip markdown code fences
|
|
131
|
-
* - Handle arrow functions, function declarations, export default
|
|
132
|
-
* - Otherwise leave code as a statement block; callers must explicitly `return` a value
|
|
133
|
-
*/
|
|
134
|
-
export function normalizeCode(code: string): string {
|
|
135
|
-
let normalized = code.trim();
|
|
136
|
-
|
|
137
|
-
// Strip markdown code fences
|
|
138
|
-
normalized = normalized.replace(/^```(?:js|javascript|typescript|ts)?\s*\n?/i, "");
|
|
139
|
-
normalized = normalized.replace(/\n?```\s*$/i, "");
|
|
140
|
-
normalized = normalized.trim();
|
|
141
|
-
|
|
142
|
-
// If it's an arrow function or function expression, invoke it
|
|
143
|
-
if (/^(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/.test(normalized)) {
|
|
144
|
-
return `return (${normalized})();`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// If it's a function declaration, invoke it
|
|
148
|
-
if (/^(?:async\s+)?function\s+/.test(normalized)) {
|
|
149
|
-
const match = normalized.match(/^(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
150
|
-
if (match) {
|
|
151
|
-
return `${normalized}\nreturn ${match[1]}();`;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// If it starts with export default, strip the export default
|
|
156
|
-
if (/^export\s+default\s+/.test(normalized)) {
|
|
157
|
-
normalized = normalized.replace(/^export\s+default\s+/, "");
|
|
158
|
-
return `return (${normalized})();`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Otherwise, treat as a code block — return the last expression
|
|
162
|
-
return normalized;
|
|
163
|
-
}
|
package/src/code-mode/index.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import type { McpClientManager, McpTool } from "../mcp/index.js";
|
|
2
|
-
import { getEligibleTools } from "./eligibility.js";
|
|
3
|
-
import type { ExecuteResult, ExecutorOptions } from "./executor.js";
|
|
4
|
-
import { executeInSandbox } from "./executor.js";
|
|
5
|
-
import { createCodeExecuteTool, createCodeSearchTool } from "./tools.js";
|
|
6
|
-
import { generateTypeHints } from "./type-hints.js";
|
|
7
|
-
|
|
8
|
-
export { getEligibleTools, isEligibleForCodeMode } from "./eligibility.js";
|
|
9
|
-
export type { ExecuteResult, ExecutorOptions } from "./executor.js";
|
|
10
|
-
export { executeInSandbox, normalizeCode } from "./executor.js";
|
|
11
|
-
export { createCodeExecuteTool, createCodeSearchTool } from "./tools.js";
|
|
12
|
-
export { generateTypeHints, jsonSchemaToTypeString, sanitizeToolName } from "./type-hints.js";
|
|
13
|
-
|
|
14
|
-
export interface CodeModeManagerOptions extends ExecutorOptions {
|
|
15
|
-
/** Log function for status messages. */
|
|
16
|
-
log?: (msg: string) => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Orchestrates code mode: discovers eligible tools, generates type hints,
|
|
21
|
-
* and executes model-generated code in a sandbox with tool dispatch.
|
|
22
|
-
*/
|
|
23
|
-
export class CodeModeManager {
|
|
24
|
-
private mcpManager: McpClientManager | null = null;
|
|
25
|
-
private eligibleTools: McpTool[] = [];
|
|
26
|
-
private typeHints = "";
|
|
27
|
-
private options: CodeModeManagerOptions;
|
|
28
|
-
|
|
29
|
-
constructor(options: CodeModeManagerOptions = {}) {
|
|
30
|
-
this.options = options;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Initialize with MCP manager, discover eligible tools, generate type hints. */
|
|
34
|
-
initialize(mcpManager: McpClientManager): void {
|
|
35
|
-
this.mcpManager = mcpManager;
|
|
36
|
-
this.refresh();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Refresh eligible tools and type hints (call on tools/list_changed). */
|
|
40
|
-
refresh(): void {
|
|
41
|
-
if (!this.mcpManager) return;
|
|
42
|
-
this.eligibleTools = getEligibleTools(this.mcpManager);
|
|
43
|
-
this.typeHints = generateTypeHints(this.eligibleTools);
|
|
44
|
-
this.options.log?.(
|
|
45
|
-
`[code-mode] ${this.eligibleTools.length} eligible tool(s), ${this.typeHints.length} chars of type hints`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Get the type hints string for injection into system prompt. */
|
|
50
|
-
getTypeHints(): string {
|
|
51
|
-
return this.typeHints;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Get eligible tools. */
|
|
55
|
-
getEligibleTools(): McpTool[] {
|
|
56
|
-
return this.eligibleTools;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Whether code mode has any eligible tools. */
|
|
60
|
-
get isActive(): boolean {
|
|
61
|
-
return this.eligibleTools.length > 0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Execute code in search mode (tool catalog queries). */
|
|
65
|
-
async searchTools(code: string): Promise<ExecuteResult> {
|
|
66
|
-
return this.execute(code);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Execute code that chains MCP tool calls. */
|
|
70
|
-
async executeCode(code: string): Promise<ExecuteResult> {
|
|
71
|
-
return this.execute(code);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Create the Pi tool definitions for code_search and code_execute. */
|
|
75
|
-
createTools() {
|
|
76
|
-
return {
|
|
77
|
-
codeSearch: createCodeSearchTool(this),
|
|
78
|
-
codeExecute: createCodeExecuteTool(this),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Format a system prompt section for code mode. */
|
|
83
|
-
formatSystemPromptSection(): string {
|
|
84
|
-
if (!this.isActive) return "";
|
|
85
|
-
|
|
86
|
-
return [
|
|
87
|
-
"",
|
|
88
|
-
"<code_mode>",
|
|
89
|
-
"## Code mode",
|
|
90
|
-
"",
|
|
91
|
-
"`code_execute` runs vanilla JavaScript in a sandboxed V8 isolate. Use it for:",
|
|
92
|
-
"",
|
|
93
|
-
"1. **Arbitrary computation** — math, string manipulation, date arithmetic, data transformation,",
|
|
94
|
-
" or any calculation the user asks for. No MCP tools needed; plain JS works.",
|
|
95
|
-
"2. **Multi-tool aggregation** — counting, filtering, trending, or transforming results across",
|
|
96
|
-
" many tool calls. Write a loop inside one `code_execute` instead of making many separate tool calls.",
|
|
97
|
-
"3. **Pagination** — fetch batches in a loop until exhausted, then compute over the full dataset.",
|
|
98
|
-
"",
|
|
99
|
-
"Use `code_search` first to discover what tools are available before writing execution code.",
|
|
100
|
-
"",
|
|
101
|
-
"**Code mode tools are always available — you do not need to call `load_skill` first.**",
|
|
102
|
-
"",
|
|
103
|
-
"### Choosing the right approach",
|
|
104
|
-
"",
|
|
105
|
-
"- **Skill** (`load_skill`) — a curated workflow exists for this domain task (e.g., a GitHub skill for PR management).",
|
|
106
|
-
"- **tool-cli** — you need to discover what tools exist, or make a quick ad-hoc tool call.",
|
|
107
|
-
"- **Code mode** (`code_execute`) — you need computation: math, aggregation, looping over results,",
|
|
108
|
-
" data transformation, or chaining multiple tool calls with logic in between.",
|
|
109
|
-
"",
|
|
110
|
-
"Pick based on what the task needs, not a fixed order. A calculation goes straight to code mode;",
|
|
111
|
-
"a single lookup goes to a skill or tool-cli; exploration starts with tool-cli or `code_search`.",
|
|
112
|
-
"",
|
|
113
|
-
"**If unsure what's available**, start with `code_search` or `tool-cli --help` to see what you have.",
|
|
114
|
-
"After producing a result, verify it makes sense — run a quick sanity check or spot-check values.",
|
|
115
|
-
"",
|
|
116
|
-
"### How to write code",
|
|
117
|
-
"",
|
|
118
|
-
"Write vanilla JavaScript (not TypeScript, not Node.js). No `require`, `import`, `fetch`,",
|
|
119
|
-
"`fs`, `process`, or any Node.js/browser APIs. The only external API is the `codemode` namespace",
|
|
120
|
-
"for MCP tool calls (optional — pure computation works without it). Always `return` the final result.",
|
|
121
|
-
"",
|
|
122
|
-
"**Write ONE `code_execute` call that does the whole job.** Loops, comparisons, pagination,",
|
|
123
|
-
"and aggregation all happen inside a single execution.",
|
|
124
|
-
"",
|
|
125
|
-
"```javascript",
|
|
126
|
-
"// Pure computation — no tools needed",
|
|
127
|
-
"const factorial = (n) => n <= 1 ? 1 : n * factorial(n - 1);",
|
|
128
|
-
"return { result: factorial(20), formatted: factorial(20).toLocaleString() };",
|
|
129
|
-
"```",
|
|
130
|
-
"",
|
|
131
|
-
"```javascript",
|
|
132
|
-
"// Aggregate across paginated MCP tool results",
|
|
133
|
-
"const counts = {};",
|
|
134
|
-
"let page = 1;",
|
|
135
|
-
"while (true) {",
|
|
136
|
-
" const result = await codemode.list_items({ page, perPage: 100 });",
|
|
137
|
-
" for (const item of result.items) {",
|
|
138
|
-
" counts[item.category] = (counts[item.category] || 0) + 1;",
|
|
139
|
-
" }",
|
|
140
|
-
" if (result.items.length < 100) break;",
|
|
141
|
-
" page++;",
|
|
142
|
-
"}",
|
|
143
|
-
"return counts;",
|
|
144
|
-
"```",
|
|
145
|
-
"",
|
|
146
|
-
"### Available tools",
|
|
147
|
-
"",
|
|
148
|
-
"```typescript",
|
|
149
|
-
this.typeHints,
|
|
150
|
-
"```",
|
|
151
|
-
"</code_mode>",
|
|
152
|
-
].join("\n");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async execute(code: string): Promise<ExecuteResult> {
|
|
156
|
-
if (!this.mcpManager) {
|
|
157
|
-
return { result: undefined, error: "Code mode not initialized", logs: [] };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Refresh eligible tools in case MCP servers changed since initialization
|
|
161
|
-
this.refresh();
|
|
162
|
-
|
|
163
|
-
const toolNames = this.eligibleTools.map((t) => t.name);
|
|
164
|
-
|
|
165
|
-
const dispatch = async (toolName: string, args: Record<string, unknown>) => {
|
|
166
|
-
const tool = this.eligibleTools.find((t) => t.name === toolName);
|
|
167
|
-
if (!tool) {
|
|
168
|
-
throw new Error(`Tool "${toolName}" not found in code mode eligible tools`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const client = this.mcpManager?.getClient(tool.serverName);
|
|
172
|
-
if (!client) {
|
|
173
|
-
throw new Error(`MCP server "${tool.serverName}" is not connected`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const result = await client.callTool({ name: toolName, arguments: args });
|
|
177
|
-
|
|
178
|
-
// Prefer structuredContent (typed output) over raw content
|
|
179
|
-
if (result.structuredContent) {
|
|
180
|
-
return result.structuredContent;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Fall back to parsing text content
|
|
184
|
-
if (Array.isArray(result.content)) {
|
|
185
|
-
const textParts = result.content
|
|
186
|
-
.filter(
|
|
187
|
-
(c): c is { type: string; text: string } =>
|
|
188
|
-
typeof c === "object" && c !== null && "text" in c,
|
|
189
|
-
)
|
|
190
|
-
.map((c) => c.text);
|
|
191
|
-
|
|
192
|
-
const combined = textParts.join("\n");
|
|
193
|
-
try {
|
|
194
|
-
return JSON.parse(combined);
|
|
195
|
-
} catch {
|
|
196
|
-
return combined;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return result;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
return executeInSandbox(code, toolNames, dispatch, {
|
|
204
|
-
memoryLimit: this.options.memoryLimit,
|
|
205
|
-
timeoutMs: this.options.timeoutMs,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
package/src/code-mode/tools.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import type { AgentToolResult, ExtensionContext } from "@sammorrowdrums/mcpi";
|
|
2
|
-
import { Type, type Static } from "typebox";
|
|
3
|
-
import type { ExecuteResult } from "./executor.js";
|
|
4
|
-
import type { CodeModeManager } from "./index.js";
|
|
5
|
-
|
|
6
|
-
const CodeInput = Type.Object({
|
|
7
|
-
code: Type.String({
|
|
8
|
-
description:
|
|
9
|
-
"JavaScript code to execute. Use the `codemode` namespace to call tools (e.g. `codemode.search_docs({ query: 'test' })`). Always `return` your final result. Code runs in a sandbox with no access to filesystem, network, or Node.js APIs.",
|
|
10
|
-
}),
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
type CodeInputType = Static<typeof CodeInput>;
|
|
14
|
-
|
|
15
|
-
export interface CodeModeToolDetails {
|
|
16
|
-
executionMs: number;
|
|
17
|
-
logs: string[];
|
|
18
|
-
error?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Format an ExecuteResult into a tool response. Falls back to logs if result is undefined. */
|
|
22
|
-
function formatResult(
|
|
23
|
-
result: ExecuteResult,
|
|
24
|
-
executionMs: number,
|
|
25
|
-
errorPrefix: string,
|
|
26
|
-
): AgentToolResult<CodeModeToolDetails> {
|
|
27
|
-
if (result.error) {
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: "text", text: `${errorPrefix}: ${result.error}` }],
|
|
30
|
-
details: { executionMs, logs: result.logs, error: result.error },
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// If code didn't return a value, fall back to captured console output
|
|
35
|
-
const output =
|
|
36
|
-
result.result !== undefined && result.result !== null
|
|
37
|
-
? typeof result.result === "string"
|
|
38
|
-
? result.result
|
|
39
|
-
: JSON.stringify(result.result, null, 2)
|
|
40
|
-
: result.logs.length > 0
|
|
41
|
-
? result.logs.join("\n")
|
|
42
|
-
: "(no return value)";
|
|
43
|
-
|
|
44
|
-
const logsSection =
|
|
45
|
-
result.result !== undefined && result.logs.length > 0
|
|
46
|
-
? `\n\nLogs:\n${result.logs.join("\n")}`
|
|
47
|
-
: "";
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
content: [{ type: "text", text: output + logsSection }],
|
|
51
|
-
details: { executionMs, logs: result.logs },
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Create the `code_search` tool for querying the tool catalog.
|
|
57
|
-
*
|
|
58
|
-
* The model writes JavaScript to discover and filter available tools.
|
|
59
|
-
* The sandbox provides `codemode.listTools()` and `codemode.describeTools(names)`.
|
|
60
|
-
*/
|
|
61
|
-
export function createCodeSearchTool(manager: CodeModeManager) {
|
|
62
|
-
return {
|
|
63
|
-
name: "code_search",
|
|
64
|
-
label: "Code Search",
|
|
65
|
-
description:
|
|
66
|
-
"Discover available read-only MCP tools by writing JavaScript. Use `codemode.listTools()` to list tools and `codemode.describeTools(names)` for type info. Use this before code_execute to understand what data sources are available.",
|
|
67
|
-
parameters: CodeInput,
|
|
68
|
-
|
|
69
|
-
async execute(
|
|
70
|
-
_toolCallId: string,
|
|
71
|
-
params: CodeInputType,
|
|
72
|
-
_signal: AbortSignal | undefined,
|
|
73
|
-
_onUpdate: undefined,
|
|
74
|
-
_ctx: ExtensionContext,
|
|
75
|
-
): Promise<AgentToolResult<CodeModeToolDetails>> {
|
|
76
|
-
const start = performance.now();
|
|
77
|
-
const result = await manager.searchTools(params.code);
|
|
78
|
-
return formatResult(result, Math.round(performance.now() - start), "Code search error");
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Create the `code_execute` tool for chaining tool calls.
|
|
85
|
-
*
|
|
86
|
-
* The model writes JavaScript that calls read-only MCP tools via
|
|
87
|
-
* the `codemode` namespace (e.g. `codemode.search_docs({ query: 'test' })`).
|
|
88
|
-
* Code runs in a sandbox with no access to filesystem, network, or Node.js APIs.
|
|
89
|
-
*/
|
|
90
|
-
export function createCodeExecuteTool(manager: CodeModeManager) {
|
|
91
|
-
return {
|
|
92
|
-
name: "code_execute",
|
|
93
|
-
label: "Code Execute",
|
|
94
|
-
description:
|
|
95
|
-
"Execute JavaScript that chains read-only MCP tool calls for computation over data. Use when you need to aggregate, filter, loop, or transform results across multiple tool calls. Access tools via `codemode.toolName(args)`. Runs in a sandbox — no filesystem, network, or Node.js API access.",
|
|
96
|
-
parameters: CodeInput,
|
|
97
|
-
|
|
98
|
-
async execute(
|
|
99
|
-
_toolCallId: string,
|
|
100
|
-
params: CodeInputType,
|
|
101
|
-
_signal: AbortSignal | undefined,
|
|
102
|
-
_onUpdate: undefined,
|
|
103
|
-
_ctx: ExtensionContext,
|
|
104
|
-
): Promise<AgentToolResult<CodeModeToolDetails>> {
|
|
105
|
-
const start = performance.now();
|
|
106
|
-
const result = await manager.executeCode(params.code);
|
|
107
|
-
return formatResult(result, Math.round(performance.now() - start), "Code execution error");
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}
|