webmcp-codegen 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-I4ZL527H.js → chunk-EAKYM4YS.js} +2 -5
- package/dist/chunk-EAKYM4YS.js.map +1 -0
- package/dist/cli.d.ts +32 -0
- package/dist/cli.js +1490 -224
- package/dist/cli.js.map +1 -1
- package/dist/generators/index.d.ts +1 -1
- package/dist/generators/index.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/sources/index.d.ts +1 -1
- package/dist/{types-DWUum51l.d.ts → types-DfK3AA5H.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-CCVNYNJ5.js +0 -235
- package/dist/chunk-CCVNYNJ5.js.map +0 -1
- package/dist/chunk-I4ZL527H.js.map +0 -1
- package/dist/server-DWP6IHCS.js +0 -638
- package/dist/server-DWP6IHCS.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,28 +1,1392 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
resolveSetup,
|
|
6
|
-
saveDataFile
|
|
7
|
-
} from "./chunk-CCVNYNJ5.js";
|
|
8
|
-
import {
|
|
3
|
+
CONFIG_FILE_NAMES,
|
|
4
|
+
loadConfig,
|
|
9
5
|
runGenerate
|
|
10
6
|
} from "./chunk-MJQ5B6HB.js";
|
|
11
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
js
|
|
9
|
+
} from "./chunk-EAKYM4YS.js";
|
|
10
|
+
import {
|
|
11
|
+
openapi
|
|
12
|
+
} from "./chunk-3LTHWIAP.js";
|
|
12
13
|
import "./chunk-FWSATV7C.js";
|
|
13
|
-
import "./chunk-I4ZL527H.js";
|
|
14
14
|
import "./chunk-KSQMJERY.js";
|
|
15
15
|
|
|
16
16
|
// src/cli.ts
|
|
17
|
-
import { existsSync
|
|
18
|
-
import { writeFile as
|
|
19
|
-
import { join as
|
|
20
|
-
import "readline/promises";
|
|
17
|
+
import { existsSync as existsSync2 } from "fs";
|
|
18
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
19
|
+
import { join as join6 } from "path";
|
|
21
20
|
import { parseArgs } from "util";
|
|
22
21
|
|
|
23
|
-
// src/
|
|
22
|
+
// src/cli-output.ts
|
|
23
|
+
var ESC = "\x1B[";
|
|
24
|
+
var RESET = `${ESC}0m`;
|
|
25
|
+
var BOLD = `${ESC}1m`;
|
|
26
|
+
var DIM = `${ESC}2m`;
|
|
27
|
+
var ITALIC = `${ESC}3m`;
|
|
28
|
+
var FG = {
|
|
29
|
+
red: `${ESC}31m`,
|
|
30
|
+
green: `${ESC}32m`,
|
|
31
|
+
yellow: `${ESC}33m`,
|
|
32
|
+
blue: `${ESC}34m`,
|
|
33
|
+
magenta: `${ESC}35m`,
|
|
34
|
+
cyan: `${ESC}36m`,
|
|
35
|
+
white: `${ESC}37m`,
|
|
36
|
+
gray: `${ESC}90m`
|
|
37
|
+
};
|
|
38
|
+
function c(color, text) {
|
|
39
|
+
return `${FG[color]}${text}${RESET}`;
|
|
40
|
+
}
|
|
41
|
+
function bold(text) {
|
|
42
|
+
return `${BOLD}${text}${RESET}`;
|
|
43
|
+
}
|
|
44
|
+
function dim(text) {
|
|
45
|
+
return `${DIM}${text}${RESET}`;
|
|
46
|
+
}
|
|
47
|
+
function italic(text) {
|
|
48
|
+
return `${ITALIC}${text}${RESET}`;
|
|
49
|
+
}
|
|
50
|
+
function header(title, subtitle) {
|
|
51
|
+
const line = "\u2500".repeat(Math.max(title.length, subtitle.length) + 4);
|
|
52
|
+
return `${c("cyan", line)}
|
|
53
|
+
${bold(title)}
|
|
54
|
+
${subtitle}
|
|
55
|
+
${c("cyan", line)}`;
|
|
56
|
+
}
|
|
57
|
+
function badge(risk) {
|
|
58
|
+
switch (risk) {
|
|
59
|
+
case "read":
|
|
60
|
+
return c("green", "[read]");
|
|
61
|
+
case "write":
|
|
62
|
+
return c("yellow", "[write]");
|
|
63
|
+
case "destructive":
|
|
64
|
+
return c("red", "[destructive]");
|
|
65
|
+
default:
|
|
66
|
+
return `[${risk}]`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function groupFindings(findings) {
|
|
70
|
+
const groups = {};
|
|
71
|
+
for (const f of findings) {
|
|
72
|
+
const kind = f.message.includes("sign-in") || f.message.includes("auth") ? "auth" : f.message.includes("Admin") ? "admin" : f.message.includes("PII") || f.message.includes("email") ? "PII" : f.message.includes("POST treated as a read") ? "POST as read" : "other";
|
|
73
|
+
groups[kind] = (groups[kind] ?? 0) + 1;
|
|
74
|
+
}
|
|
75
|
+
return groups;
|
|
76
|
+
}
|
|
77
|
+
function renderSummary(result, setup, _cwd, wiring) {
|
|
78
|
+
const { tools, findings } = result;
|
|
79
|
+
const reads = tools.filter((t) => t.sideEffect === "read").length;
|
|
80
|
+
const writes = tools.filter((t) => t.sideEffect === "write").length;
|
|
81
|
+
const destructives = tools.filter((t) => t.sideEffect === "destructive").length;
|
|
82
|
+
const skipped = result.skipped.length;
|
|
83
|
+
const warnings = findings.filter((f) => f.level === "warning").length;
|
|
84
|
+
const errors = findings.filter((f) => f.level === "error").length;
|
|
85
|
+
console.log("");
|
|
86
|
+
console.log(header("webmcp-codegen", `${tools.length} tools from ${setup.label}`));
|
|
87
|
+
console.log("");
|
|
88
|
+
const summaryParts = [
|
|
89
|
+
c("green", `${reads} read`),
|
|
90
|
+
c("yellow", `${writes} write`),
|
|
91
|
+
destructives > 0 ? c("red", `${destructives} destructive`) : null,
|
|
92
|
+
skipped > 0 ? dim(`${skipped} skipped`) : null
|
|
93
|
+
].filter(Boolean);
|
|
94
|
+
console.log(` ${summaryParts.join(" ")}
|
|
95
|
+
`);
|
|
96
|
+
if (warnings > 0 || errors > 0) {
|
|
97
|
+
const groups = groupFindings(findings);
|
|
98
|
+
const parts = Object.entries(groups).map(([kind, count]) => {
|
|
99
|
+
const color = kind === "auth" || kind === "admin" ? "yellow" : "gray";
|
|
100
|
+
return `${c(color, kind)} ${count}`;
|
|
101
|
+
});
|
|
102
|
+
console.log(` ${c("yellow", "\u26A0")} ${warnings + errors} finding(s): ${parts.join(", ")}`);
|
|
103
|
+
console.log(dim(` Run with --verbose to see details
|
|
104
|
+
`));
|
|
105
|
+
}
|
|
106
|
+
const outDir = setup.config.generate[0]?.outDir ?? "src/webmcp";
|
|
107
|
+
console.log(` ${c("cyan", "\u2192")} ${bold(outDir)}
|
|
108
|
+
`);
|
|
109
|
+
if (wiring && !wiring.alreadyWired) {
|
|
110
|
+
console.log(` ${c("green", "\u2714")} Registration wired into your app
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
console.log(` ${bold("Next:")} ${c("cyan", "npx webmcp-codegen dev")} to review and test
|
|
114
|
+
`);
|
|
115
|
+
}
|
|
116
|
+
function renderVerbose(result, setup, _cwd) {
|
|
117
|
+
const { tools, findings, skipped } = result;
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log(bold(`webmcp-codegen: ${tools.length} tools from ${setup.label}`));
|
|
120
|
+
console.log("");
|
|
121
|
+
if (skipped.length > 0) {
|
|
122
|
+
console.log(c("gray", "Skipped:"));
|
|
123
|
+
for (const s of skipped) {
|
|
124
|
+
console.log(` ${dim(s.ref)}`);
|
|
125
|
+
console.log(` ${italic(s.reason)}`);
|
|
126
|
+
}
|
|
127
|
+
console.log("");
|
|
128
|
+
}
|
|
129
|
+
const byRisk = {
|
|
130
|
+
read: tools.filter((t) => t.sideEffect === "read"),
|
|
131
|
+
write: tools.filter((t) => t.sideEffect === "write"),
|
|
132
|
+
destructive: tools.filter((t) => t.sideEffect === "destructive")
|
|
133
|
+
};
|
|
134
|
+
for (const [risk, group] of Object.entries(byRisk)) {
|
|
135
|
+
if (group.length === 0) continue;
|
|
136
|
+
console.log(badge(risk));
|
|
137
|
+
for (const tool of group) {
|
|
138
|
+
const disabled = tool.enabledByDefault ? "" : dim(" (starts disabled)");
|
|
139
|
+
console.log(` ${tool.name}${disabled}`);
|
|
140
|
+
if (tool.description) console.log(` ${dim(tool.description)}`);
|
|
141
|
+
}
|
|
142
|
+
console.log("");
|
|
143
|
+
}
|
|
144
|
+
if (findings.length > 0) {
|
|
145
|
+
console.log(bold("Findings:"));
|
|
146
|
+
for (const f of findings) {
|
|
147
|
+
const icon = f.level === "error" ? c("red", "\u2716") : c("yellow", "\u26A0");
|
|
148
|
+
const where = f.tool ? dim(` (${f.tool})`) : "";
|
|
149
|
+
console.log(` ${icon} ${f.message}${where}`);
|
|
150
|
+
}
|
|
151
|
+
console.log("");
|
|
152
|
+
}
|
|
153
|
+
console.log(` ${c("cyan", "\u2192")} Files in ${setup.config.generate[0]?.outDir ?? "src/webmcp"}
|
|
154
|
+
`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/data-file.ts
|
|
24
158
|
import { readFile, writeFile } from "fs/promises";
|
|
25
|
-
import {
|
|
159
|
+
import { join } from "path";
|
|
160
|
+
var DATA_FILE_NAME = ".webmcp-codegen.json";
|
|
161
|
+
async function loadDataFile(cwd) {
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(await readFile(join(cwd, DATA_FILE_NAME), "utf8"));
|
|
164
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
165
|
+
} catch {
|
|
166
|
+
return {};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function saveDataFile(cwd, patch) {
|
|
170
|
+
const current = await loadDataFile(cwd);
|
|
171
|
+
const next = { ...current, ...patch };
|
|
172
|
+
if (JSON.stringify(next) === JSON.stringify(current)) return;
|
|
173
|
+
await writeFile(join(cwd, DATA_FILE_NAME), `${JSON.stringify(next, null, 2)}
|
|
174
|
+
`, "utf8");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/detect.ts
|
|
178
|
+
import { readdir } from "fs/promises";
|
|
179
|
+
import { join as join2, relative } from "path";
|
|
180
|
+
var SPEC_FILE_PATTERN = /^(openapi|swagger|api)\.(ya?ml|json)$/i;
|
|
181
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
182
|
+
"node_modules",
|
|
183
|
+
".git",
|
|
184
|
+
".turbo",
|
|
185
|
+
".next",
|
|
186
|
+
"dist",
|
|
187
|
+
"build",
|
|
188
|
+
"coverage"
|
|
189
|
+
]);
|
|
190
|
+
var MAX_DEPTH = 5;
|
|
191
|
+
async function findSpecs(cwd) {
|
|
192
|
+
const found = [];
|
|
193
|
+
async function walk(dir, depth) {
|
|
194
|
+
if (depth > MAX_DEPTH) return;
|
|
195
|
+
let entries;
|
|
196
|
+
try {
|
|
197
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
198
|
+
} catch {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (entry.isDirectory()) {
|
|
203
|
+
if (!IGNORED_DIRS.has(entry.name)) await walk(join2(dir, entry.name), depth + 1);
|
|
204
|
+
} else if (SPEC_FILE_PATTERN.test(entry.name)) {
|
|
205
|
+
found.push({ path: join2(dir, entry.name), depth });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
await walk(cwd, 0);
|
|
210
|
+
return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/dev/server.ts
|
|
214
|
+
import { spawn } from "child_process";
|
|
215
|
+
import { createServer } from "http";
|
|
216
|
+
|
|
217
|
+
// src/setup.ts
|
|
218
|
+
import { existsSync } from "fs";
|
|
219
|
+
import { basename, join as join4 } from "path";
|
|
220
|
+
import { createInterface } from "readline/promises";
|
|
221
|
+
|
|
222
|
+
// src/detect-app.ts
|
|
223
|
+
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
224
|
+
import { join as join3 } from "path";
|
|
225
|
+
var FRAMEWORKS = [
|
|
226
|
+
{ dep: "next", framework: "next" },
|
|
227
|
+
{ dep: "nuxt", framework: "nuxt" },
|
|
228
|
+
{ dep: "@sveltejs/kit", framework: "sveltekit" }
|
|
229
|
+
];
|
|
230
|
+
async function findWebApps(cwd) {
|
|
231
|
+
const packageDirs = await findPackageDirs(cwd);
|
|
232
|
+
const apps = [];
|
|
233
|
+
for (const dir of packageDirs) {
|
|
234
|
+
const pkg = await readPackageJson(join3(cwd, dir));
|
|
235
|
+
if (!pkg) continue;
|
|
236
|
+
const deps = {
|
|
237
|
+
...pkg.dependencies,
|
|
238
|
+
...pkg.devDependencies
|
|
239
|
+
};
|
|
240
|
+
const known = FRAMEWORKS.find(({ dep }) => deps[dep]);
|
|
241
|
+
const framework = known?.framework ?? (deps.react && deps.vite ? "vite-react" : void 0);
|
|
242
|
+
if (framework) apps.push({ dir, framework });
|
|
243
|
+
}
|
|
244
|
+
return apps.sort((a, b) => score(b) - score(a));
|
|
245
|
+
function score(app) {
|
|
246
|
+
return (app.framework === "unknown" ? 0 : 10) + (/(^|\/)(web|app|frontend|client)$/.test(app.dir) ? 2 : 0);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function findPackageDirs(cwd) {
|
|
250
|
+
const dirs = [];
|
|
251
|
+
const root = await readPackageJson(join3(cwd, ""));
|
|
252
|
+
if (root) {
|
|
253
|
+
dirs.push(".");
|
|
254
|
+
for (const pattern of await workspaceGlobs(cwd, root)) {
|
|
255
|
+
dirs.push(...await expandShallowGlob(cwd, pattern));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return [...new Set(dirs)];
|
|
259
|
+
}
|
|
260
|
+
async function workspaceGlobs(cwd, rootPkg) {
|
|
261
|
+
const workspaces = rootPkg.workspaces;
|
|
262
|
+
if (Array.isArray(workspaces)) return workspaces;
|
|
263
|
+
if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
|
|
264
|
+
return workspaces.packages;
|
|
265
|
+
}
|
|
266
|
+
return readPnpmWorkspaceGlobs(cwd);
|
|
267
|
+
}
|
|
268
|
+
async function readPnpmWorkspaceGlobs(cwd) {
|
|
269
|
+
try {
|
|
270
|
+
const text = await readFile2(join3(cwd, "pnpm-workspace.yaml"), "utf8");
|
|
271
|
+
const packagesBlock = /^packages:\s*\n((?:\s+-\s+.+\n?)+)/m.exec(text);
|
|
272
|
+
if (!packagesBlock) return [];
|
|
273
|
+
return [...packagesBlock[1].matchAll(/^\s+-\s+['"]?([^'"\n]+?)['"]?\s*$/gm)].map(
|
|
274
|
+
(match) => match[1]
|
|
275
|
+
);
|
|
276
|
+
} catch {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function expandShallowGlob(cwd, pattern) {
|
|
281
|
+
const starAt = pattern.indexOf("*");
|
|
282
|
+
const base = starAt === -1 ? pattern : pattern.slice(0, starAt).replace(/\/$/, "");
|
|
283
|
+
if (starAt === -1) return [base];
|
|
284
|
+
try {
|
|
285
|
+
const entries = await readdir2(join3(cwd, base), { withFileTypes: true });
|
|
286
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => `${base}/${entry.name}`);
|
|
287
|
+
} catch {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function readPackageJson(dir) {
|
|
292
|
+
try {
|
|
293
|
+
return JSON.parse(await readFile2(join3(dir, "package.json"), "utf8"));
|
|
294
|
+
} catch {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/setup.ts
|
|
300
|
+
async function resolveSetup(cwd, flags) {
|
|
301
|
+
const hasConfigFile = flags.configPath ? existsSync(join4(cwd, flags.configPath)) : CONFIG_FILE_NAMES.some((name) => existsSync(join4(cwd, name)));
|
|
302
|
+
if (hasConfigFile) {
|
|
303
|
+
const { config, path } = await loadConfig(cwd, flags.configPath);
|
|
304
|
+
if (flags.spec || flags.out) {
|
|
305
|
+
console.warn(`Note: --spec/--out are ignored; ${basename(path)} is in charge here.`);
|
|
306
|
+
}
|
|
307
|
+
const data2 = await loadDataFile(cwd);
|
|
308
|
+
const apps = await findWebApps(cwd);
|
|
309
|
+
const app2 = apps.find((candidate) => candidate.dir === data2.app) ?? apps[0];
|
|
310
|
+
return { config, label: basename(path), app: app2, fromConfigFile: true, remember: {} };
|
|
311
|
+
}
|
|
312
|
+
if (flags.configPath) {
|
|
313
|
+
throw new Error(`No config file at "${flags.configPath}".`);
|
|
314
|
+
}
|
|
315
|
+
const data = await loadDataFile(cwd);
|
|
316
|
+
const spec = flags.spec ?? data.spec ?? await detectSpec(cwd);
|
|
317
|
+
let app;
|
|
318
|
+
if (!flags.out) {
|
|
319
|
+
const apps = await findWebApps(cwd);
|
|
320
|
+
const remembered = apps.find((candidate) => candidate.dir === data.app);
|
|
321
|
+
if (remembered) {
|
|
322
|
+
app = remembered;
|
|
323
|
+
} else if (apps.length === 1) {
|
|
324
|
+
app = apps[0];
|
|
325
|
+
console.log(`Found your web app: ${app?.dir} (${app?.framework})`);
|
|
326
|
+
} else if (apps.length > 1) {
|
|
327
|
+
app = await askWhichApp(apps);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const outDir = flags.out ?? (app && app.dir !== "." ? `${app.dir}/src/webmcp` : "./src/webmcp");
|
|
331
|
+
return {
|
|
332
|
+
config: { sources: [openapi({ spec })], generate: [js({ outDir })] },
|
|
333
|
+
label: flags.spec ? `--spec ${spec}` : `detected ${spec}`,
|
|
334
|
+
app,
|
|
335
|
+
fromConfigFile: false,
|
|
336
|
+
remember: { spec, app: app?.dir }
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
async function askWhichApp(apps) {
|
|
340
|
+
if (!process.stdin.isTTY) {
|
|
341
|
+
const first = apps[0];
|
|
342
|
+
console.log(`Several packages look like web apps; using ${first.dir}. Override with --out.`);
|
|
343
|
+
return first;
|
|
344
|
+
}
|
|
345
|
+
console.log("Several packages look like the web app. Which one should the tools live in?");
|
|
346
|
+
apps.forEach((app, index) => {
|
|
347
|
+
console.log(` ${index + 1}. ${app.dir} (${app.framework})${index === 0 ? " [default]" : ""}`);
|
|
348
|
+
});
|
|
349
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
350
|
+
try {
|
|
351
|
+
const answer = await rl.question("Choice [1]: ");
|
|
352
|
+
const picked = Number.parseInt(answer.trim() || "1", 10);
|
|
353
|
+
return apps[picked - 1] ?? apps[0];
|
|
354
|
+
} finally {
|
|
355
|
+
rl.close();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async function detectSpec(cwd) {
|
|
359
|
+
const specs = await findSpecs(cwd);
|
|
360
|
+
if (specs.length === 0) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
"No OpenAPI spec found in this project.\nPoint at one: npx webmcp-codegen generate --spec path/to/openapi.json"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (specs.length > 1) {
|
|
366
|
+
const list = specs.map((spec) => ` - ${spec}`).join("\n");
|
|
367
|
+
throw new Error(
|
|
368
|
+
`Found ${specs.length} API specs:
|
|
369
|
+
${list}
|
|
370
|
+
|
|
371
|
+
Pick one: npx webmcp-codegen generate --spec ${specs[0]}`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
console.log(`Detected ${specs[0]} (override with --spec)`);
|
|
375
|
+
return specs[0];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/dev/ui.ts
|
|
379
|
+
function dashboardHtml() {
|
|
380
|
+
return `<!DOCTYPE html>
|
|
381
|
+
<html lang="en">
|
|
382
|
+
<head>
|
|
383
|
+
<meta charset="utf-8" />
|
|
384
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
385
|
+
<title>webmcp-codegen</title>
|
|
386
|
+
<style>
|
|
387
|
+
:root {
|
|
388
|
+
--baseline: #0a0b0f;
|
|
389
|
+
--surface: #10131a;
|
|
390
|
+
--surface-raised: #161a23;
|
|
391
|
+
--line: #1e2330;
|
|
392
|
+
--line-subtle: #161a23;
|
|
393
|
+
--ink: #e9ecf2;
|
|
394
|
+
--dim: #9aa3b2;
|
|
395
|
+
--faint: #5d6575;
|
|
396
|
+
--ghost: #3b4150;
|
|
397
|
+
--accent: #58a6ff;
|
|
398
|
+
--accent-dim: rgba(88, 166, 255, 0.15);
|
|
399
|
+
--signal: #e3b341;
|
|
400
|
+
--signal-dim: rgba(227, 179, 65, 0.15);
|
|
401
|
+
--fault: #f47067;
|
|
402
|
+
--fault-dim: rgba(244, 112, 103, 0.15);
|
|
403
|
+
--sans: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
404
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
405
|
+
}
|
|
406
|
+
* { box-sizing: border-box; }
|
|
407
|
+
html, body { margin: 0; height: 100%; }
|
|
408
|
+
body {
|
|
409
|
+
background: var(--baseline);
|
|
410
|
+
color: var(--ink);
|
|
411
|
+
font-family: var(--sans);
|
|
412
|
+
font-size: 14px;
|
|
413
|
+
-webkit-font-smoothing: antialiased;
|
|
414
|
+
overflow: hidden;
|
|
415
|
+
}
|
|
416
|
+
::selection { background: var(--accent); color: var(--baseline); }
|
|
417
|
+
|
|
418
|
+
/* Layout */
|
|
419
|
+
.app { display: flex; height: 100vh; }
|
|
420
|
+
.sidebar {
|
|
421
|
+
width: 320px;
|
|
422
|
+
min-width: 320px;
|
|
423
|
+
border-right: 1px solid var(--line);
|
|
424
|
+
display: flex;
|
|
425
|
+
flex-direction: column;
|
|
426
|
+
background: var(--surface);
|
|
427
|
+
}
|
|
428
|
+
.main {
|
|
429
|
+
flex: 1;
|
|
430
|
+
overflow-y: auto;
|
|
431
|
+
background: var(--baseline);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* Sidebar header */
|
|
435
|
+
.sidebar-header {
|
|
436
|
+
padding: 20px 20px 16px;
|
|
437
|
+
border-bottom: 1px solid var(--line-subtle);
|
|
438
|
+
}
|
|
439
|
+
.brand {
|
|
440
|
+
display: flex;
|
|
441
|
+
align-items: center;
|
|
442
|
+
gap: 10px;
|
|
443
|
+
font-weight: 600;
|
|
444
|
+
font-size: 15px;
|
|
445
|
+
margin-bottom: 4px;
|
|
446
|
+
}
|
|
447
|
+
.brand-mark {
|
|
448
|
+
width: 24px;
|
|
449
|
+
height: 24px;
|
|
450
|
+
background: linear-gradient(135deg, var(--accent), #7c3aed);
|
|
451
|
+
border-radius: 6px;
|
|
452
|
+
display: flex;
|
|
453
|
+
align-items: center;
|
|
454
|
+
justify-content: center;
|
|
455
|
+
font-size: 12px;
|
|
456
|
+
font-weight: 700;
|
|
457
|
+
color: white;
|
|
458
|
+
}
|
|
459
|
+
.brand-sub {
|
|
460
|
+
color: var(--faint);
|
|
461
|
+
font-size: 12px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* Search */
|
|
465
|
+
.search-wrap {
|
|
466
|
+
padding: 12px 16px;
|
|
467
|
+
border-bottom: 1px solid var(--line-subtle);
|
|
468
|
+
}
|
|
469
|
+
.search {
|
|
470
|
+
width: 100%;
|
|
471
|
+
background: var(--surface-raised);
|
|
472
|
+
border: 1px solid var(--line);
|
|
473
|
+
border-radius: 6px;
|
|
474
|
+
padding: 8px 12px 8px 32px;
|
|
475
|
+
color: var(--ink);
|
|
476
|
+
font-size: 13px;
|
|
477
|
+
font-family: inherit;
|
|
478
|
+
position: relative;
|
|
479
|
+
}
|
|
480
|
+
.search:focus {
|
|
481
|
+
outline: none;
|
|
482
|
+
border-color: var(--accent);
|
|
483
|
+
}
|
|
484
|
+
.search-icon {
|
|
485
|
+
position: absolute;
|
|
486
|
+
left: 28px;
|
|
487
|
+
top: 50%;
|
|
488
|
+
transform: translateY(-50%);
|
|
489
|
+
color: var(--faint);
|
|
490
|
+
pointer-events: none;
|
|
491
|
+
}
|
|
492
|
+
.search-wrap { position: relative; }
|
|
493
|
+
|
|
494
|
+
/* Tool list */
|
|
495
|
+
.tool-list {
|
|
496
|
+
flex: 1;
|
|
497
|
+
overflow-y: auto;
|
|
498
|
+
padding: 8px 0;
|
|
499
|
+
}
|
|
500
|
+
.tool-group {
|
|
501
|
+
padding: 8px 16px 4px;
|
|
502
|
+
font-size: 11px;
|
|
503
|
+
font-weight: 600;
|
|
504
|
+
text-transform: uppercase;
|
|
505
|
+
letter-spacing: 0.05em;
|
|
506
|
+
color: var(--faint);
|
|
507
|
+
}
|
|
508
|
+
.tool {
|
|
509
|
+
display: flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
gap: 10px;
|
|
512
|
+
width: 100%;
|
|
513
|
+
padding: 8px 16px;
|
|
514
|
+
border: none;
|
|
515
|
+
background: none;
|
|
516
|
+
color: var(--ink);
|
|
517
|
+
font-size: 13px;
|
|
518
|
+
font-family: var(--mono);
|
|
519
|
+
text-align: left;
|
|
520
|
+
cursor: pointer;
|
|
521
|
+
transition: background 0.1s;
|
|
522
|
+
}
|
|
523
|
+
.tool:hover { background: var(--surface-raised); }
|
|
524
|
+
.tool[aria-selected="true"] {
|
|
525
|
+
background: var(--accent-dim);
|
|
526
|
+
border-right: 2px solid var(--accent);
|
|
527
|
+
}
|
|
528
|
+
.tool-indicator {
|
|
529
|
+
width: 6px;
|
|
530
|
+
height: 6px;
|
|
531
|
+
border-radius: 50%;
|
|
532
|
+
flex-shrink: 0;
|
|
533
|
+
}
|
|
534
|
+
.tool-indicator.read { background: var(--accent); }
|
|
535
|
+
.tool-indicator.write { background: var(--signal); }
|
|
536
|
+
.tool-indicator.destructive { background: var(--fault); }
|
|
537
|
+
.tool-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
538
|
+
.tool-badge {
|
|
539
|
+
font-size: 10px;
|
|
540
|
+
padding: 2px 6px;
|
|
541
|
+
border-radius: 4px;
|
|
542
|
+
background: var(--surface-raised);
|
|
543
|
+
color: var(--dim);
|
|
544
|
+
text-transform: uppercase;
|
|
545
|
+
letter-spacing: 0.02em;
|
|
546
|
+
}
|
|
547
|
+
.tool-badge.disabled { color: var(--signal); }
|
|
548
|
+
|
|
549
|
+
/* Main content */
|
|
550
|
+
.detail {
|
|
551
|
+
max-width: 640px;
|
|
552
|
+
margin: 0 auto;
|
|
553
|
+
padding: 32px 40px;
|
|
554
|
+
}
|
|
555
|
+
.placeholder {
|
|
556
|
+
display: flex;
|
|
557
|
+
flex-direction: column;
|
|
558
|
+
align-items: center;
|
|
559
|
+
justify-content: center;
|
|
560
|
+
height: 100%;
|
|
561
|
+
color: var(--faint);
|
|
562
|
+
text-align: center;
|
|
563
|
+
padding: 40px;
|
|
564
|
+
}
|
|
565
|
+
.placeholder-icon {
|
|
566
|
+
width: 48px;
|
|
567
|
+
height: 48px;
|
|
568
|
+
border-radius: 12px;
|
|
569
|
+
background: var(--surface-raised);
|
|
570
|
+
display: flex;
|
|
571
|
+
align-items: center;
|
|
572
|
+
justify-content: center;
|
|
573
|
+
margin-bottom: 16px;
|
|
574
|
+
color: var(--ghost);
|
|
575
|
+
}
|
|
576
|
+
.placeholder kbd {
|
|
577
|
+
background: var(--surface-raised);
|
|
578
|
+
padding: 2px 6px;
|
|
579
|
+
border-radius: 4px;
|
|
580
|
+
font-family: var(--mono);
|
|
581
|
+
font-size: 12px;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* Detail header */
|
|
585
|
+
.detail-header {
|
|
586
|
+
margin-bottom: 24px;
|
|
587
|
+
padding-bottom: 20px;
|
|
588
|
+
border-bottom: 1px solid var(--line-subtle);
|
|
589
|
+
}
|
|
590
|
+
.detail-crumb {
|
|
591
|
+
font-size: 12px;
|
|
592
|
+
color: var(--faint);
|
|
593
|
+
margin-bottom: 8px;
|
|
594
|
+
font-family: var(--mono);
|
|
595
|
+
}
|
|
596
|
+
.detail-title {
|
|
597
|
+
font-size: 24px;
|
|
598
|
+
font-weight: 600;
|
|
599
|
+
margin: 0 0 8px;
|
|
600
|
+
font-family: var(--mono);
|
|
601
|
+
}
|
|
602
|
+
.detail-route {
|
|
603
|
+
font-family: var(--mono);
|
|
604
|
+
font-size: 13px;
|
|
605
|
+
color: var(--dim);
|
|
606
|
+
display: flex;
|
|
607
|
+
align-items: center;
|
|
608
|
+
gap: 8px;
|
|
609
|
+
}
|
|
610
|
+
.verb {
|
|
611
|
+
font-weight: 600;
|
|
612
|
+
padding: 2px 6px;
|
|
613
|
+
border-radius: 4px;
|
|
614
|
+
font-size: 11px;
|
|
615
|
+
}
|
|
616
|
+
.verb.read { color: var(--accent); background: var(--accent-dim); }
|
|
617
|
+
.verb.write { color: var(--signal); background: var(--signal-dim); }
|
|
618
|
+
.verb.destructive { color: var(--fault); background: var(--fault-dim); }
|
|
619
|
+
|
|
620
|
+
/* Badges */
|
|
621
|
+
.badges {
|
|
622
|
+
display: flex;
|
|
623
|
+
gap: 8px;
|
|
624
|
+
margin-top: 12px;
|
|
625
|
+
flex-wrap: wrap;
|
|
626
|
+
}
|
|
627
|
+
.badge {
|
|
628
|
+
font-size: 11px;
|
|
629
|
+
padding: 3px 8px;
|
|
630
|
+
border-radius: 4px;
|
|
631
|
+
font-weight: 500;
|
|
632
|
+
}
|
|
633
|
+
.badge.read { color: var(--accent); background: var(--accent-dim); }
|
|
634
|
+
.badge.write { color: var(--signal); background: var(--signal-dim); }
|
|
635
|
+
.badge.destructive { color: var(--fault); background: var(--fault-dim); }
|
|
636
|
+
.badge.disabled { color: var(--signal); background: var(--signal-dim); }
|
|
637
|
+
.badge.auth { color: var(--fault); background: var(--fault-dim); }
|
|
638
|
+
|
|
639
|
+
/* Sections */
|
|
640
|
+
.section {
|
|
641
|
+
margin-bottom: 28px;
|
|
642
|
+
}
|
|
643
|
+
.section-label {
|
|
644
|
+
font-size: 11px;
|
|
645
|
+
font-weight: 600;
|
|
646
|
+
text-transform: uppercase;
|
|
647
|
+
letter-spacing: 0.05em;
|
|
648
|
+
color: var(--faint);
|
|
649
|
+
margin-bottom: 10px;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* Description edit */
|
|
653
|
+
.description-edit {
|
|
654
|
+
width: 100%;
|
|
655
|
+
background: var(--surface);
|
|
656
|
+
border: 1px solid var(--line);
|
|
657
|
+
border-radius: 6px;
|
|
658
|
+
padding: 12px;
|
|
659
|
+
color: var(--ink);
|
|
660
|
+
font-size: 14px;
|
|
661
|
+
font-family: inherit;
|
|
662
|
+
line-height: 1.5;
|
|
663
|
+
resize: vertical;
|
|
664
|
+
min-height: 80px;
|
|
665
|
+
}
|
|
666
|
+
.description-edit:focus {
|
|
667
|
+
outline: none;
|
|
668
|
+
border-color: var(--accent);
|
|
669
|
+
}
|
|
670
|
+
.edit-actions {
|
|
671
|
+
display: flex;
|
|
672
|
+
align-items: center;
|
|
673
|
+
gap: 12px;
|
|
674
|
+
margin-top: 10px;
|
|
675
|
+
}
|
|
676
|
+
.btn {
|
|
677
|
+
padding: 8px 16px;
|
|
678
|
+
border-radius: 6px;
|
|
679
|
+
font-size: 13px;
|
|
680
|
+
font-weight: 500;
|
|
681
|
+
cursor: pointer;
|
|
682
|
+
transition: all 0.15s;
|
|
683
|
+
border: 1px solid var(--line);
|
|
684
|
+
background: var(--surface);
|
|
685
|
+
color: var(--ink);
|
|
686
|
+
}
|
|
687
|
+
.btn:hover { background: var(--surface-raised); border-color: var(--ghost); }
|
|
688
|
+
.btn-primary {
|
|
689
|
+
background: var(--accent);
|
|
690
|
+
border-color: var(--accent);
|
|
691
|
+
color: var(--baseline);
|
|
692
|
+
}
|
|
693
|
+
.btn-primary:hover { background: #4a95ee; border-color: #4a95ee; }
|
|
694
|
+
.saved-indicator {
|
|
695
|
+
font-size: 12px;
|
|
696
|
+
color: var(--accent);
|
|
697
|
+
opacity: 0;
|
|
698
|
+
transition: opacity 0.2s;
|
|
699
|
+
}
|
|
700
|
+
.saved-indicator.show { opacity: 1; }
|
|
701
|
+
.edit-hint {
|
|
702
|
+
font-size: 12px;
|
|
703
|
+
color: var(--faint);
|
|
704
|
+
margin-top: 8px;
|
|
705
|
+
line-height: 1.5;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* Toggle */
|
|
709
|
+
.toggle-row {
|
|
710
|
+
display: flex;
|
|
711
|
+
align-items: center;
|
|
712
|
+
gap: 12px;
|
|
713
|
+
padding: 14px;
|
|
714
|
+
background: var(--surface);
|
|
715
|
+
border: 1px solid var(--line);
|
|
716
|
+
border-radius: 8px;
|
|
717
|
+
}
|
|
718
|
+
.switch {
|
|
719
|
+
width: 40px;
|
|
720
|
+
height: 22px;
|
|
721
|
+
border-radius: 11px;
|
|
722
|
+
background: var(--surface-raised);
|
|
723
|
+
border: 1px solid var(--line);
|
|
724
|
+
position: relative;
|
|
725
|
+
cursor: pointer;
|
|
726
|
+
transition: all 0.2s;
|
|
727
|
+
flex-shrink: 0;
|
|
728
|
+
}
|
|
729
|
+
.switch::after {
|
|
730
|
+
content: "";
|
|
731
|
+
position: absolute;
|
|
732
|
+
width: 16px;
|
|
733
|
+
height: 16px;
|
|
734
|
+
border-radius: 50%;
|
|
735
|
+
background: var(--dim);
|
|
736
|
+
top: 2px;
|
|
737
|
+
left: 2px;
|
|
738
|
+
transition: all 0.2s;
|
|
739
|
+
}
|
|
740
|
+
.switch[aria-checked="true"] {
|
|
741
|
+
background: var(--accent);
|
|
742
|
+
border-color: var(--accent);
|
|
743
|
+
}
|
|
744
|
+
.switch[aria-checked="true"]::after {
|
|
745
|
+
left: 20px;
|
|
746
|
+
background: white;
|
|
747
|
+
}
|
|
748
|
+
.toggle-copy { font-size: 13px; line-height: 1.5; }
|
|
749
|
+
.toggle-copy strong { display: block; margin-bottom: 2px; }
|
|
750
|
+
|
|
751
|
+
/* Try it */
|
|
752
|
+
.try-section {
|
|
753
|
+
background: var(--surface);
|
|
754
|
+
border: 1px solid var(--line);
|
|
755
|
+
border-radius: 8px;
|
|
756
|
+
overflow: hidden;
|
|
757
|
+
}
|
|
758
|
+
.try-header {
|
|
759
|
+
padding: 14px 16px;
|
|
760
|
+
border-bottom: 1px solid var(--line-subtle);
|
|
761
|
+
display: flex;
|
|
762
|
+
align-items: center;
|
|
763
|
+
justify-content: space-between;
|
|
764
|
+
}
|
|
765
|
+
.try-header h3 {
|
|
766
|
+
margin: 0;
|
|
767
|
+
font-size: 13px;
|
|
768
|
+
font-weight: 600;
|
|
769
|
+
}
|
|
770
|
+
.try-note {
|
|
771
|
+
font-size: 11px;
|
|
772
|
+
color: var(--faint);
|
|
773
|
+
}
|
|
774
|
+
.try-body { padding: 16px; }
|
|
775
|
+
.auth-note {
|
|
776
|
+
background: var(--signal-dim);
|
|
777
|
+
border: 1px solid var(--signal);
|
|
778
|
+
color: var(--signal);
|
|
779
|
+
padding: 10px 12px;
|
|
780
|
+
border-radius: 6px;
|
|
781
|
+
font-size: 12px;
|
|
782
|
+
margin-bottom: 14px;
|
|
783
|
+
line-height: 1.5;
|
|
784
|
+
}
|
|
785
|
+
.base-url-input {
|
|
786
|
+
width: 100%;
|
|
787
|
+
background: var(--baseline);
|
|
788
|
+
border: 1px solid var(--line);
|
|
789
|
+
border-radius: 6px;
|
|
790
|
+
padding: 8px 12px;
|
|
791
|
+
color: var(--ink);
|
|
792
|
+
font-size: 13px;
|
|
793
|
+
font-family: var(--mono);
|
|
794
|
+
margin-bottom: 14px;
|
|
795
|
+
}
|
|
796
|
+
.base-url-input:focus {
|
|
797
|
+
outline: none;
|
|
798
|
+
border-color: var(--accent);
|
|
799
|
+
}
|
|
800
|
+
.param-list { margin-bottom: 14px; }
|
|
801
|
+
.param {
|
|
802
|
+
margin-bottom: 12px;
|
|
803
|
+
}
|
|
804
|
+
.param-label {
|
|
805
|
+
display: block;
|
|
806
|
+
font-size: 12px;
|
|
807
|
+
font-weight: 500;
|
|
808
|
+
margin-bottom: 4px;
|
|
809
|
+
color: var(--dim);
|
|
810
|
+
}
|
|
811
|
+
.param-label .req { color: var(--fault); }
|
|
812
|
+
.param-hint {
|
|
813
|
+
font-size: 11px;
|
|
814
|
+
color: var(--faint);
|
|
815
|
+
margin-top: 2px;
|
|
816
|
+
}
|
|
817
|
+
.param-input {
|
|
818
|
+
width: 100%;
|
|
819
|
+
background: var(--baseline);
|
|
820
|
+
border: 1px solid var(--line);
|
|
821
|
+
border-radius: 6px;
|
|
822
|
+
padding: 8px 12px;
|
|
823
|
+
color: var(--ink);
|
|
824
|
+
font-size: 13px;
|
|
825
|
+
font-family: var(--mono);
|
|
826
|
+
}
|
|
827
|
+
.param-input:focus {
|
|
828
|
+
outline: none;
|
|
829
|
+
border-color: var(--accent);
|
|
830
|
+
}
|
|
831
|
+
.run-btn {
|
|
832
|
+
width: 100%;
|
|
833
|
+
padding: 10px;
|
|
834
|
+
background: var(--accent);
|
|
835
|
+
border: none;
|
|
836
|
+
border-radius: 6px;
|
|
837
|
+
color: var(--baseline);
|
|
838
|
+
font-size: 13px;
|
|
839
|
+
font-weight: 600;
|
|
840
|
+
cursor: pointer;
|
|
841
|
+
transition: background 0.15s;
|
|
842
|
+
}
|
|
843
|
+
.run-btn:hover { background: #4a95ee; }
|
|
844
|
+
.run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
845
|
+
.result {
|
|
846
|
+
margin-top: 14px;
|
|
847
|
+
padding: 12px;
|
|
848
|
+
background: var(--baseline);
|
|
849
|
+
border: 1px solid var(--line);
|
|
850
|
+
border-radius: 6px;
|
|
851
|
+
font-family: var(--mono);
|
|
852
|
+
font-size: 12px;
|
|
853
|
+
white-space: pre-wrap;
|
|
854
|
+
word-break: break-all;
|
|
855
|
+
max-height: 300px;
|
|
856
|
+
overflow-y: auto;
|
|
857
|
+
}
|
|
858
|
+
.result.ok { border-color: var(--accent); }
|
|
859
|
+
.result.err { border-color: var(--fault); }
|
|
860
|
+
|
|
861
|
+
/* Findings */
|
|
862
|
+
.findings {
|
|
863
|
+
margin-bottom: 20px;
|
|
864
|
+
}
|
|
865
|
+
.finding {
|
|
866
|
+
display: flex;
|
|
867
|
+
gap: 8px;
|
|
868
|
+
padding: 10px 12px;
|
|
869
|
+
background: var(--surface);
|
|
870
|
+
border: 1px solid var(--line);
|
|
871
|
+
border-radius: 6px;
|
|
872
|
+
margin-bottom: 8px;
|
|
873
|
+
font-size: 13px;
|
|
874
|
+
line-height: 1.5;
|
|
875
|
+
}
|
|
876
|
+
.finding.warning { border-left: 3px solid var(--signal); }
|
|
877
|
+
.finding.error { border-left: 3px solid var(--fault); }
|
|
878
|
+
.finding-icon { flex-shrink: 0; }
|
|
879
|
+
|
|
880
|
+
/* Scrollbar */
|
|
881
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
882
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
883
|
+
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
|
|
884
|
+
::-webkit-scrollbar-thumb:hover { background: var(--ghost); }
|
|
885
|
+
</style>
|
|
886
|
+
</head>
|
|
887
|
+
<body>
|
|
888
|
+
<div class="app">
|
|
889
|
+
<aside class="sidebar">
|
|
890
|
+
<div class="sidebar-header">
|
|
891
|
+
<div class="brand">
|
|
892
|
+
<div class="brand-mark">W</div>
|
|
893
|
+
<span>webmcp-codegen</span>
|
|
894
|
+
</div>
|
|
895
|
+
<div class="brand-sub" id="tool-count"></div>
|
|
896
|
+
</div>
|
|
897
|
+
<div class="search-wrap">
|
|
898
|
+
<svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
899
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
900
|
+
<path d="m21 21-4.35-4.35"></path>
|
|
901
|
+
</svg>
|
|
902
|
+
<input type="text" class="search" id="search" placeholder="Search tools..." spellcheck="false" />
|
|
903
|
+
</div>
|
|
904
|
+
<div class="tool-list" id="tool-list"></div>
|
|
905
|
+
</aside>
|
|
906
|
+
<main class="main" id="main">
|
|
907
|
+
<div class="placeholder" id="placeholder">
|
|
908
|
+
<div class="placeholder-icon">
|
|
909
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
910
|
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
|
|
911
|
+
</svg>
|
|
912
|
+
</div>
|
|
913
|
+
<p>Select a tool to view details</p>
|
|
914
|
+
<p style="font-size: 12px; margin-top: 8px;">
|
|
915
|
+
<kbd>\u2191</kbd> <kbd>\u2193</kbd> to navigate \xB7 <kbd>\u2318K</kbd> to search
|
|
916
|
+
</p>
|
|
917
|
+
</div>
|
|
918
|
+
<div class="detail" id="detail" hidden></div>
|
|
919
|
+
</main>
|
|
920
|
+
</div>
|
|
921
|
+
|
|
922
|
+
<script>
|
|
923
|
+
(function () {
|
|
924
|
+
var state = null;
|
|
925
|
+
var selected = null;
|
|
926
|
+
var filter = "";
|
|
927
|
+
|
|
928
|
+
var listEl = document.getElementById("tool-list");
|
|
929
|
+
var detailEl = document.getElementById("detail");
|
|
930
|
+
var placeholderEl = document.getElementById("placeholder");
|
|
931
|
+
var searchEl = document.getElementById("search");
|
|
932
|
+
var countEl = document.getElementById("tool-count");
|
|
933
|
+
|
|
934
|
+
function esc(text) {
|
|
935
|
+
var div = document.createElement("div");
|
|
936
|
+
div.textContent = text == null ? "" : String(text);
|
|
937
|
+
return div.innerHTML;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function api(path, options) {
|
|
941
|
+
return fetch(path, options).then(function (res) {
|
|
942
|
+
if (!res.ok) throw new Error("Request failed: " + res.status);
|
|
943
|
+
return res.json();
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function load() {
|
|
948
|
+
api("/api/state").then(function (data) {
|
|
949
|
+
state = data;
|
|
950
|
+
countEl.textContent = data.tools.length + " tools from " + data.label;
|
|
951
|
+
renderList();
|
|
952
|
+
renderDetail();
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function visibleTools() {
|
|
957
|
+
if (!state) return [];
|
|
958
|
+
var f = filter.toLowerCase();
|
|
959
|
+
return state.tools.filter(function (tool) {
|
|
960
|
+
return tool.name.toLowerCase().indexOf(f) !== -1 ||
|
|
961
|
+
(tool.description && tool.description.toLowerCase().indexOf(f) !== -1);
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function groupTools(tools) {
|
|
966
|
+
var groups = { read: [], write: [], destructive: [] };
|
|
967
|
+
tools.forEach(function (tool) {
|
|
968
|
+
var key = tool.sideEffect || "read";
|
|
969
|
+
if (!groups[key]) groups[key] = [];
|
|
970
|
+
groups[key].push(tool);
|
|
971
|
+
});
|
|
972
|
+
return groups;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function renderList() {
|
|
976
|
+
var tools = visibleTools();
|
|
977
|
+
var groups = groupTools(tools);
|
|
978
|
+
var html = "";
|
|
979
|
+
|
|
980
|
+
["read", "write", "destructive"].forEach(function (risk) {
|
|
981
|
+
var group = groups[risk];
|
|
982
|
+
if (!group || group.length === 0) return;
|
|
983
|
+
html += '<div class="tool-group">' + risk + ' (' + group.length + ')</div>';
|
|
984
|
+
group.forEach(function (tool) {
|
|
985
|
+
var isSelected = tool.name === selected;
|
|
986
|
+
html += '<button class="tool" data-name="' + esc(tool.name) + '" aria-selected="' + isSelected + '">' +
|
|
987
|
+
'<span class="tool-indicator ' + risk + '"></span>' +
|
|
988
|
+
'<span class="tool-name">' + esc(tool.name) + "</span>" +
|
|
989
|
+
(!tool.enabled ? '<span class="tool-badge disabled">off</span>' : "") +
|
|
990
|
+
"</button>";
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
if (tools.length === 0) {
|
|
995
|
+
html = '<div style="padding: 20px; text-align: center; color: var(--faint);">No tools match your search</div>';
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
listEl.innerHTML = html;
|
|
999
|
+
|
|
1000
|
+
Array.prototype.forEach.call(listEl.querySelectorAll(".tool"), function (btn) {
|
|
1001
|
+
btn.addEventListener("click", function () {
|
|
1002
|
+
selected = btn.getAttribute("data-name");
|
|
1003
|
+
renderList();
|
|
1004
|
+
renderDetail();
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function currentTool() {
|
|
1010
|
+
if (!state || !selected) return null;
|
|
1011
|
+
return state.tools.find(function (tool) { return tool.name === selected; });
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function renderDetail() {
|
|
1015
|
+
var tool = currentTool();
|
|
1016
|
+
if (!tool) {
|
|
1017
|
+
detailEl.hidden = true;
|
|
1018
|
+
placeholderEl.hidden = false;
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
placeholderEl.hidden = true;
|
|
1023
|
+
detailEl.hidden = false;
|
|
1024
|
+
|
|
1025
|
+
var badges = [
|
|
1026
|
+
'<span class="badge ' + tool.sideEffect + '">' + tool.sideEffect + "</span>",
|
|
1027
|
+
!tool.enabled ? '<span class="badge disabled">starts disabled</span>' : "",
|
|
1028
|
+
tool.endpointRole !== "endpoint" ? '<span class="badge auth">' + tool.endpointRole + "</span>" : "",
|
|
1029
|
+
tool.piiInOutput.length > 0 ? '<span class="badge write">pii: ' + esc(tool.piiInOutput.join(", ")) + "</span>" : "",
|
|
1030
|
+
].filter(Boolean).join("");
|
|
1031
|
+
|
|
1032
|
+
var findings = tool.findings.map(function (finding) {
|
|
1033
|
+
var icon = finding.level === "error" ? "\u2716" : "\u26A0";
|
|
1034
|
+
return '<div class="finding ' + finding.level + '"><span class="finding-icon">' + icon + "</span><span>" + esc(finding.message) + "</span></div>";
|
|
1035
|
+
}).join("");
|
|
1036
|
+
|
|
1037
|
+
var schema = tool.inputSchema || {};
|
|
1038
|
+
var properties = schema.properties || {};
|
|
1039
|
+
var required = schema.required || [];
|
|
1040
|
+
var fields = Object.keys(properties).map(function (key) {
|
|
1041
|
+
var field = properties[key];
|
|
1042
|
+
var type = field.type === "number" || field.type === "integer" ? "number" : "text";
|
|
1043
|
+
var req = required.indexOf(key) !== -1 ? ' <span class="req">*</span>' : "";
|
|
1044
|
+
var hint = field.description ? '<div class="param-hint">' + esc(field.description) + "</div>" : "";
|
|
1045
|
+
return '<div class="param"><label class="param-label">' + esc(key) + req + '</label>' +
|
|
1046
|
+
'<input class="param-input" data-field="' + esc(key) + '" data-type="' + esc(field.type || "string") + '" type="' + type + '" spellcheck="false" />' +
|
|
1047
|
+
hint + "</div>";
|
|
1048
|
+
}).join("");
|
|
1049
|
+
|
|
1050
|
+
var baseUrl = "";
|
|
1051
|
+
try { baseUrl = localStorage.getItem("webmcp-codegen:baseUrl") || tool.serverUrl || ""; } catch (e) {}
|
|
1052
|
+
|
|
1053
|
+
detailEl.innerHTML =
|
|
1054
|
+
'<div class="detail-header">' +
|
|
1055
|
+
'<div class="detail-crumb">' + esc(state.label) + (state.outDir ? " \u2192 " + esc(state.outDir) : "") + "</div>" +
|
|
1056
|
+
'<h1 class="detail-title">' + esc(tool.name) + "</h1>" +
|
|
1057
|
+
'<div class="detail-route">' +
|
|
1058
|
+
'<span class="verb ' + tool.sideEffect + '">' + esc(tool.verb || "GET") + "</span>" +
|
|
1059
|
+
"<span>" + esc(tool.path || "") + "</span>" +
|
|
1060
|
+
"</div>" +
|
|
1061
|
+
'<div class="badges">' + badges + "</div>" +
|
|
1062
|
+
"</div>" +
|
|
1063
|
+
|
|
1064
|
+
(findings ? '<div class="section"><div class="section-label">Audit findings</div>' + findings + "</div>" : "") +
|
|
1065
|
+
|
|
1066
|
+
'<div class="section">' +
|
|
1067
|
+
'<div class="section-label">Description</div>' +
|
|
1068
|
+
'<textarea class="description-edit" id="desc" spellcheck="false">' + esc(tool.description) + "</textarea>" +
|
|
1069
|
+
'<div class="edit-actions">' +
|
|
1070
|
+
'<button class="btn btn-primary" id="save-desc">Save</button>' +
|
|
1071
|
+
'<span class="saved-indicator" id="saved">Saved</span>' +
|
|
1072
|
+
"</div>" +
|
|
1073
|
+
'<div class="edit-hint">Agents pick tools by this text. Saved to .webmcp-codegen.json, so it survives regeneration. \u2318S to save.</div>' +
|
|
1074
|
+
"</div>" +
|
|
1075
|
+
|
|
1076
|
+
'<div class="section">' +
|
|
1077
|
+
'<div class="section-label">Status</div>' +
|
|
1078
|
+
'<div class="toggle-row">' +
|
|
1079
|
+
'<button class="switch" id="toggle-enabled" role="switch" aria-checked="' + tool.enabled + '" aria-label="Enabled"></button>' +
|
|
1080
|
+
'<div class="toggle-copy"><strong>' + (tool.enabled ? "Enabled" : "Disabled") + "</strong>" +
|
|
1081
|
+
(tool.enabled
|
|
1082
|
+
? "This tool works as soon as the app registers it."
|
|
1083
|
+
: "The generated code is there, commented out. Flipping this regenerates it enabled on the next run.") +
|
|
1084
|
+
"</div></div>" +
|
|
1085
|
+
"</div>" +
|
|
1086
|
+
|
|
1087
|
+
'<div class="section">' +
|
|
1088
|
+
'<div class="section-label">Test</div>' +
|
|
1089
|
+
'<div class="try-section">' +
|
|
1090
|
+
'<div class="try-header"><h3>Run this tool</h3><span class="try-note">server-side, no browser session</span></div>' +
|
|
1091
|
+
'<div class="try-body">' +
|
|
1092
|
+
(tool.requiresAuth
|
|
1093
|
+
? '<div class="auth-note">\u26A0 This endpoint requires a browser session. The dashboard runs server-side, so you will get a 401. Test it in Chrome DevTools where you are signed in.</div>'
|
|
1094
|
+
: "") +
|
|
1095
|
+
'<input class="base-url-input" id="base-url" type="text" placeholder="Base URL (e.g. http://localhost:3000)" value="' + esc(baseUrl) + '" spellcheck="false" />' +
|
|
1096
|
+
(fields || '<div style="color: var(--faint); font-size: 13px; margin-bottom: 14px;">This tool takes no inputs.</div>') +
|
|
1097
|
+
'<button class="run-btn" id="run">Run tool</button>' +
|
|
1098
|
+
'<pre class="result" id="result" hidden></pre>' +
|
|
1099
|
+
"</div></div>" +
|
|
1100
|
+
"</div>";
|
|
1101
|
+
|
|
1102
|
+
document.getElementById("save-desc").addEventListener("click", saveDescription);
|
|
1103
|
+
document.getElementById("toggle-enabled").addEventListener("click", toggleEnabled);
|
|
1104
|
+
document.getElementById("run").addEventListener("click", runTool);
|
|
1105
|
+
document.getElementById("base-url").addEventListener("change", function (event) {
|
|
1106
|
+
try { localStorage.setItem("webmcp-codegen:baseUrl", event.target.value); } catch (e) {}
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function saveDescription() {
|
|
1111
|
+
var tool = currentTool();
|
|
1112
|
+
var desc = document.getElementById("desc").value.trim();
|
|
1113
|
+
if (!tool || !desc) return;
|
|
1114
|
+
api("/api/override", {
|
|
1115
|
+
method: "POST",
|
|
1116
|
+
headers: { "content-type": "application/json" },
|
|
1117
|
+
body: JSON.stringify({ name: tool.name, description: desc }),
|
|
1118
|
+
}).then(function () {
|
|
1119
|
+
tool.description = desc;
|
|
1120
|
+
var saved = document.getElementById("saved");
|
|
1121
|
+
saved.classList.add("show");
|
|
1122
|
+
setTimeout(function () { saved.classList.remove("show"); }, 2000);
|
|
1123
|
+
}).catch(function (error) { alert(error.message); });
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function toggleEnabled() {
|
|
1127
|
+
var tool = currentTool();
|
|
1128
|
+
if (!tool) return;
|
|
1129
|
+
var next = !tool.enabled;
|
|
1130
|
+
api("/api/override", {
|
|
1131
|
+
method: "POST",
|
|
1132
|
+
headers: { "content-type": "application/json" },
|
|
1133
|
+
body: JSON.stringify({ name: tool.name, enabled: next }),
|
|
1134
|
+
}).then(function () {
|
|
1135
|
+
tool.enabled = next;
|
|
1136
|
+
renderList();
|
|
1137
|
+
renderDetail();
|
|
1138
|
+
}).catch(function (error) { alert(error.message); });
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function runTool() {
|
|
1142
|
+
var tool = currentTool();
|
|
1143
|
+
if (!tool) return;
|
|
1144
|
+
var input = {};
|
|
1145
|
+
Array.prototype.forEach.call(document.querySelectorAll("[data-field]"), function (field) {
|
|
1146
|
+
var value = field.value;
|
|
1147
|
+
if (value === "") return;
|
|
1148
|
+
var type = field.getAttribute("data-type");
|
|
1149
|
+
if (type === "number" || type === "integer") value = Number(value);
|
|
1150
|
+
if (type === "boolean") value = value === "true";
|
|
1151
|
+
if (type === "object" || type === "array") {
|
|
1152
|
+
try { value = JSON.parse(value); } catch (e) { /* keep as string */ }
|
|
1153
|
+
}
|
|
1154
|
+
input[field.getAttribute("data-field")] = value;
|
|
1155
|
+
});
|
|
1156
|
+
var baseUrl = document.getElementById("base-url").value.trim();
|
|
1157
|
+
var resultEl = document.getElementById("result");
|
|
1158
|
+
var runEl = document.getElementById("run");
|
|
1159
|
+
runEl.disabled = true;
|
|
1160
|
+
runEl.textContent = "Running...";
|
|
1161
|
+
resultEl.hidden = true;
|
|
1162
|
+
api("/api/run", {
|
|
1163
|
+
method: "POST",
|
|
1164
|
+
headers: { "content-type": "application/json" },
|
|
1165
|
+
body: JSON.stringify({ name: tool.name, input: input, baseUrl: baseUrl || undefined }),
|
|
1166
|
+
}).then(function (result) {
|
|
1167
|
+
resultEl.hidden = false;
|
|
1168
|
+
resultEl.className = "result " + (result.ok ? "ok" : "err");
|
|
1169
|
+
resultEl.textContent =
|
|
1170
|
+
(result.status ? "HTTP " + result.status + "
|
|
1171
|
+
|
|
1172
|
+
" : "") +
|
|
1173
|
+
(result.error ? result.error : JSON.stringify(result.body, null, 2));
|
|
1174
|
+
}).catch(function (error) {
|
|
1175
|
+
resultEl.hidden = false;
|
|
1176
|
+
resultEl.className = "result err";
|
|
1177
|
+
resultEl.textContent = error.message;
|
|
1178
|
+
}).finally(function () {
|
|
1179
|
+
runEl.disabled = false;
|
|
1180
|
+
runEl.textContent = "Run tool";
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/* Keyboard navigation */
|
|
1185
|
+
document.addEventListener("keydown", function (event) {
|
|
1186
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "k") {
|
|
1187
|
+
event.preventDefault();
|
|
1188
|
+
searchEl.focus();
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "s") {
|
|
1192
|
+
event.preventDefault();
|
|
1193
|
+
saveDescription();
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (event.target === searchEl || event.target.tagName === "TEXTAREA" || event.target.tagName === "INPUT") {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
1200
|
+
var tools = visibleTools();
|
|
1201
|
+
var index = tools.findIndex(function (tool) { return tool.name === selected; });
|
|
1202
|
+
var next = event.key === "ArrowDown" ? index + 1 : index - 1;
|
|
1203
|
+
if (next < 0 || next >= tools.length) return;
|
|
1204
|
+
event.preventDefault();
|
|
1205
|
+
selected = tools[next].name;
|
|
1206
|
+
renderList();
|
|
1207
|
+
renderDetail();
|
|
1208
|
+
var button = listEl.querySelector('[aria-selected="true"]');
|
|
1209
|
+
if (button) button.scrollIntoView({ block: "nearest" });
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
searchEl.addEventListener("input", function (event) {
|
|
1213
|
+
filter = event.target.value;
|
|
1214
|
+
renderList();
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
load();
|
|
1218
|
+
})();
|
|
1219
|
+
</script>
|
|
1220
|
+
</body>
|
|
1221
|
+
</html>`;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/dev/server.ts
|
|
1225
|
+
async function startDevServer(options) {
|
|
1226
|
+
const setup = await resolveSetup(options.cwd, {
|
|
1227
|
+
dryRun: true,
|
|
1228
|
+
skipAudit: false,
|
|
1229
|
+
force: false,
|
|
1230
|
+
watch: false
|
|
1231
|
+
});
|
|
1232
|
+
async function currentState() {
|
|
1233
|
+
const data = await loadDataFile(options.cwd);
|
|
1234
|
+
const result = await runGenerate(setup.config, {
|
|
1235
|
+
cwd: options.cwd,
|
|
1236
|
+
dryRun: true,
|
|
1237
|
+
overrides: data.overrides
|
|
1238
|
+
});
|
|
1239
|
+
return {
|
|
1240
|
+
label: setup.label,
|
|
1241
|
+
outDir: setup.config.generate[0]?.outDir,
|
|
1242
|
+
tools: result.tools.map((tool) => toUiTool(tool, result.findings)),
|
|
1243
|
+
skipped: result.skipped,
|
|
1244
|
+
notes: result.notes
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
const server = createServer(async (request, response) => {
|
|
1248
|
+
try {
|
|
1249
|
+
await route(request, response);
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
async function route(request, response) {
|
|
1255
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
1256
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
1257
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1258
|
+
response.end(dashboardHtml());
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (request.method === "GET" && url.pathname === "/api/state") {
|
|
1262
|
+
sendJson(response, 200, await currentState());
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
if (request.method === "POST" && url.pathname === "/api/override") {
|
|
1266
|
+
const body = await readJson(request);
|
|
1267
|
+
if (!body.name) {
|
|
1268
|
+
sendJson(response, 400, { error: "Missing tool name." });
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const data = await loadDataFile(options.cwd);
|
|
1272
|
+
const overrides = { ...data.overrides ?? {} };
|
|
1273
|
+
const existing = overrides[body.name] ?? {};
|
|
1274
|
+
overrides[body.name] = {
|
|
1275
|
+
...existing,
|
|
1276
|
+
...body.description !== void 0 ? { description: body.description } : {},
|
|
1277
|
+
...body.enabled !== void 0 ? { enabled: body.enabled } : {}
|
|
1278
|
+
};
|
|
1279
|
+
await saveDataFile(options.cwd, { overrides });
|
|
1280
|
+
sendJson(response, 200, { ok: true, saved: `.webmcp-codegen.json` });
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (request.method === "POST" && url.pathname === "/api/run") {
|
|
1284
|
+
const body = await readJson(request);
|
|
1285
|
+
const state = await currentState();
|
|
1286
|
+
const tool = state.tools.find((candidate) => candidate.name === body.name);
|
|
1287
|
+
if (!tool) {
|
|
1288
|
+
sendJson(response, 404, { error: `No tool named "${body.name}".` });
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const result = await runEndpoint(tool, body.input ?? {}, body.baseUrl);
|
|
1292
|
+
sendJson(response, result.ok ? 200 : 502, result);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
sendJson(response, 404, { error: "Not found" });
|
|
1296
|
+
}
|
|
1297
|
+
await new Promise(
|
|
1298
|
+
(resolveListen) => server.listen(options.port, "127.0.0.1", resolveListen)
|
|
1299
|
+
);
|
|
1300
|
+
if (options.open !== false) openBrowser(`http://localhost:${options.port}`);
|
|
1301
|
+
return server;
|
|
1302
|
+
}
|
|
1303
|
+
function toUiTool(tool, findings) {
|
|
1304
|
+
const [verb, ...rest] = tool.source.ref.split(" ");
|
|
1305
|
+
return {
|
|
1306
|
+
name: tool.name,
|
|
1307
|
+
verb,
|
|
1308
|
+
path: rest.join(" "),
|
|
1309
|
+
description: tool.description,
|
|
1310
|
+
sideEffect: tool.sideEffect,
|
|
1311
|
+
riskTier: tool.riskTier,
|
|
1312
|
+
enabled: tool.enabledByDefault,
|
|
1313
|
+
endpointRole: tool.endpointRole,
|
|
1314
|
+
piiInOutput: tool.piiInOutput,
|
|
1315
|
+
inputSchema: tool.inputSchema,
|
|
1316
|
+
...tool.pathTemplate ? { pathTemplate: tool.pathTemplate } : {},
|
|
1317
|
+
...tool.paramLocations ? { paramLocations: tool.paramLocations } : {},
|
|
1318
|
+
...tool.serverUrl ? { serverUrl: tool.serverUrl } : {},
|
|
1319
|
+
requiresAuth: tool.requiresAuth,
|
|
1320
|
+
findings: findings.filter((finding) => finding.tool === tool.name).map((finding) => ({ level: finding.level, message: finding.message }))
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
async function runEndpoint(tool, input, baseUrlOverride) {
|
|
1324
|
+
const base = baseUrlOverride ?? tool.serverUrl;
|
|
1325
|
+
if (!base) {
|
|
1326
|
+
return {
|
|
1327
|
+
ok: false,
|
|
1328
|
+
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.`
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
if (!tool.pathTemplate || !tool.verb) {
|
|
1332
|
+
return { ok: false, error: "This tool has no route to call." };
|
|
1333
|
+
}
|
|
1334
|
+
let path = tool.pathTemplate;
|
|
1335
|
+
for (const param of tool.paramLocations?.path ?? []) {
|
|
1336
|
+
path = path.replace(`{${param}}`, encodeURIComponent(String(input[param] ?? "")));
|
|
1337
|
+
}
|
|
1338
|
+
const url = new URL(path, base);
|
|
1339
|
+
for (const param of tool.paramLocations?.query ?? []) {
|
|
1340
|
+
const value = input[param];
|
|
1341
|
+
if (value !== void 0 && value !== null) url.searchParams.set(param, String(value));
|
|
1342
|
+
}
|
|
1343
|
+
const bodyFields = tool.paramLocations?.body ?? [];
|
|
1344
|
+
const body = bodyFields.length === 1 && bodyFields[0] === "body" ? input.body : bodyFields.length > 0 ? Object.fromEntries(bodyFields.map((field) => [field, input[field]])) : void 0;
|
|
1345
|
+
try {
|
|
1346
|
+
const response = await fetch(url, {
|
|
1347
|
+
method: tool.verb,
|
|
1348
|
+
headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
|
|
1349
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1350
|
+
});
|
|
1351
|
+
const text = await response.text();
|
|
1352
|
+
let parsed = text;
|
|
1353
|
+
try {
|
|
1354
|
+
parsed = JSON.parse(text);
|
|
1355
|
+
} catch {
|
|
1356
|
+
}
|
|
1357
|
+
return { ok: response.ok, status: response.status, body: parsed };
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function sendJson(response, status, body) {
|
|
1363
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
1364
|
+
response.end(JSON.stringify(body));
|
|
1365
|
+
}
|
|
1366
|
+
function readJson(request) {
|
|
1367
|
+
return new Promise((resolveRead, reject) => {
|
|
1368
|
+
let text = "";
|
|
1369
|
+
request.on("data", (chunk) => {
|
|
1370
|
+
text += chunk.toString("utf8");
|
|
1371
|
+
});
|
|
1372
|
+
request.on("end", () => {
|
|
1373
|
+
try {
|
|
1374
|
+
resolveRead(text ? JSON.parse(text) : {});
|
|
1375
|
+
} catch {
|
|
1376
|
+
reject(new Error("Invalid JSON body"));
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
request.on("error", reject);
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
function openBrowser(url) {
|
|
1383
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1384
|
+
spawn(command, [url], { stdio: "ignore", shell: process.platform === "win32" }).unref();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// src/wire.ts
|
|
1388
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
1389
|
+
import { dirname, join as join5, relative as relative2 } from "path";
|
|
26
1390
|
async function planWiring(cwd, app, outDir) {
|
|
27
1391
|
switch (app.framework) {
|
|
28
1392
|
case "next":
|
|
@@ -35,28 +1399,28 @@ async function planWiring(cwd, app, outDir) {
|
|
|
35
1399
|
}
|
|
36
1400
|
async function applyWiring(plan) {
|
|
37
1401
|
for (const edit of plan.edits) {
|
|
38
|
-
await
|
|
1402
|
+
await writeFile2(edit.path, edit.contents, "utf8");
|
|
39
1403
|
}
|
|
40
1404
|
}
|
|
41
1405
|
async function planNextWiring(cwd, app, outDir) {
|
|
42
1406
|
const layoutCandidates = [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1407
|
+
join5(cwd, app.dir, "src/app/layout.tsx"),
|
|
1408
|
+
join5(cwd, app.dir, "src/app/layout.jsx"),
|
|
1409
|
+
join5(cwd, app.dir, "app/layout.tsx"),
|
|
1410
|
+
join5(cwd, app.dir, "app/layout.jsx")
|
|
47
1411
|
];
|
|
48
1412
|
const layoutPath = await firstExisting(layoutCandidates);
|
|
49
1413
|
if (!layoutPath) return null;
|
|
50
|
-
const registerPath =
|
|
51
|
-
const layout = await
|
|
1414
|
+
const registerPath = join5(cwd, outDir, "register.tsx");
|
|
1415
|
+
const layout = await readFile3(layoutPath, "utf8");
|
|
52
1416
|
if (layout.includes("WebMCPRegister")) return { edits: [], alreadyWired: true };
|
|
53
|
-
const importPath = withoutExtension(
|
|
1417
|
+
const importPath = withoutExtension(relative2(dirname(layoutPath), registerPath));
|
|
54
1418
|
const edits = [
|
|
55
1419
|
{
|
|
56
1420
|
path: registerPath,
|
|
57
1421
|
action: "create",
|
|
58
1422
|
contents: nextRegisterComponent(),
|
|
59
|
-
summary: `created ${
|
|
1423
|
+
summary: `created ${relative2(cwd, registerPath)} (a client component that registers your tools on page load)`
|
|
60
1424
|
}
|
|
61
1425
|
];
|
|
62
1426
|
const withImport = insertAfterLastImport(
|
|
@@ -73,7 +1437,7 @@ async function planNextWiring(cwd, app, outDir) {
|
|
|
73
1437
|
path: layoutPath,
|
|
74
1438
|
action: "modify",
|
|
75
1439
|
contents: withComponent,
|
|
76
|
-
summary: `added 2 lines to ${
|
|
1440
|
+
summary: `added 2 lines to ${relative2(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`
|
|
77
1441
|
});
|
|
78
1442
|
return { edits };
|
|
79
1443
|
}
|
|
@@ -97,16 +1461,16 @@ export function WebMCPRegister() {
|
|
|
97
1461
|
}
|
|
98
1462
|
async function planViteWiring(cwd, app, outDir) {
|
|
99
1463
|
const entryCandidates = [
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1464
|
+
join5(cwd, app.dir, "src/main.tsx"),
|
|
1465
|
+
join5(cwd, app.dir, "src/main.jsx"),
|
|
1466
|
+
join5(cwd, app.dir, "src/index.tsx"),
|
|
1467
|
+
join5(cwd, app.dir, "src/index.jsx")
|
|
104
1468
|
];
|
|
105
1469
|
const entryPath = await firstExisting(entryCandidates);
|
|
106
1470
|
if (!entryPath) return null;
|
|
107
|
-
const entry = await
|
|
1471
|
+
const entry = await readFile3(entryPath, "utf8");
|
|
108
1472
|
if (entry.includes("registerAllTools")) return { edits: [], alreadyWired: true };
|
|
109
|
-
const importPath = withoutExtension(
|
|
1473
|
+
const importPath = withoutExtension(relative2(dirname(entryPath), join5(cwd, outDir, "index")));
|
|
110
1474
|
const withWiring = insertAfterLastImport(
|
|
111
1475
|
entry,
|
|
112
1476
|
`import { registerAllTools } from "${importPath}";
|
|
@@ -120,7 +1484,7 @@ void registerAllTools();`
|
|
|
120
1484
|
path: entryPath,
|
|
121
1485
|
action: "modify",
|
|
122
1486
|
contents: withWiring,
|
|
123
|
-
summary: `added 2 lines to ${
|
|
1487
|
+
summary: `added 2 lines to ${relative2(cwd, entryPath)} (an import and a registerAllTools() call)`
|
|
124
1488
|
}
|
|
125
1489
|
]
|
|
126
1490
|
};
|
|
@@ -142,7 +1506,7 @@ function withoutExtension(path) {
|
|
|
142
1506
|
async function firstExisting(paths) {
|
|
143
1507
|
for (const path of paths) {
|
|
144
1508
|
try {
|
|
145
|
-
await
|
|
1509
|
+
await readFile3(path, "utf8");
|
|
146
1510
|
return path;
|
|
147
1511
|
} catch {
|
|
148
1512
|
}
|
|
@@ -151,38 +1515,46 @@ async function firstExisting(paths) {
|
|
|
151
1515
|
}
|
|
152
1516
|
|
|
153
1517
|
// src/cli.ts
|
|
154
|
-
var HELP = `
|
|
1518
|
+
var HELP = `
|
|
1519
|
+
webmcp-codegen \u2014 generate WebMCP tools from your OpenAPI spec
|
|
155
1520
|
|
|
156
|
-
|
|
157
|
-
npx webmcp-codegen
|
|
158
|
-
npx webmcp-codegen generate Write the tool files, wire them up
|
|
1521
|
+
Usage
|
|
1522
|
+
npx webmcp-codegen [command] [flags]
|
|
159
1523
|
|
|
160
|
-
Commands
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
dev Open the tools dashboard (list, edit, try tools)
|
|
1524
|
+
Commands
|
|
1525
|
+
generate Generate tool files from your spec (default when no command given)
|
|
1526
|
+
dev Open the tools dashboard (list, describe, toggle, test)
|
|
1527
|
+
init Write a codegen.config.mjs for full control
|
|
165
1528
|
|
|
166
|
-
Flags
|
|
1529
|
+
Flags
|
|
167
1530
|
--spec PATH Which OpenAPI spec to use (auto-detected when omitted)
|
|
168
|
-
--out DIR Where
|
|
1531
|
+
--out DIR Where tool files go (default: your web app's src/webmcp)
|
|
169
1532
|
--dry-run Preview what would be written, write nothing
|
|
170
|
-
--
|
|
1533
|
+
--verbose Show every tool, not just the summary
|
|
171
1534
|
--force Write files even when the audit reports errors
|
|
1535
|
+
--skip-audit Skip the safety report
|
|
172
1536
|
--config PATH Use a config file at PATH
|
|
173
|
-
|
|
174
|
-
Flags for dev:
|
|
175
1537
|
--port N Dashboard port (default: 4700)
|
|
1538
|
+
--help Show this help
|
|
1539
|
+
|
|
1540
|
+
Examples
|
|
1541
|
+
npx webmcp-codegen generate # detect spec, generate tools
|
|
1542
|
+
npx webmcp-codegen generate --dry-run # preview without writing
|
|
1543
|
+
npx webmcp-codegen dev # open the dashboard
|
|
1544
|
+
npx webmcp-codegen generate --verbose # see all 72 tools listed
|
|
1545
|
+
|
|
1546
|
+
Docs
|
|
1547
|
+
https://webmcp-codegen.vercel.app/docs
|
|
176
1548
|
`;
|
|
177
|
-
var CONFIG_FILE = "codegen.config.mjs";
|
|
178
|
-
var MAX_LISTED_TOOLS = 15;
|
|
179
1549
|
async function main() {
|
|
180
|
-
const {
|
|
1550
|
+
const { values, positionals } = parseArgs({
|
|
1551
|
+
args: process.argv.slice(2),
|
|
181
1552
|
allowPositionals: true,
|
|
182
1553
|
options: {
|
|
183
1554
|
"dry-run": { type: "boolean", default: false },
|
|
184
1555
|
"skip-audit": { type: "boolean", default: false },
|
|
185
1556
|
force: { type: "boolean", default: false },
|
|
1557
|
+
verbose: { type: "boolean", default: false },
|
|
186
1558
|
watch: { type: "boolean", default: false },
|
|
187
1559
|
config: { type: "string" },
|
|
188
1560
|
spec: { type: "string" },
|
|
@@ -191,28 +1563,32 @@ async function main() {
|
|
|
191
1563
|
help: { type: "boolean", default: false }
|
|
192
1564
|
}
|
|
193
1565
|
});
|
|
194
|
-
const
|
|
195
|
-
|
|
1566
|
+
const flags = {
|
|
1567
|
+
dryRun: values["dry-run"] ?? false,
|
|
1568
|
+
skipAudit: values["skip-audit"] ?? false,
|
|
1569
|
+
force: values.force ?? false,
|
|
1570
|
+
verbose: values.verbose ?? false,
|
|
1571
|
+
watch: values.watch ?? false,
|
|
1572
|
+
config: values.config,
|
|
1573
|
+
spec: values.spec,
|
|
1574
|
+
out: values.out,
|
|
1575
|
+
port: values.port ? Number.parseInt(values.port, 10) : void 0,
|
|
1576
|
+
help: values.help
|
|
1577
|
+
};
|
|
1578
|
+
if (flags.help) {
|
|
196
1579
|
console.log(HELP);
|
|
197
1580
|
return 0;
|
|
198
1581
|
}
|
|
1582
|
+
const command = positionals[0] ?? "generate";
|
|
199
1583
|
switch (command) {
|
|
200
1584
|
case "init":
|
|
201
1585
|
return init();
|
|
202
1586
|
case "dev":
|
|
203
|
-
return dev(
|
|
1587
|
+
return dev(flags.port ?? 4700);
|
|
204
1588
|
case "generate":
|
|
205
|
-
return generate(
|
|
206
|
-
dryRun: values["dry-run"],
|
|
207
|
-
skipAudit: values["skip-audit"],
|
|
208
|
-
force: values.force,
|
|
209
|
-
watch: values.watch,
|
|
210
|
-
configPath: values.config,
|
|
211
|
-
spec: values.spec,
|
|
212
|
-
out: values.out
|
|
213
|
-
});
|
|
1589
|
+
return generate(flags);
|
|
214
1590
|
default:
|
|
215
|
-
console.error(`Unknown command
|
|
1591
|
+
console.error(`Unknown command: ${command}
|
|
216
1592
|
`);
|
|
217
1593
|
console.log(HELP);
|
|
218
1594
|
return 1;
|
|
@@ -220,14 +1596,17 @@ async function main() {
|
|
|
220
1596
|
}
|
|
221
1597
|
async function init() {
|
|
222
1598
|
const cwd = process.cwd();
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
1599
|
+
const configFile = CONFIG_FILE_NAMES[0] ?? "codegen.config.mjs";
|
|
1600
|
+
const configPath = join6(cwd, configFile);
|
|
1601
|
+
if (existsSync2(configPath)) {
|
|
1602
|
+
console.error(`
|
|
1603
|
+
\u2716 ${configFile} already exists. Nothing to do.
|
|
1604
|
+
`);
|
|
226
1605
|
return 1;
|
|
227
1606
|
}
|
|
228
1607
|
const specs = await findSpecs(cwd);
|
|
229
1608
|
const specPath = specs.length > 0 ? `./${specs[0]}` : "./openapi.yaml";
|
|
230
|
-
await
|
|
1609
|
+
await writeFile3(
|
|
231
1610
|
configPath,
|
|
232
1611
|
`import { defineConfig } from "webmcp-codegen";
|
|
233
1612
|
import { openapi } from "webmcp-codegen/sources";
|
|
@@ -245,40 +1624,48 @@ export default defineConfig({
|
|
|
245
1624
|
});
|
|
246
1625
|
`
|
|
247
1626
|
);
|
|
248
|
-
console.log(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
} else {
|
|
254
|
-
console.log(`No OpenAPI spec found, so ${CONFIG_FILE} points at ./openapi.yaml.`);
|
|
255
|
-
console.log("Edit the `spec` path to point at your spec, then run:");
|
|
256
|
-
console.log("\n npx webmcp-codegen generate --dry-run");
|
|
257
|
-
}
|
|
1627
|
+
console.log(`
|
|
1628
|
+
\u2714 Wrote ${configFile}
|
|
1629
|
+
`);
|
|
1630
|
+
console.log("Edit it to add sources, change the output directory, or set safety options.");
|
|
1631
|
+
console.log("Docs: https://webmcp-codegen.vercel.app/docs/configuration\n");
|
|
258
1632
|
return 0;
|
|
259
1633
|
}
|
|
260
|
-
async function
|
|
1634
|
+
async function dev(port) {
|
|
261
1635
|
const cwd = process.cwd();
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
1636
|
+
const server = await startDevServer({ cwd, port, open: true });
|
|
1637
|
+
console.log(`
|
|
1638
|
+
\u2714 Dashboard: http://localhost:${port}
|
|
1639
|
+
`);
|
|
1640
|
+
console.log("List, describe, enable, and test your tools. Ctrl+C to stop.\n");
|
|
1641
|
+
await new Promise((resolveExit) => {
|
|
1642
|
+
process.on("SIGINT", () => {
|
|
1643
|
+
server.close();
|
|
1644
|
+
resolveExit();
|
|
1645
|
+
});
|
|
1646
|
+
});
|
|
1647
|
+
return 0;
|
|
268
1648
|
}
|
|
269
|
-
async function
|
|
270
|
-
const
|
|
271
|
-
const
|
|
1649
|
+
async function generate(flags) {
|
|
1650
|
+
const cwd = process.cwd();
|
|
1651
|
+
const setup = await resolveSetup(cwd, {
|
|
1652
|
+
dryRun: flags.dryRun,
|
|
1653
|
+
skipAudit: flags.skipAudit,
|
|
1654
|
+
force: flags.force,
|
|
1655
|
+
watch: flags.watch,
|
|
1656
|
+
spec: flags.spec,
|
|
1657
|
+
out: flags.out,
|
|
1658
|
+
configPath: flags.config
|
|
1659
|
+
});
|
|
272
1660
|
const result = await runGenerate(setup.config, {
|
|
273
1661
|
cwd,
|
|
274
1662
|
dryRun: flags.dryRun,
|
|
275
|
-
skipAudit: flags.skipAudit,
|
|
276
1663
|
force: flags.force,
|
|
277
|
-
|
|
1664
|
+
skipAudit: flags.skipAudit
|
|
278
1665
|
});
|
|
279
1666
|
let wiring = null;
|
|
280
1667
|
if (!result.blocked && setup.app) {
|
|
281
|
-
const outDir =
|
|
1668
|
+
const outDir = setup.config.generate[0]?.outDir;
|
|
282
1669
|
if (outDir) {
|
|
283
1670
|
wiring = await planWiring(cwd, setup.app, outDir);
|
|
284
1671
|
if (wiring && wiring.edits.length > 0 && !flags.dryRun && result.wrote) {
|
|
@@ -289,143 +1676,22 @@ async function runOnce(cwd, flags) {
|
|
|
289
1676
|
if (!setup.fromConfigFile && !flags.dryRun && result.wrote) {
|
|
290
1677
|
await saveDataFile(cwd, setup.remember);
|
|
291
1678
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return config.generate[0]?.outDir;
|
|
297
|
-
}
|
|
298
|
-
function printReport(result, flags, setup, wiring, cwd) {
|
|
299
|
-
const { tools, skipped, findings, files, notes, blocked } = result;
|
|
300
|
-
console.log(`
|
|
301
|
-
webmcp-codegen (${setup.label}): ${tools.length} tool(s)`);
|
|
302
|
-
for (const note of notes) {
|
|
303
|
-
console.log(`
|
|
304
|
-
note: ${note}`);
|
|
305
|
-
}
|
|
306
|
-
if (skipped.length > 0) {
|
|
307
|
-
console.log(`
|
|
308
|
-
${skipped.length} endpoint(s) skipped:`);
|
|
309
|
-
for (const entry of skipped) {
|
|
310
|
-
console.log(` ${entry.ref}: ${entry.reason}`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
console.log("");
|
|
314
|
-
const listed = tools.slice(0, MAX_LISTED_TOOLS);
|
|
315
|
-
for (const tool of listed) {
|
|
316
|
-
const state = tool.enabledByDefault ? "" : " starts disabled";
|
|
317
|
-
console.log(` ${tool.name} [${tool.sideEffect}]${state} \u2190 ${tool.source.ref}`);
|
|
318
|
-
}
|
|
319
|
-
if (tools.length > listed.length) {
|
|
320
|
-
console.log(` \u2026and ${tools.length - listed.length} more`);
|
|
321
|
-
}
|
|
322
|
-
if (findings.length > 0) {
|
|
323
|
-
console.log("");
|
|
324
|
-
for (const finding of findings) {
|
|
325
|
-
const icon = finding.level === "error" ? "\u2716" : "\u26A0";
|
|
326
|
-
const where = finding.tool ? ` (${finding.tool})` : "";
|
|
327
|
-
console.log(` ${icon} ${finding.message}${where}`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
if (files.length > 0) {
|
|
331
|
-
console.log("");
|
|
332
|
-
for (const file of files) {
|
|
333
|
-
if (file.action === "unchanged" && !file.conflict) continue;
|
|
334
|
-
const shown = file.conflict ? `conflict \u2192 wrote ${relative2(cwd, file.conflict)}` : file.action;
|
|
335
|
-
console.log(` ${shown}: ${relative2(cwd, file.path)}`);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
if (wiring) {
|
|
339
|
-
if (wiring.alreadyWired) {
|
|
340
|
-
console.log("\n registration: already wired into your app");
|
|
341
|
-
} else if (wiring.edits.length > 0) {
|
|
342
|
-
console.log(flags.dryRun ? "\n registration (would do):" : "\n registration:");
|
|
343
|
-
for (const edit of wiring.edits) {
|
|
344
|
-
console.log(` ${edit.summary}`);
|
|
345
|
-
}
|
|
346
|
-
if (!flags.dryRun) {
|
|
347
|
-
console.log(" undo: delete the added lines (nothing else was touched)");
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
} else if (!blocked && setup.app) {
|
|
351
|
-
console.log("\n registration: could not find your app's entry file, so add this by hand:");
|
|
352
|
-
console.log(' import { registerAllTools } from "<path-to>/src/webmcp";');
|
|
353
|
-
console.log(" void registerAllTools();");
|
|
354
|
-
}
|
|
355
|
-
if (blocked) {
|
|
356
|
-
console.log(
|
|
357
|
-
"\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway."
|
|
358
|
-
);
|
|
359
|
-
return;
|
|
1679
|
+
if (flags.verbose) {
|
|
1680
|
+
renderVerbose(result, setup, cwd);
|
|
1681
|
+
} else {
|
|
1682
|
+
renderSummary(result, setup, cwd, wiring);
|
|
360
1683
|
}
|
|
361
|
-
if (flags.dryRun) {
|
|
362
|
-
console.log("\
|
|
363
|
-
return;
|
|
1684
|
+
if (flags.watch && !flags.dryRun) {
|
|
1685
|
+
console.log(dim("\n --watch is not implemented yet. Run generate again after changes.\n"));
|
|
364
1686
|
}
|
|
365
|
-
|
|
1687
|
+
return result.blocked ? 1 : 0;
|
|
366
1688
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
parts.push(`${disabled.length} tool(s) start disabled (open the file and uncomment to enable)`);
|
|
374
|
-
}
|
|
375
|
-
console.log(`
|
|
376
|
-
Done. ${parts.join("; ")}.`);
|
|
377
|
-
const suggestion = pickSuggestedTool(result.tools);
|
|
378
|
-
console.log("\nTry it:");
|
|
379
|
-
console.log(" 1. Start your app and open it in Chrome.");
|
|
380
|
-
console.log(" 2. Turn on chrome://flags/#enable-webmcp-testing and reload the page.");
|
|
381
|
-
if (suggestion) {
|
|
382
|
-
console.log(` 3. Ask the agent: "${suggestion}"`);
|
|
383
|
-
} else {
|
|
384
|
-
console.log(" 3. Ask the agent to use one of your tools.");
|
|
1689
|
+
main().then(
|
|
1690
|
+
(code) => process.exit(code),
|
|
1691
|
+
(error) => {
|
|
1692
|
+
console.error("\n\u2716 Unexpected error:", error instanceof Error ? error.message : error);
|
|
1693
|
+
console.error("\nPlease report this: https://github.com/SouravInsights/groundstate/issues\n");
|
|
1694
|
+
process.exit(1);
|
|
385
1695
|
}
|
|
386
|
-
|
|
387
|
-
function pickSuggestedTool(tools) {
|
|
388
|
-
const reads = tools.filter((tool2) => tool2.enabledByDefault && tool2.endpointRole === "endpoint");
|
|
389
|
-
if (reads.length === 0) return void 0;
|
|
390
|
-
const tool = reads.find((candidate) => /^(list|get|search|find|fetch|recent)-/.test(candidate.name)) ?? reads[0];
|
|
391
|
-
if (!tool) return void 0;
|
|
392
|
-
const description = tool.description.trim().replace(/\.$/, "");
|
|
393
|
-
const looksTemplated = /^(GET|POST|PUT|PATCH|DELETE)\s/.test(description);
|
|
394
|
-
const phrase = looksTemplated ? tool.name.replace(/-/g, " ") : description.charAt(0).toLowerCase() + description.slice(1);
|
|
395
|
-
return phrase;
|
|
396
|
-
}
|
|
397
|
-
async function dev(port) {
|
|
398
|
-
const { startDevServer } = await import("./server-DWP6IHCS.js");
|
|
399
|
-
const server = await startDevServer({ cwd: process.cwd(), port });
|
|
400
|
-
console.log(`
|
|
401
|
-
webmcp-codegen dashboard: http://localhost:${port}`);
|
|
402
|
-
console.log("List, describe, enable, and try your tools. Ctrl+C to stop.\n");
|
|
403
|
-
await new Promise((resolveExit) => {
|
|
404
|
-
process.on("SIGINT", () => {
|
|
405
|
-
server.close();
|
|
406
|
-
resolveExit();
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
return 0;
|
|
410
|
-
}
|
|
411
|
-
async function watchLoop(cwd, flags) {
|
|
412
|
-
await runOnce(cwd, { ...flags, dryRun: false });
|
|
413
|
-
console.log("\nWatching for changes\u2026 (Ctrl+C to stop)");
|
|
414
|
-
let timer;
|
|
415
|
-
watch(cwd, { recursive: true }, (_event, filename) => {
|
|
416
|
-
if (!filename) return;
|
|
417
|
-
if (/node_modules|\.git|\/dist|\/src\/webmcp|\.webmcp-codegen\.json/.test(filename)) return;
|
|
418
|
-
if (!/\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;
|
|
419
|
-
clearTimeout(timer);
|
|
420
|
-
timer = setTimeout(() => {
|
|
421
|
-
runOnce(cwd, { ...flags, dryRun: false }).catch((error) => {
|
|
422
|
-
console.error(error instanceof Error ? error.message : error);
|
|
423
|
-
});
|
|
424
|
-
}, 300);
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
main().then((code) => process.exit(code)).catch((error) => {
|
|
428
|
-
console.error(error instanceof Error ? error.message : error);
|
|
429
|
-
process.exit(1);
|
|
430
|
-
});
|
|
1696
|
+
);
|
|
431
1697
|
//# sourceMappingURL=cli.js.map
|