mockpit 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +232 -2
  3. package/bin/demoData.js +132 -0
  4. package/bin/initDesign.js +846 -0
  5. package/bin/mockpit.js +1830 -0
  6. package/bin/serveUrl.d.ts +1 -0
  7. package/bin/serveUrl.js +7 -0
  8. package/dist/mcp/server.d.ts +2 -0
  9. package/dist/mcp/server.js +230 -0
  10. package/dist/server/apiViews.d.ts +329 -0
  11. package/dist/server/apiViews.js +135 -0
  12. package/dist/server/app.d.ts +53 -0
  13. package/dist/server/app.js +3112 -0
  14. package/dist/server/base64.d.ts +1 -0
  15. package/dist/server/base64.js +16 -0
  16. package/dist/server/cdn.d.ts +6 -0
  17. package/dist/server/cdn.js +33 -0
  18. package/dist/server/codeRunner.d.ts +5 -0
  19. package/dist/server/codeRunner.js +89 -0
  20. package/dist/server/codeWorker.d.ts +1 -0
  21. package/dist/server/codeWorker.js +196 -0
  22. package/dist/server/designGuide.d.ts +16 -0
  23. package/dist/server/designGuide.js +311 -0
  24. package/dist/server/events.d.ts +53 -0
  25. package/dist/server/events.js +14 -0
  26. package/dist/server/feedbackBatch.d.ts +43 -0
  27. package/dist/server/feedbackBatch.js +98 -0
  28. package/dist/server/icons.d.ts +50 -0
  29. package/dist/server/icons.js +225 -0
  30. package/dist/server/index.d.ts +1 -0
  31. package/dist/server/index.js +64 -0
  32. package/dist/server/kits.d.ts +36 -0
  33. package/dist/server/kits.js +279 -0
  34. package/dist/server/knobs.d.ts +15 -0
  35. package/dist/server/knobs.js +333 -0
  36. package/dist/server/mcpHttp.d.ts +53 -0
  37. package/dist/server/mcpHttp.js +143 -0
  38. package/dist/server/mcpSpec.d.ts +191 -0
  39. package/dist/server/mcpSpec.js +402 -0
  40. package/dist/server/parts.d.ts +31 -0
  41. package/dist/server/parts.js +338 -0
  42. package/dist/server/postMarkdown.d.ts +10 -0
  43. package/dist/server/postMarkdown.js +271 -0
  44. package/dist/server/postSurfaces.d.ts +40 -0
  45. package/dist/server/postSurfaces.js +407 -0
  46. package/dist/server/public.d.ts +5 -0
  47. package/dist/server/public.js +5 -0
  48. package/dist/server/push.d.ts +45 -0
  49. package/dist/server/push.js +234 -0
  50. package/dist/server/richRender.d.ts +15 -0
  51. package/dist/server/richRender.js +392 -0
  52. package/dist/server/run.d.ts +82 -0
  53. package/dist/server/run.js +290 -0
  54. package/dist/server/runApi.d.ts +6 -0
  55. package/dist/server/runApi.js +82 -0
  56. package/dist/server/slots.d.ts +14 -0
  57. package/dist/server/slots.js +71 -0
  58. package/dist/server/sqlStore.d.ts +119 -0
  59. package/dist/server/sqlStore.js +954 -0
  60. package/dist/server/sqliteStorage.d.ts +2 -0
  61. package/dist/server/sqliteStorage.js +90 -0
  62. package/dist/server/surfacePage.d.ts +48 -0
  63. package/dist/server/surfacePage.js +1099 -0
  64. package/dist/server/themes.d.ts +48 -0
  65. package/dist/server/themes.js +305 -0
  66. package/dist/server/types.d.ts +599 -0
  67. package/dist/server/types.js +299 -0
  68. package/dist/workers/public.d.ts +1 -0
  69. package/dist/workers/public.js +3 -0
  70. package/extensions/mockpit.js +703 -0
  71. package/guide/AGENT_SETUP.md +23 -0
  72. package/guide/topics/asks.md +87 -0
  73. package/guide/topics/feedback.md +127 -0
  74. package/guide/topics/html.md +175 -0
  75. package/guide/topics/http.md +76 -0
  76. package/guide/topics/knobs.md +72 -0
  77. package/guide/topics/scripts.md +129 -0
  78. package/guide/topics/surfaces.md +100 -0
  79. package/package.json +165 -4
  80. package/skills/launch-video/SKILL.md +159 -0
  81. package/skills/mockpit/SKILL.md +40 -0
  82. package/viewer/dist/index.html +1922 -0
