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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/dev/server.ts","../src/dev/ui.ts"],"sourcesContent":["/**\n * The dev dashboard server: `npx webmcp-codegen dev`.\n *\n * A small HTTP server on localhost that serves the tools UI and answers its\n * three kinds of requests: list the tools, save an override (description /\n * enabled) to .webmcp-codegen.json, and run a tool's endpoint for a direct\n * test. It reuses the exact pipeline the CLI runs, so what the dashboard\n * shows is what a generate run would write.\n *\n * It exists only while the command is running, listens on localhost only,\n * and nothing about it ever touches the user's app bundle — by design, so\n * this dev tool can never leak into production.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { createServer, type Server } from \"node:http\";\nimport { loadDataFile, saveDataFile } from \"../data-file.js\";\nimport { runGenerate } from \"../pipeline.js\";\nimport { resolveSetup } from \"../setup.js\";\nimport type { JsonSchema, ReviewedTool } from \"../types.js\";\nimport { dashboardHtml } from \"./ui.js\";\n\nexport interface DevServerOptions {\n cwd: string;\n port: number;\n /** Open the browser automatically. False in tests and CI. */\n open?: boolean;\n}\n\ninterface RunRequest {\n name: string;\n input: Record<string, unknown>;\n /** Absolute base URL when the API is not same-origin with the dashboard. */\n baseUrl?: string;\n}\n\n/** The JSON shape the UI renders. */\ninterface DashboardState {\n label: string;\n outDir?: string;\n tools: {\n name: string;\n verb?: string;\n path?: string;\n description: string;\n sideEffect: string;\n riskTier: string;\n enabled: boolean;\n endpointRole: string;\n piiInOutput: string[];\n inputSchema: JsonSchema;\n /** Route info the direct \"run it\" test needs to build a real request. */\n pathTemplate?: string;\n paramLocations?: { path: string[]; query: string[]; body: string[] };\n serverUrl?: string;\n findings: { level: string; message: string }[];\n }[];\n skipped: { ref: string; reason: string }[];\n notes: string[];\n}\n\nexport async function startDevServer(options: DevServerOptions): Promise<Server> {\n const setup = await resolveSetup(options.cwd, {\n dryRun: true,\n skipAudit: false,\n force: false,\n watch: false,\n });\n\n /** Re-run the pipeline fresh on every state request: edits to the spec\n * show up on reload without restarting the dashboard. */\n async function currentState(): Promise<DashboardState> {\n const data = await loadDataFile(options.cwd);\n const result = await runGenerate(setup.config, {\n cwd: options.cwd,\n dryRun: true,\n overrides: data.overrides,\n });\n return {\n label: setup.label,\n outDir: setup.config.generate[0]?.outDir,\n tools: result.tools.map((tool) => toUiTool(tool, result.findings)),\n skipped: result.skipped,\n notes: result.notes,\n };\n }\n\n const server = createServer(async (request, response) => {\n try {\n await route(request, response);\n } catch (error) {\n sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });\n }\n });\n\n async function route(\n request: import(\"node:http\").IncomingMessage,\n response: import(\"node:http\").ServerResponse,\n ): Promise<void> {\n const url = new URL(request.url ?? \"/\", \"http://localhost\");\n\n if (request.method === \"GET\" && url.pathname === \"/\") {\n response.writeHead(200, { \"content-type\": \"text/html; charset=utf-8\" });\n response.end(dashboardHtml());\n return;\n }\n\n if (request.method === \"GET\" && url.pathname === \"/api/state\") {\n sendJson(response, 200, await currentState());\n return;\n }\n\n if (request.method === \"POST\" && url.pathname === \"/api/override\") {\n const body = (await readJson(request)) as {\n name?: string;\n description?: string;\n enabled?: boolean;\n };\n if (!body.name) {\n sendJson(response, 400, { error: \"Missing tool name.\" });\n return;\n }\n const data = await loadDataFile(options.cwd);\n const overrides = { ...(data.overrides ?? {}) };\n const existing = overrides[body.name] ?? {};\n // Explicit fields win over what's stored; undefined means \"untouched\".\n overrides[body.name] = {\n ...existing,\n ...(body.description !== undefined ? { description: body.description } : {}),\n ...(body.enabled !== undefined ? { enabled: body.enabled } : {}),\n };\n await saveDataFile(options.cwd, { overrides });\n sendJson(response, 200, { ok: true, saved: `.webmcp-codegen.json` });\n return;\n }\n\n if (request.method === \"POST\" && url.pathname === \"/api/run\") {\n const body = (await readJson(request)) as RunRequest;\n const state = await currentState();\n const tool = state.tools.find((candidate) => candidate.name === body.name);\n if (!tool) {\n sendJson(response, 404, { error: `No tool named \"${body.name}\".` });\n return;\n }\n const result = await runEndpoint(tool, body.input ?? {}, body.baseUrl);\n sendJson(response, result.ok ? 200 : 502, result);\n return;\n }\n\n sendJson(response, 404, { error: \"Not found\" });\n }\n\n await new Promise<void>((resolveListen) =>\n server.listen(options.port, \"127.0.0.1\", resolveListen),\n );\n\n if (options.open !== false) openBrowser(`http://localhost:${options.port}`);\n return server;\n}\n\nfunction toUiTool(\n tool: ReviewedTool,\n findings: { level: string; tool?: string; message: string }[],\n): DashboardState[\"tools\"][number] {\n const [verb, ...rest] = tool.source.ref.split(\" \");\n return {\n name: tool.name,\n verb,\n path: rest.join(\" \"),\n description: tool.description,\n sideEffect: tool.sideEffect,\n riskTier: tool.riskTier,\n enabled: tool.enabledByDefault,\n endpointRole: tool.endpointRole,\n piiInOutput: tool.piiInOutput,\n inputSchema: tool.inputSchema,\n ...(tool.pathTemplate ? { pathTemplate: tool.pathTemplate } : {}),\n ...(tool.paramLocations ? { paramLocations: tool.paramLocations } : {}),\n ...(tool.serverUrl ? { serverUrl: tool.serverUrl } : {}),\n findings: findings\n .filter((finding) => finding.tool === tool.name)\n .map((finding) => ({ level: finding.level, message: finding.message })),\n };\n}\n\n/**\n * The direct \"run it\" test: call the endpoint the way the generated\n * execute() would, but server-side. Two honest limitations the UI states:\n * there is no browser session here (auth cookies do not apply), and the\n * call needs an absolute base URL — the spec's servers entry or one the\n * developer types in.\n */\nasync function runEndpoint(\n tool: DashboardState[\"tools\"][number],\n input: Record<string, unknown>,\n baseUrlOverride?: string,\n): Promise<{ ok: boolean; status?: number; body?: unknown; error?: string }> {\n const base = baseUrlOverride ?? tool.serverUrl;\n if (!base) {\n return {\n ok: false,\n error:\n \"No base URL: the spec lists no absolute server. Type your app's URL \" +\n '(e.g. http://localhost:3000) in the \"base URL\" field and run again.',\n };\n }\n if (!tool.pathTemplate || !tool.verb) {\n return { ok: false, error: \"This tool has no route to call.\" };\n }\n\n let path = tool.pathTemplate;\n for (const param of tool.paramLocations?.path ?? []) {\n path = path.replace(`{${param}}`, encodeURIComponent(String(input[param] ?? \"\")));\n }\n const url = new URL(path, base);\n for (const param of tool.paramLocations?.query ?? []) {\n const value = input[param];\n if (value !== undefined && value !== null) url.searchParams.set(param, String(value));\n }\n const bodyFields = tool.paramLocations?.body ?? [];\n const body =\n bodyFields.length === 1 && bodyFields[0] === \"body\"\n ? input.body\n : bodyFields.length > 0\n ? Object.fromEntries(bodyFields.map((field) => [field, input[field]]))\n : undefined;\n\n try {\n const response = await fetch(url, {\n method: tool.verb,\n headers: body !== undefined ? { \"content-type\": \"application/json\" } : undefined,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n const text = await response.text();\n let parsed: unknown = text;\n try {\n parsed = JSON.parse(text);\n } catch {\n // Plain-text response; keep it as text.\n }\n return { ok: response.ok, status: response.status, body: parsed };\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) };\n }\n}\n\nfunction sendJson(\n response: import(\"node:http\").ServerResponse,\n status: number,\n body: unknown,\n): void {\n response.writeHead(status, { \"content-type\": \"application/json\" });\n response.end(JSON.stringify(body));\n}\n\nfunction readJson(request: import(\"node:http\").IncomingMessage): Promise<unknown> {\n return new Promise((resolveRead, reject) => {\n let text = \"\";\n request.on(\"data\", (chunk: Buffer) => {\n text += chunk.toString(\"utf8\");\n });\n request.on(\"end\", () => {\n try {\n resolveRead(text ? JSON.parse(text) : {});\n } catch {\n reject(new Error(\"Invalid JSON body\"));\n }\n });\n request.on(\"error\", reject);\n });\n}\n\nfunction openBrowser(url: string): void {\n const command =\n process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"start\" : \"xdg-open\";\n spawn(command, [url], { stdio: \"ignore\", shell: process.platform === \"win32\" }).unref();\n}\n","/**\n * The dashboard UI: one self-contained HTML page (inline CSS and JS), served\n * by the dev server. No framework, no build step, no CDN: the page must work\n * offline and add zero dependencies to the package.\n *\n * Design bar (per the first-run spec): this is a product surface, in the\n * idiom of Storybook / Mintlify / Scalar. Same tokens as the site: dark\n * neutral surfaces, one accent blue, hairline borders, system sans for prose\n * and mono for names and code. Keyboard navigation throughout: up/down move\n * through tools, ⌘K focuses search, ⌘S saves an edit.\n */\n\nexport function dashboardHtml(): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>webmcp-codegen</title>\n<style>\n :root {\n --baseline: #0a0b0f;\n --panel: #0f1117;\n --panel-raised: #151822;\n --line: #20242f;\n --ink: #e9ecf2;\n --dim: #9aa3b2;\n --faint: #5d6575;\n --ghost: #3b4150;\n --accent: #58a6ff;\n --signal: #e3b341;\n --fault: #f47067;\n --sans: ui-sans-serif, system-ui, -apple-system, sans-serif;\n --mono: ui-monospace, SFMono-Regular, Menlo, monospace;\n }\n * { box-sizing: border-box; }\n html, body { margin: 0; height: 100%; }\n body {\n background: var(--baseline);\n color: var(--ink);\n font-family: var(--sans);\n font-size: 14px;\n -webkit-font-smoothing: antialiased;\n }\n ::selection { background: var(--accent); color: var(--baseline); }\n button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }\n input, textarea { font: inherit; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: 6px; }\n input:focus-visible, textarea:focus-visible, button:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }\n\n #app { display: grid; grid-template-columns: 300px 1fr; height: 100vh; }\n\n /* ── Sidebar ─────────────────────────────────────────────── */\n aside {\n border-right: 1px solid var(--line);\n display: flex;\n flex-direction: column;\n min-height: 0;\n }\n .brand {\n display: flex; align-items: center; gap: 10px;\n padding: 16px 16px 12px;\n font-family: var(--mono); font-size: 13px; letter-spacing: -0.01em;\n }\n .brand::before { content: \"\"; width: 8px; height: 8px; background: var(--accent); }\n .brand .meta { color: var(--faint); font-size: 11px; margin-left: auto; }\n .search { padding: 0 12px 12px; }\n .search input {\n width: 100%; padding: 7px 10px; font-family: var(--mono); font-size: 12px;\n }\n .search input::placeholder { color: var(--ghost); }\n .tools { overflow-y: auto; flex: 1; padding: 4px 8px 16px; }\n .tool {\n display: flex; align-items: center; gap: 8px; width: 100%;\n padding: 7px 8px; border-radius: 6px; text-align: left;\n color: var(--dim);\n }\n .tool:hover { background: var(--panel); color: var(--ink); }\n .tool[aria-selected=\"true\"] { background: var(--panel-raised); color: var(--ink); }\n .tool .name { font-family: var(--mono); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .tool .verb {\n margin-left: auto; font-family: var(--mono); font-size: 10px;\n padding: 2px 6px; border-radius: 4px; border: 1px solid var(--line);\n color: var(--faint); flex-shrink: 0;\n }\n .tool .verb.read { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent); }\n .tool .verb.write { color: var(--signal); border-color: color-mix(in srgb, var(--signal) 35%, transparent); }\n .tool .verb.destructive { color: var(--fault); border-color: color-mix(in srgb, var(--fault) 35%, transparent); }\n .tool .off {\n width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;\n background: var(--ghost);\n }\n .tool .off[title] { cursor: help; }\n .empty { padding: 24px 16px; color: var(--faint); font-size: 13px; line-height: 1.6; }\n\n /* ── Detail pane ─────────────────────────────────────────── */\n main { overflow-y: auto; min-width: 0; }\n .detail { max-width: 780px; padding: 32px 40px 80px; }\n .crumb { font-family: var(--mono); font-size: 11px; color: var(--ghost); margin-bottom: 10px; }\n h1 { font-size: 22px; font-weight: 600; letter-spacing: -0.01em; margin: 0 0 6px; font-family: var(--mono); }\n .route { font-family: var(--mono); font-size: 12px; color: var(--faint); margin-bottom: 24px; }\n .badges { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }\n .badge {\n font-family: var(--mono); font-size: 11px; padding: 3px 8px;\n border: 1px solid var(--line); border-radius: 999px; color: var(--dim);\n }\n .badge.accent { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent); }\n .badge.warn { color: var(--signal); border-color: color-mix(in srgb, var(--signal) 35%, transparent); }\n .badge.err { color: var(--fault); border-color: color-mix(in srgb, var(--fault) 35%, transparent); }\n\n .field-label {\n font-family: var(--mono); font-size: 10px; text-transform: uppercase;\n letter-spacing: 0.18em; color: var(--faint); margin: 28px 0 8px;\n }\n textarea#desc {\n width: 100%; min-height: 70px; padding: 10px 12px; font-size: 14px;\n line-height: 1.55; resize: vertical;\n }\n .hint { color: var(--faint); font-size: 12px; margin-top: 6px; line-height: 1.5; }\n .row { display: flex; align-items: center; gap: 12px; }\n .save {\n margin-top: 10px; padding: 7px 14px; border: 1px solid var(--line);\n border-radius: 6px; font-size: 13px; color: var(--ink); background: var(--panel-raised);\n }\n .save:hover { border-color: var(--accent); }\n .save[disabled] { opacity: 0.4; cursor: default; }\n .saved { color: var(--accent); font-size: 12px; font-family: var(--mono); }\n\n /* Toggle */\n .toggle-row { display: flex; align-items: center; gap: 12px; padding: 14px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); margin-top: 28px; }\n .switch { position: relative; width: 34px; height: 20px; border-radius: 999px; background: var(--ghost); transition: background 150ms; flex-shrink: 0; }\n .switch[aria-checked=\"true\"] { background: var(--accent); }\n .switch::after {\n content: \"\"; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;\n border-radius: 50%; background: var(--ink); transition: left 150ms;\n }\n .switch[aria-checked=\"true\"]::after { left: 16px; }\n .toggle-copy { font-size: 13px; color: var(--dim); line-height: 1.5; }\n .toggle-copy strong { color: var(--ink); font-weight: 600; }\n\n /* Findings */\n .finding {\n display: flex; gap: 8px; padding: 10px 12px; border: 1px solid var(--line);\n border-radius: 6px; font-size: 12.5px; line-height: 1.5; color: var(--dim);\n margin-bottom: 8px; background: var(--panel);\n }\n .finding.warn { border-color: color-mix(in srgb, var(--signal) 30%, transparent); }\n .finding.error { border-color: color-mix(in srgb, var(--fault) 30%, transparent); }\n .finding .icon { color: var(--signal); }\n .finding.error .icon { color: var(--fault); }\n\n /* Try it */\n .try { margin-top: 28px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }\n .try > header { padding: 12px 16px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 10px; }\n .try > header h2 { font-size: 13px; margin: 0; font-weight: 600; }\n .try > header .note { color: var(--faint); font-size: 11.5px; margin-left: auto; }\n .try .body { padding: 16px; }\n .param { display: grid; grid-template-columns: 180px 1fr; gap: 12px; align-items: center; margin-bottom: 10px; }\n .param label { font-family: var(--mono); font-size: 12px; color: var(--dim); }\n .param label .req { color: var(--fault); }\n .param input { padding: 7px 10px; font-family: var(--mono); font-size: 12px; width: 100%; }\n .base-url { margin-bottom: 14px; }\n .base-url input { width: 100%; padding: 7px 10px; font-family: var(--mono); font-size: 12px; }\n .run {\n margin-top: 6px; padding: 8px 16px; border-radius: 6px; font-size: 13px;\n background: var(--accent); color: var(--baseline); font-weight: 600;\n }\n .run[disabled] { opacity: 0.5; cursor: default; }\n pre.result {\n margin: 14px 0 0; padding: 12px 14px; background: var(--baseline);\n border: 1px solid var(--line); border-radius: 6px; font-family: var(--mono);\n font-size: 12px; line-height: 1.55; overflow-x: auto; max-height: 320px;\n color: var(--dim); white-space: pre-wrap; word-break: break-word;\n }\n pre.result.ok { border-color: color-mix(in srgb, var(--accent) 35%, transparent); }\n pre.result.err { border-color: color-mix(in srgb, var(--fault) 35%, transparent); }\n\n .placeholder { max-width: 480px; margin: 20vh auto 0; text-align: center; color: var(--faint); line-height: 1.7; }\n .placeholder kbd {\n font-family: var(--mono); font-size: 11px; border: 1px solid var(--line);\n border-radius: 4px; padding: 1px 5px; color: var(--dim);\n }\n .toast {\n position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);\n background: var(--panel-raised); border: 1px solid var(--line); color: var(--ink);\n padding: 8px 14px; border-radius: 6px; font-size: 12.5px; opacity: 0;\n transition: opacity 150ms; pointer-events: none;\n }\n .toast.show { opacity: 1; }\n</style>\n</head>\n<body>\n<div id=\"app\">\n <aside>\n <div class=\"brand\">webmcp-codegen <span class=\"meta\" id=\"tool-count\"></span></div>\n <div class=\"search\"><input id=\"search\" type=\"text\" placeholder=\"filter tools (⌘K)\" spellcheck=\"false\" /></div>\n <div class=\"tools\" id=\"tool-list\" role=\"listbox\" aria-label=\"Tools\"></div>\n </aside>\n <main id=\"detail\">\n <div class=\"placeholder\">\n <p>Loading your tools…</p>\n </div>\n </main>\n</div>\n<div class=\"toast\" id=\"toast\"></div>\n\n<script>\n(function () {\n var state = null;\n var selected = null;\n var filter = \"\";\n\n var listEl = document.getElementById(\"tool-list\");\n var detailEl = document.getElementById(\"detail\");\n var searchEl = document.getElementById(\"search\");\n var toastEl = document.getElementById(\"toast\");\n\n function esc(text) {\n return String(text).replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\");\n }\n\n function toast(message) {\n toastEl.textContent = message;\n toastEl.classList.add(\"show\");\n setTimeout(function () { toastEl.classList.remove(\"show\"); }, 2200);\n }\n\n function api(path, options) {\n return fetch(path, options).then(function (res) {\n return res.json().then(function (body) {\n if (!res.ok) throw new Error(body.error || (\"Request failed: \" + res.status));\n return body;\n });\n });\n }\n\n function load() {\n return api(\"/api/state\").then(function (next) {\n state = next;\n if (!selected && state.tools.length > 0) selected = state.tools[0].name;\n renderList();\n renderDetail();\n }).catch(function (error) {\n detailEl.innerHTML = '<div class=\"placeholder\"><p>Could not load tools.</p><p>' + esc(error.message) + \"</p></div>\";\n });\n }\n\n function visibleTools() {\n if (!filter) return state.tools;\n var needle = filter.toLowerCase();\n return state.tools.filter(function (tool) {\n return tool.name.indexOf(needle) !== -1 ||\n (tool.path || \"\").toLowerCase().indexOf(needle) !== -1 ||\n tool.description.toLowerCase().indexOf(needle) !== -1;\n });\n }\n\n function renderList() {\n document.getElementById(\"tool-count\").textContent = state.tools.length + \" tools\";\n var tools = visibleTools();\n if (tools.length === 0) {\n listEl.innerHTML = '<div class=\"empty\">No tools match.</div>';\n return;\n }\n listEl.innerHTML = tools.map(function (tool) {\n var disabled = tool.enabled ? \"\" : '<span class=\"off\" title=\"starts disabled\"></span>';\n return '<button class=\"tool\" role=\"option\" aria-selected=\"' + (tool.name === selected) + '\" data-name=\"' + esc(tool.name) + '\">' +\n disabled +\n '<span class=\"name\">' + esc(tool.name) + \"</span>\" +\n '<span class=\"verb ' + esc(tool.sideEffect) + '\">' + esc(tool.verb || \"\") + \"</span>\" +\n \"</button>\";\n }).join(\"\");\n Array.prototype.forEach.call(listEl.children, function (child) {\n child.addEventListener(\"click\", function () {\n selected = child.getAttribute(\"data-name\");\n renderList();\n renderDetail();\n });\n });\n }\n\n function currentTool() {\n return state.tools.find(function (tool) { return tool.name === selected; });\n }\n\n function renderDetail() {\n var tool = currentTool();\n if (!tool) {\n detailEl.innerHTML = '<div class=\"placeholder\"><p>Select a tool on the left.</p><p><kbd>↑</kbd> <kbd>↓</kbd> to move, <kbd>⌘K</kbd> to search.</p></div>';\n return;\n }\n\n var badges = [\n '<span class=\"badge accent\">' + esc(tool.sideEffect) + \"</span>\",\n tool.enabled ? '<span class=\"badge\">enabled</span>' : '<span class=\"badge warn\">starts disabled</span>',\n tool.endpointRole !== \"endpoint\" ? '<span class=\"badge err\">' + esc(tool.endpointRole) + \" endpoint</span>\" : \"\",\n tool.piiInOutput.length > 0 ? '<span class=\"badge warn\">pii: ' + esc(tool.piiInOutput.join(\", \")) + \"</span>\" : \"\",\n ].filter(Boolean).join(\"\");\n\n var findings = tool.findings.map(function (finding) {\n var icon = finding.level === \"error\" ? \"✖\" : \"⚠\";\n return '<div class=\"finding ' + esc(finding.level) + '\"><span class=\"icon\">' + icon + \"</span><span>\" + esc(finding.message) + \"</span></div>\";\n }).join(\"\");\n\n var schema = tool.inputSchema || {};\n var properties = schema.properties || {};\n var required = schema.required || [];\n var fields = Object.keys(properties).map(function (key) {\n var field = properties[key];\n var type = field.type === \"number\" || field.type === \"integer\" ? \"number\" : \"text\";\n var req = required.indexOf(key) !== -1 ? ' <span class=\"req\">*</span>' : \"\";\n var label = esc(key) + req + (field.description ? '<div class=\"hint\">' + esc(field.description) + \"</div>\" : \"\");\n return '<div class=\"param\"><label for=\"f-' + esc(key) + '\">' + label + '</label>' +\n '<input id=\"f-' + esc(key) + '\" data-field=\"' + esc(key) + '\" data-type=\"' + esc(field.type || \"string\") + '\" type=\"' + type + '\" spellcheck=\"false\" /></div>';\n }).join(\"\");\n\n var baseUrl = \"\";\n try { baseUrl = localStorage.getItem(\"webmcp-codegen:baseUrl\") || tool.serverUrl || \"\"; } catch (e) {}\n\n detailEl.innerHTML =\n '<div class=\"detail\">' +\n '<div class=\"crumb\">' + esc(state.label) + (state.outDir ? \" → \" + esc(state.outDir) : \"\") + \"</div>\" +\n \"<h1>\" + esc(tool.name) + \"</h1>\" +\n '<div class=\"route\">' + esc((tool.verb || \"\") + \" \" + (tool.path || \"\")) + \"</div>\" +\n '<div class=\"badges\">' + badges + \"</div>\" +\n\n (findings ? '<div class=\"field-label\">audit findings</div>' + findings : \"\") +\n\n '<div class=\"field-label\">description</div>' +\n '<textarea id=\"desc\" spellcheck=\"false\">' + esc(tool.description) + \"</textarea>\" +\n '<div class=\"row\"><button class=\"save\" id=\"save-desc\">save description</button><span class=\"saved\" id=\"saved-desc\"></span></div>' +\n '<div class=\"hint\">Agents pick tools by this text. Saved to .webmcp-codegen.json, so it survives regeneration. ⌘S to save.</div>' +\n\n '<div class=\"toggle-row\">' +\n '<button class=\"switch\" id=\"toggle-enabled\" role=\"switch\" aria-checked=\"' + tool.enabled + '\" aria-label=\"Enabled\"></button>' +\n '<div class=\"toggle-copy\"><strong>' + (tool.enabled ? \"Enabled\" : \"Disabled\") + \".</strong> \" +\n (tool.enabled\n ? \"This tool works as soon as the app registers it.\"\n : \"The generated code is there, commented out. Flipping this regenerates it enabled on the next run.\") +\n \"</div></div>\" +\n\n '<div class=\"try\"><header><h2>try it</h2><span class=\"note\">direct call, server-side. no browser session here.</span></header>' +\n '<div class=\"body\">' +\n '<div class=\"base-url\"><input id=\"base-url\" type=\"text\" placeholder=\"base URL, e.g. http://localhost:3000\" value=\"' + esc(baseUrl) + '\" spellcheck=\"false\" /></div>' +\n (fields || '<div class=\"hint\">This tool takes no inputs.</div>') +\n '<button class=\"run\" id=\"run\">run</button>' +\n '<pre class=\"result\" id=\"result\" hidden></pre>' +\n \"</div></div>\" +\n \"</div>\";\n\n document.getElementById(\"save-desc\").addEventListener(\"click\", saveDescription);\n document.getElementById(\"toggle-enabled\").addEventListener(\"click\", toggleEnabled);\n document.getElementById(\"run\").addEventListener(\"click\", runTool);\n document.getElementById(\"base-url\").addEventListener(\"change\", function (event) {\n try { localStorage.setItem(\"webmcp-codegen:baseUrl\", event.target.value); } catch (e) {}\n });\n }\n\n function saveDescription() {\n var tool = currentTool();\n var desc = document.getElementById(\"desc\").value.trim();\n if (!tool || !desc) return;\n api(\"/api/override\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ name: tool.name, description: desc }),\n }).then(function () {\n tool.description = desc;\n toast(\"saved to .webmcp-codegen.json\");\n }).catch(function (error) { toast(error.message); });\n }\n\n function toggleEnabled() {\n var tool = currentTool();\n if (!tool) return;\n var next = !tool.enabled;\n api(\"/api/override\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ name: tool.name, enabled: next }),\n }).then(function () {\n tool.enabled = next;\n renderList();\n renderDetail();\n toast(next ? \"enabled on next generate\" : \"disabled on next generate\");\n }).catch(function (error) { toast(error.message); });\n }\n\n function runTool() {\n var tool = currentTool();\n if (!tool) return;\n var input = {};\n Array.prototype.forEach.call(document.querySelectorAll(\"[data-field]\"), function (field) {\n var value = field.value;\n if (value === \"\") return;\n var type = field.getAttribute(\"data-type\");\n if (type === \"number\" || type === \"integer\") value = Number(value);\n if (type === \"boolean\") value = value === \"true\";\n if (type === \"object\" || type === \"array\") {\n try { value = JSON.parse(value); } catch (e) { /* keep as string */ }\n }\n input[field.getAttribute(\"data-field\")] = value;\n });\n var baseUrl = document.getElementById(\"base-url\").value.trim();\n var resultEl = document.getElementById(\"result\");\n var runEl = document.getElementById(\"run\");\n runEl.disabled = true;\n resultEl.hidden = true;\n api(\"/api/run\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ name: tool.name, input: input, baseUrl: baseUrl || undefined }),\n }).then(function (result) {\n resultEl.hidden = false;\n resultEl.className = \"result \" + (result.ok ? \"ok\" : \"err\");\n resultEl.textContent =\n (result.status ? \"HTTP \" + result.status + \"\\\\n\\\\n\" : \"\") +\n (result.error ? result.error : JSON.stringify(result.body, null, 2));\n }).catch(function (error) {\n resultEl.hidden = false;\n resultEl.className = \"result err\";\n resultEl.textContent = error.message;\n }).finally(function () {\n runEl.disabled = false;\n });\n }\n\n /* Keyboard: up/down moves through the visible tools, cmd-K focuses\n search, cmd-S saves the description being edited. */\n document.addEventListener(\"keydown\", function (event) {\n if ((event.metaKey || event.ctrlKey) && event.key === \"k\") {\n event.preventDefault();\n searchEl.focus();\n return;\n }\n if ((event.metaKey || event.ctrlKey) && event.key === \"s\") {\n event.preventDefault();\n saveDescription();\n return;\n }\n if (event.target === searchEl || event.target.tagName === \"TEXTAREA\" || event.target.tagName === \"INPUT\") {\n return;\n }\n if (event.key !== \"ArrowDown\" && event.key !== \"ArrowUp\") return;\n var tools = visibleTools();\n var index = tools.findIndex(function (tool) { return tool.name === selected; });\n var next = event.key === \"ArrowDown\" ? index + 1 : index - 1;\n if (next < 0 || next >= tools.length) return;\n event.preventDefault();\n selected = tools[next].name;\n renderList();\n renderDetail();\n var button = listEl.querySelector('[aria-selected=\"true\"]');\n if (button) button.scrollIntoView({ block: \"nearest\" });\n });\n\n searchEl.addEventListener(\"input\", function (event) {\n filter = event.target.value;\n renderList();\n });\n\n load();\n})();\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,SAAS,aAAa;AACtB,SAAS,oBAAiC;;;ACHnC,SAAS,gBAAwB;AACtC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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","names":[]}
@@ -1,4 +1,4 @@
1
- import { S as Source } from '../types-Bf5MxWeH.js';
1
+ import { S as Source } from '../types-DWUum51l.js';
2
2
 
3
3
  /**
4
4
  * The OpenAPI source.
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  openapi
3
- } from "../chunk-WYGVTIGI.js";
4
- import "../chunk-BIKKPCRT.js";
5
- import "../chunk-5L4KN6F4.js";
3
+ } from "../chunk-3LTHWIAP.js";
4
+ import "../chunk-FWSATV7C.js";
5
+ import "../chunk-KSQMJERY.js";
6
6
  export {
7
7
  openapi
8
8
  };
@@ -60,9 +60,26 @@ interface CandidateTool {
60
60
  /** Name of the generated TypeScript input type, e.g. "GetOrderStatusInput". */
61
61
  inputTypeName: string;
62
62
  httpMethod?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
63
+ /**
64
+ * The URL path template, e.g. "/pets/{id}". Sources that know it provide it
65
+ * so generators can emit a working request, not just a TODO.
66
+ */
67
+ pathTemplate?: string;
68
+ /**
69
+ * Which input fields go where in the request. The generated execute() uses
70
+ * this to place each field: path params are interpolated into the URL,
71
+ * query params become the search string, body fields become the JSON body.
72
+ */
73
+ paramLocations?: {
74
+ path: string[];
75
+ query: string[];
76
+ body: string[];
77
+ };
78
+ /** Absolute API base URL from the source (e.g. the spec's servers list), when known. */
79
+ serverUrl?: string;
63
80
  sideEffect: SideEffect;
64
81
  requiresAuth: boolean;
65
- /** Where the description text came from always reviewable before commit. */
82
+ /** Where the description text came from. Always reviewable before commit. */
66
83
  description: string;
67
84
  descriptionSource: "openapi-summary" | "generated-template";
68
85
  }
