devframe 0.7.16 → 0.8.1

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 (80) hide show
  1. package/bin/devframe.mjs +8 -0
  2. package/dist/adapters/build.d.mts +1 -1
  3. package/dist/adapters/build.mjs +4 -4
  4. package/dist/adapters/cac.d.mts +1 -1
  5. package/dist/adapters/cac.mjs +1 -1
  6. package/dist/adapters/cli.d.mts +1 -1
  7. package/dist/adapters/cli.mjs +1 -1
  8. package/dist/adapters/dev.d.mts +2 -2
  9. package/dist/adapters/dev.mjs +1 -1
  10. package/dist/adapters/embedded.d.mts +1 -1
  11. package/dist/adapters/mcp.d.mts +51 -3
  12. package/dist/adapters/mcp.mjs +2 -2
  13. package/dist/agent-tool-name-C3b5vEwJ.mjs +34 -0
  14. package/dist/{cac-CPaOy3LD.mjs → cac-B1LM3zit.mjs} +18 -13
  15. package/dist/cli/main.d.mts +11 -0
  16. package/dist/cli/main.mjs +284 -0
  17. package/dist/client/index.d.mts +40 -11
  18. package/dist/client/index.mjs +129 -19
  19. package/dist/constants.d.mts +13 -6
  20. package/dist/constants.mjs +13 -6
  21. package/dist/{context-DY72brRI.mjs → context-CQefP2jR.mjs} +3 -2
  22. package/dist/{context-CzqPrJSz.d.mts → context-DFzmxCLa.d.mts} +1 -1
  23. package/dist/{revoke-BtQDKTp7.mjs → crypto-token-XCqTSMg9.mjs} +1 -35
  24. package/dist/{dev-BCeaQuGE.mjs → dev-BSHFZGZr.mjs} +22 -11
  25. package/dist/{devframe-Ckhf3kFw.d.mts → devframe-qgCKL683.d.mts} +77 -15
  26. package/dist/diagnostics-B5-qHeqD.mjs +94 -0
  27. package/dist/{diagnostics-BXwBQmoN.mjs → diagnostics-hwjXp_UV.mjs} +8 -0
  28. package/dist/{dump-Cz7yVvsB.mjs → dump-CgZShDRB.mjs} +82 -6
  29. package/dist/fetch-CK0S253E.mjs +534 -0
  30. package/dist/helpers/vite.d.mts +15 -14
  31. package/dist/helpers/vite.mjs +8 -8
  32. package/dist/{host-h3-CgOetlNE.mjs → host-h3-Kz7t5Xab.mjs} +83 -28
  33. package/dist/http-Dvgxujcu.mjs +38 -0
  34. package/dist/{index-8dndvIxR.d.mts → index-B7RgLw--.d.mts} +11 -6
  35. package/dist/{index-DgsLFhZg.d.mts → index-Dbw1p5ch.d.mts} +27 -3
  36. package/dist/{index-YDSJgsyG.d.mts → index-vAX6bYuc.d.mts} +1 -1
  37. package/dist/index.d.mts +5 -5
  38. package/dist/instance-registry-D6fxYu36.mjs +190 -0
  39. package/dist/node/auth.d.mts +2 -2
  40. package/dist/node/auth.mjs +2 -2
  41. package/dist/node/hub-internals.d.mts +2 -2
  42. package/dist/node/hub-internals.mjs +1 -1
  43. package/dist/node/index.d.mts +124 -6
  44. package/dist/node/index.mjs +5 -4
  45. package/dist/recipes/common-rpc-functions.d.mts +33 -33
  46. package/dist/recipes/common-rpc-functions.mjs +6 -6
  47. package/dist/recipes/interactive-auth.d.mts +2 -2
  48. package/dist/recipes/interactive-auth.mjs +23 -15
  49. package/dist/revoke-DZbJ7Cl0.mjs +35 -0
  50. package/dist/rpc/dump.d.mts +1 -1
  51. package/dist/rpc/dump.mjs +1 -1
  52. package/dist/rpc/index.d.mts +3 -3
  53. package/dist/rpc/index.mjs +4 -4
  54. package/dist/rpc/transports/ws-client.d.mts +1 -1
  55. package/dist/rpc/transports/ws-client.mjs +1 -1
  56. package/dist/rpc/transports/ws-server.d.mts +2 -2
  57. package/dist/rpc/transports/ws-server.mjs +50 -3
  58. package/dist/{serialization-DpLXCy13.mjs → serialization-C8Mnw9hK.mjs} +1 -1
  59. package/dist/{server-DykHCza_.mjs → server-CXaDDUl0.mjs} +2 -2
  60. package/dist/{server-B0wiP_Oi.d.mts → server-E9z4ZWbd.d.mts} +4 -3
  61. package/dist/simple-schema-BDzLeJDk.d.mts +102 -0
  62. package/dist/simple-schema-DQPZrAaZ.mjs +154 -0
  63. package/dist/{state-WX3HT5a3.mjs → state-BeUHDDjk.mjs} +13 -6
  64. package/dist/{storage-gEnj9ASQ.mjs → storage-Dzoc3NVC.mjs} +2 -66
  65. package/dist/{transports-BmDVn4SF.mjs → transports-vhizgqXM.mjs} +1 -1
  66. package/dist/types/index.d.mts +4 -4
  67. package/dist/{types-CrzNxXKq.d.mts → types-CnJSgRVa.d.mts} +33 -19
  68. package/dist/utils/agent-tool-name.d.mts +27 -0
  69. package/dist/utils/agent-tool-name.mjs +34 -0
  70. package/dist/utils/events.d.mts +1 -1
  71. package/dist/utils/shared-state.d.mts +1 -1
  72. package/dist/utils/simple-schema.d.mts +2 -0
  73. package/dist/utils/simple-schema.mjs +154 -0
  74. package/dist/utils/streaming-channel.d.mts +1 -1
  75. package/dist/{ws-client-DSXiI2h7.d.mts → ws-client-B6Tnr0ic.d.mts} +1 -1
  76. package/dist/{ws-server-9-wn7MNQ.d.mts → ws-server-J-zkiOyp.d.mts} +24 -3
  77. package/package.json +18 -7
  78. package/skills/devframe/SKILL.md +4 -3
  79. package/dist/build-server-C7vTKjiQ.mjs +0 -359
  80. package/dist/http-_pPbryTQ.mjs +0 -114
