@sammorrowdrums/mcpi-ext 0.2.0 → 1.0.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/LICENSE +21 -0
- package/README.md +137 -47
- package/dist/code-mode/eligibility.d.ts +35 -0
- package/dist/code-mode/eligibility.js +64 -0
- package/dist/code-mode/executor.d.ts +53 -0
- package/dist/code-mode/executor.js +185 -0
- package/dist/code-mode/index.d.ts +107 -0
- package/dist/code-mode/index.js +267 -0
- package/dist/code-mode/isolated-vm.d.ts +65 -0
- package/dist/code-mode/isolated-vm.js +116 -0
- package/dist/code-mode/tools.d.ts +48 -0
- package/dist/code-mode/tools.js +73 -0
- package/dist/code-mode/type-hints.d.ts +20 -0
- package/dist/code-mode/type-hints.js +205 -0
- package/dist/docker-e2e.d.ts +54 -0
- package/dist/docker-e2e.js +234 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +227 -0
- package/dist/mcp/call-tool-result.d.ts +10 -0
- package/dist/mcp/call-tool-result.js +70 -0
- package/dist/mcp/client-factory.d.ts +59 -0
- package/dist/mcp/client-factory.js +76 -0
- package/dist/mcp/client-manager.d.ts +88 -0
- package/dist/mcp/client-manager.js +307 -0
- package/dist/mcp/config-loader.d.ts +7 -0
- package/dist/mcp/config-loader.js +42 -0
- package/dist/mcp/config.d.ts +56 -0
- package/dist/mcp/config.js +42 -0
- package/dist/mcp/gateway-defaults.d.ts +14 -0
- package/dist/mcp/gateway-defaults.js +21 -0
- package/dist/mcp/host-approval.d.ts +16 -0
- package/dist/mcp/host-approval.js +19 -0
- package/dist/mcp/host-elicitation.d.ts +19 -0
- package/dist/mcp/host-elicitation.js +94 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/policy.d.ts +314 -0
- package/dist/mcp/policy.js +848 -0
- package/dist/routing/facilities.d.ts +109 -0
- package/dist/routing/facilities.js +175 -0
- package/dist/routing/format.d.ts +24 -0
- package/dist/routing/format.js +64 -0
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +4 -0
- package/dist/routing/seam.d.ts +35 -0
- package/dist/routing/seam.js +28 -0
- package/dist/routing/tripwire.d.ts +42 -0
- package/dist/routing/tripwire.js +80 -0
- package/dist/skills/discover.d.ts +12 -0
- package/dist/skills/discover.js +85 -0
- package/dist/skills/format.d.ts +13 -0
- package/dist/skills/format.js +42 -0
- package/dist/skills/index.d.ts +6 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/load-skill-tool.d.ts +56 -0
- package/dist/skills/load-skill-tool.js +183 -0
- package/dist/skills/mcp-tool-proxy.d.ts +7 -0
- package/dist/skills/mcp-tool-proxy.js +41 -0
- package/dist/skills/sep2640/client.d.ts +91 -0
- package/dist/skills/sep2640/client.js +160 -0
- package/dist/skills/sep2640/discover.d.ts +33 -0
- package/dist/skills/sep2640/discover.js +82 -0
- package/dist/skills/sep2640/index.d.ts +13 -0
- package/dist/skills/sep2640/index.js +13 -0
- package/dist/skills/sep2640/integrity.d.ts +58 -0
- package/dist/skills/sep2640/integrity.js +185 -0
- package/dist/skills/sep2640/load.d.ts +61 -0
- package/dist/skills/sep2640/load.js +90 -0
- package/dist/skills/sep2640/protocol.d.ts +146 -0
- package/dist/skills/sep2640/protocol.js +214 -0
- package/dist/skills/sep2640/spec.d.ts +76 -0
- package/dist/skills/sep2640/spec.js +88 -0
- package/dist/skills/skill-registry.d.ts +85 -0
- package/dist/skills/skill-registry.js +85 -0
- package/dist/tool-cli/bridge.d.ts +14 -0
- package/dist/tool-cli/bridge.js +196 -0
- package/dist/tool-cli/format.d.ts +20 -0
- package/dist/tool-cli/format.js +73 -0
- package/dist/tool-cli/index.d.ts +7 -0
- package/dist/tool-cli/index.js +5 -0
- package/dist/tool-cli/provider.d.ts +21 -0
- package/dist/tool-cli/provider.js +114 -0
- package/dist/tool-cli/startup.d.ts +22 -0
- package/dist/tool-cli/startup.js +47 -0
- package/package.json +78 -26
- 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/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/format.test.ts +0 -33
- package/src/tool-cli/format.ts +0 -56
- package/src/tool-cli/index.ts +0 -3
- package/tsconfig.json +0 -21
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy adapter for the optional `isolated-vm` native addon.
|
|
3
|
+
*
|
|
4
|
+
* Code Mode is the only feature that needs a real V8 isolate. `isolated-vm` is a
|
|
5
|
+
* native addon, so it can legitimately be absent: an unsupported platform, a
|
|
6
|
+
* missing prebuild with no toolchain to compile one, or a deliberate
|
|
7
|
+
* `--omit=optional` install. When that happens Code Mode must report itself
|
|
8
|
+
* unavailable and the rest of the extension — skills, tool-cli, routing — must
|
|
9
|
+
* keep working.
|
|
10
|
+
*
|
|
11
|
+
* Two rules govern this module:
|
|
12
|
+
*
|
|
13
|
+
* 1. **No top-level import.** The specifier is resolved through a dynamic
|
|
14
|
+
* `import()` behind a variable so neither Node nor TypeScript binds to the
|
|
15
|
+
* addon at module-evaluation time. Loading the extension never throws
|
|
16
|
+
* because a native addon is missing.
|
|
17
|
+
* 2. **Never fall back to Node's `vm`.** `node:vm` shares the host heap and
|
|
18
|
+
* realm; it is a code-organisation tool, not a security boundary. Silently
|
|
19
|
+
* downgrading to it would turn "sandboxed execution" into a false claim.
|
|
20
|
+
* If the addon is absent, Code Mode is unavailable — full stop.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Resolved through a variable rather than a string literal so that TypeScript
|
|
24
|
+
* does not require `isolated-vm` types to be present to build, and so no
|
|
25
|
+
* bundler or loader statically links the addon into the module graph.
|
|
26
|
+
*/
|
|
27
|
+
const ISOLATED_VM_SPECIFIER = "isolated-vm";
|
|
28
|
+
const MISSING_MODULE_CODES = new Set([
|
|
29
|
+
"ERR_MODULE_NOT_FOUND",
|
|
30
|
+
"MODULE_NOT_FOUND",
|
|
31
|
+
"ERR_DLOPEN_FAILED",
|
|
32
|
+
]);
|
|
33
|
+
let cached;
|
|
34
|
+
let inFlight;
|
|
35
|
+
/**
|
|
36
|
+
* Load the native addon at most once per process.
|
|
37
|
+
*
|
|
38
|
+
* The result — success *or* failure — is cached. A failed load is not retried:
|
|
39
|
+
* a missing or broken native addon does not become present later in the same
|
|
40
|
+
* process, and retrying would repeat the cost on every Code Mode call.
|
|
41
|
+
*/
|
|
42
|
+
export async function loadIsolatedVm() {
|
|
43
|
+
if (cached)
|
|
44
|
+
return cached;
|
|
45
|
+
inFlight ??= attemptLoad().then((result) => {
|
|
46
|
+
cached = result;
|
|
47
|
+
inFlight = undefined;
|
|
48
|
+
return result;
|
|
49
|
+
});
|
|
50
|
+
return inFlight;
|
|
51
|
+
}
|
|
52
|
+
/** Synchronously report a previously resolved load, if one has happened. */
|
|
53
|
+
export function peekIsolatedVm() {
|
|
54
|
+
return cached;
|
|
55
|
+
}
|
|
56
|
+
/** Reset the memoised state. Test-only. */
|
|
57
|
+
export function resetIsolatedVmCacheForTests() {
|
|
58
|
+
cached = undefined;
|
|
59
|
+
inFlight = undefined;
|
|
60
|
+
}
|
|
61
|
+
/** Seed the memoised state. Test-only — lets tests simulate an absent addon. */
|
|
62
|
+
export function setIsolatedVmForTests(load) {
|
|
63
|
+
cached = load;
|
|
64
|
+
inFlight = undefined;
|
|
65
|
+
}
|
|
66
|
+
async function attemptLoad() {
|
|
67
|
+
try {
|
|
68
|
+
const imported = await import(ISOLATED_VM_SPECIFIER);
|
|
69
|
+
const candidate = unwrapDefault(imported);
|
|
70
|
+
if (!isIsolatedVmModule(candidate)) {
|
|
71
|
+
return {
|
|
72
|
+
available: false,
|
|
73
|
+
reason: "the optional isolated-vm addon loaded but did not expose the expected Isolate/Callback/Reference API",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { available: true, module: candidate };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return { available: false, reason: describeLoadFailure(error) };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function unwrapDefault(imported) {
|
|
83
|
+
if (typeof imported !== "object" || imported === null)
|
|
84
|
+
return imported;
|
|
85
|
+
const namespace = imported;
|
|
86
|
+
return namespace.default ?? imported;
|
|
87
|
+
}
|
|
88
|
+
function isIsolatedVmModule(value) {
|
|
89
|
+
if (typeof value !== "object" || value === null)
|
|
90
|
+
return false;
|
|
91
|
+
const candidate = value;
|
|
92
|
+
return (typeof candidate.Isolate === "function" &&
|
|
93
|
+
typeof candidate.Callback === "function" &&
|
|
94
|
+
typeof candidate.Reference === "function");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Turn a load failure into a reason a human can act on.
|
|
98
|
+
*
|
|
99
|
+
* The two failure shapes are meaningfully different: "not installed" is fixed
|
|
100
|
+
* by installing it, while "installed but failed to load" points at an ABI or
|
|
101
|
+
* platform mismatch and needs a rebuild.
|
|
102
|
+
*/
|
|
103
|
+
function describeLoadFailure(error) {
|
|
104
|
+
const code = getErrorCode(error);
|
|
105
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
106
|
+
if (code && MISSING_MODULE_CODES.has(code)) {
|
|
107
|
+
return `the optional isolated-vm native addon is not installed or failed to load (${code}: ${detail})`;
|
|
108
|
+
}
|
|
109
|
+
return `the optional isolated-vm native addon could not be loaded (${detail})`;
|
|
110
|
+
}
|
|
111
|
+
function getErrorCode(error) {
|
|
112
|
+
if (typeof error !== "object" || error === null)
|
|
113
|
+
return undefined;
|
|
114
|
+
const code = error.code;
|
|
115
|
+
return typeof code === "string" ? code : undefined;
|
|
116
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AgentToolResult, ExtensionContext } from "@sammorrowdrums/mcpi";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
import type { CodeModeManager } from "./index.js";
|
|
4
|
+
declare const CodeInput: Type.TObject<{
|
|
5
|
+
code: Type.TString;
|
|
6
|
+
}>;
|
|
7
|
+
type CodeInputType = Static<typeof CodeInput>;
|
|
8
|
+
export interface CodeModeToolDetails {
|
|
9
|
+
executionMs: number;
|
|
10
|
+
logs: string[];
|
|
11
|
+
error?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
alternatives?: string[];
|
|
14
|
+
toolName?: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create the `code_search` tool for querying the tool catalog.
|
|
19
|
+
*
|
|
20
|
+
* The model writes JavaScript to discover and filter available tools.
|
|
21
|
+
* The sandbox provides `codemode.listTools()` and `codemode.describeTools(names)`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createCodeSearchTool(manager: CodeModeManager): {
|
|
24
|
+
name: string;
|
|
25
|
+
label: string;
|
|
26
|
+
description: string;
|
|
27
|
+
parameters: Type.TObject<{
|
|
28
|
+
code: Type.TString;
|
|
29
|
+
}>;
|
|
30
|
+
execute(_toolCallId: string, params: CodeInputType, _signal: AbortSignal | undefined, _onUpdate: undefined, _ctx: ExtensionContext): Promise<AgentToolResult<CodeModeToolDetails>>;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Create the `code_execute` tool for chaining tool calls.
|
|
34
|
+
*
|
|
35
|
+
* The model writes JavaScript that calls read-only MCP tools via
|
|
36
|
+
* the `codemode` namespace (e.g. `codemode.search_docs({ query: 'test' })`).
|
|
37
|
+
* Code runs in a sandbox with no access to filesystem, network, or Node.js APIs.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createCodeExecuteTool(manager: CodeModeManager): {
|
|
40
|
+
name: string;
|
|
41
|
+
label: string;
|
|
42
|
+
description: string;
|
|
43
|
+
parameters: Type.TObject<{
|
|
44
|
+
code: Type.TString;
|
|
45
|
+
}>;
|
|
46
|
+
execute(_toolCallId: string, params: CodeInputType, _signal: AbortSignal | undefined, _onUpdate: undefined, _ctx: ExtensionContext): Promise<AgentToolResult<CodeModeToolDetails>>;
|
|
47
|
+
};
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
const CodeInput = Type.Object({
|
|
3
|
+
code: Type.String({
|
|
4
|
+
description: "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.",
|
|
5
|
+
}),
|
|
6
|
+
});
|
|
7
|
+
/** Format an ExecuteResult into a tool response. Falls back to logs if result is undefined. */
|
|
8
|
+
function formatResult(result, executionMs, errorPrefix) {
|
|
9
|
+
if (result.error) {
|
|
10
|
+
const errorDetails = result.errorDetails ?? {
|
|
11
|
+
error: "execution_failed",
|
|
12
|
+
message: result.error,
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: `${errorPrefix}: ${result.error}` }],
|
|
16
|
+
details: { executionMs, logs: result.logs, ...errorDetails },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
// If code didn't return a value, fall back to captured console output
|
|
20
|
+
const output = result.result !== undefined
|
|
21
|
+
? typeof result.result === "string"
|
|
22
|
+
? result.result
|
|
23
|
+
: JSON.stringify(result.result, null, 2)
|
|
24
|
+
: result.logs.length > 0
|
|
25
|
+
? result.logs.join("\n")
|
|
26
|
+
: "(no return value)";
|
|
27
|
+
const logsSection = result.result !== undefined && result.logs.length > 0
|
|
28
|
+
? `\n\nLogs:\n${result.logs.join("\n")}`
|
|
29
|
+
: "";
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text", text: output + logsSection }],
|
|
32
|
+
details: { executionMs, logs: result.logs },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create the `code_search` tool for querying the tool catalog.
|
|
37
|
+
*
|
|
38
|
+
* The model writes JavaScript to discover and filter available tools.
|
|
39
|
+
* The sandbox provides `codemode.listTools()` and `codemode.describeTools(names)`.
|
|
40
|
+
*/
|
|
41
|
+
export function createCodeSearchTool(manager) {
|
|
42
|
+
return {
|
|
43
|
+
name: "code_search",
|
|
44
|
+
label: "Code Search",
|
|
45
|
+
description: "Discover all available MCP tools by writing JavaScript. Use `codemode.listTools()` to list tools and `codemode.describeTools(names)` for type info. Type hints identify which tools Code Mode can call; non-read-only tools remain discovery-only.",
|
|
46
|
+
parameters: CodeInput,
|
|
47
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
48
|
+
const start = performance.now();
|
|
49
|
+
const result = await manager.searchTools(params.code);
|
|
50
|
+
return formatResult(result, Math.round(performance.now() - start), "Code search error");
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create the `code_execute` tool for chaining tool calls.
|
|
56
|
+
*
|
|
57
|
+
* The model writes JavaScript that calls read-only MCP tools via
|
|
58
|
+
* the `codemode` namespace (e.g. `codemode.search_docs({ query: 'test' })`).
|
|
59
|
+
* Code runs in a sandbox with no access to filesystem, network, or Node.js APIs.
|
|
60
|
+
*/
|
|
61
|
+
export function createCodeExecuteTool(manager) {
|
|
62
|
+
return {
|
|
63
|
+
name: "code_execute",
|
|
64
|
+
label: "Code Execute",
|
|
65
|
+
description: "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.",
|
|
66
|
+
parameters: CodeInput,
|
|
67
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
68
|
+
const start = performance.now();
|
|
69
|
+
const result = await manager.executeCode(params.code);
|
|
70
|
+
return formatResult(result, Math.round(performance.now() - start), "Code execution error");
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { McpTool } from "../mcp/index.js";
|
|
2
|
+
import { type CodeModeTool } from "./eligibility.js";
|
|
3
|
+
type JsonSchema = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Convert a JSON Schema to a TypeScript type string.
|
|
6
|
+
*
|
|
7
|
+
* Handles objects, arrays, primitives, enums, anyOf/oneOf/allOf, $ref,
|
|
8
|
+
* and circular references (depth guard at 20).
|
|
9
|
+
*/
|
|
10
|
+
export declare function jsonSchemaToTypeString(schema: JsonSchema, definitions?: Record<string, JsonSchema>, depth?: number, seen?: Set<JsonSchema>): string;
|
|
11
|
+
/** Sanitize a tool name to be a valid JS identifier. */
|
|
12
|
+
export declare function sanitizeToolName(name: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Generate TypeScript type declarations for a set of MCP tools.
|
|
15
|
+
*
|
|
16
|
+
* Produces a `declare const codemode: { ... }` block with type-safe
|
|
17
|
+
* method signatures the model can use when writing code.
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateTypeHints(tools: readonly (McpTool | CodeModeTool)[]): string;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { getCodeModeDiagnostics, toCodeModeTool } from "./eligibility.js";
|
|
2
|
+
/**
|
|
3
|
+
* Convert a JSON Schema to a TypeScript type string.
|
|
4
|
+
*
|
|
5
|
+
* Handles objects, arrays, primitives, enums, anyOf/oneOf/allOf, $ref,
|
|
6
|
+
* and circular references (depth guard at 20).
|
|
7
|
+
*/
|
|
8
|
+
export function jsonSchemaToTypeString(schema, definitions, depth = 0, seen = new Set()) {
|
|
9
|
+
if (depth > 20)
|
|
10
|
+
return "unknown";
|
|
11
|
+
if (seen.has(schema))
|
|
12
|
+
return "unknown";
|
|
13
|
+
seen.add(schema);
|
|
14
|
+
// Handle $ref
|
|
15
|
+
if (typeof schema.$ref === "string") {
|
|
16
|
+
const refPath = schema.$ref;
|
|
17
|
+
const refName = refPath.replace(/^#\/(definitions|components\/schemas|\\$defs)\//, "");
|
|
18
|
+
const resolved = definitions?.[refName];
|
|
19
|
+
if (resolved) {
|
|
20
|
+
return jsonSchemaToTypeString(resolved, definitions, depth + 1, seen);
|
|
21
|
+
}
|
|
22
|
+
return "unknown";
|
|
23
|
+
}
|
|
24
|
+
// Handle enum
|
|
25
|
+
if (Array.isArray(schema.enum)) {
|
|
26
|
+
return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
27
|
+
}
|
|
28
|
+
// Handle const
|
|
29
|
+
if ("const" in schema) {
|
|
30
|
+
return JSON.stringify(schema.const);
|
|
31
|
+
}
|
|
32
|
+
// Handle anyOf / oneOf
|
|
33
|
+
const unionKey = schema.anyOf ? "anyOf" : schema.oneOf ? "oneOf" : null;
|
|
34
|
+
if (unionKey && Array.isArray(schema[unionKey])) {
|
|
35
|
+
const variants = schema[unionKey].map((s) => jsonSchemaToTypeString(s, definitions, depth + 1, seen));
|
|
36
|
+
return variants.join(" | ");
|
|
37
|
+
}
|
|
38
|
+
// Handle allOf
|
|
39
|
+
if (Array.isArray(schema.allOf)) {
|
|
40
|
+
const parts = schema.allOf.map((s) => jsonSchemaToTypeString(s, definitions, depth + 1, seen));
|
|
41
|
+
return parts.join(" & ");
|
|
42
|
+
}
|
|
43
|
+
const type = schema.type;
|
|
44
|
+
// Handle type arrays (e.g. ["string", "null"])
|
|
45
|
+
if (Array.isArray(type)) {
|
|
46
|
+
const types = type.map((t) => primitiveToTs(t));
|
|
47
|
+
return types.join(" | ");
|
|
48
|
+
}
|
|
49
|
+
switch (type) {
|
|
50
|
+
case "object":
|
|
51
|
+
return objectToTs(schema, definitions, depth, seen);
|
|
52
|
+
case "array":
|
|
53
|
+
return arrayToTs(schema, definitions, depth, seen);
|
|
54
|
+
case "string":
|
|
55
|
+
case "number":
|
|
56
|
+
case "integer":
|
|
57
|
+
case "boolean":
|
|
58
|
+
case "null":
|
|
59
|
+
return primitiveToTs(type);
|
|
60
|
+
default:
|
|
61
|
+
// No type specified — try to infer from properties
|
|
62
|
+
if (schema.properties) {
|
|
63
|
+
return objectToTs(schema, definitions, depth, seen);
|
|
64
|
+
}
|
|
65
|
+
return "unknown";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function primitiveToTs(type) {
|
|
69
|
+
switch (type) {
|
|
70
|
+
case "string":
|
|
71
|
+
return "string";
|
|
72
|
+
case "number":
|
|
73
|
+
case "integer":
|
|
74
|
+
return "number";
|
|
75
|
+
case "boolean":
|
|
76
|
+
return "boolean";
|
|
77
|
+
case "null":
|
|
78
|
+
return "null";
|
|
79
|
+
default:
|
|
80
|
+
return "unknown";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function objectToTs(schema, definitions, depth, seen) {
|
|
84
|
+
const properties = schema.properties;
|
|
85
|
+
if (!properties || Object.keys(properties).length === 0) {
|
|
86
|
+
return "Record<string, unknown>";
|
|
87
|
+
}
|
|
88
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
89
|
+
const lines = [];
|
|
90
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
91
|
+
const desc = propSchema.description;
|
|
92
|
+
if (desc) {
|
|
93
|
+
lines.push(` /** ${desc} */`);
|
|
94
|
+
}
|
|
95
|
+
const optional = required.has(key) ? "" : "?";
|
|
96
|
+
const typeStr = jsonSchemaToTypeString(propSchema, definitions, depth + 1, seen);
|
|
97
|
+
lines.push(` ${safeName(key)}${optional}: ${typeStr};`);
|
|
98
|
+
}
|
|
99
|
+
return `{\n${lines.join("\n")}\n}`;
|
|
100
|
+
}
|
|
101
|
+
function arrayToTs(schema, definitions, depth, seen) {
|
|
102
|
+
const items = schema.items;
|
|
103
|
+
if (!items)
|
|
104
|
+
return "unknown[]";
|
|
105
|
+
// Tuple form
|
|
106
|
+
if (Array.isArray(items)) {
|
|
107
|
+
const tupleTypes = items.map((s) => jsonSchemaToTypeString(s, definitions, depth + 1, seen));
|
|
108
|
+
return `[${tupleTypes.join(", ")}]`;
|
|
109
|
+
}
|
|
110
|
+
const itemType = jsonSchemaToTypeString(items, definitions, depth + 1, seen);
|
|
111
|
+
return `${itemType}[]`;
|
|
112
|
+
}
|
|
113
|
+
/** Ensure property name is a valid JS identifier, quote otherwise. */
|
|
114
|
+
function safeName(name) {
|
|
115
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
116
|
+
}
|
|
117
|
+
/** Sanitize a tool name to be a valid JS identifier. */
|
|
118
|
+
export function sanitizeToolName(name) {
|
|
119
|
+
return name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Generate TypeScript type declarations for a set of MCP tools.
|
|
123
|
+
*
|
|
124
|
+
* Produces a `declare const codemode: { ... }` block with type-safe
|
|
125
|
+
* method signatures the model can use when writing code.
|
|
126
|
+
*/
|
|
127
|
+
export function generateTypeHints(tools) {
|
|
128
|
+
const codeModeTools = tools.map(normalizeCodeModeTool);
|
|
129
|
+
const diagnostics = getCodeModeDiagnostics(codeModeTools);
|
|
130
|
+
const methods = [];
|
|
131
|
+
for (const codeModeTool of codeModeTools) {
|
|
132
|
+
const tool = codeModeTool.tool;
|
|
133
|
+
const safeName = sanitizeToolName(tool.name);
|
|
134
|
+
const inputSchema = tool.inputSchema;
|
|
135
|
+
const outputSchema = codeModeTool.outputSchema;
|
|
136
|
+
const definitions = (inputSchema.$defs ??
|
|
137
|
+
inputSchema.definitions ??
|
|
138
|
+
outputSchema?.$defs ??
|
|
139
|
+
outputSchema?.definitions);
|
|
140
|
+
// Build input type
|
|
141
|
+
const inputType = generateInputType(safeName, inputSchema, definitions);
|
|
142
|
+
// Build output type
|
|
143
|
+
const outputType = outputSchema ? jsonSchemaToTypeString(outputSchema, definitions) : "unknown";
|
|
144
|
+
// Build JSDoc
|
|
145
|
+
const jsdoc = buildJsDoc(codeModeTool, inputSchema);
|
|
146
|
+
methods.push(`${jsdoc} ${safeName}: (input: ${inputType}) => Promise<${outputType}>;`);
|
|
147
|
+
}
|
|
148
|
+
const toolListType = codeModeTools.length > 0
|
|
149
|
+
? codeModeTools.map((entry) => `"${escapeStr(entry.tool.name)}"`).join(" | ")
|
|
150
|
+
: "never";
|
|
151
|
+
return [
|
|
152
|
+
"// Code mode type hints — auto-generated from MCP tool schemas",
|
|
153
|
+
"// Available tools are accessed via the `codemode` namespace",
|
|
154
|
+
`// MCP catalog: ${diagnostics.totalTools} tool(s); ${diagnostics.callableTools} callable, ${diagnostics.refusedTools} dispatch-refused`,
|
|
155
|
+
`// Output schemas: ${diagnostics.declaredOutputSchemas} declared, ${diagnostics.synthesizedOutputSchemas} synthesized, ${diagnostics.unavailableOutputSchemas} unavailable`,
|
|
156
|
+
"",
|
|
157
|
+
`declare const codemode: {`,
|
|
158
|
+
` /** List all available code mode tool names. */`,
|
|
159
|
+
` listTools: () => Promise<(${toolListType})[]>;`,
|
|
160
|
+
` /** Get full type information for specific tools. */`,
|
|
161
|
+
` describeTools: (names: string[]) => Promise<string>;`,
|
|
162
|
+
...methods.map((m) => m),
|
|
163
|
+
`};`,
|
|
164
|
+
].join("\n");
|
|
165
|
+
}
|
|
166
|
+
function generateInputType(_toolSafeName, inputSchema, definitions) {
|
|
167
|
+
const properties = inputSchema.properties;
|
|
168
|
+
if (!properties || Object.keys(properties).length === 0) {
|
|
169
|
+
return "Record<string, never>";
|
|
170
|
+
}
|
|
171
|
+
// Always inline the type — avoids emitting unreferenced named type aliases
|
|
172
|
+
const typeStr = jsonSchemaToTypeString(inputSchema, definitions);
|
|
173
|
+
return typeStr;
|
|
174
|
+
}
|
|
175
|
+
function buildJsDoc(codeModeTool, inputSchema) {
|
|
176
|
+
const tool = codeModeTool.tool;
|
|
177
|
+
const lines = [" /**"];
|
|
178
|
+
if (tool.description) {
|
|
179
|
+
lines.push(` * ${tool.description}`);
|
|
180
|
+
}
|
|
181
|
+
if (codeModeTool.callable) {
|
|
182
|
+
lines.push(" * Code Mode dispatch: callable (explicitly read-only and non-destructive).");
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
lines.push(" * Code Mode dispatch: refused. Use a permission-aware non-Code-Mode path.");
|
|
186
|
+
}
|
|
187
|
+
lines.push(` * Output schema provenance: ${codeModeTool.outputSchemaProvenance}.`);
|
|
188
|
+
const properties = inputSchema.properties;
|
|
189
|
+
if (properties) {
|
|
190
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
191
|
+
const desc = propSchema.description;
|
|
192
|
+
if (desc) {
|
|
193
|
+
lines.push(` * @param input.${key} - ${desc}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
lines.push(" */");
|
|
198
|
+
return lines.join("\n") + "\n";
|
|
199
|
+
}
|
|
200
|
+
function escapeStr(s) {
|
|
201
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ");
|
|
202
|
+
}
|
|
203
|
+
function normalizeCodeModeTool(tool) {
|
|
204
|
+
return "tool" in tool && "outputSchemaProvenance" in tool ? tool : toCodeModeTool(tool);
|
|
205
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@sammorrowdrums/mcpi";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
declare const DockerE2EParams: Type.TObject<{
|
|
4
|
+
task: Type.TString;
|
|
5
|
+
token: Type.TString;
|
|
6
|
+
model: Type.TOptional<Type.TString>;
|
|
7
|
+
timeout: Type.TOptional<Type.TNumber>;
|
|
8
|
+
}>;
|
|
9
|
+
type DockerE2EInput = Static<typeof DockerE2EParams>;
|
|
10
|
+
interface RunLogEntry {
|
|
11
|
+
type: string;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
raw: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DockerE2EDetails {
|
|
16
|
+
exitCode: number | null;
|
|
17
|
+
log: RunLogEntry[];
|
|
18
|
+
duration: number;
|
|
19
|
+
model?: string;
|
|
20
|
+
usage?: {
|
|
21
|
+
input: number;
|
|
22
|
+
output: number;
|
|
23
|
+
cacheRead: number;
|
|
24
|
+
cacheWrite: number;
|
|
25
|
+
cost: number;
|
|
26
|
+
turns: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function parseJsonlLine(line: string): RunLogEntry | null;
|
|
30
|
+
export declare function extractAssistantText(log: RunLogEntry[]): string;
|
|
31
|
+
export declare function extractUsage(log: RunLogEntry[]): {
|
|
32
|
+
input: number;
|
|
33
|
+
output: number;
|
|
34
|
+
cacheRead: number;
|
|
35
|
+
cacheWrite: number;
|
|
36
|
+
cost: number;
|
|
37
|
+
turns: number;
|
|
38
|
+
};
|
|
39
|
+
export declare function extractModel(log: RunLogEntry[]): string | undefined;
|
|
40
|
+
export declare function buildDockerArgs(image: string, params: DockerE2EInput): string[];
|
|
41
|
+
export declare const dockerE2ETool: {
|
|
42
|
+
name: string;
|
|
43
|
+
label: string;
|
|
44
|
+
description: string;
|
|
45
|
+
promptSnippet: string;
|
|
46
|
+
parameters: Type.TObject<{
|
|
47
|
+
task: Type.TString;
|
|
48
|
+
token: Type.TString;
|
|
49
|
+
model: Type.TOptional<Type.TString>;
|
|
50
|
+
timeout: Type.TOptional<Type.TNumber>;
|
|
51
|
+
}>;
|
|
52
|
+
execute(_toolCallId: string, params: DockerE2EInput, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<DockerE2EDetails> | undefined, _ctx: ExtensionContext): Promise<AgentToolResult<DockerE2EDetails>>;
|
|
53
|
+
};
|
|
54
|
+
export {};
|