@@ -72,16 +89,38 @@ interface ToolHints {
72
89
  destructiveHint: boolean;
73
90
  idempotentHint: boolean;
74
91
  }
92
+ /**
93
+ * What kind of endpoint a tool wraps. Most are ordinary "endpoint"s; the
94
+ * special roles drive special handling:
95
+ *
96
+ * webhook skipped entirely: it receives server callbacks, so an agent
97
+ * has nothing to call
98
+ * auth sign-in/session endpoints; generated disabled, flagged loudly
99
+ * admin admin operations; generated disabled, flagged loudly
100
+ */
101
+ type EndpointRole = "endpoint" | "webhook" | "auth" | "admin";
75
102
  /** A candidate after safety review: classified, hinted, and linted. */
76
103
  interface ReviewedTool extends CandidateTool {
77
104
  riskTier: RiskTier;
78
105
  hints: ToolHints;
106
+ endpointRole: EndpointRole;
107
+ /**
108
+ * Whether the generated tool works out of the box. Reads start enabled;
109
+ * mutations and risky endpoints start disabled (the working code is
110
+ * generated but commented out, one edit away from live).
111
+ */
112
+ enabledByDefault: boolean;
79
113
  /**
80
114
  * Output field paths the PII heuristics flagged (e.g. "user.email").
81
115
  * These are fields that would leave the page and reach the agent.
82
116
  */
83
117
  piiInOutput: string[];
84
118
  }
