@teddysc/mcp-codemode 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/.claude/settings.local.json +14 -0
- package/CLAUDE.md +115 -0
- package/Makefile +15 -0
- package/bun.lock +206 -0
- package/mcp-codemode.toml.example +33 -0
- package/package.json +21 -0
- package/src/cli.ts +249 -0
- package/src/config.ts +236 -0
- package/src/executor.ts +116 -0
- package/src/server.ts +550 -0
- package/src/types.ts +216 -0
- package/tsconfig.json +14 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type generation for MCP tool schemas.
|
|
3
|
+
* Converts MCP JSON Schema tool descriptors into TypeScript type strings
|
|
4
|
+
* that the LLM can understand (the "codemode" system prompt injection).
|
|
5
|
+
*
|
|
6
|
+
* Adapted from @cloudflare/codemode/src/types.ts — JSON Schema path only,
|
|
7
|
+
* since Playwright MCP uses JSON Schema (not Zod).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type JSONSchema = {
|
|
11
|
+
type?: string | string[];
|
|
12
|
+
properties?: Record<string, JSONSchema>;
|
|
13
|
+
required?: string[];
|
|
14
|
+
description?: string;
|
|
15
|
+
enum?: unknown[];
|
|
16
|
+
const?: unknown;
|
|
17
|
+
items?: JSONSchema;
|
|
18
|
+
prefixItems?: JSONSchema[];
|
|
19
|
+
anyOf?: JSONSchema[];
|
|
20
|
+
oneOf?: JSONSchema[];
|
|
21
|
+
allOf?: JSONSchema[];
|
|
22
|
+
$ref?: string;
|
|
23
|
+
format?: string;
|
|
24
|
+
nullable?: boolean; // OpenAPI 3.0
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ToolDescriptor = {
|
|
28
|
+
description?: string;
|
|
29
|
+
inputSchema?: JSONSchema;
|
|
30
|
+
execute?: (...args: unknown[]) => Promise<unknown>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const JS_RESERVED_WORDS = new Set([
|
|
34
|
+
"break", "case", "catch", "continue", "debugger", "default", "delete",
|
|
35
|
+
"do", "else", "finally", "for", "function", "if", "in", "instanceof",
|
|
36
|
+
"new", "return", "switch", "this", "throw", "try", "typeof", "var",
|
|
37
|
+
"void", "while", "with", "class", "const", "enum", "export", "extends",
|
|
38
|
+
"import", "super", "implements", "interface", "let", "package", "private",
|
|
39
|
+
"protected", "public", "static", "yield", "null", "true", "false"
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Converts an MCP tool name (which may contain hyphens, dots, etc.) into
|
|
44
|
+
* a valid JavaScript identifier. Matches codemode's sanitizeToolName exactly.
|
|
45
|
+
*/
|
|
46
|
+
export function sanitizeToolName(name: string): string {
|
|
47
|
+
let result = name.replace(/[-.\s]/g, "_").replace(/[^a-zA-Z0-9_]/g, "");
|
|
48
|
+
if (!result) return "_";
|
|
49
|
+
if (/^[0-9]/.test(result)) result = "_" + result;
|
|
50
|
+
if (JS_RESERVED_WORDS.has(result)) result = result + "_";
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Converts a sanitized tool name to a PascalCase type name. */
|
|
55
|
+
function toTypeName(sanitized: string): string {
|
|
56
|
+
return sanitized
|
|
57
|
+
.split("_")
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
60
|
+
.join("");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Ensures a property name is a valid identifier; otherwise quotes it. */
|
|
64
|
+
function safePropertyName(key: string): string {
|
|
65
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const MAX_DEPTH = 15;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Recursively converts a JSON Schema to a TypeScript type string.
|
|
72
|
+
*/
|
|
73
|
+
export function jsonSchemaToTypeString(
|
|
74
|
+
schema: JSONSchema,
|
|
75
|
+
indent = "",
|
|
76
|
+
depth = 0
|
|
77
|
+
): string {
|
|
78
|
+
if (depth > MAX_DEPTH) return "unknown";
|
|
79
|
+
if (!schema || typeof schema !== "object") return "unknown";
|
|
80
|
+
|
|
81
|
+
// anyOf / oneOf → union
|
|
82
|
+
if (schema.anyOf || schema.oneOf) {
|
|
83
|
+
const variants = (schema.anyOf ?? schema.oneOf)!;
|
|
84
|
+
const parts = variants.map(v => jsonSchemaToTypeString(v, indent, depth + 1));
|
|
85
|
+
return parts.join(" | ");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// allOf → intersection
|
|
89
|
+
if (schema.allOf) {
|
|
90
|
+
const parts = schema.allOf.map(v =>
|
|
91
|
+
jsonSchemaToTypeString(v, indent, depth + 1)
|
|
92
|
+
);
|
|
93
|
+
return parts.join(" & ");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// const → literal
|
|
97
|
+
if ("const" in schema && schema.const !== undefined) {
|
|
98
|
+
return JSON.stringify(schema.const);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// enum → literal union
|
|
102
|
+
if (schema.enum !== undefined) {
|
|
103
|
+
return schema.enum.map(v => JSON.stringify(v)).join(" | ");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const rawType = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
107
|
+
const nullable = schema.nullable === true ||
|
|
108
|
+
(Array.isArray(schema.type) && schema.type.includes("null"));
|
|
109
|
+
|
|
110
|
+
let typeStr: string;
|
|
111
|
+
|
|
112
|
+
switch (rawType) {
|
|
113
|
+
case "string":
|
|
114
|
+
typeStr = "string";
|
|
115
|
+
break;
|
|
116
|
+
case "number":
|
|
117
|
+
case "integer":
|
|
118
|
+
typeStr = "number";
|
|
119
|
+
break;
|
|
120
|
+
case "boolean":
|
|
121
|
+
typeStr = "boolean";
|
|
122
|
+
break;
|
|
123
|
+
case "null":
|
|
124
|
+
return "null";
|
|
125
|
+
case "array": {
|
|
126
|
+
if (schema.prefixItems) {
|
|
127
|
+
// Tuple
|
|
128
|
+
const items = schema.prefixItems.map(s =>
|
|
129
|
+
jsonSchemaToTypeString(s, indent + " ", depth + 1)
|
|
130
|
+
);
|
|
131
|
+
typeStr = `[${items.join(", ")}]`;
|
|
132
|
+
} else {
|
|
133
|
+
let itemType = schema.items
|
|
134
|
+
? jsonSchemaToTypeString(schema.items, indent, depth + 1)
|
|
135
|
+
: "unknown";
|
|
136
|
+
// Wrap union/intersection items in parens so `(A | B)[]` is valid
|
|
137
|
+
if (itemType.includes(" | ") || itemType.includes(" & ")) {
|
|
138
|
+
itemType = `(${itemType})`;
|
|
139
|
+
}
|
|
140
|
+
typeStr = `${itemType}[]`;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "object": {
|
|
145
|
+
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
|
146
|
+
typeStr = "Record<string, unknown>";
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
const required = new Set(schema.required ?? []);
|
|
150
|
+
const innerIndent = indent + " ";
|
|
151
|
+
const props = Object.entries(schema.properties).map(([key, val]) => {
|
|
152
|
+
const optional = required.has(key) ? "" : "?";
|
|
153
|
+
const desc = val.description
|
|
154
|
+
? `${innerIndent}/** ${val.description.replace(/\*\//g, "* /")} */\n`
|
|
155
|
+
: "";
|
|
156
|
+
const propType = jsonSchemaToTypeString(val, innerIndent, depth + 1);
|
|
157
|
+
return `${desc}${innerIndent}${safePropertyName(key)}${optional}: ${propType}`;
|
|
158
|
+
});
|
|
159
|
+
typeStr = `{\n${props.join(";\n")};\n${indent}}`;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
default:
|
|
163
|
+
typeStr = "unknown";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return nullable ? `${typeStr} | null` : typeStr;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Generates TypeScript type definitions for all tools, producing the
|
|
171
|
+
* "codemode" declaration block that gets injected into the LLM system prompt.
|
|
172
|
+
*
|
|
173
|
+
* Example output:
|
|
174
|
+
* type BrowserNavigateInput = { url: string; timeout?: number };
|
|
175
|
+
* declare const codemode: {
|
|
176
|
+
* browser_navigate: (input: BrowserNavigateInput) => Promise<unknown>;
|
|
177
|
+
* };
|
|
178
|
+
*/
|
|
179
|
+
export function generateTypes(tools: Record<string, ToolDescriptor>): string {
|
|
180
|
+
const typeLines: string[] = [];
|
|
181
|
+
const codemodeProps: string[] = [];
|
|
182
|
+
|
|
183
|
+
for (const [rawName, tool] of Object.entries(tools)) {
|
|
184
|
+
const sanitized = sanitizeToolName(rawName);
|
|
185
|
+
const typeName = toTypeName(sanitized) + "Input";
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const schema = tool.inputSchema;
|
|
189
|
+
if (schema && Object.keys(schema).length > 0) {
|
|
190
|
+
const typeStr = jsonSchemaToTypeString(schema, "");
|
|
191
|
+
typeLines.push(`type ${typeName} = ${typeStr};`);
|
|
192
|
+
} else {
|
|
193
|
+
typeLines.push(`type ${typeName} = Record<string, unknown>;`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const descComment = tool.description
|
|
197
|
+
? ` /** ${tool.description.replace(/\*\//g, "* /")} */\n`
|
|
198
|
+
: "";
|
|
199
|
+
codemodeProps.push(
|
|
200
|
+
`${descComment} ${sanitized}: (input: ${typeName}) => Promise<unknown>`
|
|
201
|
+
);
|
|
202
|
+
} catch {
|
|
203
|
+
// Don't let one bad tool break the rest
|
|
204
|
+
typeLines.push(`type ${typeName} = unknown;`);
|
|
205
|
+
codemodeProps.push(` ${sanitized}: (input: ${typeName}) => Promise<unknown>`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return [
|
|
210
|
+
...typeLines,
|
|
211
|
+
"",
|
|
212
|
+
"declare const codemode: {",
|
|
213
|
+
codemodeProps.join(";\n"),
|
|
214
|
+
"};",
|
|
215
|
+
].join("\n");
|
|
216
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"outDir": "dist"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|