@sammorrowdrums/mcpi-ext 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +6 -0
- package/.github/dependabot.yml +25 -0
- package/.github/workflows/ci.yml +50 -0
- package/.hadolint.yaml +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +7 -0
- package/AGENTS.md +118 -0
- package/DECISIONS.md +99 -0
- package/Dockerfile +39 -0
- package/README.md +278 -0
- package/aube-lock.yaml +4146 -0
- 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 +128 -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/dist/mcp/index.d.ts +5 -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/dist/skills/index.d.ts +6 -0
- 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/dist/test-servers/weather-stdio.js +11 -0
- 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/dist/tool-cli/format.js +57 -0
- 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/dist/tool-cli/index.d.ts +4 -0
- package/dist/tool-cli/index.d.ts.map +1 -0
- package/dist/tool-cli/index.js +4 -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/eslint.config.js +27 -0
- 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 +65 -0
- package/package.json +46 -0
- package/scripts/docker-entrypoint.sh +29 -0
- package/scripts/test-echo-server.mjs +28 -0
- package/scripts/test-mcp-integration.mjs +59 -0
- package/scripts/test-tool-cli-smoke.mjs +62 -0
- package/src/code-mode/code-mode-integration.test.ts +135 -0
- package/src/code-mode/eligibility.test.ts +60 -0
- package/src/code-mode/eligibility.ts +16 -0
- package/src/code-mode/executor.test.ts +168 -0
- package/src/code-mode/executor.ts +163 -0
- package/src/code-mode/index.ts +208 -0
- package/src/code-mode/tools.ts +110 -0
- package/src/code-mode/type-hints.test.ts +187 -0
- package/src/code-mode/type-hints.ts +249 -0
- package/src/docker-e2e.test.ts +195 -0
- package/src/docker-e2e.ts +277 -0
- package/src/index.test.ts +8 -0
- package/src/index.ts +158 -0
- package/src/mcp/client-manager.test.ts +250 -0
- package/src/mcp/client-manager.ts +199 -0
- package/src/mcp/config-loader.test.ts +115 -0
- package/src/mcp/config-loader.ts +46 -0
- package/src/mcp/config.ts +40 -0
- package/src/mcp/index.ts +4 -0
- package/src/skills/discover.ts +81 -0
- package/src/skills/format.test.ts +70 -0
- package/src/skills/format.ts +42 -0
- package/src/skills/index.ts +9 -0
- package/src/skills/load-skill-tool.ts +136 -0
- package/src/skills/mcp-tool-proxy.ts +143 -0
- package/src/skills/skill-integration.test.ts +139 -0
- package/src/skills/skill-registry.test.ts +83 -0
- package/src/skills/skill-registry.ts +69 -0
- package/src/test-servers/weather-server.ts +171 -0
- package/src/test-servers/weather-stdio.ts +11 -0
- package/src/tool-cli/cli.ts +232 -0
- package/src/tool-cli/constants.ts +15 -0
- package/src/tool-cli/format.test.ts +33 -0
- package/src/tool-cli/format.ts +56 -0
- package/src/tool-cli/index.ts +3 -0
- package/src/tool-cli/rpc-client.ts +39 -0
- package/src/tool-cli/rpc-server.test.ts +193 -0
- package/src/tool-cli/rpc-server.ts +287 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { McpTool } from "../mcp/index.js";
|
|
3
|
+
import { generateTypeHints, jsonSchemaToTypeString, sanitizeToolName } from "./type-hints.js";
|
|
4
|
+
|
|
5
|
+
describe("jsonSchemaToTypeString", () => {
|
|
6
|
+
it("converts string type", () => {
|
|
7
|
+
expect(jsonSchemaToTypeString({ type: "string" })).toBe("string");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("converts number type", () => {
|
|
11
|
+
expect(jsonSchemaToTypeString({ type: "number" })).toBe("number");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("converts integer type", () => {
|
|
15
|
+
expect(jsonSchemaToTypeString({ type: "integer" })).toBe("number");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("converts boolean type", () => {
|
|
19
|
+
expect(jsonSchemaToTypeString({ type: "boolean" })).toBe("boolean");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("converts null type", () => {
|
|
23
|
+
expect(jsonSchemaToTypeString({ type: "null" })).toBe("null");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("converts type array", () => {
|
|
27
|
+
expect(jsonSchemaToTypeString({ type: ["string", "null"] })).toBe("string | null");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("converts enum to union", () => {
|
|
31
|
+
expect(jsonSchemaToTypeString({ enum: ["a", "b", "c"] })).toBe('"a" | "b" | "c"');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("converts const", () => {
|
|
35
|
+
expect(jsonSchemaToTypeString({ const: "fixed" })).toBe('"fixed"');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("converts simple object", () => {
|
|
39
|
+
const result = jsonSchemaToTypeString({
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
name: { type: "string" },
|
|
43
|
+
age: { type: "number" },
|
|
44
|
+
},
|
|
45
|
+
required: ["name"],
|
|
46
|
+
});
|
|
47
|
+
expect(result).toContain("name: string;");
|
|
48
|
+
expect(result).toContain("age?: number;");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("converts empty object to Record", () => {
|
|
52
|
+
expect(jsonSchemaToTypeString({ type: "object" })).toBe("Record<string, unknown>");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("converts array with items", () => {
|
|
56
|
+
expect(jsonSchemaToTypeString({ type: "array", items: { type: "string" } })).toBe("string[]");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("converts array without items", () => {
|
|
60
|
+
expect(jsonSchemaToTypeString({ type: "array" })).toBe("unknown[]");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("handles anyOf", () => {
|
|
64
|
+
const result = jsonSchemaToTypeString({
|
|
65
|
+
anyOf: [{ type: "string" }, { type: "number" }],
|
|
66
|
+
});
|
|
67
|
+
expect(result).toBe("string | number");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("handles oneOf", () => {
|
|
71
|
+
const result = jsonSchemaToTypeString({
|
|
72
|
+
oneOf: [{ type: "boolean" }, { type: "null" }],
|
|
73
|
+
});
|
|
74
|
+
expect(result).toBe("boolean | null");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("handles allOf", () => {
|
|
78
|
+
const result = jsonSchemaToTypeString({
|
|
79
|
+
allOf: [
|
|
80
|
+
{ type: "object", properties: { a: { type: "string" } }, required: ["a"] },
|
|
81
|
+
{ type: "object", properties: { b: { type: "number" } }, required: ["b"] },
|
|
82
|
+
],
|
|
83
|
+
});
|
|
84
|
+
expect(result).toContain("a: string;");
|
|
85
|
+
expect(result).toContain("b: number;");
|
|
86
|
+
expect(result).toContain("&");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("resolves $ref", () => {
|
|
90
|
+
const result = jsonSchemaToTypeString(
|
|
91
|
+
{ $ref: "#/definitions/Foo" },
|
|
92
|
+
{ Foo: { type: "string" } },
|
|
93
|
+
);
|
|
94
|
+
expect(result).toBe("string");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("handles unknown $ref gracefully", () => {
|
|
98
|
+
expect(jsonSchemaToTypeString({ $ref: "#/definitions/Missing" }, {})).toBe("unknown");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("guards against deep recursion", () => {
|
|
102
|
+
// Create a deeply nested schema
|
|
103
|
+
let schema: Record<string, unknown> = { type: "string" };
|
|
104
|
+
for (let i = 0; i < 25; i++) {
|
|
105
|
+
schema = { type: "object", properties: { nested: schema }, required: ["nested"] };
|
|
106
|
+
}
|
|
107
|
+
const result = jsonSchemaToTypeString(schema);
|
|
108
|
+
expect(result).toContain("unknown"); // Should hit depth guard
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("includes description as JSDoc in object properties", () => {
|
|
112
|
+
const result = jsonSchemaToTypeString({
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
query: { type: "string", description: "Search term" },
|
|
116
|
+
},
|
|
117
|
+
required: ["query"],
|
|
118
|
+
});
|
|
119
|
+
expect(result).toContain("/** Search term */");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("sanitizeToolName", () => {
|
|
124
|
+
it("replaces hyphens with underscores", () => {
|
|
125
|
+
expect(sanitizeToolName("list-issues")).toBe("list_issues");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("replaces dots with underscores", () => {
|
|
129
|
+
expect(sanitizeToolName("github.search")).toBe("github_search");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("keeps valid identifiers unchanged", () => {
|
|
133
|
+
expect(sanitizeToolName("searchDocs")).toBe("searchDocs");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("generateTypeHints", () => {
|
|
138
|
+
it("returns empty string for no tools", () => {
|
|
139
|
+
expect(generateTypeHints([])).toBe("");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("generates declaration for a simple tool", () => {
|
|
143
|
+
const tools: McpTool[] = [
|
|
144
|
+
{
|
|
145
|
+
name: "search_docs",
|
|
146
|
+
description: "Search documentation",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
query: { type: "string", description: "Search term" },
|
|
151
|
+
},
|
|
152
|
+
required: ["query"],
|
|
153
|
+
},
|
|
154
|
+
outputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
results: { type: "array", items: { type: "string" } },
|
|
158
|
+
},
|
|
159
|
+
required: ["results"],
|
|
160
|
+
},
|
|
161
|
+
serverName: "docs",
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const result = generateTypeHints(tools);
|
|
166
|
+
expect(result).toContain("declare const codemode");
|
|
167
|
+
expect(result).toContain("search_docs:");
|
|
168
|
+
expect(result).toContain("Promise<");
|
|
169
|
+
expect(result).toContain("listTools");
|
|
170
|
+
expect(result).toContain("describeTools");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("sanitizes tool names with special characters", () => {
|
|
174
|
+
const tools: McpTool[] = [
|
|
175
|
+
{
|
|
176
|
+
name: "github.list-repos",
|
|
177
|
+
description: "List repos",
|
|
178
|
+
inputSchema: { type: "object", properties: {} },
|
|
179
|
+
outputSchema: { type: "object", properties: {} },
|
|
180
|
+
serverName: "github",
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const result = generateTypeHints(tools);
|
|
185
|
+
expect(result).toContain("github_list_repos:");
|
|
186
|
+
});
|
|
187
|
+
});
|