@@ -0,0 +1,333 @@
1
+ // Knob schema and value validation (plan Q3: values are checked against the
2
+ // declared schema before they are stored or used). Runtime-agnostic. The config
3
+ // shape mirrors tunekit's usePane config; see the knob types in types.ts.
4
+ const MAX_KNOBS = 100;
5
+ const MAX_OPTIONS = 100;
6
+ const MAX_STRING = 500;
7
+ const MAX_TEXT_VALUE = 2000;
8
+ // A path is "size" (global) or "<part>.<name>"; segments stay CSS/attribute-safe
9
+ // because the bridge turns them into `--k-<path>` vars and `data-k-<path>` attrs.
10
+ const PATH_RE = /^[A-Za-z_][\w-]{0,63}(\.[A-Za-z_][\w-]{0,63}){0,3}$/;
11
+ // Color values end up inside CSS custom properties, so only the characters a
12
+ // CSS color or gradient needs are allowed.
13
+ const COLOR_RE = /^[#\w\s(),.%/+-]{1,300}$/;
14
+ // The character class above still admits `url(//host/x)` and `image(...)`, which
15
+ // would turn a colour var into a network fetch inside the sandbox; a colour never
16
+ // needs either function.
17
+ const COLOR_FETCH_RE = /\b(url|image|image-set|src)\s*\(/i;
18
+ const isCssColor = (v) => COLOR_RE.test(v) && !COLOR_FETCH_RE.test(v);
19
+ const COLOR_HINT = /^(#|rgba?\(|hsla?\(|hwb\(|lab\(|lch\(|oklab\(|oklch\(|color\(|(linear|radial|conic)-gradient\()/i;
20
+ const isNum = (v) => typeof v === "number" && Number.isFinite(v);
21
+ const isStr = (v, max = MAX_STRING) => typeof v === "string" && v.length <= max;
22
+ function checkAxis(raw, name) {
23
+ if (!Array.isArray(raw) || raw.length < 3 || raw.length > 4 || !raw.every(isNum)) {
24
+ return { ok: false, error: `${name} must be [default, min, max, step?] numbers` };
25
+ }
26
+ const [value, min, max, step] = raw;
27
+ if (min > max)
28
+ return { ok: false, error: `${name} min is above max` };
29
+ if (value < min || value > max)
30
+ return { ok: false, error: `${name} default is outside min..max` };
31
+ if (step !== undefined && step <= 0)
32
+ return { ok: false, error: `${name} step must be positive` };
33
+ return { ok: true, value: raw.slice() };
34
+ }
35
+ function checkOptions(raw, name) {
36
+ if (!Array.isArray(raw) || raw.length === 0 || raw.length > MAX_OPTIONS) {
37
+ return { ok: false, error: `${name} options must be a non-empty array` };
38
+ }
39
+ const out = [];
40
+ for (const o of raw) {
41
+ if (isStr(o))
42
+ out.push(o);
43
+ else if (o && typeof o === "object" && isStr(o.value) && isStr(o.label)) {
44
+ out.push({ value: o.value, label: o.label });
45
+ }
46
+ else
47
+ return { ok: false, error: `${name} options must be strings or {value, label}` };
48
+ }
49
+ return { ok: true, value: out };
50
+ }
51
+ const optionValues = (options) => (options ?? []).map((o) => (typeof o === "string" ? o : o.value));
52
+ // Expand tunekit's shorthands into an explicit control, the same way tunekit's
53
+ // config parser does (a string is a color when it looks like one, else text).
54
+ export function explicitKnob(config) {
55
+ if (Array.isArray(config)) {
56
+ const [value, min, max, step] = config;
57
+ return { type: "slider", value, min, max, ...(step === undefined ? {} : { step }) };
58
+ }
59
+ if (typeof config === "number") {
60
+ if (config >= 0 && config <= 1)
61
+ return { type: "slider", value: config, min: 0, max: 1 };
62
+ if (config >= 0)
63
+ return { type: "slider", value: config, min: 0, max: config * 3 || 10 };
64
+ return { type: "slider", value: config, min: config * 3, max: -config * 3 };
65
+ }
66
+ if (typeof config === "boolean")
67
+ return { type: "toggle", value: config };
68
+ if (typeof config === "string") {
69
+ return COLOR_HINT.test(config)
70
+ ? { type: "color", value: config }
71
+ : { type: "text", value: config };
72
+ }
73
+ return config;
74
+ }
75
+ // Validate one declared knob and return a clean copy (unknown keys dropped).
76
+ export function checkKnobConfig(path, raw) {
77
+ const name = `knob "${path}"`;
78
+ if (Array.isArray(raw))
79
+ return checkAxis(raw, name);
80
+ if (isNum(raw) || typeof raw === "boolean")
81
+ return { ok: true, value: raw };
82
+ if (typeof raw === "string") {
83
+ return raw.length <= MAX_STRING
84
+ ? { ok: true, value: raw }
85
+ : { ok: false, error: `${name} is too long` };
86
+ }
87
+ if (!raw || typeof raw !== "object")
88
+ return { ok: false, error: `${name} is not a knob` };
89
+ const k = raw;
90
+ switch (k.type) {
91
+ case "slider": {
92
+ const axis = checkAxis([k.value, k.min, k.max, ...(k.step === undefined ? [] : [k.step])], name);
93
+ if (!axis.ok)
94
+ return axis;
95
+ const [value, min, max, step] = axis.value;
96
+ return {
97
+ ok: true,
98
+ value: { type: "slider", value, min, max, ...(step === undefined ? {} : { step }) },
99
+ };
100
+ }
101
+ case "toggle":
102
+ return typeof k.value === "boolean"
103
+ ? { ok: true, value: { type: "toggle", value: k.value } }
104
+ : { ok: false, error: `${name} toggle needs a boolean value` };
105
+ case "select": {
106
+ const options = checkOptions(k.options, name);
107
+ if (!options.ok)
108
+ return options;
109
+ if (k.value !== undefined && !optionValues(options.value).includes(k.value)) {
110
+ return { ok: false, error: `${name} value is not one of its options` };
111
+ }
112
+ return {
113
+ ok: true,
114
+ value: {
115
+ type: "select",
116
+ options: options.value,
117
+ ...(k.value === undefined ? {} : { value: k.value }),
118
+ },
119
+ };
120
+ }
121
+ case "color": {
122
+ if (k.value !== undefined && !(typeof k.value === "string" && isCssColor(k.value))) {
123
+ return { ok: false, error: `${name} color value is not a CSS color` };
124
+ }
125
+ return {
126
+ ok: true,
127
+ value: {
128
+ type: "color",
129
+ ...(k.value === undefined ? {} : { value: k.value }),
130
+ ...(k.gradient === true ? { gradient: true } : {}),
131
+ ...(isStr(k.contrast) && isCssColor(k.contrast) ? { contrast: k.contrast } : {}),
132
+ },
133
+ };
134
+ }
135
+ case "text":
136
+ if (k.value !== undefined && !isStr(k.value, MAX_TEXT_VALUE)) {
137
+ return { ok: false, error: `${name} text value must be a string` };
138
+ }
139
+ return {
140
+ ok: true,
141
+ value: {
142
+ type: "text",
143
+ ...(k.value === undefined ? {} : { value: k.value }),
144
+ ...(isStr(k.placeholder) ? { placeholder: k.placeholder } : {}),
145
+ },
146
+ };
147
+ case "spring": {
148
+ const out = { type: "spring" };
149
+ for (const key of ["stiffness", "damping", "mass", "visualDuration", "bounce"]) {
150
+ if (k[key] === undefined)
151
+ continue;
152
+ if (!isNum(k[key]))
153
+ return { ok: false, error: `${name} ${key} must be a number` };
154
+ out[key] = k[key];
155
+ }
156
+ return { ok: true, value: out };
157
+ }
158
+ case "easing": {
159
+ if (!isNum(k.duration) || k.duration < 0) {
160
+ return { ok: false, error: `${name} easing needs a duration` };
161
+ }
162
+ if (!Array.isArray(k.ease) || k.ease.length !== 4 || !k.ease.every(isNum)) {
163
+ return { ok: false, error: `${name} ease must be four numbers` };
164
+ }
165
+ return {
166
+ ok: true,
167
+ value: {
168
+ type: "easing",
169
+ duration: k.duration,
170
+ ease: [...k.ease],
171
+ },
172
+ };
173
+ }
174
+ case "image": {
175
+ let options;
176
+ if (k.options !== undefined) {
177
+ const checked = checkOptions(k.options, name);
178
+ if (!checked.ok)
179
+ return checked;
180
+ options = checked.value;
181
+ }
182
+ if (k.value !== undefined && !isStr(k.value, MAX_TEXT_VALUE)) {
183
+ return { ok: false, error: `${name} image value must be a string` };
184
+ }
185
+ return {
186
+ ok: true,
187
+ value: {
188
+ type: "image",
189
+ ...(k.value === undefined ? {} : { value: k.value }),
190
+ ...(options ? { options } : {}),
191
+ },
192
+ };
193
+ }
194
+ case "pad": {
195
+ const out = { type: "pad" };
196
+ for (const axis of ["x", "y"]) {
197
+ if (k[axis] === undefined)
198
+ continue;
199
+ const checked = checkAxis(k[axis], `${name} ${axis}`);
200
+ if (!checked.ok)
201
+ return checked;
202
+ out[axis] = checked.value;
203
+ }
204
+ if (k.labels && typeof k.labels === "object") {
205
+ out.labels = {
206
+ ...(isStr(k.labels.x) ? { x: k.labels.x } : {}),
207
+ ...(isStr(k.labels.y) ? { y: k.labels.y } : {}),
208
+ };
209
+ }
210
+ return { ok: true, value: out };
211
+ }
212
+ default:
213
+ return { ok: false, error: `${name} has unknown type "${String(k.type)}"` };
214
+ }
215
+ }
216
+ export function checkKnobs(raw) {
217
+ if (raw === undefined || raw === null)
218
+ return { ok: true, value: {} };
219
+ if (typeof raw !== "object" || Array.isArray(raw)) {
220
+ return { ok: false, error: "knobs must be an object of path → knob" };
221
+ }
222
+ const entries = Object.entries(raw);
223
+ if (entries.length > MAX_KNOBS)
224
+ return { ok: false, error: `at most ${MAX_KNOBS} knobs` };
225
+ const out = {};
226
+ for (const [path, config] of entries) {
227
+ if (!PATH_RE.test(path)) {
228
+ return { ok: false, error: `knob path "${path}" must be "name" or "part.name"` };
229
+ }
230
+ const checked = checkKnobConfig(path, config);
231
+ if (!checked.ok)
232
+ return checked;
233
+ out[path] = checked.value;
234
+ }
235
+ return { ok: true, value: out };
236
+ }
237
+ const axisOf = (axis) => axis ? [axis[1], axis[2]] : [-1, 1];
238
+ // Whether `value` is something the declared knob could hold.
239
+ export function checkKnobValue(path, config, value) {
240
+ const knob = explicitKnob(config);
241
+ const bad = (what) => ({
242
+ ok: false,
243
+ error: `value for knob "${path}" ${what}`,
244
+ });
245
+ switch (knob.type) {
246
+ case "slider":
247
+ if (!isNum(value))
248
+ return bad("must be a number");
249
+ if (value < knob.min || value > knob.max)
250
+ return bad(`must be within ${knob.min}..${knob.max}`);
251
+ return { ok: true, value };
252
+ case "toggle":
253
+ return typeof value === "boolean" ? { ok: true, value } : bad("must be true or false");
254
+ case "select":
255
+ return typeof value === "string" && optionValues(knob.options).includes(value)
256
+ ? { ok: true, value }
257
+ : bad(`must be one of ${optionValues(knob.options).join(", ")}`);
258
+ case "color":
259
+ return typeof value === "string" && isCssColor(value)
260
+ ? { ok: true, value }
261
+ : bad("must be a CSS color");
262
+ case "text":
263
+ return isStr(value, MAX_TEXT_VALUE) ? { ok: true, value } : bad("must be a string");
264
+ case "image":
265
+ if (!isStr(value, MAX_TEXT_VALUE))
266
+ return bad("must be a string");
267
+ if (knob.options && !optionValues(knob.options).includes(value)) {
268
+ return bad(`must be one of ${optionValues(knob.options).join(", ")}`);
269
+ }
270
+ return { ok: true, value };
271
+ case "pad": {
272
+ if (!value || typeof value !== "object")
273
+ return bad("must be {x, y}");
274
+ const { x, y } = value;
275
+ const [xMin, xMax] = axisOf(knob.x);
276
+ const [yMin, yMax] = axisOf(knob.y);
277
+ if (!isNum(x) || !isNum(y) || x < xMin || x > xMax || y < yMin || y > yMax) {
278
+ return bad("must be {x, y} within the pad's range");
279
+ }
280
+ return { ok: true, value: { x, y } };
281
+ }
282
+ case "spring":
283
+ case "easing": {
284
+ // tunekit lets a transition knob switch between spring and easing modes.
285
+ const checked = checkKnobConfig(path, value);
286
+ if (!checked.ok)
287
+ return checked;
288
+ const t = checked.value;
289
+ return t && typeof t === "object" && (t.type === "spring" || t.type === "easing")
290
+ ? { ok: true, value: t }
291
+ : bad("must be a spring or easing");
292
+ }
293
+ }
294
+ }
295
+ // The declared knob for a path: the mock's own, else a variant's override.
296
+ export function knobFor(path, mock, posts) {
297
+ if (Object.hasOwn(mock.knobs, path))
298
+ return mock.knobs[path];
299
+ for (const p of posts)
300
+ if (p.knobs && Object.hasOwn(p.knobs, path))
301
+ return p.knobs[path];
302
+ return undefined;
303
+ }
304
+ export function checkKnobValues(raw, mock, posts, what) {
305
+ if (raw === undefined || raw === null)
306
+ return { ok: true, value: {} };
307
+ if (typeof raw !== "object" || Array.isArray(raw)) {
308
+ return { ok: false, error: `${what} must be an object of knob path → value` };
309
+ }
310
+ const out = {};
311
+ for (const [path, value] of Object.entries(raw)) {
312
+ const config = knobFor(path, mock, posts);
313
+ if (config === undefined)
314
+ return { ok: false, error: `${what}: no knob "${path}" is declared` };
315
+ const checked = checkKnobValue(path, config, value);
316
+ if (!checked.ok)
317
+ return checked;
318
+ out[path] = checked.value;
319
+ }
320
+ return { ok: true, value: out };
321
+ }
322
+ // How many discrete choices a knob offers, or null when it is continuous. A knob
323
+ // with only a handful is usually a decision dressed as a control (plan D12).
324
+ export function discreteChoices(config) {
325
+ const knob = explicitKnob(config);
326
+ if (knob.type === "toggle")
327
+ return 2;
328
+ if (knob.type === "select")
329
+ return knob.options.length;
330
+ if (knob.type === "image" && knob.options)
331
+ return knob.options.length;
332
+ return null;
333
+ }
@@ -0,0 +1,53 @@
1
+ import type { Hono } from "hono";
2
+ import type { FlowContext, FlowResult } from "./app.ts";
3
+ import { type GuideTopic } from "./designGuide.ts";
4
+ import type { Asset, AssetKind, Store } from "./types.ts";
5
+ type Flow<A extends unknown[]> = (...args: A) => Promise<FlowResult>;
6
+ export interface McpFlows {
7
+ publish: Flow<[body: any, ctx: FlowContext]>;
8
+ list: Flow<[query: {
9
+ project?: string;
10
+ }]>;
11
+ get: Flow<[ref: unknown, query: {
12
+ project?: unknown;
13
+ body?: boolean;
14
+ history?: boolean;
15
+ }]>;
16
+ ask: Flow<[ref: unknown, body: any, ctx: FlowContext]>;
17
+ exportMock: Flow<[
18
+ ref: unknown,
19
+ query: {
20
+ project?: unknown;
21
+ state?: unknown;
22
+ variant?: unknown;
23
+ },
24
+ ctx: FlowContext
25
+ ]>;
26
+ say: Flow<[ref: unknown, body: any]>;
27
+ feedback: Flow<[query: {
28
+ session?: unknown;
29
+ project?: unknown;
30
+ }]>;
31
+ }
32
+ export interface McpDeps {
33
+ store: Store;
34
+ basePath?: (request: Request) => string;
35
+ flows: McpFlows;
36
+ run: Flow<[body: any, ctx: FlowContext]>;
37
+ uploadAsset(input: {
38
+ data: Uint8Array;
39
+ contentType: string;
40
+ filename?: string;
41
+ kind?: AssetKind;
42
+ session?: string;
43
+ }): Promise<{
44
+ asset: Omit<Asset, "data">;
45
+ } | {
46
+ error: string;
47
+ status: number;
48
+ }>;
49
+ guide(project: string, topic?: GuideTopic): string | Promise<string>;
50
+ }
51
+ export declare function unwrap(result: FlowResult): unknown;
52
+ export declare function registerMcp(app: Hono, deps: McpDeps): void;
53
+ export {};
@@ -0,0 +1,143 @@
1
+ import { decodeBase64 } from "./base64.js";
2
+ import { isGuideTopic, unknownTopicMessage } from "./designGuide.js";
3
+ import { HTTP_MCP_TOOLS, HTTP_RUN_TOOLS, MCP_INSTRUCTIONS, MCP_SERVER_INFO, runToolResult, toolResult, } from "./mcpSpec.js";
4
+ import { RUN_INSTRUCTIONS } from "./runApi.js";
5
+ // A flow's error status becomes a tool error carrying the same JSON body the
6
+ // REST tier returns, so the agent sees the hint (states, variants) either way.
7
+ export function unwrap(result) {
8
+ if (result.status >= 400) {
9
+ const { error, ...extra } = result.body ?? {};
10
+ throw new Error(Object.keys(extra).length ? `${error} ${JSON.stringify(extra)}` : String(error));
11
+ }
12
+ return result.body;
13
+ }
14
+ export function registerMcp(app, deps) {
15
+ const { flows, store } = deps;
16
+ async function projectOf(args) {
17
+ if (typeof args.project === "string" && args.project)
18
+ return args.project;
19
+ if (typeof args.session === "string") {
20
+ const session = await store.getSession(args.session);
21
+ if (session?.project)
22
+ return session.project;
23
+ }
24
+ const projects = await store.listProjects();
25
+ return projects[0]?.name ?? "workspace";
26
+ }
27
+ async function callTool(name, args, ctx) {
28
+ switch (name) {
29
+ case "publish":
30
+ return unwrap(await flows.publish(args, ctx));
31
+ case "ask":
32
+ return unwrap(await flows.ask(args.mock, args, ctx));
33
+ case "read":
34
+ if (args.mock === undefined)
35
+ return unwrap(await flows.list({ project: args.project }));
36
+ return unwrap(await flows.get(args.mock, {
37
+ project: args.project,
38
+ body: args.body === true,
39
+ history: args.history === true,
40
+ }));
41
+ case "feedback":
42
+ return unwrap(await flows.feedback({ session: args.session, project: args.project }));
43
+ case "say":
44
+ return unwrap(await flows.say(args.mock, args));
45
+ case "export":
46
+ return unwrap(await flows.exportMock(args.mock, args, ctx));
47
+ case "upload": {
48
+ if (typeof args.data !== "string" || args.data.length === 0) {
49
+ throw new Error("upload needs base64 `data`");
50
+ }
51
+ const result = await deps.uploadAsset({
52
+ data: decodeBase64(args.data),
53
+ contentType: typeof args.contentType === "string" ? args.contentType : "",
54
+ filename: typeof args.filename === "string" ? args.filename : undefined,
55
+ kind: args.kind === "image" || args.kind === "file" ? args.kind : undefined,
56
+ session: typeof args.session === "string" ? args.session : undefined,
57
+ });
58
+ if ("error" in result)
59
+ throw new Error(result.error);
60
+ return {
61
+ id: result.asset.id,
62
+ sessionId: result.asset.sessionId,
63
+ url: `${ctx.base}/a/${result.asset.id}`,
64
+ contentType: result.asset.contentType,
65
+ byteLength: result.asset.byteLength,
66
+ kind: result.asset.kind,
67
+ };
68
+ }
69
+ case "guide": {
70
+ if (args.topic === undefined)
71
+ return await deps.guide(await projectOf(args));
72
+ if (!isGuideTopic(args.topic))
73
+ throw new Error(unknownTopicMessage(String(args.topic)));
74
+ return await deps.guide("", args.topic);
75
+ }
76
+ default:
77
+ throw new Error(`unknown tool: ${name}`);
78
+ }
79
+ }
80
+ app.post("/mcp", async (c) => {
81
+ const rpc = (id, result) => c.json({ jsonrpc: "2.0", id, result });
82
+ // `?mode=code` is the opt-in codemode catalog for clients without a
83
+ // harness of their own: one `run` tool instead of the mock tools.
84
+ const code = c.req.query("mode") === "code";
85
+ const rpcError = (id, code, message, status = 200) => c.json({ jsonrpc: "2.0", id, error: { code, message } }, status);
86
+ let msg;
87
+ try {
88
+ msg = await c.req.json();
89
+ }
90
+ catch {
91
+ return rpcError(null, -32700, "parse error", 400);
92
+ }
93
+ if (Array.isArray(msg)) {
94
+ return rpcError(null, -32600, "batch requests are not supported", 400);
95
+ }
96
+ if (msg.method === "initialize") {
97
+ return rpc(msg.id, {
98
+ protocolVersion: typeof msg.params?.protocolVersion === "string"
99
+ ? msg.params.protocolVersion
100
+ : "2025-03-26",
101
+ capabilities: { tools: { listChanged: false } },
102
+ serverInfo: MCP_SERVER_INFO,
103
+ instructions: code ? RUN_INSTRUCTIONS : MCP_INSTRUCTIONS,
104
+ });
105
+ }
106
+ if (msg.id === undefined)
107
+ return c.body(null, 202); // notifications
108
+ if (msg.method === "ping")
109
+ return rpc(msg.id, {});
110
+ if (msg.method === "tools/list") {
111
+ return rpc(msg.id, { tools: code ? HTTP_RUN_TOOLS : HTTP_MCP_TOOLS });
112
+ }
113
+ if (msg.method === "tools/call") {
114
+ const url = new URL(c.req.url);
115
+ const ctx = {
116
+ base: `${url.origin}${deps.basePath?.(c.req.raw) ?? ""}`,
117
+ // An MCP caller is an agent, never the viewer.
118
+ viewer: false,
119
+ strict: false,
120
+ request: c.req.raw,
121
+ signal: c.req.raw.signal,
122
+ };
123
+ try {
124
+ const name = msg.params?.name;
125
+ if (name === "run") {
126
+ const args = msg.params?.arguments ?? {};
127
+ return rpc(msg.id, runToolResult(unwrap(await deps.run(args, ctx))));
128
+ }
129
+ return rpc(msg.id, toolResult(name, await callTool(name, msg.params?.arguments ?? {}, ctx)));
130
+ }
131
+ catch (err) {
132
+ return rpc(msg.id, {
133
+ content: [{ type: "text", text: `error: ${err instanceof Error ? err.message : err}` }],
134
+ isError: true,
135
+ });
136
+ }
137
+ }
138
+ return rpcError(msg.id, -32601, `method not found: ${msg.method}`);
139
+ });
140
+ // Stateless server: no SSE stream to resume, no session to delete.
141
+ app.get("/mcp", (c) => c.text("mockpit MCP is stateless — POST JSON-RPC messages here", 405));
142
+ app.delete("/mcp", (c) => c.body(null, 405));
143
+ }