@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,187 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
import type { McpTool } from "../mcp/index.js";
|
|
2
|
-
|
|
3
|
-
type JsonSchema = Record<string, unknown>;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Convert a JSON Schema to a TypeScript type string.
|
|
7
|
-
*
|
|
8
|
-
* Handles objects, arrays, primitives, enums, anyOf/oneOf/allOf, $ref,
|
|
9
|
-
* and circular references (depth guard at 20).
|
|
10
|
-
*/
|
|
11
|
-
export function jsonSchemaToTypeString(
|
|
12
|
-
schema: JsonSchema,
|
|
13
|
-
definitions?: Record<string, JsonSchema>,
|
|
14
|
-
depth = 0,
|
|
15
|
-
seen = new Set<JsonSchema>(),
|
|
16
|
-
): string {
|
|
17
|
-
if (depth > 20) return "unknown";
|
|
18
|
-
if (seen.has(schema)) return "unknown";
|
|
19
|
-
seen.add(schema);
|
|
20
|
-
|
|
21
|
-
// Handle $ref
|
|
22
|
-
if (typeof schema.$ref === "string") {
|
|
23
|
-
const refPath = schema.$ref;
|
|
24
|
-
const refName = refPath.replace(/^#\/(definitions|components\/schemas|\\$defs)\//, "");
|
|
25
|
-
const resolved = definitions?.[refName];
|
|
26
|
-
if (resolved) {
|
|
27
|
-
return jsonSchemaToTypeString(resolved, definitions, depth + 1, seen);
|
|
28
|
-
}
|
|
29
|
-
return "unknown";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Handle enum
|
|
33
|
-
if (Array.isArray(schema.enum)) {
|
|
34
|
-
return schema.enum.map((v: unknown) => JSON.stringify(v)).join(" | ");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Handle const
|
|
38
|
-
if ("const" in schema) {
|
|
39
|
-
return JSON.stringify(schema.const);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Handle anyOf / oneOf
|
|
43
|
-
const unionKey = schema.anyOf ? "anyOf" : schema.oneOf ? "oneOf" : null;
|
|
44
|
-
if (unionKey && Array.isArray(schema[unionKey])) {
|
|
45
|
-
const variants = (schema[unionKey] as JsonSchema[]).map((s) =>
|
|
46
|
-
jsonSchemaToTypeString(s, definitions, depth + 1, seen),
|
|
47
|
-
);
|
|
48
|
-
return variants.join(" | ");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Handle allOf
|
|
52
|
-
if (Array.isArray(schema.allOf)) {
|
|
53
|
-
const parts = (schema.allOf as JsonSchema[]).map((s) =>
|
|
54
|
-
jsonSchemaToTypeString(s, definitions, depth + 1, seen),
|
|
55
|
-
);
|
|
56
|
-
return parts.join(" & ");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const type = schema.type as string | string[] | undefined;
|
|
60
|
-
|
|
61
|
-
// Handle type arrays (e.g. ["string", "null"])
|
|
62
|
-
if (Array.isArray(type)) {
|
|
63
|
-
const types = type.map((t: string) => primitiveToTs(t));
|
|
64
|
-
return types.join(" | ");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
switch (type) {
|
|
68
|
-
case "object":
|
|
69
|
-
return objectToTs(schema, definitions, depth, seen);
|
|
70
|
-
case "array":
|
|
71
|
-
return arrayToTs(schema, definitions, depth, seen);
|
|
72
|
-
case "string":
|
|
73
|
-
case "number":
|
|
74
|
-
case "integer":
|
|
75
|
-
case "boolean":
|
|
76
|
-
case "null":
|
|
77
|
-
return primitiveToTs(type);
|
|
78
|
-
default:
|
|
79
|
-
// No type specified — try to infer from properties
|
|
80
|
-
if (schema.properties) {
|
|
81
|
-
return objectToTs(schema, definitions, depth, seen);
|
|
82
|
-
}
|
|
83
|
-
return "unknown";
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function primitiveToTs(type: string): string {
|
|
88
|
-
switch (type) {
|
|
89
|
-
case "string":
|
|
90
|
-
return "string";
|
|
91
|
-
case "number":
|
|
92
|
-
case "integer":
|
|
93
|
-
return "number";
|
|
94
|
-
case "boolean":
|
|
95
|
-
return "boolean";
|
|
96
|
-
case "null":
|
|
97
|
-
return "null";
|
|
98
|
-
default:
|
|
99
|
-
return "unknown";
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function objectToTs(
|
|
104
|
-
schema: JsonSchema,
|
|
105
|
-
definitions: Record<string, JsonSchema> | undefined,
|
|
106
|
-
depth: number,
|
|
107
|
-
seen: Set<JsonSchema>,
|
|
108
|
-
): string {
|
|
109
|
-
const properties = schema.properties as Record<string, JsonSchema> | undefined;
|
|
110
|
-
if (!properties || Object.keys(properties).length === 0) {
|
|
111
|
-
return "Record<string, unknown>";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const required = new Set<string>(
|
|
115
|
-
Array.isArray(schema.required) ? (schema.required as string[]) : [],
|
|
116
|
-
);
|
|
117
|
-
const lines: string[] = [];
|
|
118
|
-
|
|
119
|
-
for (const [key, propSchema] of Object.entries(properties)) {
|
|
120
|
-
const desc = propSchema.description as string | undefined;
|
|
121
|
-
if (desc) {
|
|
122
|
-
lines.push(` /** ${desc} */`);
|
|
123
|
-
}
|
|
124
|
-
const optional = required.has(key) ? "" : "?";
|
|
125
|
-
const typeStr = jsonSchemaToTypeString(propSchema, definitions, depth + 1, seen);
|
|
126
|
-
lines.push(` ${safeName(key)}${optional}: ${typeStr};`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return `{\n${lines.join("\n")}\n}`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function arrayToTs(
|
|
133
|
-
schema: JsonSchema,
|
|
134
|
-
definitions: Record<string, JsonSchema> | undefined,
|
|
135
|
-
depth: number,
|
|
136
|
-
seen: Set<JsonSchema>,
|
|
137
|
-
): string {
|
|
138
|
-
const items = schema.items as JsonSchema | undefined;
|
|
139
|
-
if (!items) return "unknown[]";
|
|
140
|
-
|
|
141
|
-
// Tuple form
|
|
142
|
-
if (Array.isArray(items)) {
|
|
143
|
-
const tupleTypes = items.map((s: JsonSchema) =>
|
|
144
|
-
jsonSchemaToTypeString(s, definitions, depth + 1, seen),
|
|
145
|
-
);
|
|
146
|
-
return `[${tupleTypes.join(", ")}]`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const itemType = jsonSchemaToTypeString(items, definitions, depth + 1, seen);
|
|
150
|
-
return `${itemType}[]`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Ensure property name is a valid JS identifier, quote otherwise. */
|
|
154
|
-
function safeName(name: string): string {
|
|
155
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Sanitize a tool name to be a valid JS identifier. */
|
|
159
|
-
export function sanitizeToolName(name: string): string {
|
|
160
|
-
return name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Generate TypeScript type declarations for a set of MCP tools.
|
|
165
|
-
*
|
|
166
|
-
* Produces a `declare const codemode: { ... }` block with type-safe
|
|
167
|
-
* method signatures the model can use when writing code.
|
|
168
|
-
*/
|
|
169
|
-
export function generateTypeHints(tools: McpTool[]): string {
|
|
170
|
-
if (tools.length === 0) return "";
|
|
171
|
-
|
|
172
|
-
const methods: string[] = [];
|
|
173
|
-
|
|
174
|
-
for (const tool of tools) {
|
|
175
|
-
const safeName = sanitizeToolName(tool.name);
|
|
176
|
-
const inputSchema = tool.inputSchema as JsonSchema;
|
|
177
|
-
const outputSchema = tool.outputSchema as JsonSchema | undefined;
|
|
178
|
-
const definitions = (inputSchema.$defs ??
|
|
179
|
-
inputSchema.definitions ??
|
|
180
|
-
outputSchema?.$defs ??
|
|
181
|
-
outputSchema?.definitions) as Record<string, JsonSchema> | undefined;
|
|
182
|
-
|
|
183
|
-
// Build input type
|
|
184
|
-
const inputType = generateInputType(safeName, inputSchema, definitions);
|
|
185
|
-
|
|
186
|
-
// Build output type
|
|
187
|
-
const outputType = outputSchema ? jsonSchemaToTypeString(outputSchema, definitions) : "unknown";
|
|
188
|
-
|
|
189
|
-
// Build JSDoc
|
|
190
|
-
const jsdoc = buildJsDoc(tool, inputSchema);
|
|
191
|
-
|
|
192
|
-
methods.push(`${jsdoc} ${safeName}: (input: ${inputType}) => Promise<${outputType}>;`);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const toolListType = tools.map((t) => `"${escapeStr(t.name)}"`).join(" | ");
|
|
196
|
-
|
|
197
|
-
return [
|
|
198
|
-
"// Code mode type hints — auto-generated from MCP tool schemas",
|
|
199
|
-
"// Available tools are accessed via the `codemode` namespace",
|
|
200
|
-
"",
|
|
201
|
-
`declare const codemode: {`,
|
|
202
|
-
` /** List all available code mode tool names. */`,
|
|
203
|
-
` listTools: () => Promise<(${toolListType})[]>;`,
|
|
204
|
-
` /** Get full type information for specific tools. */`,
|
|
205
|
-
` describeTools: (names: string[]) => Promise<string>;`,
|
|
206
|
-
...methods.map((m) => m),
|
|
207
|
-
`};`,
|
|
208
|
-
].join("\n");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function generateInputType(
|
|
212
|
-
_toolSafeName: string,
|
|
213
|
-
inputSchema: JsonSchema,
|
|
214
|
-
definitions: Record<string, JsonSchema> | undefined,
|
|
215
|
-
): string {
|
|
216
|
-
const properties = inputSchema.properties as Record<string, JsonSchema> | undefined;
|
|
217
|
-
if (!properties || Object.keys(properties).length === 0) {
|
|
218
|
-
return "Record<string, never>";
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Always inline the type — avoids emitting unreferenced named type aliases
|
|
222
|
-
const typeStr = jsonSchemaToTypeString(inputSchema, definitions);
|
|
223
|
-
return typeStr;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function buildJsDoc(tool: McpTool, inputSchema: JsonSchema): string {
|
|
227
|
-
const lines: string[] = [" /**"];
|
|
228
|
-
|
|
229
|
-
if (tool.description) {
|
|
230
|
-
lines.push(` * ${tool.description}`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const properties = inputSchema.properties as Record<string, JsonSchema> | undefined;
|
|
234
|
-
if (properties) {
|
|
235
|
-
for (const [key, propSchema] of Object.entries(properties)) {
|
|
236
|
-
const desc = propSchema.description as string | undefined;
|
|
237
|
-
if (desc) {
|
|
238
|
-
lines.push(` * @param input.${key} - ${desc}`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
lines.push(" */");
|
|
244
|
-
return lines.join("\n") + "\n";
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function escapeStr(s: string): string {
|
|
248
|
-
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ");
|
|
249
|
-
}
|
package/src/docker-e2e.test.ts
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
parseJsonlLine,
|
|
4
|
-
extractAssistantText,
|
|
5
|
-
extractUsage,
|
|
6
|
-
extractModel,
|
|
7
|
-
buildDockerArgs,
|
|
8
|
-
} from "./docker-e2e.js";
|
|
9
|
-
|
|
10
|
-
describe("parseJsonlLine", () => {
|
|
11
|
-
it("parses valid JSON event", () => {
|
|
12
|
-
const line = '{"type":"message_start","message":{"role":"assistant"}}';
|
|
13
|
-
const result = parseJsonlLine(line);
|
|
14
|
-
expect(result).not.toBeNull();
|
|
15
|
-
expect(result?.type).toBe("message_start");
|
|
16
|
-
expect(result?.raw).toBe(line);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("returns raw type for non-JSON", () => {
|
|
20
|
-
const result = parseJsonlLine("some plain text output");
|
|
21
|
-
expect(result).not.toBeNull();
|
|
22
|
-
expect(result?.type).toBe("raw");
|
|
23
|
-
expect(result?.raw).toBe("some plain text output");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("returns null for empty lines", () => {
|
|
27
|
-
expect(parseJsonlLine("")).toBeNull();
|
|
28
|
-
expect(parseJsonlLine(" ")).toBeNull();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("defaults to unknown type when type field missing", () => {
|
|
32
|
-
const result = parseJsonlLine('{"data":"hello"}');
|
|
33
|
-
expect(result?.type).toBe("unknown");
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("extractAssistantText", () => {
|
|
38
|
-
it("extracts text from message_end events", () => {
|
|
39
|
-
const log = [
|
|
40
|
-
{
|
|
41
|
-
type: "message_end",
|
|
42
|
-
timestamp: 0,
|
|
43
|
-
raw: JSON.stringify({
|
|
44
|
-
type: "message_end",
|
|
45
|
-
message: {
|
|
46
|
-
role: "assistant",
|
|
47
|
-
content: [{ type: "text", text: "Hello world" }],
|
|
48
|
-
},
|
|
49
|
-
}),
|
|
50
|
-
},
|
|
51
|
-
];
|
|
52
|
-
expect(extractAssistantText(log)).toBe("Hello world");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("concatenates multiple assistant messages", () => {
|
|
56
|
-
const makeEnd = (text: string) => ({
|
|
57
|
-
type: "message_end",
|
|
58
|
-
timestamp: 0,
|
|
59
|
-
raw: JSON.stringify({
|
|
60
|
-
type: "message_end",
|
|
61
|
-
message: {
|
|
62
|
-
role: "assistant",
|
|
63
|
-
content: [{ type: "text", text }],
|
|
64
|
-
},
|
|
65
|
-
}),
|
|
66
|
-
});
|
|
67
|
-
const log = [makeEnd("First"), makeEnd("Second")];
|
|
68
|
-
expect(extractAssistantText(log)).toBe("First\nSecond");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("ignores non-assistant messages", () => {
|
|
72
|
-
const log = [
|
|
73
|
-
{
|
|
74
|
-
type: "message_end",
|
|
75
|
-
timestamp: 0,
|
|
76
|
-
raw: JSON.stringify({
|
|
77
|
-
type: "message_end",
|
|
78
|
-
message: {
|
|
79
|
-
role: "user",
|
|
80
|
-
content: [{ type: "text", text: "ignored" }],
|
|
81
|
-
},
|
|
82
|
-
}),
|
|
83
|
-
},
|
|
84
|
-
];
|
|
85
|
-
expect(extractAssistantText(log)).toBe("");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("ignores non-message_end events", () => {
|
|
89
|
-
const log = [{ type: "message_start", timestamp: 0, raw: "{}" }];
|
|
90
|
-
expect(extractAssistantText(log)).toBe("");
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("extractUsage", () => {
|
|
95
|
-
it("accumulates usage from assistant messages", () => {
|
|
96
|
-
const log = [
|
|
97
|
-
{
|
|
98
|
-
type: "message_end",
|
|
99
|
-
timestamp: 0,
|
|
100
|
-
raw: JSON.stringify({
|
|
101
|
-
type: "message_end",
|
|
102
|
-
message: {
|
|
103
|
-
role: "assistant",
|
|
104
|
-
content: [],
|
|
105
|
-
usage: {
|
|
106
|
-
input: 100,
|
|
107
|
-
output: 50,
|
|
108
|
-
cacheRead: 10,
|
|
109
|
-
cacheWrite: 5,
|
|
110
|
-
cost: { total: 0.01 },
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
}),
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
type: "message_end",
|
|
117
|
-
timestamp: 0,
|
|
118
|
-
raw: JSON.stringify({
|
|
119
|
-
type: "message_end",
|
|
120
|
-
message: {
|
|
121
|
-
role: "assistant",
|
|
122
|
-
content: [],
|
|
123
|
-
usage: {
|
|
124
|
-
input: 200,
|
|
125
|
-
output: 100,
|
|
126
|
-
cacheRead: 20,
|
|
127
|
-
cacheWrite: 10,
|
|
128
|
-
cost: { total: 0.02 },
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
}),
|
|
132
|
-
},
|
|
133
|
-
];
|
|
134
|
-
const usage = extractUsage(log);
|
|
135
|
-
expect(usage.turns).toBe(2);
|
|
136
|
-
expect(usage.input).toBe(300);
|
|
137
|
-
expect(usage.output).toBe(150);
|
|
138
|
-
expect(usage.cacheRead).toBe(30);
|
|
139
|
-
expect(usage.cacheWrite).toBe(15);
|
|
140
|
-
expect(usage.cost).toBeCloseTo(0.03);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("returns zeroes for empty log", () => {
|
|
144
|
-
const usage = extractUsage([]);
|
|
145
|
-
expect(usage.turns).toBe(0);
|
|
146
|
-
expect(usage.input).toBe(0);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe("extractModel", () => {
|
|
151
|
-
it("extracts model from message_end", () => {
|
|
152
|
-
const log = [
|
|
153
|
-
{
|
|
154
|
-
type: "message_end",
|
|
155
|
-
timestamp: 0,
|
|
156
|
-
raw: JSON.stringify({
|
|
157
|
-
type: "message_end",
|
|
158
|
-
message: { model: "claude-sonnet-4-5", role: "assistant", content: [] },
|
|
159
|
-
}),
|
|
160
|
-
},
|
|
161
|
-
];
|
|
162
|
-
expect(extractModel(log)).toBe("claude-sonnet-4-5");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("returns undefined for empty log", () => {
|
|
166
|
-
expect(extractModel([])).toBeUndefined();
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe("buildDockerArgs", () => {
|
|
171
|
-
it("builds basic args", () => {
|
|
172
|
-
const args = buildDockerArgs("my-image", {
|
|
173
|
-
task: "Fix the bug",
|
|
174
|
-
token: "ghp_abc123",
|
|
175
|
-
});
|
|
176
|
-
expect(args).toEqual([
|
|
177
|
-
"run",
|
|
178
|
-
"--rm",
|
|
179
|
-
"-e",
|
|
180
|
-
"GITHUB_TOKEN=ghp_abc123",
|
|
181
|
-
"my-image",
|
|
182
|
-
"Fix the bug",
|
|
183
|
-
]);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("includes model env var when specified", () => {
|
|
187
|
-
const args = buildDockerArgs("my-image", {
|
|
188
|
-
task: "Fix it",
|
|
189
|
-
token: "ghp_abc",
|
|
190
|
-
model: "claude-opus-4-5",
|
|
191
|
-
});
|
|
192
|
-
expect(args).toContain("-e");
|
|
193
|
-
expect(args).toContain("PI_MODEL=claude-opus-4-5");
|
|
194
|
-
});
|
|
195
|
-
});
|