webmcp-codegen 0.2.0 → 0.3.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/README.md +45 -36
- package/dist/{chunk-WYGVTIGI.js → chunk-3LTHWIAP.js} +37 -4
- package/dist/chunk-3LTHWIAP.js.map +1 -0
- package/dist/{chunk-BIKKPCRT.js → chunk-FWSATV7C.js} +26 -2
- package/dist/chunk-FWSATV7C.js.map +1 -0
- package/dist/chunk-JVBVTHZ7.js +235 -0
- package/dist/chunk-JVBVTHZ7.js.map +1 -0
- package/dist/{chunk-5L4KN6F4.js → chunk-KSQMJERY.js} +3 -3
- package/dist/chunk-KSQMJERY.js.map +1 -0
- package/dist/{chunk-R3DEBBQ3.js → chunk-MJQ5B6HB.js} +111 -21
- package/dist/chunk-MJQ5B6HB.js.map +1 -0
- package/dist/chunk-TGOJ3HUE.js +382 -0
- package/dist/chunk-TGOJ3HUE.js.map +1 -0
- package/dist/cli.js +263 -102
- package/dist/cli.js.map +1 -1
- package/dist/generators/index.d.ts +3 -3
- package/dist/generators/index.js +2 -2
- package/dist/index.d.ts +15 -5
- package/dist/index.js +3 -3
- package/dist/server-OR4IMRQ5.js +638 -0
- package/dist/server-OR4IMRQ5.js.map +1 -0
- package/dist/sources/index.d.ts +1 -1
- package/dist/sources/index.js +3 -3
- package/dist/{types-Bf5MxWeH.d.ts → types-DWUum51l.d.ts} +56 -3
- package/package.json +1 -1
- package/dist/chunk-5L4KN6F4.js.map +0 -1
- package/dist/chunk-BIKKPCRT.js.map +0 -1
- package/dist/chunk-GDJDVR4E.js +0 -251
- package/dist/chunk-GDJDVR4E.js.map +0 -1
- package/dist/chunk-R3DEBBQ3.js.map +0 -1
- package/dist/chunk-WYGVTIGI.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generators/js.ts","../src/generators/js-templates.ts"],"sourcesContent":["/**\n * The `js` generator, named after what lands in your repo: plain JavaScript/\n * TypeScript files that call the spec's imperative API\n * (`document.modelContext.registerTool`).\n *\n * Output layout for `js({ outDir: \"./src/webmcp\" })`:\n *\n * src/webmcp/\n * ├── runtime.webmcp.ts ← fully generated, never edit\n * ├── index.ts ← fully generated, registers everything\n * ├── get-order-status.webmcp.ts ← generated contract + YOUR execute()\n * └── ...\n *\n * Each per-tool file has two regions, divided by marker comments:\n *\n * generated region schema, input type, tool definition, register()\n * ── end generated ── everything below survives regeneration\n * your region execute(), scaffolded once, then owned by you\n *\n * This file contains only the *file mechanics*: which files exist, and how to\n * update them without destroying hand-written code. The text of the generated\n * code itself lives in js-templates.ts, keeping \"what the output looks like\"\n * separate from \"how files get written\" is what keeps both readable.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { GeneratedFile, ReviewedTool, ToolGenerator } from \"../types.js\";\nimport {\n barrelSource,\n generatedRegion,\n ownedRegionScaffold,\n runtimeSource,\n} from \"./js-templates.js\";\n\nexport interface JsGeneratorOptions {\n /** Where the tool files go, relative to the project root. */\n outDir: string;\n}\n\n/**\n * The marker lines that split a per-tool file in two. They are the merge\n * contract: we may rewrite everything up to and including GENERATED_END,\n * and we must never touch anything after it. js-templates.ts imports these\n * so the marker text is defined in exactly one place.\n */\nexport const GENERATED_START = \"// ─── webmcp-codegen: generated. Do not edit this region. ───\";\nexport const GENERATED_END =\n \"// ─── webmcp-codegen: end generated. Your code below survives regeneration. ───\";\n\n/** Create the `js` generator for the config's `generate` array. */\nexport function js(options: JsGeneratorOptions): ToolGenerator {\n return {\n kind: \"js\",\n outDir: options.outDir,\n async generate(tools, cwd) {\n const outDir = join(cwd, options.outDir);\n const files: GeneratedFile[] = [];\n\n // The runtime and the barrel are regenerated wholesale every run;\n // their headers say \"do not edit\", and we mean it.\n files.push(await plainFile(join(outDir, \"runtime.webmcp.ts\"), runtimeSource()));\n files.push(await plainFile(join(outDir, \"index.ts\"), barrelSource(tools)));\n\n for (const tool of tools) {\n files.push(await toolFile(tool, outDir));\n }\n return files;\n },\n };\n}\n\n/** A fully-generated file: create if missing, overwrite if changed, skip if same. */\nasync function plainFile(path: string, contents: string): Promise<GeneratedFile> {\n try {\n const existing = await readFile(path, \"utf8\");\n return { path, contents, action: existing === contents ? \"unchanged\" : \"update\" };\n } catch {\n return { path, contents, action: \"create\" };\n }\n}\n\n/**\n * Build (or merge) one per-tool file. The only I/O here is reading the\n * existing file to check for a hand-written region worth keeping.\n */\nasync function toolFile(tool: ReviewedTool, outDir: string): Promise<GeneratedFile> {\n const path = join(outDir, `${tool.name}.webmcp.ts`);\n const head = generatedRegion(tool);\n\n let existing: string | undefined;\n try {\n existing = await readFile(path, \"utf8\");\n } catch {\n // No file yet: brand new tool, so we also lay down the execute() scaffold.\n return { path, contents: `${head}\\n${ownedRegionScaffold(tool)}`, action: \"create\" };\n }\n\n const markerIndex = existing.indexOf(GENERATED_END);\n if (markerIndex === -1) {\n // Someone removed the markers or hand-wrote this path from scratch.\n // Never clobber their work: report a conflict and let the pipeline put\n // our version in a `.new` sibling for a human to merge.\n return { path, contents: existing, action: \"unchanged\", conflict: `${path}.new` };\n }\n\n // Keep everything the developer wrote below the marker, word for word.\n const preservedTail = existing.slice(markerIndex + GENERATED_END.length);\n const contents = head + preservedTail;\n return { path, contents, action: contents === existing ? \"unchanged\" : \"update\" };\n}\n","/**\n * The text of the code the `js` generator writes.\n *\n * Heads up before reading on: every function here returns *TypeScript source\n * code as a string*. When you see `export const ...` inside quotes, that's\n * the output a user's repo will contain, not this module's own logic.\n * Building output from arrays of lines (rather than nested template strings)\n * keeps the quoting readable; the only escaping left is for code samples\n * inside the generated comments.\n *\n * Three kinds of output are built here:\n * - generatedRegion() the per-tool contract (regenerated freely)\n * - ownedRegionScaffold() the execute() body (written once, then owned)\n * - runtimeSource() / barrelSource() fully-generated support files\n *\n * The contract the output fulfills: read tools work out of the box (a real\n * request to the endpoint), mutation tools start disabled with the working\n * code generated but commented out, and the user-confirmation step for\n * mutations lives in the generated region so it cannot be edited away.\n */\n\nimport { jsonSchemaToTs, pascalCase } from \"../schema.js\";\nimport type { ReviewedTool } from \"../types.js\";\nimport { GENERATED_END, GENERATED_START } from \"./js.js\";\n\n/**\n * Everything above the end-marker of a per-tool file: the parts that must\n * track the API contract exactly: name, description, schema, input type,\n * hints, and the register() wrapper.\n */\nexport function generatedRegion(tool: ReviewedTool): string {\n const pascal = pascalCase(tool.name);\n const camel = lowercaseFirst(pascal);\n const schemaJson = JSON.stringify(tool.inputSchema, null, 2);\n const inputType = jsonSchemaToTs(tool.inputSchema, undefined);\n const mutates = tool.riskTier !== \"safe-read\";\n\n // The imports cover what this file's regions use: the generated register()\n // and the owned execute() scaffold. A developer who replaces the scaffold\n // with their own API client can trim the imports they stop using.\n const runtimeImports = [\n \"getModelContext\",\n ...(mutates ? [\"requestUserConfirmation\"] : []),\n \"callApi\",\n \"toolResult\",\n ...(tool.enabledByDefault ? [] : [\"toolDisabled\"]),\n ].join(\", \");\n\n const registerBody = mutates\n ? [\n ` await modelContext.registerTool(`,\n ` {`,\n ` ...${camel}Tool,`,\n ` execute: async (input) => {`,\n ` // This tool changes things, so the user is always asked first. The`,\n ` // confirmation lives in the generated region: it cannot be edited away.`,\n ` const confirmed = await requestUserConfirmation(`,\n ` ${JSON.stringify(`Allow the agent to: ${tool.description}`)},`,\n ` );`,\n ` if (!confirmed) {`,\n ` return {`,\n ` content: [{ type: \"text\", text: \"The user declined this action.\" }],`,\n ` isError: true,`,\n ` };`,\n ` }`,\n ` // The browser has already validated the agent's input against the schema.`,\n ` return execute${pascal}(input as ${tool.inputTypeName});`,\n ` },`,\n ` },`,\n ` { signal },`,\n ` );`,\n ]\n : [\n ` await modelContext.registerTool(`,\n ` {`,\n ` ...${camel}Tool,`,\n ` // The browser has already validated the agent's input against the schema.`,\n ` execute: (input) => execute${pascal}(input as ${tool.inputTypeName}),`,\n ` },`,\n ` { signal },`,\n ` );`,\n ];\n\n return [\n `import { ${runtimeImports} } from \"./runtime.webmcp\";`,\n ``,\n GENERATED_START,\n `/**`,\n ` * ${tool.description}`,\n ` *`,\n ` * Source: ${tool.source.ref} (${tool.source.kind}). Risk: ${tool.riskTier}.`,\n ` * Starts ${tool.enabledByDefault ? \"enabled\" : \"disabled\"} (see execute${pascal} below).`,\n ` * Regenerate with: npx webmcp-codegen generate`,\n ` */`,\n ``,\n `/** The exact contract advertised to the agent. Derived from the API spec. Do not hand-edit. */`,\n `export const ${camel}InputSchema = ${schemaJson};`,\n ``,\n `/** What \\`execute\\` receives. The browser validates agent input against the schema above. */`,\n `export type ${tool.inputTypeName} = ${inputType};`,\n ``,\n `/** Safety hints computed by webmcp-codegen. Informational metadata for hosts and UIs. */`,\n `export const ${camel}Hints = ${JSON.stringify(tool.hints)} as const;`,\n ``,\n `/** The tool definition, minus \\`execute\\` (which is yours, below the marker). */`,\n `export const ${camel}Tool = {`,\n ` name: ${JSON.stringify(tool.name)},`,\n ` description: ${JSON.stringify(tool.description)},`,\n ` inputSchema: ${camel}InputSchema,`,\n `};`,\n ``,\n `/**`,\n ` * Register this tool with WebMCP. Call it once on page load, or use`,\n ` * registerAllTools() from the generated index.ts.`,\n ` *`,\n ` * Pass an AbortSignal to unregister later: controller.abort().`,\n ` */`,\n `export async function register${pascal}(signal?: AbortSignal): Promise<void> {`,\n ` const modelContext = getModelContext();`,\n ...registerBody,\n `}`,\n ``,\n GENERATED_END,\n ].join(\"\\n\");\n}\n\n/**\n * The scaffold below the marker, written exactly once (when the file is\n * first created). After that the developer owns it and regeneration never\n * touches it. That promise is the whole reason the marker split exists.\n *\n * The scaffold is real code, not a TODO: the spec knows the method, the\n * path, and which fields go where, so the default implementation actually\n * calls the endpoint from the page, with the signed-in user's session.\n * Reads are born working; mutations are born disabled (the working code is\n * right there, commented out, one deliberate edit away from live).\n */\nexport function ownedRegionScaffold(tool: ReviewedTool): string {\n const pascal = pascalCase(tool.name);\n const call = requestCall(tool);\n const lines: string[] = [\n ``,\n `/**`,\n ` * What actually happens when the agent calls \"${tool.name}\".`,\n ` *`,\n ` * Default implementation: calls ${tool.source.ref} from this page, with the`,\n ` * signed-in user's session. Replace it with your app's own API client`,\n ` * whenever you like; the contract above never changes.`,\n ];\n\n if (tool.serverUrl) {\n lines.push(\n ` *`,\n ` * Your spec lists the API at ${tool.serverUrl}. If the app and the API`,\n ` * are on different hosts, pass the full URL to callApi instead.`,\n );\n }\n\n if (tool.riskTier !== \"safe-read\") {\n lines.push(\n ` *`,\n ` * This tool is ${tool.riskTier}: it ${\n tool.riskTier === \"destructive-confirm\" ? \"cannot easily be undone\" : \"changes things\"\n }.`,\n ` * The user is asked to confirm every call (built into the generated region).`,\n );\n }\n lines.push(` */`);\n\n if (tool.piiInOutput.length > 0) {\n lines.push(\n `//`,\n `// ⚠ webmcp-codegen flagged these response fields as likely PII: ${tool.piiInOutput.join(\", \")}.`,\n `// Everything you return reaches the agent. Leave those fields out of what you`,\n `// return unless the agent genuinely needs them, and say so in a comment if you keep them.`,\n );\n }\n\n if (tool.enabledByDefault) {\n lines.push(\n `export async function execute${pascal}(input: ${tool.inputTypeName}) {`,\n ` ${call}`,\n ` return toolResult(data);`,\n `}`,\n );\n } else {\n lines.push(\n `export async function execute${pascal}(input: ${tool.inputTypeName}) {`,\n ` // This tool starts disabled: it ${\n tool.endpointRole === \"endpoint\"\n ? \"changes things\"\n : `wraps an ${tool.endpointRole} endpoint`\n }. Agents can see it, and calling it tells`,\n ` // them it is disabled. To enable it, delete the line below and uncomment the code.`,\n ` return toolDisabled(\"${tool.name}.webmcp.ts\");`,\n ``,\n ` // ${call}`,\n ` // return toolResult(data);`,\n `}`,\n );\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * The one working request line inside a scaffold, built from what the spec\n * knows: the path template becomes a template literal, query params become\n * the search string, body fields become the JSON body.\n *\n * \"/pets/{id}\" + DELETE → const data = await callApi(`/pets/${input.id}`, { method: \"DELETE\" });\n *\n * When the source carries no route information, we fall back to an honest\n * TODO instead of inventing a URL.\n */\nfunction requestCall(tool: ReviewedTool): string {\n if (!tool.httpMethod || !tool.pathTemplate || !tool.paramLocations) {\n return `const data = null; // TODO: call your app's existing code here.`;\n }\n\n const { path: pathParams, query: queryParams, body: bodyParams } = tool.paramLocations;\n\n // \"/pets/{id}\" → `/pets/${input.id}`. Params the schema knows by name.\n let pathExpr = `\\`${tool.pathTemplate.replace(/\\{([^}]+)\\}/g, (_m, param: string) => `\\${${inputRef(param)}}`)}\\``;\n if (pathParams.length === 0) pathExpr = JSON.stringify(tool.pathTemplate);\n\n const options: string[] = [`method: ${JSON.stringify(tool.httpMethod)}`];\n if (queryParams.length > 0) {\n const entries = queryParams.map((name) => `${safeKey(name)}: ${inputRef(name)}`).join(\", \");\n options.push(`query: { ${entries} }`);\n }\n if (bodyParams.length > 0) {\n if (bodyParams.length === 1 && bodyParams[0] === \"body\") {\n // A non-object request body arrives as a single \"body\" field.\n options.push(`body: input.body`);\n } else {\n const entries = bodyParams.map((name) => `${safeKey(name)}: ${inputRef(name)}`).join(\", \");\n options.push(`body: { ${entries} }`);\n }\n }\n\n return `const data = await callApi(${pathExpr}, { ${options.join(\", \")} });`;\n}\n\n/**\n * How generated code reads a field off `input`. Dot access for identifier\n * names (\"input.limit\"), bracket access for the rest (\"input[\"pet-id\"]\").\n */\nfunction inputRef(name: string): string {\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)\n ? `input.${name}`\n : `input[${JSON.stringify(name)}]`;\n}\n\n/** Quote an object key only when it needs it. */\nfunction safeKey(name: string): string {\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name);\n}\n\n/**\n * The shared runtime: the minimal WebMCP browser types plus the helpers the\n * generated files use. Kept tiny on purpose: this is the only browser\n * coupling in the output.\n */\nexport function runtimeSource(): string {\n return `/**\n * Generated by webmcp-codegen. This file is fully regenerated on every run.\n * Do not edit by hand; your changes will be lost.\n */\n\n/** The result shape tools return (same as MCP tool results). */\nexport interface WebMcpToolResult {\n content: { type: \"text\"; text: string }[];\n isError?: boolean;\n [key: string]: unknown;\n}\n\n/** A tool as the browser runtime understands it. */\nexport interface WebMcpToolDefinition {\n name: string;\n description: string;\n inputSchema?: Record<string, unknown>;\n execute: (input: Record<string, unknown>) => unknown | Promise<unknown>;\n}\n\n/** The slice of the WebMCP draft spec the generated code uses. */\nexport interface ModelContext {\n registerTool(\n tool: WebMcpToolDefinition,\n options?: { signal?: AbortSignal },\n ): Promise<void>;\n}\n\n/**\n * Access the page's WebMCP model context, with a helpful error when the\n * browser doesn't have one (rather than an undefined-callsite mystery).\n */\nexport function getModelContext(): ModelContext {\n const modelContext = (document as unknown as { modelContext?: ModelContext }).modelContext;\n if (!modelContext) {\n throw new Error(\n \"WebMCP is not available in this browser. \" +\n \"Enable chrome://flags/#enable-webmcp-testing (Chrome 146+), \" +\n \"or add the WebMCP polyfill to your app.\",\n );\n }\n return modelContext;\n}\n\n/**\n * Call your API from the page. Same origin by default (pass a full URL when\n * the API lives on another host), always with the signed-in user's session\n * cookies. Throws on HTTP errors; returns the parsed JSON body, or raw text\n * when the response is not JSON.\n */\nexport async function callApi(\n path: string,\n options: { method?: string; query?: Record<string, unknown>; body?: unknown } = {},\n): Promise<unknown> {\n const url = new URL(path, window.location.origin);\n for (const [key, value] of Object.entries(options.query ?? {})) {\n if (value !== undefined && value !== null) url.searchParams.set(key, String(value));\n }\n const response = await fetch(url, {\n method: options.method ?? \"GET\",\n credentials: \"include\",\n headers: options.body !== undefined ? { \"content-type\": \"application/json\" } : undefined,\n body: options.body !== undefined ? JSON.stringify(options.body) : undefined,\n });\n if (!response.ok) {\n throw new Error(\"Request failed: \" + response.status + \" \" + response.statusText);\n }\n if (response.status === 204) return null;\n const text = await response.text();\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\n/** Wrap a result in the MCP shape, so tool bodies stay one line. */\nexport function toolResult(data: unknown): WebMcpToolResult {\n return {\n content: [\n { type: \"text\", text: typeof data === \"string\" ? data : JSON.stringify(data, null, 2) },\n ],\n };\n}\n\n/**\n * What a disabled tool tells the agent. The tool stays visible (so the agent\n * knows it exists and can ask the human to enable it) but does nothing.\n */\nexport function toolDisabled(fileName: string): WebMcpToolResult {\n return {\n content: [\n {\n type: \"text\",\n text:\n \"This tool is currently disabled by the app developer. Ask them to enable it \" +\n \"(uncomment the implementation in \" + fileName + \").\",\n },\n ],\n isError: true,\n };\n}\n\n/**\n * Default \"agent proposes, human confirms\" gate for write/destructive tools.\n * Deliberately minimal (window.confirm). Replace it with your app's own\n * dialog when you outgrow it. The point is that the user always gets a say.\n */\nexport function requestUserConfirmation(message: string): Promise<boolean> {\n return Promise.resolve(window.confirm(message));\n}\n`;\n}\n\n/** The barrel: one import that registers every generated tool. */\nexport function barrelSource(tools: ReviewedTool[]): string {\n const imports = tools\n .map((tool) => `import { register${pascalCase(tool.name)} } from \"./${tool.name}.webmcp\";`)\n .join(\"\\n\");\n const names = tools.map((tool) => `register${pascalCase(tool.name)}`).join(\",\\n \");\n\n return `/**\n * Generated by webmcp-codegen. This file is fully regenerated on every run.\n * Import registerAllTools() once at app startup:\n *\n * import { registerAllTools } from \"./webmcp\";\n * await registerAllTools();\n */\n\n${imports}\n\nconst registrations = [\n ${names}\n];\n\n/**\n * Register every generated tool with WebMCP. One tool failing (for example\n * because the page's Permissions-Policy disables tools) never takes the\n * others down with it. The failure is logged and registration continues.\n */\nexport async function registerAllTools(signal?: AbortSignal): Promise<void> {\n for (const register of registrations) {\n try {\n await register(signal);\n } catch (error) {\n console.warn(\"[webmcp-codegen] a tool failed to register:\", error);\n }\n }\n}\n`;\n}\n\n/** \"GetOrderStatus\" → \"getOrderStatus\" (for the generated const names). */\nfunction lowercaseFirst(pascal: string): string {\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n"],"mappings":"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","names":[]}
|
package/dist/cli.js
CHANGED
|
@@ -1,81 +1,181 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
js
|
|
9
|
-
} from "./chunk-GDJDVR4E.js";
|
|
3
|
+
findSpecs,
|
|
4
|
+
loadDataFile,
|
|
5
|
+
resolveSetup,
|
|
6
|
+
saveDataFile
|
|
7
|
+
} from "./chunk-JVBVTHZ7.js";
|
|
10
8
|
import {
|
|
11
|
-
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
9
|
+
runGenerate
|
|
10
|
+
} from "./chunk-MJQ5B6HB.js";
|
|
11
|
+
import "./chunk-TGOJ3HUE.js";
|
|
12
|
+
import "./chunk-3LTHWIAP.js";
|
|
13
|
+
import "./chunk-FWSATV7C.js";
|
|
14
|
+
import "./chunk-KSQMJERY.js";
|
|
15
15
|
|
|
16
16
|
// src/cli.ts
|
|
17
17
|
import { existsSync, watch } from "fs";
|
|
18
|
-
import { writeFile } from "fs/promises";
|
|
19
|
-
import {
|
|
18
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
19
|
+
import { join as join2, relative as relative2 } from "path";
|
|
20
|
+
import "readline/promises";
|
|
20
21
|
import { parseArgs } from "util";
|
|
21
22
|
|
|
22
|
-
// src/
|
|
23
|
-
import {
|
|
24
|
-
import { join, relative } from "path";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
// src/wire.ts
|
|
24
|
+
import { readFile, writeFile } from "fs/promises";
|
|
25
|
+
import { dirname, join, relative } from "path";
|
|
26
|
+
async function planWiring(cwd, app, outDir) {
|
|
27
|
+
switch (app.framework) {
|
|
28
|
+
case "next":
|
|
29
|
+
return planNextWiring(cwd, app, outDir);
|
|
30
|
+
case "vite-react":
|
|
31
|
+
return planViteWiring(cwd, app, outDir);
|
|
32
|
+
default:
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function applyWiring(plan) {
|
|
37
|
+
for (const edit of plan.edits) {
|
|
38
|
+
await writeFile(edit.path, edit.contents, "utf8");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function planNextWiring(cwd, app, outDir) {
|
|
42
|
+
const layoutCandidates = [
|
|
43
|
+
join(cwd, app.dir, "src/app/layout.tsx"),
|
|
44
|
+
join(cwd, app.dir, "src/app/layout.jsx"),
|
|
45
|
+
join(cwd, app.dir, "app/layout.tsx"),
|
|
46
|
+
join(cwd, app.dir, "app/layout.jsx")
|
|
47
|
+
];
|
|
48
|
+
const layoutPath = await firstExisting(layoutCandidates);
|
|
49
|
+
if (!layoutPath) return null;
|
|
50
|
+
const registerPath = join(cwd, outDir, "register.tsx");
|
|
51
|
+
const layout = await readFile(layoutPath, "utf8");
|
|
52
|
+
if (layout.includes("WebMCPRegister")) return { edits: [], alreadyWired: true };
|
|
53
|
+
const importPath = withoutExtension(relative(dirname(layoutPath), registerPath));
|
|
54
|
+
const edits = [
|
|
55
|
+
{
|
|
56
|
+
path: registerPath,
|
|
57
|
+
action: "create",
|
|
58
|
+
contents: nextRegisterComponent(),
|
|
59
|
+
summary: `created ${relative(cwd, registerPath)} (a client component that registers your tools on page load)`
|
|
45
60
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
];
|
|
62
|
+
const withImport = insertAfterLastImport(
|
|
63
|
+
layout,
|
|
64
|
+
`import { WebMCPRegister } from "${importPath}";`
|
|
65
|
+
);
|
|
66
|
+
if (!withImport) return null;
|
|
67
|
+
const withComponent = withImport.replace(
|
|
68
|
+
/<body([^>]*)>\s*/,
|
|
69
|
+
"<body$1>\n <WebMCPRegister />\n "
|
|
70
|
+
);
|
|
71
|
+
if (withComponent === withImport) return null;
|
|
72
|
+
edits.push({
|
|
73
|
+
path: layoutPath,
|
|
74
|
+
action: "modify",
|
|
75
|
+
contents: withComponent,
|
|
76
|
+
summary: `added 2 lines to ${relative(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`
|
|
77
|
+
});
|
|
78
|
+
return { edits };
|
|
79
|
+
}
|
|
80
|
+
function nextRegisterComponent() {
|
|
81
|
+
return `"use client";
|
|
82
|
+
|
|
83
|
+
import { useEffect } from "react";
|
|
84
|
+
import { registerAllTools } from "./index";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Registers the generated WebMCP tools once, on page load.
|
|
88
|
+
* Generated by webmcp-codegen. Safe to move; keep it mounted near the root.
|
|
89
|
+
*/
|
|
90
|
+
export function WebMCPRegister() {
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
void registerAllTools();
|
|
93
|
+
}, []);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
async function planViteWiring(cwd, app, outDir) {
|
|
99
|
+
const entryCandidates = [
|
|
100
|
+
join(cwd, app.dir, "src/main.tsx"),
|
|
101
|
+
join(cwd, app.dir, "src/main.jsx"),
|
|
102
|
+
join(cwd, app.dir, "src/index.tsx"),
|
|
103
|
+
join(cwd, app.dir, "src/index.jsx")
|
|
104
|
+
];
|
|
105
|
+
const entryPath = await firstExisting(entryCandidates);
|
|
106
|
+
if (!entryPath) return null;
|
|
107
|
+
const entry = await readFile(entryPath, "utf8");
|
|
108
|
+
if (entry.includes("registerAllTools")) return { edits: [], alreadyWired: true };
|
|
109
|
+
const importPath = withoutExtension(relative(dirname(entryPath), join(cwd, outDir, "index")));
|
|
110
|
+
const withWiring = insertAfterLastImport(
|
|
111
|
+
entry,
|
|
112
|
+
`import { registerAllTools } from "${importPath}";
|
|
113
|
+
|
|
114
|
+
void registerAllTools();`
|
|
115
|
+
);
|
|
116
|
+
if (!withWiring) return null;
|
|
117
|
+
return {
|
|
118
|
+
edits: [
|
|
119
|
+
{
|
|
120
|
+
path: entryPath,
|
|
121
|
+
action: "modify",
|
|
122
|
+
contents: withWiring,
|
|
123
|
+
summary: `added 2 lines to ${relative(cwd, entryPath)} (an import and a registerAllTools() call)`
|
|
51
124
|
}
|
|
125
|
+
]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function insertAfterLastImport(source, line) {
|
|
129
|
+
const lines = source.split("\n");
|
|
130
|
+
let lastImport = -1;
|
|
131
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
132
|
+
if (/^import\s/.test(lines[index])) lastImport = index;
|
|
133
|
+
}
|
|
134
|
+
if (lastImport === -1) return null;
|
|
135
|
+
lines.splice(lastImport + 1, 0, line);
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
function withoutExtension(path) {
|
|
139
|
+
const bare = path.replace(/\.(tsx?|jsx?)$/, "").replace(/\/index$/, "");
|
|
140
|
+
return bare.startsWith(".") ? bare : `./${bare}`;
|
|
141
|
+
}
|
|
142
|
+
async function firstExisting(paths) {
|
|
143
|
+
for (const path of paths) {
|
|
144
|
+
try {
|
|
145
|
+
await readFile(path, "utf8");
|
|
146
|
+
return path;
|
|
147
|
+
} catch {
|
|
52
148
|
}
|
|
53
149
|
}
|
|
54
|
-
|
|
55
|
-
return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));
|
|
150
|
+
return void 0;
|
|
56
151
|
}
|
|
57
152
|
|
|
58
153
|
// src/cli.ts
|
|
59
|
-
var HELP = `webmcp-codegen
|
|
154
|
+
var HELP = `webmcp-codegen: generate WebMCP tools from the API contracts you already have
|
|
60
155
|
|
|
61
156
|
Fastest start (no install, no config):
|
|
62
157
|
npx webmcp-codegen generate --dry-run Detect your spec, preview the tools
|
|
63
|
-
npx webmcp-codegen generate Write the tool files
|
|
158
|
+
npx webmcp-codegen generate Write the tool files, wire them up
|
|
64
159
|
|
|
65
160
|
Commands:
|
|
66
161
|
init Write a codegen.config.mjs for full control
|
|
67
162
|
generate Generate (or update) your WebMCP tools
|
|
68
163
|
generate --watch Re-generate when files change
|
|
164
|
+
dev Open the tools dashboard (list, edit, try tools)
|
|
69
165
|
|
|
70
166
|
Flags for generate:
|
|
71
167
|
--spec PATH Which OpenAPI spec to use (auto-detected when omitted)
|
|
72
|
-
--out DIR Where the tool files go (default:
|
|
168
|
+
--out DIR Where the tool files go (default: your web app's src/webmcp)
|
|
73
169
|
--dry-run Preview what would be written, write nothing
|
|
74
170
|
--skip-audit Skip the safety report
|
|
75
171
|
--force Write files even when the audit reports errors
|
|
76
172
|
--config PATH Use a config file at PATH
|
|
173
|
+
|
|
174
|
+
Flags for dev:
|
|
175
|
+
--port N Dashboard port (default: 4700)
|
|
77
176
|
`;
|
|
78
177
|
var CONFIG_FILE = "codegen.config.mjs";
|
|
178
|
+
var MAX_LISTED_TOOLS = 15;
|
|
79
179
|
async function main() {
|
|
80
180
|
const { positionals, values } = parseArgs({
|
|
81
181
|
allowPositionals: true,
|
|
@@ -87,6 +187,7 @@ async function main() {
|
|
|
87
187
|
config: { type: "string" },
|
|
88
188
|
spec: { type: "string" },
|
|
89
189
|
out: { type: "string" },
|
|
190
|
+
port: { type: "string" },
|
|
90
191
|
help: { type: "boolean", default: false }
|
|
91
192
|
}
|
|
92
193
|
});
|
|
@@ -98,6 +199,8 @@ async function main() {
|
|
|
98
199
|
switch (command) {
|
|
99
200
|
case "init":
|
|
100
201
|
return init();
|
|
202
|
+
case "dev":
|
|
203
|
+
return dev(Number.parseInt(values.port ?? "4700", 10));
|
|
101
204
|
case "generate":
|
|
102
205
|
return generate({
|
|
103
206
|
dryRun: values["dry-run"],
|
|
@@ -119,12 +222,12 @@ async function init() {
|
|
|
119
222
|
const cwd = process.cwd();
|
|
120
223
|
const configPath = join2(cwd, CONFIG_FILE);
|
|
121
224
|
if (existsSync(configPath)) {
|
|
122
|
-
console.error(`${CONFIG_FILE} already exists
|
|
225
|
+
console.error(`${CONFIG_FILE} already exists. Nothing to do.`);
|
|
123
226
|
return 1;
|
|
124
227
|
}
|
|
125
228
|
const specs = await findSpecs(cwd);
|
|
126
229
|
const specPath = specs.length > 0 ? `./${specs[0]}` : "./openapi.yaml";
|
|
127
|
-
await
|
|
230
|
+
await writeFile2(
|
|
128
231
|
configPath,
|
|
129
232
|
`import { defineConfig } from "webmcp-codegen";
|
|
130
233
|
import { openapi } from "webmcp-codegen/sources";
|
|
@@ -145,7 +248,7 @@ export default defineConfig({
|
|
|
145
248
|
console.log("Installed the package? A config file needs it:");
|
|
146
249
|
console.log(" npm install -D webmcp-codegen\n");
|
|
147
250
|
if (specs.length > 0) {
|
|
148
|
-
console.log(`Found ${specs[0]}
|
|
251
|
+
console.log(`Found ${specs[0]}. Wrote ${CONFIG_FILE}.`);
|
|
149
252
|
console.log("\nNext: npx webmcp-codegen generate --dry-run");
|
|
150
253
|
} else {
|
|
151
254
|
console.log(`No OpenAPI spec found, so ${CONFIG_FILE} points at ./openapi.yaml.`);
|
|
@@ -163,63 +266,58 @@ async function generate(flags) {
|
|
|
163
266
|
const result = await runOnce(cwd, flags);
|
|
164
267
|
return result.blocked ? 1 : 0;
|
|
165
268
|
}
|
|
166
|
-
async function resolveConfig(cwd, flags) {
|
|
167
|
-
const hasConfigFile = flags.configPath ? existsSync(join2(cwd, flags.configPath)) : CONFIG_FILE_NAMES.some((name) => existsSync(join2(cwd, name)));
|
|
168
|
-
if (hasConfigFile) {
|
|
169
|
-
const { config, path } = await loadConfig(cwd, flags.configPath);
|
|
170
|
-
if (flags.spec || flags.out) {
|
|
171
|
-
console.warn(`Note: --spec/--out are ignored \u2014 ${basename(path)} is in charge here.`);
|
|
172
|
-
}
|
|
173
|
-
return { config, label: basename(path) };
|
|
174
|
-
}
|
|
175
|
-
if (flags.configPath) {
|
|
176
|
-
throw new Error(`No config file at "${flags.configPath}".`);
|
|
177
|
-
}
|
|
178
|
-
const spec = flags.spec ?? await detectSpec(cwd);
|
|
179
|
-
const outDir = flags.out ?? "./src/webmcp";
|
|
180
|
-
return {
|
|
181
|
-
config: { sources: [openapi({ spec })], generate: [js({ outDir })] },
|
|
182
|
-
label: flags.spec ? `--spec ${spec}` : `detected ${spec}`
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
async function detectSpec(cwd) {
|
|
186
|
-
const specs = await findSpecs(cwd);
|
|
187
|
-
if (specs.length === 0) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
"No OpenAPI spec found in this project.\nPoint at one: npx webmcp-codegen generate --spec path/to/openapi.json"
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
if (specs.length > 1) {
|
|
193
|
-
const list = specs.map((spec) => ` - ${spec}`).join("\n");
|
|
194
|
-
throw new Error(
|
|
195
|
-
`Found ${specs.length} API specs:
|
|
196
|
-
${list}
|
|
197
|
-
|
|
198
|
-
Pick one: npx webmcp-codegen generate --spec ${specs[0]}`
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
console.log(`Detected ${specs[0]} (override with --spec)
|
|
202
|
-
`);
|
|
203
|
-
return specs[0];
|
|
204
|
-
}
|
|
205
269
|
async function runOnce(cwd, flags) {
|
|
206
|
-
const
|
|
207
|
-
const
|
|
270
|
+
const setup = await resolveSetup(cwd, flags);
|
|
271
|
+
const data = await loadDataFile(cwd);
|
|
272
|
+
const result = await runGenerate(setup.config, {
|
|
208
273
|
cwd,
|
|
209
274
|
dryRun: flags.dryRun,
|
|
210
275
|
skipAudit: flags.skipAudit,
|
|
211
|
-
force: flags.force
|
|
276
|
+
force: flags.force,
|
|
277
|
+
overrides: data.overrides
|
|
212
278
|
});
|
|
213
|
-
|
|
279
|
+
let wiring = null;
|
|
280
|
+
if (!result.blocked && setup.app) {
|
|
281
|
+
const outDir = findOutDir(setup.config);
|
|
282
|
+
if (outDir) {
|
|
283
|
+
wiring = await planWiring(cwd, setup.app, outDir);
|
|
284
|
+
if (wiring && wiring.edits.length > 0 && !flags.dryRun && result.wrote) {
|
|
285
|
+
await applyWiring(wiring);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!setup.fromConfigFile && !flags.dryRun && result.wrote) {
|
|
290
|
+
await saveDataFile(cwd, setup.remember);
|
|
291
|
+
}
|
|
292
|
+
printReport(result, flags, setup, wiring, cwd);
|
|
214
293
|
return result;
|
|
215
294
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
295
|
+
function findOutDir(config) {
|
|
296
|
+
return config.generate[0]?.outDir;
|
|
297
|
+
}
|
|
298
|
+
function printReport(result, flags, setup, wiring, cwd) {
|
|
299
|
+
const { tools, skipped, findings, files, notes, blocked } = result;
|
|
218
300
|
console.log(`
|
|
219
|
-
webmcp-codegen (${
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
301
|
+
webmcp-codegen (${setup.label}): ${tools.length} tool(s)`);
|
|
302
|
+
for (const note of notes) {
|
|
303
|
+
console.log(`
|
|
304
|
+
note: ${note}`);
|
|
305
|
+
}
|
|
306
|
+
if (skipped.length > 0) {
|
|
307
|
+
console.log(`
|
|
308
|
+
${skipped.length} endpoint(s) skipped:`);
|
|
309
|
+
for (const entry of skipped) {
|
|
310
|
+
console.log(` ${entry.ref}: ${entry.reason}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
console.log("");
|
|
314
|
+
const listed = tools.slice(0, MAX_LISTED_TOOLS);
|
|
315
|
+
for (const tool of listed) {
|
|
316
|
+
const state = tool.enabledByDefault ? "" : " starts disabled";
|
|
317
|
+
console.log(` ${tool.name} [${tool.sideEffect}]${state} \u2190 ${tool.source.ref}`);
|
|
318
|
+
}
|
|
319
|
+
if (tools.length > listed.length) {
|
|
320
|
+
console.log(` \u2026and ${tools.length - listed.length} more`);
|
|
223
321
|
}
|
|
224
322
|
if (findings.length > 0) {
|
|
225
323
|
console.log("");
|
|
@@ -237,23 +335,86 @@ webmcp-codegen (${configName}) \u2014 ${tools.length} tool(s)
|
|
|
237
335
|
console.log(` ${shown}: ${relative2(cwd, file.path)}`);
|
|
238
336
|
}
|
|
239
337
|
}
|
|
338
|
+
if (wiring) {
|
|
339
|
+
if (wiring.alreadyWired) {
|
|
340
|
+
console.log("\n registration: already wired into your app");
|
|
341
|
+
} else if (wiring.edits.length > 0) {
|
|
342
|
+
console.log(flags.dryRun ? "\n registration (would do):" : "\n registration:");
|
|
343
|
+
for (const edit of wiring.edits) {
|
|
344
|
+
console.log(` ${edit.summary}`);
|
|
345
|
+
}
|
|
346
|
+
if (!flags.dryRun) {
|
|
347
|
+
console.log(" undo: delete the added lines (nothing else was touched)");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} else if (!blocked && setup.app) {
|
|
351
|
+
console.log("\n registration: could not find your app's entry file, so add this by hand:");
|
|
352
|
+
console.log(' import { registerAllTools } from "<path-to>/src/webmcp";');
|
|
353
|
+
console.log(" void registerAllTools();");
|
|
354
|
+
}
|
|
240
355
|
if (blocked) {
|
|
241
356
|
console.log(
|
|
242
357
|
"\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway."
|
|
243
358
|
);
|
|
244
|
-
|
|
245
|
-
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (flags.dryRun) {
|
|
362
|
+
console.log("\nDry run: nothing written. Re-run without --dry-run to write these files.");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
printNextSteps(result);
|
|
366
|
+
}
|
|
367
|
+
function printNextSteps(result) {
|
|
368
|
+
const enabled = result.tools.filter((tool) => tool.enabledByDefault);
|
|
369
|
+
const disabled = result.tools.filter((tool) => !tool.enabledByDefault);
|
|
370
|
+
const parts = [];
|
|
371
|
+
if (enabled.length > 0) parts.push(`${enabled.length} read tool(s) work out of the box`);
|
|
372
|
+
if (disabled.length > 0) {
|
|
373
|
+
parts.push(`${disabled.length} tool(s) start disabled (open the file and uncomment to enable)`);
|
|
374
|
+
}
|
|
375
|
+
console.log(`
|
|
376
|
+
Done. ${parts.join("; ")}.`);
|
|
377
|
+
const suggestion = pickSuggestedTool(result.tools);
|
|
378
|
+
console.log("\nTry it:");
|
|
379
|
+
console.log(" 1. Start your app and open it in Chrome.");
|
|
380
|
+
console.log(" 2. Turn on chrome://flags/#enable-webmcp-testing and reload the page.");
|
|
381
|
+
if (suggestion) {
|
|
382
|
+
console.log(` 3. Ask the agent: "${suggestion}"`);
|
|
246
383
|
} else {
|
|
247
|
-
console.log("
|
|
384
|
+
console.log(" 3. Ask the agent to use one of your tools.");
|
|
248
385
|
}
|
|
249
386
|
}
|
|
387
|
+
function pickSuggestedTool(tools) {
|
|
388
|
+
const reads = tools.filter((tool2) => tool2.enabledByDefault && tool2.endpointRole === "endpoint");
|
|
389
|
+
if (reads.length === 0) return void 0;
|
|
390
|
+
const tool = reads.find((candidate) => /^(list|get|search|find|fetch|recent)-/.test(candidate.name)) ?? reads[0];
|
|
391
|
+
if (!tool) return void 0;
|
|
392
|
+
const description = tool.description.trim().replace(/\.$/, "");
|
|
393
|
+
const looksTemplated = /^(GET|POST|PUT|PATCH|DELETE)\s/.test(description);
|
|
394
|
+
const phrase = looksTemplated ? tool.name.replace(/-/g, " ") : description.charAt(0).toLowerCase() + description.slice(1);
|
|
395
|
+
return phrase;
|
|
396
|
+
}
|
|
397
|
+
async function dev(port) {
|
|
398
|
+
const { startDevServer } = await import("./server-OR4IMRQ5.js");
|
|
399
|
+
const server = await startDevServer({ cwd: process.cwd(), port });
|
|
400
|
+
console.log(`
|
|
401
|
+
webmcp-codegen dashboard: http://localhost:${port}`);
|
|
402
|
+
console.log("List, describe, enable, and try your tools. Ctrl+C to stop.\n");
|
|
403
|
+
await new Promise((resolveExit) => {
|
|
404
|
+
process.on("SIGINT", () => {
|
|
405
|
+
server.close();
|
|
406
|
+
resolveExit();
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
return 0;
|
|
410
|
+
}
|
|
250
411
|
async function watchLoop(cwd, flags) {
|
|
251
412
|
await runOnce(cwd, { ...flags, dryRun: false });
|
|
252
413
|
console.log("\nWatching for changes\u2026 (Ctrl+C to stop)");
|
|
253
414
|
let timer;
|
|
254
415
|
watch(cwd, { recursive: true }, (_event, filename) => {
|
|
255
416
|
if (!filename) return;
|
|
256
|
-
if (/node_modules|\.git|\/dist|\/src\/webmcp/.test(filename)) return;
|
|
417
|
+
if (/node_modules|\.git|\/dist|\/src\/webmcp|\.webmcp-codegen\.json/.test(filename)) return;
|
|
257
418
|
if (!/\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;
|
|
258
419
|
clearTimeout(timer);
|
|
259
420
|
timer = setTimeout(() => {
|