119
+ /** An endpoint the safety layer decided not to generate, with the reason. */
120
+ interface SkippedEndpoint {
121
+ ref: string;
122
+ reason: string;
123
+ }
85
124
  /** One audit finding. Errors block generation (unless --force); warnings don't. */
86
125
  interface AuditFinding {
87
126
  level: "error" | "warning";
@@ -89,13 +128,25 @@ interface AuditFinding {
89
128
  tool?: string;
90
129
  message: string;
91
130
  }
131
+ /**
132
+ * Hand-authored tweaks, usually written by the dev dashboard
133
+ * (`webmcp-codegen dev`) into `.webmcp-codegen.json`. They are applied after
134
+ * the safety review and survive regeneration, because they live outside the
135
+ * generated files.
136
+ */
137
+ interface ToolOverrides {
138
+ [toolName: string]: {
139
+ description?: string;
140
+ enabled?: boolean;
141
+ };
142
+ }
92
143
  /** A file the generator wants to write. */
93
144
  interface GeneratedFile {
94
145
  /** Absolute path on disk. */
95
146
  path: string;
96
147
  /** Full new contents. */
97
148
  contents: string;
98
- /** What writing this file would do used for the report and --dry-run. */
149
+ /** What writing this file would do. Used for the report and --dry-run. */
99
150
  action: "create" | "update" | "unchanged";
100
151
  /**
101
152
  * Present when an existing file was edited by hand in the generated region,
@@ -117,6 +168,8 @@ interface Source {
117
168
  */
118
169
  interface ToolGenerator {
119
170
  readonly kind: string;
171
+ /** Where the files go, relative to the project root. Reported by the CLI. */
172
+ readonly outDir: string;
120
173
  generate(tools: ReviewedTool[], cwd: string): Promise<GeneratedFile[]>;
121
174
  }
122
175
  /** Safety knobs. Everything here extends defaults; nothing is required. */
@@ -133,4 +186,4 @@ interface CodegenConfig {
133
186
  safety?: SafetyOptions;
134
187
  }
135
188
 
136
- export type { AuditFinding as A, CodegenConfig as C, GeneratedFile as G, JsonSchema as J, ReviewedTool as R, Source as S, ToolGenerator as T, CandidateTool as a, RiskTier as b, SafetyOptions as c, SideEffect as d, SourceKind as e, ToolHints as f };
189
+ export type { AuditFinding as A, CodegenConfig as C, GeneratedFile as G, JsonSchema as J, ReviewedTool as R, Source as S, ToolGenerator as T, ToolOverrides as a, SkippedEndpoint as b, CandidateTool as c, RiskTier as d, SafetyOptions as e, SideEffect as f, SourceKind as g, ToolHints as h };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webmcp-codegen",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Generate safe, typed, human-reviewed WebMCP tools from the API contracts you already have (OpenAPI, tRPC, Zod).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/schema.ts"],"sourcesContent":["/**\n * JSON Schema helpers: resolving `$ref` pointers and turning a schema into\n * TypeScript source text for the generated input types.\n *\n * Scope, deliberately small:\n * - Only *local* refs (`#/components/schemas/...`) are resolved. External\n * refs (other files or URLs) produce a clear error instead of a silent\n * wrong answer. This covers the overwhelming majority of hand-written and\n * framework-emitted OpenAPI specs.\n * - The TypeScript printer covers the shapes REST APIs actually use:\n * objects with required/optional fields, arrays, enums, primitives, and\n * `anyOf`/`oneOf` unions. Anything more exotic becomes `unknown` with a\n * TODO comment rather than a plausible-looking lie.\n */\n\nimport type { JsonSchema } from \"./types.js\";\n\n/**\n * Resolve a local `$ref` like \"#/components/schemas/Order\" against the\n * parsed spec document. Throws a clear error for external refs.\n */\nexport function resolveLocalRef(spec: unknown, ref: string): unknown {\n if (!ref.startsWith(\"#/\")) {\n throw new Error(\n `Cannot resolve external $ref \"${ref}\". ` +\n `Only local refs (starting with \"#/\") are supported — bundle the spec first if it is split across files.`,\n );\n }\n let node: unknown = spec;\n for (const segment of ref.slice(2).split(\"/\")) {\n // JSON Pointer escapes: \"~1\" is \"/\", \"~0\" is \"~\"\n const key = segment.replace(/~1/g, \"/\").replace(/~0/g, \"~\");\n if (node === null || typeof node !== \"object\" || !(key in node)) {\n throw new Error(`$ref \"${ref}\" does not point at anything in this spec (missing \"${key}\").`);\n }\n node = (node as Record<string, unknown>)[key];\n }\n return node;\n}\n\n/**\n * If `schema` is a `$ref`, resolve it (one level — recursion happens as the\n * caller walks the tree). Sibling keywords next to `$ref` are merged over\n * the resolved schema, which is what OpenAPI 3.1 semantics require.\n */\nexport function deref(schema: JsonSchema, spec: unknown): JsonSchema {\n const ref = schema.$ref;\n if (typeof ref !== \"string\") return schema;\n const target = resolveLocalRef(spec, ref) as JsonSchema;\n const { $ref: _ignored, ...siblings } = schema;\n return { ...target, ...siblings };\n}\n\n/**\n * Resolve `$ref`s at every depth, so the generated `inputSchema` is a\n * self-contained JSON Schema — the browser has no idea what\n * \"#/components/schemas/Order\" means, so refs must not survive codegen.\n *\n * Recursive models (Order → LineItem → Order) would loop forever, so a ref\n * that points back to one of its own ancestors resolves to a plain object\n * with a note instead. The tool schema stays finite and honest.\n */\nexport function deepDeref(\n schema: JsonSchema,\n spec: unknown,\n ancestorRefs: Set<string> = new Set(),\n): JsonSchema {\n const ref = schema.$ref;\n if (typeof ref === \"string\") {\n if (ancestorRefs.has(ref)) {\n return {\n type: \"object\",\n description: `Recursive reference to ${ref} (resolved once to keep the schema finite).`,\n };\n }\n const target = resolveLocalRef(spec, ref) as JsonSchema;\n return deepDeref(target, spec, new Set(ancestorRefs).add(ref));\n }\n\n const out: JsonSchema = { ...schema };\n if (out.properties) {\n out.properties = Object.fromEntries(\n Object.entries(out.properties).map(([key, value]) => [\n key,\n deepDeref(value, spec, ancestorRefs),\n ]),\n );\n }\n if (out.items) out.items = deepDeref(out.items, spec, ancestorRefs);\n for (const unionKeyword of [\"anyOf\", \"oneOf\", \"allOf\"] as const) {\n const variants = out[unionKeyword];\n if (variants) {\n out[unionKeyword] = variants.map((variant) => deepDeref(variant, spec, ancestorRefs));\n }\n }\n return out;\n}\n\n/**\n * Print a JSON Schema as TypeScript type source, e.g. for the generated\n * `GetOrderStatusInput` interface. `spec` is the root document, needed to\n * resolve any `$ref`s encountered along the way.\n */\nexport function jsonSchemaToTs(schema: JsonSchema, spec: unknown): string {\n const node = deref(schema, spec);\n\n if (node.enum && Array.isArray(node.enum)) {\n return node.enum.map((value) => JSON.stringify(value)).join(\" | \");\n }\n\n if (node.anyOf || node.oneOf) {\n const variants = (node.anyOf ?? node.oneOf) as JsonSchema[];\n return variants.map((variant) => jsonSchemaToTs(variant, spec)).join(\" | \");\n }\n\n if (node.allOf) {\n return node.allOf.map((part) => jsonSchemaToTs(part, spec)).join(\" & \");\n }\n\n switch (node.type) {\n case \"string\":\n return \"string\";\n case \"number\":\n case \"integer\":\n return \"number\";\n case \"boolean\":\n return \"boolean\";\n case \"null\":\n return \"null\";\n case \"array\": {\n const items = node.items ? jsonSchemaToTs(node.items, spec) : \"unknown\";\n // Wrap unions so `string | number[]` doesn't silently change meaning.\n return items.includes(\"|\") ? `Array<${items}>` : `${items}[]`;\n }\n case \"object\":\n case undefined: {\n // Schemas without an explicit \"type\" but with \"properties\" are objects.\n const properties = node.properties;\n if (!properties || Object.keys(properties).length === 0) {\n return \"Record<string, unknown>\";\n }\n const required = new Set(node.required ?? []);\n const fields = Object.entries(properties).map(([key, fieldSchema]) => {\n const optional = required.has(key) ? \"\" : \"?\";\n const nullable = fieldSchema.nullable ? \" | null\" : \"\";\n return `${JSON.stringify(key)}${optional}: ${jsonSchemaToTs(fieldSchema, spec)}${nullable}`;\n });\n return `{ ${fields.join(\"; \")} }`;\n }\n default:\n return \"unknown /* TODO: webmcp-codegen could not express this schema — tighten it by hand */\";\n }\n}\n\n/** \"get-order-status\" → \"GetOrderStatus\" (for generated type names). */\nexport function pascalCase(name: string): string {\n return name\n .split(/[-_]/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"\");\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/naming.ts"],"sourcesContent":["/**\n * Tool naming.\n *\n * Tool names are the vocabulary an agent reasons over, so we make them\n * boring and predictable: kebab-case, derived from the operationId when the\n * source has one, and always matching the character set the WebMCP runtime\n * accepts (the same rule the groundstate core registry enforces).\n */\n\n/** The character set the WebMCP runtime accepts for tool names. */\nexport const TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;\n\n/**\n * Turn an operationId or route into a valid, readable tool name.\n *\n * Examples:\n * \"getOrderStatus\" → \"get-order-status\"\n * \"GET /orders/{id}\" → \"get-orders-id\"\n * \"list_pets\" → \"list-pets\"\n */\nexport function toToolName(raw: string): string {\n const name = raw\n // Split acronym boundaries first: \"getHTTPStatus\" → \"get-HTTPStatus\"\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1-$2\")\n // Then camelCase and PascalCase boundaries: \"getOrder\" → \"get-Order\"\n .replace(/([a-z0-9])([A-Z])/g, \"$1-$2\")\n // Path placeholders and separators become dashes: \"/orders/{id}\" → \"-orders-id\"\n .replace(/[{}/_\\s.]+/g, \"-\")\n // Anything left that isn't a letter, digit or dash is dropped\n .replace(/[^a-zA-Z0-9-]/g, \"\")\n .toLowerCase()\n // Collapse and trim dashes\n .replace(/-+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n\n // A name must start with a letter. If it doesn't (e.g. it came from a bare\n // numeric path), give it a neutral prefix rather than failing.\n const prefixed = /^[a-zA-Z]/.test(name) ? name : `tool-${name}`;\n return prefixed || \"unnamed-tool\";\n}\n\n/**\n * Build the fallback name for an operation that has no operationId:\n * the HTTP method plus the path, e.g. GET /orders/{id} → \"get-orders-id\".\n */\nexport function nameFromRoute(method: string, path: string): string {\n return toToolName(`${method.toLowerCase()} ${path}`);\n}\n\n/**\n * Make every name unique. When two operations slugify to the same name we\n * append the HTTP method (\"get-order-status-post\" would be worse); when that\n * still collides we append a counter. Returns the final names plus a list of\n * renames so the audit report can show them.\n */\nexport function dedupeNames(candidates: { name: string; httpMethod?: string }[]): {\n names: string[];\n renames: { from: string; to: string }[];\n} {\n const seen = new Set<string>();\n const names: string[] = [];\n const renames: { from: string; to: string }[] = [];\n\n for (const candidate of candidates) {\n let name = candidate.name;\n if (seen.has(name) && candidate.httpMethod) {\n name = `${name}-${candidate.httpMethod.toLowerCase()}`;\n }\n let counter = 2;\n const base = name;\n while (seen.has(name)) {\n name = `${base}-${counter}`;\n counter += 1;\n }\n if (name !== candidate.name) {\n renames.push({ from: candidate.name, to: name });\n }\n seen.add(name);\n names.push(name);\n }\n\n return { names, renames };\n}\n"],"mappings":";AAoBO,SAAS,WAAW,KAAqB;AAC9C,QAAM,OAAO,IAEV,QAAQ,yBAAyB,OAAO,EAExC,QAAQ,sBAAsB,OAAO,EAErC,QAAQ,eAAe,GAAG,EAE1B,QAAQ,kBAAkB,EAAE,EAC5B,YAAY,EAEZ,QAAQ,OAAO,GAAG,EAClB,QAAQ,YAAY,EAAE;AAIzB,QAAM,WAAW,YAAY,KAAK,IAAI,IAAI,OAAO,QAAQ,IAAI;AAC7D,SAAO,YAAY;AACrB;AAMO,SAAS,cAAc,QAAgB,MAAsB;AAClE,SAAO,WAAW,GAAG,OAAO,YAAY,CAAC,IAAI,IAAI,EAAE;AACrD;AAQO,SAAS,YAAY,YAG1B;AACA,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,QAAkB,CAAC;AACzB,QAAM,UAA0C,CAAC;AAEjD,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,UAAU;AACrB,QAAI,KAAK,IAAI,IAAI,KAAK,UAAU,YAAY;AAC1C,aAAO,GAAG,IAAI,IAAI,UAAU,WAAW,YAAY,CAAC;AAAA,IACtD;AACA,QAAI,UAAU;AACd,UAAM,OAAO;AACb,WAAO,KAAK,IAAI,IAAI,GAAG;AACrB,aAAO,GAAG,IAAI,IAAI,OAAO;AACzB,iBAAW;AAAA,IACb;AACA,QAAI,SAAS,UAAU,MAAM;AAC3B,cAAQ,KAAK,EAAE,MAAM,UAAU,MAAM,IAAI,KAAK,CAAC;AAAA,IACjD;AACA,SAAK,IAAI,IAAI;AACb,UAAM,KAAK,IAAI;AAAA,EACjB;AAEA,SAAO,EAAE,OAAO,QAAQ;AAC1B;","names":[]}
@@ -1,251 +0,0 @@
1
- import {
2
- jsonSchemaToTs,
3
- pascalCase
4
- } from "./chunk-5L4KN6F4.js";
5
-
6
- // src/generators/js.ts
7
- import { readFile } from "fs/promises";
8
- import { join } from "path";
9
-
10
- // src/generators/js-templates.ts
11
- function generatedRegion(tool) {
12
- const pascal = pascalCase(tool.name);
13
- const camel = lowercaseFirst(pascal);
14
- const schemaJson = JSON.stringify(tool.inputSchema, null, 2);
15
- const inputType = jsonSchemaToTs(tool.inputSchema, void 0);
16
- return [
17
- `import { getModelContext } from "./runtime.webmcp";`,
18
- ``,
19
- GENERATED_START,
20
- `/**`,
21
- ` * ${tool.description}`,
22
- ` *`,
23
- ` * Source: ${tool.source.ref} (${tool.source.kind}) \xB7 risk: ${tool.riskTier}`,
24
- ` * Regenerate with: npx webmcp-codegen generate`,
25
- ` */`,
26
- ``,
27
- `/** The exact contract advertised to the agent. Derived from the API spec \u2014 do not hand-edit. */`,
28
- `export const ${camel}InputSchema = ${schemaJson};`,
29
- ``,
30
- `/** What \`execute\` receives. The browser validates agent input against the schema above. */`,
31
- `export type ${tool.inputTypeName} = ${inputType};`,
32
- ``,
33
- `/** Safety hints computed by webmcp-codegen. Informational metadata for hosts and UIs. */`,
34
- `export const ${camel}Hints = ${JSON.stringify(tool.hints)} as const;`,
35
- ``,
36
- `/** The tool definition, minus \`execute\` (which is yours, below the marker). */`,
37
- `export const ${camel}Tool = {`,
38
- ` name: ${JSON.stringify(tool.name)},`,
39
- ` description: ${JSON.stringify(tool.description)},`,
40
- ` inputSchema: ${camel}InputSchema,`,
41
- `};`,
42
- ``,
43
- `/**`,
44
- ` * Register this tool with WebMCP. Call it once on page load, or use`,
45
- ` * registerAllTools() from the generated index.ts.`,
46
- ` *`,
47
- ` * Pass an AbortSignal to unregister later: controller.abort().`,
48
- ` */`,
49
- `export async function register${pascal}(signal?: AbortSignal): Promise<void> {`,
50
- ` const modelContext = getModelContext();`,
51
- ` await modelContext.registerTool(`,
52
- ` {`,
53
- ` ...${camel}Tool,`,
54
- ` // The browser has already validated the agent's input against the schema.`,
55
- ` execute: (input) => execute${pascal}(input as ${tool.inputTypeName}),`,
56
- ` },`,
57
- ` { signal },`,
58
- ` );`,
59
- `}`,
60
- ``,
61
- GENERATED_END
62
- ].join("\n");
63
- }
64
- function ownedRegionScaffold(tool) {
65
- const pascal = pascalCase(tool.name);
66
- const lines = [
67
- ``,
68
- `/**`,
69
- ` * What actually happens when the agent calls "${tool.name}".`,
70
- ` *`,
71
- ` * Source: ${tool.source.ref} \u2014 call your existing client code here.`,
72
- ` * Return { content: [{ type: "text", text: ... }] } (the MCP result shape).`
73
- ];
74
- if (tool.riskTier !== "safe-read") {
75
- lines.push(
76
- ` *`,
77
- ` * \u26A0 This tool is ${tool.riskTier}: it ${tool.riskTier === "destructive-confirm" ? "cannot easily be undone" : "changes things"}.`,
78
- ` * Ask the user before acting \u2014 see requestUserConfirmation() in runtime.webmcp.ts.`
79
- );
80
- }
81
- lines.push(` */`);
82
- if (tool.piiInOutput.length > 0) {
83
- lines.push(
84
- `//`,
85
- `// \u26A0 webmcp-codegen flagged these response fields as likely PII: ${tool.piiInOutput.join(", ")}.`,
86
- `// Everything you return reaches the agent. Leave those fields out unless`,
87
- `// the agent genuinely needs them, and say so in a comment if you keep them.`
88
- );
89
- }
90
- lines.push(
91
- `export async function execute${pascal}(input: ${tool.inputTypeName}) {`,
92
- ...usageExample(tool),
93
- ` throw new Error("Not implemented: execute${pascal}");`,
94
- `}`
95
- );
96
- return lines.join("\n");
97
- }
98
- function usageExample(tool) {
99
- if (!tool.httpMethod) {
100
- return [` // TODO: implement using your app's existing code.`];
101
- }
102
- const path = tool.source.ref.replace(/^[A-Z]+ /, "");
103
- const exampleUrl = path.replace(/\{(\w+)\}/g, (_match, param) => `" + input.${param} + "`).replace(/^"" \+ /, "").replace(/ \+ ""$/, "");
104
- const fetchArgs = tool.httpMethod === "GET" ? `"${exampleUrl}"` : `"${exampleUrl}", { method: "${tool.httpMethod}" }`;
105
- return [
106
- ` // TODO: implement using your app's existing code, e.g.:`,
107
- ` // const response = await fetch(${fetchArgs});`,
108
- ` // if (!response.ok) throw new Error("Request failed: " + response.status);`,
109
- ` // return { content: [{ type: "text", text: "Done" }] };`
110
- ];
111
- }
112
- function runtimeSource() {
113
- return `/**
114
- * Generated by webmcp-codegen \u2014 this file is fully regenerated on every run.
115
- * Do not edit by hand; your changes will be lost.
116
- */
117
-
118
- /** The result shape tools return (same as MCP tool results). */
119
- export interface WebMcpToolResult {
120
- content: { type: "text"; text: string }[];
121
- [key: string]: unknown;
122
- }
123
-
124
- /** A tool as the browser runtime understands it. */
125
- export interface WebMcpToolDefinition {
126
- name: string;
127
- description: string;
128
- inputSchema?: Record<string, unknown>;
129
- execute: (input: Record<string, unknown>) => unknown | Promise<unknown>;
130
- }
131
-
132
- /** The slice of the WebMCP draft spec the generated code uses. */
133
- export interface ModelContext {
134
- registerTool(
135
- tool: WebMcpToolDefinition,
136
- options?: { signal?: AbortSignal },
137
- ): Promise<void>;
138
- }
139
-
140
- /**
141
- * Access the page's WebMCP model context, with a helpful error when the
142
- * browser doesn't have one (rather than an undefined-callsite mystery).
143
- */
144
- export function getModelContext(): ModelContext {
145
- const modelContext = (document as unknown as { modelContext?: ModelContext }).modelContext;
146
- if (!modelContext) {
147
- throw new Error(
148
- "WebMCP is not available in this browser. " +
149
- "Enable chrome://flags/#enable-webmcp-testing (Chrome 146+), " +
150
- "or add the WebMCP polyfill to your app.",
151
- );
152
- }
153
- return modelContext;
154
- }
155
-
156
- /**
157
- * Default "agent proposes, human confirms" gate for write/destructive tools.
158
- * Deliberately minimal (window.confirm) \u2014 replace it with your app's own
159
- * dialog when you outgrow it. The point is that the user always gets a say.
160
- */
161
- export function requestUserConfirmation(message: string): Promise<boolean> {
162
- return Promise.resolve(window.confirm(message));
163
- }
164
- `;
165
- }
166
- function barrelSource(tools) {
167
- const imports = tools.map((tool) => `import { register${pascalCase(tool.name)} } from "./${tool.name}.webmcp";`).join("\n");
168
- const names = tools.map((tool) => `register${pascalCase(tool.name)}`).join(",\n ");
169
- return `/**
170
- * Generated by webmcp-codegen \u2014 this file is fully regenerated on every run.
171
- * Import registerAllTools() once at app startup:
172
- *
173
- * import { registerAllTools } from "./webmcp";
174
- * await registerAllTools();
175
- */
176
-
177
- ${imports}
178
-
179
- const registrations = [
180
- ${names}
181
- ];
182
-
183
- /**
184
- * Register every generated tool with WebMCP. One tool failing (for example
185
- * because the page's Permissions-Policy disables tools) never takes the
186
- * others down with it \u2014 the failure is logged and registration continues.
187
- */
188
- export async function registerAllTools(signal?: AbortSignal): Promise<void> {
189
- for (const register of registrations) {
190
- try {
191
- await register(signal);
192
- } catch (error) {
193
- console.warn("[webmcp-codegen] a tool failed to register:", error);
194
- }
195
- }
196
- }
197
- `;
198
- }
199
- function lowercaseFirst(pascal) {
200
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
201
- }
202
-
203
- // src/generators/js.ts
204
- var GENERATED_START = "// \u2500\u2500\u2500 webmcp-codegen: generated \u2014 do not edit this region \u2500\u2500\u2500";
205
- var GENERATED_END = "// \u2500\u2500\u2500 webmcp-codegen: end generated \u2014 your code below survives regeneration \u2500\u2500\u2500";
206
- function js(options) {
207
- return {
208
- kind: "js",
209
- async generate(tools, cwd) {
210
- const outDir = join(cwd, options.outDir);
211
- const files = [];
212
- files.push(await plainFile(join(outDir, "runtime.webmcp.ts"), runtimeSource()));
213
- files.push(await plainFile(join(outDir, "index.ts"), barrelSource(tools)));
214
- for (const tool of tools) {
215
- files.push(await toolFile(tool, outDir));
216
- }
217
- return files;
218
- }
219
- };
220
- }
221
- async function plainFile(path, contents) {
222
- try {
223
- const existing = await readFile(path, "utf8");
224
- return { path, contents, action: existing === contents ? "unchanged" : "update" };
225
- } catch {
226
- return { path, contents, action: "create" };
227
- }
228
- }
229
- async function toolFile(tool, outDir) {
230
- const path = join(outDir, `${tool.name}.webmcp.ts`);
231
- const head = generatedRegion(tool);
232
- let existing;
233
- try {
234
- existing = await readFile(path, "utf8");
235
- } catch {
236
- return { path, contents: `${head}
237
- ${ownedRegionScaffold(tool)}`, action: "create" };
238
- }
239
- const markerIndex = existing.indexOf(GENERATED_END);
240
- if (markerIndex === -1) {
241
- return { path, contents: existing, action: "unchanged", conflict: `${path}.new` };
242
- }
243
- const preservedTail = existing.slice(markerIndex + GENERATED_END.length);
244
- const contents = head + preservedTail;
245
- return { path, contents, action: contents === existing ? "unchanged" : "update" };
246
- }
247
-
248
- export {
249
- js
250
- };
251
- //# sourceMappingURL=chunk-GDJDVR4E.js.map
@@ -1 +0,0 @@
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 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() stub (written once, then owned)\n * - runtimeSource() / barrelSource() fully-generated support files\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\n return [\n `import { getModelContext } from \"./runtime.webmcp\";`,\n ``,\n GENERATED_START,\n `/**`,\n ` * ${tool.description}`,\n ` *`,\n ` * Source: ${tool.source.ref} (${tool.source.kind}) · risk: ${tool.riskTier}`,\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 ` 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 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 */\nexport function ownedRegionScaffold(tool: ReviewedTool): string {\n const pascal = pascalCase(tool.name);\n const lines: string[] = [\n ``,\n `/**`,\n ` * What actually happens when the agent calls \"${tool.name}\".`,\n ` *`,\n ` * Source: ${tool.source.ref} — call your existing client code here.`,\n ` * Return { content: [{ type: \"text\", text: ... }] } (the MCP result shape).`,\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 ` * Ask the user before acting — see requestUserConfirmation() in runtime.webmcp.ts.`,\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 unless`,\n `// the agent genuinely needs them, and say so in a comment if you keep them.`,\n );\n }\n\n lines.push(\n `export async function execute${pascal}(input: ${tool.inputTypeName}) {`,\n ...usageExample(tool),\n ` throw new Error(\"Not implemented: execute${pascal}\");`,\n `}`,\n );\n\n return lines.join(\"\\n\");\n}\n\n/**\n * The TODO example inside a fresh scaffold. When the source knows the route\n * (OpenAPI always does), the example shows the actual call — seeing\n * `fetch(\"/pets/\" + input.id, …)` beats an abstract placeholder every time.\n */\nfunction usageExample(tool: ReviewedTool): string[] {\n if (!tool.httpMethod) {\n return [` // TODO: implement using your app's existing code.`];\n }\n const path = tool.source.ref.replace(/^[A-Z]+ /, \"\");\n // Turn \"/pets/{id}\" into '\"/pets/\" + input.id' — a copy-pasteable example.\n const exampleUrl = path\n .replace(/\\{(\\w+)\\}/g, (_match, param: string) => `\" + input.${param} + \"`)\n // Trim the empty-string concat a leading/trailing placeholder leaves behind.\n .replace(/^\"\" \\+ /, \"\")\n .replace(/ \\+ \"\"$/, \"\");\n const fetchArgs =\n tool.httpMethod === \"GET\"\n ? `\"${exampleUrl}\"`\n : `\"${exampleUrl}\", { method: \"${tool.httpMethod}\" }`;\n return [\n ` // TODO: implement using your app's existing code, e.g.:`,\n ` // const response = await fetch(${fetchArgs});`,\n ` // if (!response.ok) throw new Error(\"Request failed: \" + response.status);`,\n ` // return { content: [{ type: \"text\", text: \"Done\" }] };`,\n ];\n}\n\n/**\n * The shared runtime: the minimal WebMCP browser types plus getModelContext().\n * Kept tiny on purpose — this is the only browser 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 [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 * 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":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/config.ts","../src/pipeline.ts","../src/safety.ts"],"sourcesContent":["/**\n * Config: `defineConfig` for authoring, `loadConfig` for the CLI.\n *\n * Config files are plain JavaScript (`codegen.config.mjs`) so the CLI can\n * load them with a plain dynamic import — no TypeScript loader, no build\n * step, no extra dependencies. If you want types while authoring, that is\n * what `defineConfig` is for:\n *\n * import { defineConfig } from \"webmcp-codegen\";\n * export default defineConfig({ ... });\n */\n\nimport { access } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { CodegenConfig } from \"./types.js\";\n\n/** Identity function whose only job is type-checking the config object. */\nexport function defineConfig(config: CodegenConfig): CodegenConfig {\n return config;\n}\n\nexport const CONFIG_FILE_NAMES = [\"codegen.config.mjs\", \"codegen.config.js\"];\n\n/**\n * Find and load the config file. Resolving relative to `cwd` keeps the CLI\n * usable from any directory, the same way `eslint -c` behaves.\n */\nexport async function loadConfig(\n cwd: string,\n explicitPath?: string,\n): Promise<{ config: CodegenConfig; path: string }> {\n const candidates = explicitPath\n ? [resolve(cwd, explicitPath)]\n : CONFIG_FILE_NAMES.map((name) => join(cwd, name));\n\n for (const candidate of candidates) {\n if (!(await exists(candidate))) continue;\n const module = (await import(pathToFileURL(candidate).href)) as { default?: unknown };\n const config = module.default;\n if (!isCodegenConfig(config)) {\n throw new Error(\n `${candidate} must default-export defineConfig({ sources: [...], generate: [...] }).`,\n );\n }\n return { config, path: candidate };\n }\n\n throw new Error(\n explicitPath\n ? `No config file at \"${explicitPath}\".`\n : `No codegen.config.mjs found in ${cwd}. Run \\`npx webmcp-codegen init\\` to create one.`,\n );\n}\n\n/** The lightest possible shape check — clear error beats deep validation. */\nfunction isCodegenConfig(value: unknown): value is CodegenConfig {\n if (value === null || typeof value !== \"object\") return false;\n const config = value as Record<string, unknown>;\n return Array.isArray(config.sources) && Array.isArray(config.generate);\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * The pipeline: sources → normalize → safety review → audit → write.\n *\n * This module is the only place the stages meet. It owns no opinions of its\n * own — naming, safety, and file formats all live in their own modules — it\n * just runs them in order and produces one honest report of what happened\n * (or what *would* happen, when called with `write: false`).\n */\n\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport { dedupeNames } from \"./naming.js\";\nimport { auditTools, reviewTools } from \"./safety.js\";\nimport { pascalCase } from \"./schema.js\";\nimport type { AuditFinding, CodegenConfig, GeneratedFile, ReviewedTool } from \"./types.js\";\n\nexport interface GenerateOptions {\n /** Project root. Everything (config, spec paths, outDir) resolves from here. */\n cwd: string;\n /** Preview mode: compute everything, write nothing. */\n dryRun?: boolean;\n /** Skip the audit pass entirely (classification still runs — output needs it). */\n skipAudit?: boolean;\n /** Write even when the audit found errors. The report still shows them. */\n force?: boolean;\n}\n\nexport interface GenerateResult {\n tools: ReviewedTool[];\n findings: AuditFinding[];\n files: GeneratedFile[];\n /** True when audit errors stopped any file from being written. */\n blocked: boolean;\n /** True when this run actually wrote files (false for dry runs and blocks). */\n wrote: boolean;\n}\n\nexport async function runGenerate(\n config: CodegenConfig,\n options: GenerateOptions,\n): Promise<GenerateResult> {\n // 1. Collect candidate tools from every configured source.\n const candidates = (await Promise.all(config.sources.map((source) => source.collect()))).flat();\n\n // 2. Normalize: make names unique before anything downstream sees them.\n // The input type name is derived from the *final* name so they never drift.\n const { names, renames } = dedupeNames(candidates);\n const named = candidates.map((candidate, index) => {\n const name = names[index] ?? candidate.name;\n return { ...candidate, name, inputTypeName: `${pascalCase(name)}Input` };\n });\n\n // 3. Safety review: classify side effects, compute hints, scan for PII,\n // apply config exclusions.\n const tools = reviewTools(named, config.safety);\n\n // 4. Audit. Errors block the write unless --force (or --skip-audit) was passed.\n const findings = options.skipAudit ? [] : auditTools(tools, renames);\n const errors = findings.filter((finding) => finding.level === \"error\");\n const blocked = errors.length > 0 && !options.force && !options.skipAudit;\n\n if (blocked) {\n return { tools, findings, files: [], blocked, wrote: false };\n }\n\n // 5. Generate the files, then write them (unless this is a dry run).\n const files: GeneratedFile[] = [];\n for (const generator of config.generate) {\n files.push(...(await generator.generate(tools, options.cwd)));\n }\n\n let wrote = false;\n if (!options.dryRun) {\n for (const file of files) {\n if (file.action === \"unchanged\" && !file.conflict) continue;\n // A conflict means a human edited the generated region by hand:\n // leave their file alone and put our version in a `.new` sibling.\n const target = file.conflict ?? file.path;\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, file.conflict ? conflictContents(file) : file.contents);\n }\n wrote = true;\n }\n\n return { tools, findings, files, blocked, wrote };\n}\n\n/**\n * When a hand-edited file blocks regeneration, the `.new` file explains\n * itself at the top so nobody mistakes it for something to import.\n */\nfunction conflictContents(file: GeneratedFile): string {\n return (\n `// webmcp-codegen could not regenerate ${file.path} because its generated\\n` +\n `// region was edited by hand. Review this version, then merge it manually.\\n\\n` +\n file.contents\n );\n}\n","/**\n * The safety layer.\n *\n * Nothing gets written to disk until every candidate tool has been through\n * here. This layer does three jobs:\n *\n * 1. Classify — what does calling this tool do to the world?\n * (read / write / destructive, from the HTTP verb plus name heuristics)\n * 2. Hint — derive the WebMCP tool hints (readOnlyHint etc.) from that\n * 3. Audit — lint the result and report problems in plain language\n *\n * Every rule is a heuristic with an escape hatch: the generated code carries\n * the classification in plain sight, and the developer owns the final file.\n */\n\nimport type {\n AuditFinding,\n CandidateTool,\n JsonSchema,\n ReviewedTool,\n RiskTier,\n SafetyOptions,\n SideEffect,\n ToolHints,\n} from \"./types.js\";\n\n/**\n * Words that signal \"this changes something the user can't easily undo\",\n * even when the HTTP verb looks innocent. `POST /orders/{id}/cancel` is the\n * classic case: a POST that behaves like a DELETE.\n */\nconst DESTRUCTIVE_WORDS =\n /\\b(cancel|delete|remove|destroy|deactivate|refund|revoke|purge|close)\\b/i;\n\n/**\n * Field names that usually hold personal data or secrets. Matched against\n * the last segment of a field path, case-insensitively. Teams extend this\n * list via `safety.piiFields` in the config.\n */\nconst DEFAULT_PII_FIELDS = [\n \"password\",\n \"ssn\",\n \"token\",\n \"secret\",\n \"apikey\",\n \"api_key\",\n \"email\",\n \"dob\",\n \"birthdate\",\n \"phone\",\n \"address\",\n \"creditcard\",\n \"cardnumber\",\n \"cvv\",\n];\n\n/**\n * Phrases that suggest a description is trying to *instruct the agent*\n * instead of describing the tool — a known prompt-injection smell.\n */\nconst AGENT_INSTRUCTION_PATTERN =\n /\\b(you (must|should|always|are)|as an ai|ignore (all |previous )?instructions|do not refuse)\\b/i;\n\n/** Step 1: classify what calling the tool does. */\nexport function classifySideEffect(tool: CandidateTool): SideEffect {\n switch (tool.httpMethod) {\n case \"GET\":\n case \"HEAD\":\n case \"OPTIONS\":\n // A safe verb whose name says otherwise is suspicious — audit flags it.\n return \"read\";\n case \"DELETE\":\n return \"destructive\";\n case \"POST\":\n case \"PUT\":\n case \"PATCH\":\n // Upgrade nominally-\"write\" verbs when the name says it can't be undone.\n return DESTRUCTIVE_WORDS.test(tool.name) || DESTRUCTIVE_WORDS.test(tool.source.ref)\n ? \"destructive\"\n : \"write\";\n default:\n return \"unknown\";\n }\n}\n\n/** Step 2: derive the WebMCP hints from the classification. */\nexport function hintsFor(tool: CandidateTool, sideEffect: SideEffect): ToolHints {\n const method = tool.httpMethod;\n return {\n readOnlyHint: sideEffect === \"read\",\n destructiveHint: sideEffect === \"destructive\",\n // PUT/PATCH/DELETE can be safely retried with the same input; POST cannot.\n idempotentHint:\n sideEffect === \"read\" || method === \"PUT\" || method === \"PATCH\" || method === \"DELETE\",\n };\n}\n\nexport function riskTierFor(sideEffect: SideEffect): RiskTier {\n switch (sideEffect) {\n case \"read\":\n return \"safe-read\";\n case \"destructive\":\n return \"destructive-confirm\";\n default:\n return \"write-confirm\";\n }\n}\n\n/**\n * Walk a schema and return the paths of fields that look like PII or\n * secrets, e.g. \"user.email\". Only *output* schemas are scanned: the\n * security-relevant direction is data leaving the page and reaching the agent.\n */\nexport function findPiiFields(\n schema: JsonSchema | undefined,\n extraFields: string[] = [],\n prefix = \"\",\n): string[] {\n if (!schema?.properties) return [];\n const piiNames = new Set(\n [...DEFAULT_PII_FIELDS, ...extraFields].map((name) => name.toLowerCase()),\n );\n const found: string[] = [];\n\n for (const [key, fieldSchema] of Object.entries(schema.properties)) {\n const path = prefix ? `${prefix}.${key}` : key;\n const normalizedKey = key.toLowerCase().replace(/[-_]/g, \"\");\n const looksSensitive =\n piiNames.has(key.toLowerCase()) ||\n piiNames.has(normalizedKey) ||\n [...piiNames].some((name) => normalizedKey === name.replace(/[-_]/g, \"\"));\n if (looksSensitive) found.push(path);\n // Recurse into nested objects (\"user\": { \"email\": ... }).\n found.push(...findPiiFields(fieldSchema, extraFields, path));\n }\n return found;\n}\n\n/** Run the full review: classify, hint, PII-scan. Pure — no I/O. */\nexport function reviewTools(\n candidates: CandidateTool[],\n safety: SafetyOptions = {},\n): ReviewedTool[] {\n const excluded = (safety.exclude ?? []).map((pattern) => pattern.toLowerCase());\n\n return candidates\n .filter(\n (tool) =>\n !excluded.some(\n (pattern) =>\n tool.name.toLowerCase().includes(pattern) ||\n tool.source.ref.toLowerCase().includes(pattern),\n ),\n )\n .map((tool) => {\n const sideEffect = classifySideEffect(tool);\n return {\n ...tool,\n sideEffect,\n riskTier: riskTierFor(sideEffect),\n hints: hintsFor(tool, sideEffect),\n piiInOutput: findPiiFields(tool.outputSchema, safety.piiFields),\n };\n });\n}\n\n/**\n * Step 3: audit the reviewed tools and report in plain language.\n * Errors block file writing (unless --force); warnings never do. This is\n * meant to run in CI like `npm audit` — exit codes, not vibes.\n */\nexport function auditTools(\n tools: ReviewedTool[],\n renames: { from: string; to: string }[] = [],\n): AuditFinding[] {\n const findings: AuditFinding[] = [];\n\n for (const rename of renames) {\n findings.push({\n level: \"warning\",\n tool: rename.to,\n message: `Renamed \"${rename.from}\" → \"${rename.to}\" to keep tool names unique.`,\n });\n }\n\n for (const tool of tools) {\n if (!tool.description || tool.description.trim().length === 0) {\n findings.push({\n level: \"error\",\n tool: tool.name,\n message: \"No description. Agents pick tools by description — this tool is invisible.\",\n });\n continue;\n }\n\n if (tool.descriptionSource === \"generated-template\") {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n `Description is just \"${tool.description}\" (no summary in the source). ` +\n \"Write one sentence about what it does and why — it goes straight into the agent's prompt.\",\n });\n }\n\n if (AGENT_INSTRUCTION_PATTERN.test(tool.description)) {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n \"The description reads like instructions to the agent, not a description of the tool. \" +\n \"Describe what the tool does; never try to steer the agent from here.\",\n });\n }\n\n if (tool.riskTier === \"safe-read\" && DESTRUCTIVE_WORDS.test(tool.name)) {\n findings.push({\n level: \"error\",\n tool: tool.name,\n message:\n `The name suggests something destructive but ${tool.httpMethod} is a safe verb. ` +\n \"Check the spec — a GET named like a delete is either mislabeled or a design smell.\",\n });\n }\n\n if (tool.piiInOutput.length > 0) {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n `Response may expose ${tool.piiInOutput.join(\", \")}. ` +\n \"These fields reach the agent — exclude them in execute() unless they are truly needed.\",\n });\n }\n\n if (tool.requiresAuth && tool.riskTier !== \"safe-read\") {\n findings.push({\n level: \"warning\",\n tool: tool.name,\n message:\n \"This mutating tool wraps an authenticated endpoint. It runs with the page's session — \" +\n \"make sure your server-side authorization checks apply to tool calls too.\",\n });\n }\n }\n\n return findings;\n}\n"],"mappings":"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","names":[]}