webmcp-codegen 0.2.1 → 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,638 @@
1
+ import {
2
+ loadDataFile,
3
+ resolveSetup,
4
+ saveDataFile
5
+ } from "./chunk-JVBVTHZ7.js";
6
+ import {
7
+ runGenerate
8
+ } from "./chunk-MJQ5B6HB.js";
9
+ import "./chunk-TGOJ3HUE.js";
10
+ import "./chunk-3LTHWIAP.js";
11
+ import "./chunk-FWSATV7C.js";
12
+ import "./chunk-KSQMJERY.js";
13
+
14
+ // src/dev/server.ts
15
+ import { spawn } from "child_process";
16
+ import { createServer } from "http";
17
+
18
+ // src/dev/ui.ts
19
+ function dashboardHtml() {
20
+ return `<!doctype html>
21
+ <html lang="en">
22
+ <head>
23
+ <meta charset="utf-8" />
24
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
25
+ <title>webmcp-codegen</title>
26
+ <style>
27
+ :root {
28
+ --baseline: #0a0b0f;
29
+ --panel: #0f1117;
30
+ --panel-raised: #151822;
31
+ --line: #20242f;
32
+ --ink: #e9ecf2;
33
+ --dim: #9aa3b2;
34
+ --faint: #5d6575;
35
+ --ghost: #3b4150;
36
+ --accent: #58a6ff;
37
+ --signal: #e3b341;
38
+ --fault: #f47067;
39
+ --sans: ui-sans-serif, system-ui, -apple-system, sans-serif;
40
+ --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
41
+ }
42
+ * { box-sizing: border-box; }
43
+ html, body { margin: 0; height: 100%; }
44
+ body {
45
+ background: var(--baseline);
46
+ color: var(--ink);
47
+ font-family: var(--sans);
48
+ font-size: 14px;
49
+ -webkit-font-smoothing: antialiased;
50
+ }
51
+ ::selection { background: var(--accent); color: var(--baseline); }
52
+ button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
53
+ input, textarea { font: inherit; color: var(--ink); background: var(--panel); border: 1px solid var(--line); border-radius: 6px; }
54
+ input:focus-visible, textarea:focus-visible, button:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }
55
+
56
+ #app { display: grid; grid-template-columns: 300px 1fr; height: 100vh; }
57
+
58
+ /* \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
59
+ aside {
60
+ border-right: 1px solid var(--line);
61
+ display: flex;
62
+ flex-direction: column;
63
+ min-height: 0;
64
+ }
65
+ .brand {
66
+ display: flex; align-items: center; gap: 10px;
67
+ padding: 16px 16px 12px;
68
+ font-family: var(--mono); font-size: 13px; letter-spacing: -0.01em;
69
+ }
70
+ .brand::before { content: ""; width: 8px; height: 8px; background: var(--accent); }
71
+ .brand .meta { color: var(--faint); font-size: 11px; margin-left: auto; }
72
+ .search { padding: 0 12px 12px; }
73
+ .search input {
74
+ width: 100%; padding: 7px 10px; font-family: var(--mono); font-size: 12px;
75
+ }
76
+ .search input::placeholder { color: var(--ghost); }
77
+ .tools { overflow-y: auto; flex: 1; padding: 4px 8px 16px; }
78
+ .tool {
79
+ display: flex; align-items: center; gap: 8px; width: 100%;
80
+ padding: 7px 8px; border-radius: 6px; text-align: left;
81
+ color: var(--dim);
82
+ }
83
+ .tool:hover { background: var(--panel); color: var(--ink); }
84
+ .tool[aria-selected="true"] { background: var(--panel-raised); color: var(--ink); }
85
+ .tool .name { font-family: var(--mono); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
86
+ .tool .verb {
87
+ margin-left: auto; font-family: var(--mono); font-size: 10px;
88
+ padding: 2px 6px; border-radius: 4px; border: 1px solid var(--line);
89
+ color: var(--faint); flex-shrink: 0;
90
+ }
91
+ .tool .verb.read { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent); }
92
+ .tool .verb.write { color: var(--signal); border-color: color-mix(in srgb, var(--signal) 35%, transparent); }
93
+ .tool .verb.destructive { color: var(--fault); border-color: color-mix(in srgb, var(--fault) 35%, transparent); }
94
+ .tool .off {
95
+ width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
96
+ background: var(--ghost);
97
+ }
98
+ .tool .off[title] { cursor: help; }
99
+ .empty { padding: 24px 16px; color: var(--faint); font-size: 13px; line-height: 1.6; }
100
+
101
+ /* \u2500\u2500 Detail pane \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
102
+ main { overflow-y: auto; min-width: 0; }
103
+ .detail { max-width: 780px; padding: 32px 40px 80px; }
104
+ .crumb { font-family: var(--mono); font-size: 11px; color: var(--ghost); margin-bottom: 10px; }
105
+ h1 { font-size: 22px; font-weight: 600; letter-spacing: -0.01em; margin: 0 0 6px; font-family: var(--mono); }
106
+ .route { font-family: var(--mono); font-size: 12px; color: var(--faint); margin-bottom: 24px; }
107
+ .badges { display: flex; gap: 8px; margin-bottom: 24px; flex-wrap: wrap; }
108
+ .badge {
109
+ font-family: var(--mono); font-size: 11px; padding: 3px 8px;
110
+ border: 1px solid var(--line); border-radius: 999px; color: var(--dim);
111
+ }
112
+ .badge.accent { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent); }
113
+ .badge.warn { color: var(--signal); border-color: color-mix(in srgb, var(--signal) 35%, transparent); }
114
+ .badge.err { color: var(--fault); border-color: color-mix(in srgb, var(--fault) 35%, transparent); }
115
+
116
+ .field-label {
117
+ font-family: var(--mono); font-size: 10px; text-transform: uppercase;
118
+ letter-spacing: 0.18em; color: var(--faint); margin: 28px 0 8px;
119
+ }
120
+ textarea#desc {
121
+ width: 100%; min-height: 70px; padding: 10px 12px; font-size: 14px;
122
+ line-height: 1.55; resize: vertical;
123
+ }
124
+ .hint { color: var(--faint); font-size: 12px; margin-top: 6px; line-height: 1.5; }
125
+ .row { display: flex; align-items: center; gap: 12px; }
126
+ .save {
127
+ margin-top: 10px; padding: 7px 14px; border: 1px solid var(--line);
128
+ border-radius: 6px; font-size: 13px; color: var(--ink); background: var(--panel-raised);
129
+ }
130
+ .save:hover { border-color: var(--accent); }
131
+ .save[disabled] { opacity: 0.4; cursor: default; }
132
+ .saved { color: var(--accent); font-size: 12px; font-family: var(--mono); }
133
+
134
+ /* Toggle */
135
+ .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; }
136
+ .switch { position: relative; width: 34px; height: 20px; border-radius: 999px; background: var(--ghost); transition: background 150ms; flex-shrink: 0; }
137
+ .switch[aria-checked="true"] { background: var(--accent); }
138
+ .switch::after {
139
+ content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
140
+ border-radius: 50%; background: var(--ink); transition: left 150ms;
141
+ }
142
+ .switch[aria-checked="true"]::after { left: 16px; }
143
+ .toggle-copy { font-size: 13px; color: var(--dim); line-height: 1.5; }
144
+ .toggle-copy strong { color: var(--ink); font-weight: 600; }
145
+
146
+ /* Findings */
147
+ .finding {
148
+ display: flex; gap: 8px; padding: 10px 12px; border: 1px solid var(--line);
149
+ border-radius: 6px; font-size: 12.5px; line-height: 1.5; color: var(--dim);
150
+ margin-bottom: 8px; background: var(--panel);
151
+ }
152
+ .finding.warn { border-color: color-mix(in srgb, var(--signal) 30%, transparent); }
153
+ .finding.error { border-color: color-mix(in srgb, var(--fault) 30%, transparent); }
154
+ .finding .icon { color: var(--signal); }
155
+ .finding.error .icon { color: var(--fault); }
156
+
157
+ /* Try it */
158
+ .try { margin-top: 28px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); }
159
+ .try > header { padding: 12px 16px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 10px; }
160
+ .try > header h2 { font-size: 13px; margin: 0; font-weight: 600; }
161
+ .try > header .note { color: var(--faint); font-size: 11.5px; margin-left: auto; }
162
+ .try .body { padding: 16px; }
163
+ .param { display: grid; grid-template-columns: 180px 1fr; gap: 12px; align-items: center; margin-bottom: 10px; }
164
+ .param label { font-family: var(--mono); font-size: 12px; color: var(--dim); }
165
+ .param label .req { color: var(--fault); }
166
+ .param input { padding: 7px 10px; font-family: var(--mono); font-size: 12px; width: 100%; }
167
+ .base-url { margin-bottom: 14px; }
168
+ .base-url input { width: 100%; padding: 7px 10px; font-family: var(--mono); font-size: 12px; }
169
+ .run {
170
+ margin-top: 6px; padding: 8px 16px; border-radius: 6px; font-size: 13px;
171
+ background: var(--accent); color: var(--baseline); font-weight: 600;
172
+ }
173
+ .run[disabled] { opacity: 0.5; cursor: default; }
174
+ pre.result {
175
+ margin: 14px 0 0; padding: 12px 14px; background: var(--baseline);
176
+ border: 1px solid var(--line); border-radius: 6px; font-family: var(--mono);
177
+ font-size: 12px; line-height: 1.55; overflow-x: auto; max-height: 320px;
178
+ color: var(--dim); white-space: pre-wrap; word-break: break-word;
179
+ }
180
+ pre.result.ok { border-color: color-mix(in srgb, var(--accent) 35%, transparent); }
181
+ pre.result.err { border-color: color-mix(in srgb, var(--fault) 35%, transparent); }
182
+
183
+ .placeholder { max-width: 480px; margin: 20vh auto 0; text-align: center; color: var(--faint); line-height: 1.7; }
184
+ .placeholder kbd {
185
+ font-family: var(--mono); font-size: 11px; border: 1px solid var(--line);
186
+ border-radius: 4px; padding: 1px 5px; color: var(--dim);
187
+ }
188
+ .toast {
189
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
190
+ background: var(--panel-raised); border: 1px solid var(--line); color: var(--ink);
191
+ padding: 8px 14px; border-radius: 6px; font-size: 12.5px; opacity: 0;
192
+ transition: opacity 150ms; pointer-events: none;
193
+ }
194
+ .toast.show { opacity: 1; }
195
+ </style>
196
+ </head>
197
+ <body>
198
+ <div id="app">
199
+ <aside>
200
+ <div class="brand">webmcp-codegen <span class="meta" id="tool-count"></span></div>
201
+ <div class="search"><input id="search" type="text" placeholder="filter tools (\u2318K)" spellcheck="false" /></div>
202
+ <div class="tools" id="tool-list" role="listbox" aria-label="Tools"></div>
203
+ </aside>
204
+ <main id="detail">
205
+ <div class="placeholder">
206
+ <p>Loading your tools\u2026</p>
207
+ </div>
208
+ </main>
209
+ </div>
210
+ <div class="toast" id="toast"></div>
211
+
212
+ <script>
213
+ (function () {
214
+ var state = null;
215
+ var selected = null;
216
+ var filter = "";
217
+
218
+ var listEl = document.getElementById("tool-list");
219
+ var detailEl = document.getElementById("detail");
220
+ var searchEl = document.getElementById("search");
221
+ var toastEl = document.getElementById("toast");
222
+
223
+ function esc(text) {
224
+ return String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
225
+ }
226
+
227
+ function toast(message) {
228
+ toastEl.textContent = message;
229
+ toastEl.classList.add("show");
230
+ setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
231
+ }
232
+
233
+ function api(path, options) {
234
+ return fetch(path, options).then(function (res) {
235
+ return res.json().then(function (body) {
236
+ if (!res.ok) throw new Error(body.error || ("Request failed: " + res.status));
237
+ return body;
238
+ });
239
+ });
240
+ }
241
+
242
+ function load() {
243
+ return api("/api/state").then(function (next) {
244
+ state = next;
245
+ if (!selected && state.tools.length > 0) selected = state.tools[0].name;
246
+ renderList();
247
+ renderDetail();
248
+ }).catch(function (error) {
249
+ detailEl.innerHTML = '<div class="placeholder"><p>Could not load tools.</p><p>' + esc(error.message) + "</p></div>";
250
+ });
251
+ }
252
+
253
+ function visibleTools() {
254
+ if (!filter) return state.tools;
255
+ var needle = filter.toLowerCase();
256
+ return state.tools.filter(function (tool) {
257
+ return tool.name.indexOf(needle) !== -1 ||
258
+ (tool.path || "").toLowerCase().indexOf(needle) !== -1 ||
259
+ tool.description.toLowerCase().indexOf(needle) !== -1;
260
+ });
261
+ }
262
+
263
+ function renderList() {
264
+ document.getElementById("tool-count").textContent = state.tools.length + " tools";
265
+ var tools = visibleTools();
266
+ if (tools.length === 0) {
267
+ listEl.innerHTML = '<div class="empty">No tools match.</div>';
268
+ return;
269
+ }
270
+ listEl.innerHTML = tools.map(function (tool) {
271
+ var disabled = tool.enabled ? "" : '<span class="off" title="starts disabled"></span>';
272
+ return '<button class="tool" role="option" aria-selected="' + (tool.name === selected) + '" data-name="' + esc(tool.name) + '">' +
273
+ disabled +
274
+ '<span class="name">' + esc(tool.name) + "</span>" +
275
+ '<span class="verb ' + esc(tool.sideEffect) + '">' + esc(tool.verb || "") + "</span>" +
276
+ "</button>";
277
+ }).join("");
278
+ Array.prototype.forEach.call(listEl.children, function (child) {
279
+ child.addEventListener("click", function () {
280
+ selected = child.getAttribute("data-name");
281
+ renderList();
282
+ renderDetail();
283
+ });
284
+ });
285
+ }
286
+
287
+ function currentTool() {
288
+ return state.tools.find(function (tool) { return tool.name === selected; });
289
+ }
290
+
291
+ function renderDetail() {
292
+ var tool = currentTool();
293
+ if (!tool) {
294
+ detailEl.innerHTML = '<div class="placeholder"><p>Select a tool on the left.</p><p><kbd>\u2191</kbd> <kbd>\u2193</kbd> to move, <kbd>\u2318K</kbd> to search.</p></div>';
295
+ return;
296
+ }
297
+
298
+ var badges = [
299
+ '<span class="badge accent">' + esc(tool.sideEffect) + "</span>",
300
+ tool.enabled ? '<span class="badge">enabled</span>' : '<span class="badge warn">starts disabled</span>',
301
+ tool.endpointRole !== "endpoint" ? '<span class="badge err">' + esc(tool.endpointRole) + " endpoint</span>" : "",
302
+ tool.piiInOutput.length > 0 ? '<span class="badge warn">pii: ' + esc(tool.piiInOutput.join(", ")) + "</span>" : "",
303
+ ].filter(Boolean).join("");
304
+
305
+ var findings = tool.findings.map(function (finding) {
306
+ var icon = finding.level === "error" ? "\u2716" : "\u26A0";
307
+ return '<div class="finding ' + esc(finding.level) + '"><span class="icon">' + icon + "</span><span>" + esc(finding.message) + "</span></div>";
308
+ }).join("");
309
+
310
+ var schema = tool.inputSchema || {};
311
+ var properties = schema.properties || {};
312
+ var required = schema.required || [];
313
+ var fields = Object.keys(properties).map(function (key) {
314
+ var field = properties[key];
315
+ var type = field.type === "number" || field.type === "integer" ? "number" : "text";
316
+ var req = required.indexOf(key) !== -1 ? ' <span class="req">*</span>' : "";
317
+ var label = esc(key) + req + (field.description ? '<div class="hint">' + esc(field.description) + "</div>" : "");
318
+ return '<div class="param"><label for="f-' + esc(key) + '">' + label + '</label>' +
319
+ '<input id="f-' + esc(key) + '" data-field="' + esc(key) + '" data-type="' + esc(field.type || "string") + '" type="' + type + '" spellcheck="false" /></div>';
320
+ }).join("");
321
+
322
+ var baseUrl = "";
323
+ try { baseUrl = localStorage.getItem("webmcp-codegen:baseUrl") || tool.serverUrl || ""; } catch (e) {}
324
+
325
+ detailEl.innerHTML =
326
+ '<div class="detail">' +
327
+ '<div class="crumb">' + esc(state.label) + (state.outDir ? " \u2192 " + esc(state.outDir) : "") + "</div>" +
328
+ "<h1>" + esc(tool.name) + "</h1>" +
329
+ '<div class="route">' + esc((tool.verb || "") + " " + (tool.path || "")) + "</div>" +
330
+ '<div class="badges">' + badges + "</div>" +
331
+
332
+ (findings ? '<div class="field-label">audit findings</div>' + findings : "") +
333
+
334
+ '<div class="field-label">description</div>' +
335
+ '<textarea id="desc" spellcheck="false">' + esc(tool.description) + "</textarea>" +
336
+ '<div class="row"><button class="save" id="save-desc">save description</button><span class="saved" id="saved-desc"></span></div>' +
337
+ '<div class="hint">Agents pick tools by this text. Saved to .webmcp-codegen.json, so it survives regeneration. \u2318S to save.</div>' +
338
+
339
+ '<div class="toggle-row">' +
340
+ '<button class="switch" id="toggle-enabled" role="switch" aria-checked="' + tool.enabled + '" aria-label="Enabled"></button>' +
341
+ '<div class="toggle-copy"><strong>' + (tool.enabled ? "Enabled" : "Disabled") + ".</strong> " +
342
+ (tool.enabled
343
+ ? "This tool works as soon as the app registers it."
344
+ : "The generated code is there, commented out. Flipping this regenerates it enabled on the next run.") +
345
+ "</div></div>" +
346
+
347
+ '<div class="try"><header><h2>try it</h2><span class="note">direct call, server-side. no browser session here.</span></header>' +
348
+ '<div class="body">' +
349
+ '<div class="base-url"><input id="base-url" type="text" placeholder="base URL, e.g. http://localhost:3000" value="' + esc(baseUrl) + '" spellcheck="false" /></div>' +
350
+ (fields || '<div class="hint">This tool takes no inputs.</div>') +
351
+ '<button class="run" id="run">run</button>' +
352
+ '<pre class="result" id="result" hidden></pre>' +
353
+ "</div></div>" +
354
+ "</div>";
355
+
356
+ document.getElementById("save-desc").addEventListener("click", saveDescription);
357
+ document.getElementById("toggle-enabled").addEventListener("click", toggleEnabled);
358
+ document.getElementById("run").addEventListener("click", runTool);
359
+ document.getElementById("base-url").addEventListener("change", function (event) {
360
+ try { localStorage.setItem("webmcp-codegen:baseUrl", event.target.value); } catch (e) {}
361
+ });
362
+ }
363
+
364
+ function saveDescription() {
365
+ var tool = currentTool();
366
+ var desc = document.getElementById("desc").value.trim();
367
+ if (!tool || !desc) return;
368
+ api("/api/override", {
369
+ method: "POST",
370
+ headers: { "content-type": "application/json" },
371
+ body: JSON.stringify({ name: tool.name, description: desc }),
372
+ }).then(function () {
373
+ tool.description = desc;
374
+ toast("saved to .webmcp-codegen.json");
375
+ }).catch(function (error) { toast(error.message); });
376
+ }
377
+
378
+ function toggleEnabled() {
379
+ var tool = currentTool();
380
+ if (!tool) return;
381
+ var next = !tool.enabled;
382
+ api("/api/override", {
383
+ method: "POST",
384
+ headers: { "content-type": "application/json" },
385
+ body: JSON.stringify({ name: tool.name, enabled: next }),
386
+ }).then(function () {
387
+ tool.enabled = next;
388
+ renderList();
389
+ renderDetail();
390
+ toast(next ? "enabled on next generate" : "disabled on next generate");
391
+ }).catch(function (error) { toast(error.message); });
392
+ }
393
+
394
+ function runTool() {
395
+ var tool = currentTool();
396
+ if (!tool) return;
397
+ var input = {};
398
+ Array.prototype.forEach.call(document.querySelectorAll("[data-field]"), function (field) {
399
+ var value = field.value;
400
+ if (value === "") return;
401
+ var type = field.getAttribute("data-type");
402
+ if (type === "number" || type === "integer") value = Number(value);
403
+ if (type === "boolean") value = value === "true";
404
+ if (type === "object" || type === "array") {
405
+ try { value = JSON.parse(value); } catch (e) { /* keep as string */ }
406
+ }
407
+ input[field.getAttribute("data-field")] = value;
408
+ });
409
+ var baseUrl = document.getElementById("base-url").value.trim();
410
+ var resultEl = document.getElementById("result");
411
+ var runEl = document.getElementById("run");
412
+ runEl.disabled = true;
413
+ resultEl.hidden = true;
414
+ api("/api/run", {
415
+ method: "POST",
416
+ headers: { "content-type": "application/json" },
417
+ body: JSON.stringify({ name: tool.name, input: input, baseUrl: baseUrl || undefined }),
418
+ }).then(function (result) {
419
+ resultEl.hidden = false;
420
+ resultEl.className = "result " + (result.ok ? "ok" : "err");
421
+ resultEl.textContent =
422
+ (result.status ? "HTTP " + result.status + "\\n\\n" : "") +
423
+ (result.error ? result.error : JSON.stringify(result.body, null, 2));
424
+ }).catch(function (error) {
425
+ resultEl.hidden = false;
426
+ resultEl.className = "result err";
427
+ resultEl.textContent = error.message;
428
+ }).finally(function () {
429
+ runEl.disabled = false;
430
+ });
431
+ }
432
+
433
+ /* Keyboard: up/down moves through the visible tools, cmd-K focuses
434
+ search, cmd-S saves the description being edited. */
435
+ document.addEventListener("keydown", function (event) {
436
+ if ((event.metaKey || event.ctrlKey) && event.key === "k") {
437
+ event.preventDefault();
438
+ searchEl.focus();
439
+ return;
440
+ }
441
+ if ((event.metaKey || event.ctrlKey) && event.key === "s") {
442
+ event.preventDefault();
443
+ saveDescription();
444
+ return;
445
+ }
446
+ if (event.target === searchEl || event.target.tagName === "TEXTAREA" || event.target.tagName === "INPUT") {
447
+ return;
448
+ }
449
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
450
+ var tools = visibleTools();
451
+ var index = tools.findIndex(function (tool) { return tool.name === selected; });
452
+ var next = event.key === "ArrowDown" ? index + 1 : index - 1;
453
+ if (next < 0 || next >= tools.length) return;
454
+ event.preventDefault();
455
+ selected = tools[next].name;
456
+ renderList();
457
+ renderDetail();
458
+ var button = listEl.querySelector('[aria-selected="true"]');
459
+ if (button) button.scrollIntoView({ block: "nearest" });
460
+ });
461
+
462
+ searchEl.addEventListener("input", function (event) {
463
+ filter = event.target.value;
464
+ renderList();
465
+ });
466
+
467
+ load();
468
+ })();
469
+ </script>
470
+ </body>
471
+ </html>`;
472
+ }
473
+
474
+ // src/dev/server.ts
475
+ async function startDevServer(options) {
476
+ const setup = await resolveSetup(options.cwd, {
477
+ dryRun: true,
478
+ skipAudit: false,
479
+ force: false,
480
+ watch: false
481
+ });
482
+ async function currentState() {
483
+ const data = await loadDataFile(options.cwd);
484
+ const result = await runGenerate(setup.config, {
485
+ cwd: options.cwd,
486
+ dryRun: true,
487
+ overrides: data.overrides
488
+ });
489
+ return {
490
+ label: setup.label,
491
+ outDir: setup.config.generate[0]?.outDir,
492
+ tools: result.tools.map((tool) => toUiTool(tool, result.findings)),
493
+ skipped: result.skipped,
494
+ notes: result.notes
495
+ };
496
+ }
497
+ const server = createServer(async (request, response) => {
498
+ try {
499
+ await route(request, response);
500
+ } catch (error) {
501
+ sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
502
+ }
503
+ });
504
+ async function route(request, response) {
505
+ const url = new URL(request.url ?? "/", "http://localhost");
506
+ if (request.method === "GET" && url.pathname === "/") {
507
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
508
+ response.end(dashboardHtml());
509
+ return;
510
+ }
511
+ if (request.method === "GET" && url.pathname === "/api/state") {
512
+ sendJson(response, 200, await currentState());
513
+ return;
514
+ }
515
+ if (request.method === "POST" && url.pathname === "/api/override") {
516
+ const body = await readJson(request);
517
+ if (!body.name) {
518
+ sendJson(response, 400, { error: "Missing tool name." });
519
+ return;
520
+ }
521
+ const data = await loadDataFile(options.cwd);
522
+ const overrides = { ...data.overrides ?? {} };
523
+ const existing = overrides[body.name] ?? {};
524
+ overrides[body.name] = {
525
+ ...existing,
526
+ ...body.description !== void 0 ? { description: body.description } : {},
527
+ ...body.enabled !== void 0 ? { enabled: body.enabled } : {}
528
+ };
529
+ await saveDataFile(options.cwd, { overrides });
530
+ sendJson(response, 200, { ok: true, saved: `.webmcp-codegen.json` });
531
+ return;
532
+ }
533
+ if (request.method === "POST" && url.pathname === "/api/run") {
534
+ const body = await readJson(request);
535
+ const state = await currentState();
536
+ const tool = state.tools.find((candidate) => candidate.name === body.name);
537
+ if (!tool) {
538
+ sendJson(response, 404, { error: `No tool named "${body.name}".` });
539
+ return;
540
+ }
541
+ const result = await runEndpoint(tool, body.input ?? {}, body.baseUrl);
542
+ sendJson(response, result.ok ? 200 : 502, result);
543
+ return;
544
+ }
545
+ sendJson(response, 404, { error: "Not found" });
546
+ }
547
+ await new Promise(
548
+ (resolveListen) => server.listen(options.port, "127.0.0.1", resolveListen)
549
+ );
550
+ if (options.open !== false) openBrowser(`http://localhost:${options.port}`);
551
+ return server;
552
+ }
553
+ function toUiTool(tool, findings) {
554
+ const [verb, ...rest] = tool.source.ref.split(" ");
555
+ return {
556
+ name: tool.name,
557
+ verb,
558
+ path: rest.join(" "),
559
+ description: tool.description,
560
+ sideEffect: tool.sideEffect,
561
+ riskTier: tool.riskTier,
562
+ enabled: tool.enabledByDefault,
563
+ endpointRole: tool.endpointRole,
564
+ piiInOutput: tool.piiInOutput,
565
+ inputSchema: tool.inputSchema,
566
+ ...tool.pathTemplate ? { pathTemplate: tool.pathTemplate } : {},
567
+ ...tool.paramLocations ? { paramLocations: tool.paramLocations } : {},
568
+ ...tool.serverUrl ? { serverUrl: tool.serverUrl } : {},
569
+ findings: findings.filter((finding) => finding.tool === tool.name).map((finding) => ({ level: finding.level, message: finding.message }))
570
+ };
571
+ }
572
+ async function runEndpoint(tool, input, baseUrlOverride) {
573
+ const base = baseUrlOverride ?? tool.serverUrl;
574
+ if (!base) {
575
+ return {
576
+ ok: false,
577
+ error: `No base URL: the spec lists no absolute server. Type your app's URL (e.g. http://localhost:3000) in the "base URL" field and run again.`
578
+ };
579
+ }
580
+ if (!tool.pathTemplate || !tool.verb) {
581
+ return { ok: false, error: "This tool has no route to call." };
582
+ }
583
+ let path = tool.pathTemplate;
584
+ for (const param of tool.paramLocations?.path ?? []) {
585
+ path = path.replace(`{${param}}`, encodeURIComponent(String(input[param] ?? "")));
586
+ }
587
+ const url = new URL(path, base);
588
+ for (const param of tool.paramLocations?.query ?? []) {
589
+ const value = input[param];
590
+ if (value !== void 0 && value !== null) url.searchParams.set(param, String(value));
591
+ }
592
+ const bodyFields = tool.paramLocations?.body ?? [];
593
+ const body = bodyFields.length === 1 && bodyFields[0] === "body" ? input.body : bodyFields.length > 0 ? Object.fromEntries(bodyFields.map((field) => [field, input[field]])) : void 0;
594
+ try {
595
+ const response = await fetch(url, {
596
+ method: tool.verb,
597
+ headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
598
+ body: body !== void 0 ? JSON.stringify(body) : void 0
599
+ });
600
+ const text = await response.text();
601
+ let parsed = text;
602
+ try {
603
+ parsed = JSON.parse(text);
604
+ } catch {
605
+ }
606
+ return { ok: response.ok, status: response.status, body: parsed };
607
+ } catch (error) {
608
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
609
+ }
610
+ }
611
+ function sendJson(response, status, body) {
612
+ response.writeHead(status, { "content-type": "application/json" });
613
+ response.end(JSON.stringify(body));
614
+ }
615
+ function readJson(request) {
616
+ return new Promise((resolveRead, reject) => {
617
+ let text = "";
618
+ request.on("data", (chunk) => {
619
+ text += chunk.toString("utf8");
620
+ });
621
+ request.on("end", () => {
622
+ try {
623
+ resolveRead(text ? JSON.parse(text) : {});
624
+ } catch {
625
+ reject(new Error("Invalid JSON body"));
626
+ }
627
+ });
628
+ request.on("error", reject);
629
+ });
630
+ }
631
+ function openBrowser(url) {
632
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
633
+ spawn(command, [url], { stdio: "ignore", shell: process.platform === "win32" }).unref();
634
+ }
635
+ export {
636
+ startDevServer
637
+ };
638
+ //# sourceMappingURL=server-OR4IMRQ5.js.map