@@ -0,0 +1,534 @@
1
+ import { isAllowedOrigin } from "./rpc/transports/ws-server.mjs";
2
+ import { n as createHostContext } from "./host-h3-Kz7t5Xab.mjs";
3
+ import { t as diagnostics } from "./diagnostics-B5-qHeqD.mjs";
4
+ import { t as toAgentToolName } from "./agent-tool-name-C3b5vEwJ.mjs";
5
+ import { randomUUID } from "node:crypto";
6
+ import { Diagnostic } from "nostics";
7
+ import { join } from "pathe";
8
+ import process from "node:process";
9
+ import { homedir } from "node:os";
10
+ import { Server, WebStandardStreamableHTTPServerTransport, isInitializeRequest } from "@modelcontextprotocol/server";
11
+ //#region src/adapters/mcp/stringify.ts
12
+ /**
13
+ * JSON-coercing serializer for MCP text payloads.
14
+ *
15
+ * MCP carries tool results and resource reads as plain text over a
16
+ * JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
17
+ * format the WS RPC transport falls back to for non-JSON values. Instead,
18
+ * we coerce common non-JSON types into JSON-friendly forms so the LLM
19
+ * client sees something useful instead of `[object Object]`.
20
+ *
21
+ * Coercions:
22
+ * - `BigInt` → `"123n"`
23
+ * - `Date` → ISO string (via the native `toJSON`)
24
+ * - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
25
+ * - `Set` → `{ __type: 'Set', entries: [v, …] }`
26
+ * - `Error` → `{ name, message, stack, cause? }` (cause recurses)
27
+ * - `Function` → `"[Function: name]"`
28
+ * - `Symbol` → `value.toString()`
29
+ * - cycles → `"[Circular]"`
30
+ */
31
+ function stringifyForMcp(value) {
32
+ if (value === void 0) return "undefined";
33
+ if (typeof value === "string") return value;
34
+ const seen = /* @__PURE__ */ new WeakSet();
35
+ return JSON.stringify(value, (_key, val) => {
36
+ if (typeof val === "bigint") return `${val}n`;
37
+ if (val instanceof Error) {
38
+ const out = {
39
+ name: val.name,
40
+ message: val.message,
41
+ stack: val.stack
42
+ };
43
+ if (val.cause !== void 0) out.cause = val.cause;
44
+ return out;
45
+ }
46
+ if (val instanceof Map) return {
47
+ __type: "Map",
48
+ entries: [...val.entries()]
49
+ };
50
+ if (val instanceof Set) return {
51
+ __type: "Set",
52
+ entries: [...val]
53
+ };
54
+ if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
55
+ if (typeof val === "symbol") return val.toString();
56
+ if (val !== null && typeof val === "object") {
57
+ if (seen.has(val)) return "[Circular]";
58
+ seen.add(val);
59
+ }
60
+ return val;
61
+ }, 2);
62
+ }
63
+ /**
64
+ * Format a thrown value for an MCP `isError` text payload.
65
+ *
66
+ * A nostics `Diagnostic` (every coded devframe error) becomes structured
67
+ * JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
68
+ * the actionable next step (`fix`) and the docs URL instead of a bare
69
+ * message string. Other errors surface `Error.name`/`message`, plus one
70
+ * level of `cause.message` so context isn't dropped silently.
71
+ */
72
+ function formatMcpError(error) {
73
+ if (error instanceof Diagnostic) return JSON.stringify({ error: {
74
+ code: error.code,
75
+ message: error.message,
76
+ ...error.fix ? { fix: error.fix } : {},
77
+ ...error.docs ? { docs: error.docs } : {}
78
+ } }, null, 2);
79
+ if (!(error instanceof Error)) return String(error);
80
+ const cause = error.cause;
81
+ const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
82
+ return `${error.name}: ${error.message}${causeText}`;
83
+ }
84
+ //#endregion
85
+ //#region src/adapters/mcp/to-json-schema.ts
86
+ const FALLBACK_OBJECT_SCHEMA = Object.freeze({
87
+ type: "object",
88
+ additionalProperties: true
89
+ });
90
+ /**
91
+ * Convert a Standard Schema to JSON Schema for the agent/MCP surface.
92
+ *
93
+ * Devframe stays validator-neutral, so conversion uses the schema's own
94
+ * [Standard JSON Schema](https://standardschema.dev/) converter
95
+ * (`~standard.jsonSchema`) when the validator provides one — zod 4 does,
96
+ * for example. Validators without a native converter (e.g. valibot) degrade
97
+ * to a permissive object schema rather than pulling in a converter library.
98
+ */
99
+ function safeToJsonSchema(schema) {
100
+ const standard = schema["~standard"];
101
+ if (standard.jsonSchema) try {
102
+ return standard.jsonSchema.input({ target: "draft-2020-12" });
103
+ } catch {
104
+ return FALLBACK_OBJECT_SCHEMA;
105
+ }
106
+ return FALLBACK_OBJECT_SCHEMA;
107
+ }
108
+ /**
109
+ * JSON Schema for an RPC return value on the agent/MCP surface.
110
+ * @internal
111
+ */
112
+ function returnToJsonSchema(schema) {
113
+ if (!schema) return void 0;
114
+ return safeToJsonSchema(schema);
115
+ }
116
+ /**
117
+ * JSON Schema for an RPC function's positional args on the agent/MCP
118
+ * surface. Each positional arg is advertised under `arg0` / `arg1` / … —
119
+ * matching how the agent bridge coerces the incoming object payload back
120
+ * into positional arguments.
121
+ *
122
+ * Returns `{ type: 'object', properties: {} }` when there are no args.
123
+ * @internal
124
+ */
125
+ function argsToJsonSchema(args) {
126
+ if (!args || args.length === 0) return {
127
+ schema: {
128
+ type: "object",
129
+ properties: {}
130
+ },
131
+ unwrapped: false
132
+ };
133
+ const properties = {};
134
+ const required = [];
135
+ for (let i = 0; i < args.length; i++) {
136
+ const key = `arg${i}`;
137
+ properties[key] = safeToJsonSchema(args[i]);
138
+ required.push(key);
139
+ }
140
+ return {
141
+ schema: {
142
+ type: "object",
143
+ properties,
144
+ required,
145
+ additionalProperties: false
146
+ },
147
+ unwrapped: false
148
+ };
149
+ }
150
+ //#endregion
151
+ //#region src/adapters/mcp/build-server.ts
152
+ /**
153
+ * Wire an MCP {@link Server} to a devframe context. Returns the server
154
+ * plus a disposal function for the subscriptions it sets up. The
155
+ * transport is the caller's responsibility — `createMcpServer` connects
156
+ * stdio; tests can connect an {@link InMemoryTransport} instead.
157
+ *
158
+ * @internal
159
+ */
160
+ function buildMcpServerFromContext(ctx, options) {
161
+ const server = new Server({
162
+ name: options.serverName,
163
+ version: options.serverVersion
164
+ }, { capabilities: {
165
+ tools: { listChanged: true },
166
+ resources: { listChanged: true }
167
+ } });
168
+ registerToolHandlers(server, ctx, options.exposeSharedState);
169
+ registerResourceHandlers(server, ctx, options.exposeSharedState);
170
+ const notify = (method) => {
171
+ server.notification({ method }).catch(() => {});
172
+ };
173
+ const offManifest = ctx.agent.events.on("agent:manifest:changed", () => {
174
+ notify("notifications/tools/list_changed");
175
+ notify("notifications/resources/list_changed");
176
+ });
177
+ const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
178
+ notify("notifications/resources/list_changed");
179
+ });
180
+ return {
181
+ server,
182
+ dispose: () => {
183
+ offManifest();
184
+ offKeyAdded();
185
+ }
186
+ };
187
+ }
188
+ /**
189
+ * Build an MCP server over the agent surface of a devframe definition.
190
+ * Currently supports `stdio` transport only.
191
+ *
192
+ * @experimental The agent-native surface is experimental and may change
193
+ * without a major version bump until it stabilizes.
194
+ */
195
+ async function createMcpServer(definition, options = {}) {
196
+ const transport = options.transport ?? "stdio";
197
+ if (transport !== "stdio") throw diagnostics.DF0017({
198
+ transport,
199
+ reason: "Only stdio transport is supported in this release."
200
+ });
201
+ const ctx = await createHostContext({
202
+ cwd: process.cwd(),
203
+ mode: "dev",
204
+ host: {
205
+ mountStatic: () => {},
206
+ resolveOrigin: () => "mcp://devframe",
207
+ getStorageDir: (scope) => {
208
+ if (scope === "workspace") return join(process.cwd(), ".devframe");
209
+ if (scope === "project") return join(process.cwd(), `node_modules/.${definition.id}/devframe`);
210
+ return join(homedir(), `.${definition.id}/devframe`);
211
+ }
212
+ }
213
+ });
214
+ await definition.setup(ctx);
215
+ const { server, dispose } = buildMcpServerFromContext(ctx, {
216
+ serverName: options.serverName ?? `${definition.id} (devframe)`,
217
+ serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
218
+ exposeSharedState: options.exposeSharedState ?? true
219
+ });
220
+ const { startStdioTransport } = await import("./transports-vhizgqXM.mjs");
221
+ let stop;
222
+ try {
223
+ stop = await startStdioTransport(server);
224
+ } catch (error) {
225
+ const reason = error instanceof Error ? error.message : String(error);
226
+ throw diagnostics.DF0017({
227
+ transport,
228
+ reason,
229
+ cause: error
230
+ });
231
+ }
232
+ options.onReady?.({ transport: "stdio" });
233
+ return { async stop() {
234
+ dispose();
235
+ await stop();
236
+ } };
237
+ }
238
+ /**
239
+ * Id of the built-in shared-state read tool — namespaced like every other
240
+ * built-in (`devframe:<area>:<fn>`). Tool-shaped access matters because many
241
+ * MCP clients only consume tools — the parallel `devframe://state/<key>`
242
+ * resource projection stays for the clients that do read resources.
243
+ */
244
+ const READ_STATE_TOOL = "devframe:state:read";
245
+ /** Wire name of the built-in shared-state read tool: `devframe_state_read`. */
246
+ const READ_STATE_NAME = toAgentToolName(READ_STATE_TOOL);
247
+ function sharedStateFilter(exposeSharedState) {
248
+ if (exposeSharedState === false) return void 0;
249
+ return typeof exposeSharedState === "function" ? exposeSharedState : () => true;
250
+ }
251
+ function readStateToolProjection() {
252
+ return {
253
+ name: READ_STATE_NAME,
254
+ title: "Read shared state",
255
+ description: "Read this devtool's live shared state. Call without arguments to list the available keys, then with a key to get that value as JSON. Safe to call freely.",
256
+ inputSchema: {
257
+ type: "object",
258
+ properties: { key: {
259
+ type: "string",
260
+ description: "A shared-state key from the key list. Omit to list all keys."
261
+ } }
262
+ },
263
+ annotations: {
264
+ title: "Read shared state",
265
+ readOnlyHint: true,
266
+ destructiveHint: false
267
+ }
268
+ };
269
+ }
270
+ async function readStateResult(ctx, filter, key) {
271
+ const keys = ctx.rpc.sharedState.keys().filter(filter);
272
+ if (key === void 0) return { keys };
273
+ if (!keys.includes(key)) throw diagnostics.DF0048({ key });
274
+ return {
275
+ key,
276
+ value: (await ctx.rpc.sharedState.get(key)).value()
277
+ };
278
+ }
279
+ function registerToolHandlers(server, ctx, exposeSharedState) {
280
+ const stateFilter = sharedStateFilter(exposeSharedState);
281
+ const warnedCollisions = /* @__PURE__ */ new Set();
282
+ /**
283
+ * Resolve a wire tool name back to the registered {@link AgentTool}.
284
+ * Wire-name matching runs first, in manifest order — the same tool the
285
+ * list projection advertises under that name — with a raw-id fallback so
286
+ * a colon-namespaced id keeps working as a call name.
287
+ */
288
+ const resolveTool = (name) => {
289
+ return ctx.agent.list().tools.find((tool) => toAgentToolName(tool.id) === name) ?? ctx.agent.getTool(name);
290
+ };
291
+ server.setRequestHandler("tools/list", async () => {
292
+ const byName = /* @__PURE__ */ new Map();
293
+ for (const tool of ctx.agent.list().tools) {
294
+ const name = toAgentToolName(tool.id);
295
+ const existing = byName.get(name);
296
+ if (existing) {
297
+ if (!warnedCollisions.has(`${name}|${tool.id}`)) {
298
+ warnedCollisions.add(`${name}|${tool.id}`);
299
+ diagnostics.DF0047({
300
+ name,
301
+ id: tool.id,
302
+ existing: existing.id
303
+ });
304
+ }
305
+ continue;
306
+ }
307
+ byName.set(name, tool);
308
+ }
309
+ const tools = [...byName.entries()].map(([name, tool]) => projectTool(name, tool, ctx));
310
+ if (stateFilter && !byName.has(READ_STATE_NAME)) tools.push(readStateToolProjection());
311
+ return { tools };
312
+ });
313
+ server.setRequestHandler("tools/call", async (request) => {
314
+ const { name, arguments: args } = request.params;
315
+ try {
316
+ const tool = resolveTool(name);
317
+ if (stateFilter && !tool && (name === READ_STATE_NAME || name === READ_STATE_TOOL)) {
318
+ const key = args?.key;
319
+ const result = await readStateResult(ctx, stateFilter, key);
320
+ return {
321
+ content: [{
322
+ type: "text",
323
+ text: stringifyForMcp(result)
324
+ }],
325
+ structuredContent: result
326
+ };
327
+ }
328
+ const outputSchema = tool ? usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx)) : void 0;
329
+ const result = await ctx.agent.invoke(tool?.id ?? name, args ?? {});
330
+ return {
331
+ content: [{
332
+ type: "text",
333
+ text: stringifyForMcp(result)
334
+ }],
335
+ ...outputSchema ? { structuredContent: result } : {}
336
+ };
337
+ } catch (error) {
338
+ return {
339
+ isError: true,
340
+ content: [{
341
+ type: "text",
342
+ text: `Error invoking "${name}": ${formatMcpError(error)}`
343
+ }]
344
+ };
345
+ }
346
+ });
347
+ }
348
+ function registerResourceHandlers(server, ctx, exposeSharedState) {
349
+ server.setRequestHandler("resources/list", async () => {
350
+ const resources = ctx.agent.list().resources.map((resource) => ({
351
+ uri: resource.uri,
352
+ name: resource.name,
353
+ description: resource.description,
354
+ mimeType: resource.mimeType
355
+ }));
356
+ if (exposeSharedState !== false) {
357
+ const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
358
+ for (const key of ctx.rpc.sharedState.keys()) {
359
+ if (!filter(key)) continue;
360
+ resources.push({
361
+ uri: `devframe://state/${encodeURIComponent(key)}`,
362
+ name: key,
363
+ description: `Shared state: ${key}`,
364
+ mimeType: "application/json"
365
+ });
366
+ }
367
+ }
368
+ return { resources };
369
+ });
370
+ server.setRequestHandler("resources/read", async (request) => {
371
+ const { uri } = request.params;
372
+ const parsed = parseResourceUri(uri);
373
+ if (parsed.kind === "resource") {
374
+ const content = await ctx.agent.read(parsed.id);
375
+ return { contents: [{
376
+ uri,
377
+ mimeType: content.mimeType ?? "application/json",
378
+ text: content.text ?? stringifyForMcp(content.json)
379
+ }] };
380
+ }
381
+ if (parsed.kind === "state") return { contents: [{
382
+ uri,
383
+ mimeType: "application/json",
384
+ text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
385
+ }] };
386
+ throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
387
+ });
388
+ }
389
+ /**
390
+ * MCP constrains a tool's `outputSchema` to a JSON Schema of `type:
391
+ * "object"` — clients (the SDK included) reject anything else. Non-object
392
+ * return schemas (e.g. a schema for `void` / a bare string) simply project
393
+ * no output schema; the text content still carries the result.
394
+ */
395
+ function usableOutputSchema(schema) {
396
+ return schema && typeof schema === "object" && schema.type === "object" ? schema : void 0;
397
+ }
398
+ function projectTool(name, tool, ctx) {
399
+ const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
400
+ const outputSchema = usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx));
401
+ return {
402
+ name,
403
+ title: tool.title,
404
+ description: tool.description,
405
+ inputSchema,
406
+ ...outputSchema ? { outputSchema } : {},
407
+ annotations: {
408
+ title: tool.title,
409
+ readOnlyHint: tool.safety === "read",
410
+ destructiveHint: tool.safety === "destructive"
411
+ }
412
+ };
413
+ }
414
+ function computeInputSchema(tool, ctx) {
415
+ if (tool.kind === "tool") return argsToJsonSchema(tool.args).schema;
416
+ if (tool.kind !== "rpc" || !tool.rpcName) return {
417
+ type: "object",
418
+ properties: {}
419
+ };
420
+ const def = ctx.rpc.definitions.get(tool.rpcName);
421
+ if (!def) return {
422
+ type: "object",
423
+ properties: {}
424
+ };
425
+ const args = def.args;
426
+ return argsToJsonSchema(args).schema;
427
+ }
428
+ function computeOutputSchema(tool, ctx) {
429
+ if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
430
+ const def = ctx.rpc.definitions.get(tool.rpcName);
431
+ if (!def) return void 0;
432
+ return returnToJsonSchema(def.returns);
433
+ }
434
+ function parseResourceUri(uri) {
435
+ const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
436
+ if (!match) return { kind: "unknown" };
437
+ const [, kind, rest] = match;
438
+ const decoded = decodeURIComponent(rest);
439
+ if (kind === "resource") return {
440
+ kind: "resource",
441
+ id: decoded
442
+ };
443
+ return {
444
+ kind: "state",
445
+ key: decoded
446
+ };
447
+ }
448
+ //#endregion
449
+ //#region src/adapters/mcp/fetch.ts
450
+ /**
451
+ * Build a framework-agnostic MCP Streamable-HTTP endpoint over a devframe
452
+ * context: a web-standard `Request → Response` handler any host can mount —
453
+ * h3 (see `mountMcpHttp`), a Next.js App Router route, or any other
454
+ * fetch-shaped server.
455
+ *
456
+ * Each MCP session gets its own {@link WebStandardStreamableHTTPServerTransport}
457
+ * and MCP server (built from the shared, live `ctx` via
458
+ * `buildMcpServerFromContext`), correlated by the `Mcp-Session-Id` header: an
459
+ * `initialize` POST spins up a session; later requests route to it; a `DELETE`
460
+ * (or client disconnect) tears it down. The origin gate guards every request:
461
+ * loopback-default DNS-rebinding protection that — unlike the WS upgrade's
462
+ * `isAllowedOrigin` — also rejects `Origin`-less requests, so a route-based
463
+ * endpoint isn't reachable by an arbitrary local process.
464
+ *
465
+ * @experimental
466
+ */
467
+ function createMcpFetchHandler(ctx, options) {
468
+ const sessions = /* @__PURE__ */ new Map();
469
+ const allowedOrigins = options.allowedOrigins;
470
+ function drop(sessionId) {
471
+ const session = sessions.get(sessionId);
472
+ if (!session) return;
473
+ sessions.delete(sessionId);
474
+ session.dispose();
475
+ }
476
+ async function createSession() {
477
+ let session;
478
+ const transport = new WebStandardStreamableHTTPServerTransport({
479
+ sessionIdGenerator: () => randomUUID(),
480
+ onsessioninitialized: (id) => {
481
+ sessions.set(id, session);
482
+ },
483
+ onsessionclosed: (id) => {
484
+ drop(id);
485
+ }
486
+ });
487
+ const { server, dispose } = buildMcpServerFromContext(ctx, {
488
+ serverName: options.serverName,
489
+ serverVersion: options.serverVersion,
490
+ exposeSharedState: options.exposeSharedState
491
+ });
492
+ session = {
493
+ transport,
494
+ dispose: async () => {
495
+ dispose();
496
+ await server.close();
497
+ }
498
+ };
499
+ transport.onclose = () => {
500
+ if (transport.sessionId) drop(transport.sessionId);
501
+ };
502
+ await server.connect(transport);
503
+ return session;
504
+ }
505
+ async function handle(req) {
506
+ const origin = req.headers.get("origin") ?? void 0;
507
+ if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden: origin required", { status: 403 });
508
+ const sessionId = req.headers.get("mcp-session-id") ?? void 0;
509
+ let session = sessionId ? sessions.get(sessionId) : void 0;
510
+ if (!session && req.method === "POST") {
511
+ let body;
512
+ try {
513
+ body = await req.json();
514
+ } catch {
515
+ body = void 0;
516
+ }
517
+ if (!sessionId && isInitializeRequest(body)) session = await createSession();
518
+ else return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: no valid session ID and not an initialize request", { status: sessionId ? 404 : 400 });
519
+ return session.transport.handleRequest(req, { parsedBody: body });
520
+ }
521
+ if (!session) return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: missing MCP session ID", { status: sessionId ? 404 : 400 });
522
+ return session.transport.handleRequest(req);
523
+ }
524
+ return {
525
+ fetch: handle,
526
+ dispose: async () => {
527
+ const live = [...sessions.values()];
528
+ sessions.clear();
529
+ await Promise.all(live.map((session) => session.dispose()));
530
+ }
531
+ };
532
+ }
533
+ //#endregion
534
+ export { createMcpServer as n, createMcpFetchHandler as t };
@@ -1,4 +1,4 @@
1
- import { d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition } from "../devframe-Ckhf3kFw.mjs";
1
+ import { d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition } from "../devframe-qgCKL683.mjs";
2
2
  //#region src/helpers/vite.d.ts
