@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/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
+ }