3
3
  interface ViteDevBridgeOptions {
4
4
  /**
@@ -31,16 +31,17 @@ interface ViteDevBridgeOptions {
31
31
  flags?: Record<string, unknown>;
32
32
  };
33
33
  /**
34
- * Whether the bridged devframe runs its own auth gate. This is a **hosted**
35
- * adapter — the devframe shares the host app's origin and the host owns
36
- * authentication — so it defaults to `false`: the plugin's own gate never
37
- * fires and its `cli.auth` default is ignored (matching devframe's
38
- * hosted-deployment contract). Pass `true` to force devframe's interactive
39
- * OTP gate on, or a {@link DevframeAuthHandler} to install a custom scheme.
40
- * Only applies in bridge mode (`devMiddleware`); the static-mount mode
34
+ * Whether the bridged devframe runs its own auth gate. The side-car RPC
35
+ * server is reachable by anything that can open its socket, so it **gates by
36
+ * default**: when unset, authentication resolves through `createDevServer`
37
+ * (devframe's interactive OTP gate unless the definition's `cli.auth` opts
38
+ * out), and the side-car prints its code/link banner to stdout. Pass a
39
+ * {@link DevframeAuthHandler} to install a custom scheme, or `false` to opt
40
+ * out for a single-user localhost host that owns the trust boundary another
41
+ * way. Only applies in bridge mode (`devMiddleware`); the static-mount mode
41
42
  * starts no RPC server.
42
43
  *
43
- * @default false
44
+ * @default gated (devframe's interactive OTP, unless `cli.auth` opts out)
44
45
  */
45
46
  auth?: boolean | DevframeAuthHandler;
46
47
  /**
@@ -84,11 +85,11 @@ interface DevframeVitePlugin {
84
85
  * host-served SPA can discover the WS endpoint via
85
86
  * {@link connectDevframe}.
86
87
  *
87
- * As a hosted adapter the bridge defers authentication to the host: its
88
- * side-car RPC server runs with the plugin's own auth gate **off** by
89
- * default (ignoring `def.cli?.auth`), so a plugin mounted this way never
90
- * triggers its standalone OTP prompt. Opt back in per-mount with
91
- * `options.auth` (`true` for devframe's interactive gate, or a handler).
88
+ * The side-car RPC server **gates by default** (devframe's interactive OTP
89
+ * unless the definition's `cli.auth` opts out), printing its code/link banner
90
+ * to stdout, so a bridged devframe isn't silently reachable by anything that
91
+ * can open its socket. Pass `options.auth: false` to opt out for a single-user
92
+ * localhost host, or a {@link DevframeAuthHandler} for a custom scheme.
92
93
  *
93
94
  * Use bridge mode when integrating with frameworks that own the SPA
94
95
  * (Nuxt, Astro, SolidStart, plain Vite apps). For the all-in-one
@@ -1,8 +1,8 @@
1
1
  import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_WS_ROUTE } from "../constants.mjs";
2
- import { a as diagnostics } from "../storage-gEnj9ASQ.mjs";
2
+ import { t as diagnostics } from "../diagnostics-B5-qHeqD.mjs";
3
3
  import { n as resolveBasePath, t as normalizeBasePath } from "../_shared-bWRzeSa0.mjs";
4
4
  import { serveStaticNodeMiddleware } from "../utils/serve-static.mjs";
5
- import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-BCeaQuGE.mjs";
5
+ import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-BSHFZGZr.mjs";
6
6
  import { resolve } from "pathe";
7
7
  //#region src/helpers/vite.ts
8
8
  /**
@@ -19,11 +19,11 @@ import { resolve } from "pathe";
19
19
  * host-served SPA can discover the WS endpoint via
20
20
  * {@link connectDevframe}.
21
21
  *
22
- * As a hosted adapter the bridge defers authentication to the host: its
23
- * side-car RPC server runs with the plugin's own auth gate **off** by
24
- * default (ignoring `def.cli?.auth`), so a plugin mounted this way never
25
- * triggers its standalone OTP prompt. Opt back in per-mount with
26
- * `options.auth` (`true` for devframe's interactive gate, or a handler).
22
+ * The side-car RPC server **gates by default** (devframe's interactive OTP
23
+ * unless the definition's `cli.auth` opts out), printing its code/link banner
24
+ * to stdout, so a bridged devframe isn't silently reachable by anything that
25
+ * can open its socket. Pass `options.auth: false` to opt out for a single-user
26
+ * localhost host, or a {@link DevframeAuthHandler} for a custom scheme.
27
27
  *
28
28
  * Use bridge mode when integrating with frameworks that own the SPA
29
29
  * (Nuxt, Astro, SolidStart, plain Vite apps). For the all-in-one
@@ -58,7 +58,7 @@ function viteDevBridge(d, options = {}) {
58
58
  port,
59
59
  flags: mw.flags,
60
60
  openBrowser: false,
61
- auth: options.auth ?? false,
61
+ auth: options.auth,
62
62
  mcp: options.mcp
63
63
  });
64
64
  } catch (e) {