devframe 0.9.4 → 0.9.6

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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/{_shared-BPovvJW8.d.mts → _shared-BJ6AmwTu.d.mts} +1 -1
  3. package/dist/adapters/build.d.mts +5 -4
  4. package/dist/adapters/build.mjs +4 -3
  5. package/dist/adapters/cac.d.mts +1 -1
  6. package/dist/adapters/cac.mjs +3 -2
  7. package/dist/adapters/dev.d.mts +12 -11
  8. package/dist/adapters/dev.mjs +1 -1
  9. package/dist/adapters/embedded.d.mts +1 -1
  10. package/dist/adapters/initiate.d.mts +9 -8
  11. package/dist/adapters/initiate.mjs +1 -1
  12. package/dist/adapters/mcp.d.mts +30 -26
  13. package/dist/adapters/mcp.mjs +559 -1
  14. package/dist/cli/main.mjs +1 -1
  15. package/dist/client/index.d.mts +5 -5
  16. package/dist/client/index.mjs +1 -64
  17. package/dist/constants.d.mts +13 -1
  18. package/dist/constants.mjs +1 -1
  19. package/dist/{context-_i51nYOs.d.mts → context-BhBzOXVa.d.mts} +23 -11
  20. package/dist/{context-CO7eZHJy.d.mts → context-HOnG-WvG.d.mts} +1 -1
  21. package/dist/{context-BUJGvlje.mjs → context-KUqAfawQ.mjs} +1 -1
  22. package/dist/{context-CQW0lSw8.mjs → context-omlma_1n.mjs} +3 -150
  23. package/dist/{dev-lpIvwagf.mjs → dev-Czit5IP4.mjs} +6 -5
  24. package/dist/{events-DKfSmoTj.mjs → events-4daw0uK9.mjs} +16 -2
  25. package/dist/in-page-channel/index.d.mts +306 -0
  26. package/dist/in-page-channel/index.mjs +816 -0
  27. package/dist/{index-DOg4S5wk.d.mts → index-BzyopqxV.d.mts} +1 -1
  28. package/dist/{index-CEvd-YXD.d.mts → index-DcKxIMoI.d.mts} +2 -2
  29. package/dist/index.d.mts +14 -6
  30. package/dist/index.mjs +11 -1
  31. package/dist/{instance-shell-BHvBIGP4.d.mts → instance-shell-Bh4l9Vz_.d.mts} +3 -3
  32. package/dist/{instance-shell-B-uevnK-.mjs → instance-shell-CpVW4zG0.mjs} +15 -2
  33. package/dist/internal/index.d.mts +17 -7
  34. package/dist/internal/index.mjs +3 -3
  35. package/dist/node/auth.d.mts +2 -2
  36. package/dist/node/auth.mjs +1 -1
  37. package/dist/node/hub-internals.d.mts +2 -2
  38. package/dist/node/hub-internals.mjs +1 -1
  39. package/dist/node/index.d.mts +3 -3
  40. package/dist/node/index.mjs +1 -1
  41. package/dist/recipes/common-rpc-functions.d.mts +3 -3
  42. package/dist/recipes/interactive-auth.d.mts +13 -2
  43. package/dist/recipes/interactive-auth.mjs +5 -1
  44. package/dist/{remote-assets-Bg4gCUZ_.d.mts → remote-assets-B13_vRkJ.d.mts} +1 -1
  45. package/dist/{revoke-3Q6aDKWk.mjs → revoke-COLW0cOi.mjs} +1 -1
  46. package/dist/rpc/dump.d.mts +1 -1
  47. package/dist/rpc/index.d.mts +2 -2
  48. package/dist/rpc/transports/sse-client.d.mts +1 -1
  49. package/dist/rpc/transports/sse-server.d.mts +2 -2
  50. package/dist/rpc/transports/ws-bun.d.mts +2 -2
  51. package/dist/rpc/transports/ws-client.d.mts +1 -1
  52. package/dist/rpc/transports/ws-deno.d.mts +2 -2
  53. package/dist/rpc/transports/ws-server.d.mts +1 -1
  54. package/dist/{rpc-core-CDZoGYiD.d.mts → rpc-core-BHUsDl7s.d.mts} +3 -3
  55. package/dist/{host-agent-B84av916.mjs → services-install-CqqQVls5.mjs} +150 -2
  56. package/dist/{sse-client-CdGD_B8_.d.mts → sse-client-DwX021ST.d.mts} +1 -1
  57. package/dist/types/index.d.mts +4 -4
  58. package/dist/{types-BBLeGfpt.d.mts → types-CMdPYhOx.d.mts} +1 -1
  59. package/dist/utils/events.d.mts +1 -1
  60. package/dist/utils/remote-assets.d.mts +1 -1
  61. package/dist/utils/serve-static.d.mts +1 -1
  62. package/dist/utils/serve-static.mjs +1 -1
  63. package/dist/utils/shared-state.d.mts +1 -1
  64. package/dist/utils/streaming-channel.d.mts +1 -1
  65. package/dist/{ws-client-Bo2t6hES.d.mts → ws-client-7_lZi-B9.d.mts} +1 -1
  66. package/dist/{ws-server-DLtEoLdR.d.mts → ws-server-Di2oqr4q.d.mts} +1 -1
  67. package/package.json +2 -1
  68. package/skills/devframe/SKILL.md +88 -84
  69. package/skills/devframe/templates/hub.ts +1 -1
  70. package/dist/http-BKiqHcXX.mjs +0 -569
  71. package/dist/transports-vhizgqXM.mjs +0 -15
@@ -1,2 +1,560 @@
1
- import { i as createMcpServer, n as mountMcpHttp, r as createMcpFetchHandler } from "../http-BKiqHcXX.mjs";
1
+ import { t as Diagnostic } from "../nostics-CzECRXpE.mjs";
2
+ import { t as DEVFRAME_EVENTS } from "../events-4daw0uK9.mjs";
3
+ import { i as isAllowedOrigin } from "../ws-server-BdSLrhxE.mjs";
4
+ import { t as diagnostics } from "../diagnostics-BXWW3VK-.mjs";
5
+ import { t as createHostContext } from "../context-omlma_1n.mjs";
6
+ import { t as toAgentToolName } from "../agent-tool-name-EgfoFO8C.mjs";
7
+ import process from "node:process";
8
+ import { join } from "pathe";
9
+ import { homedir } from "node:os";
10
+ import { defineHandler } from "h3";
11
+ import { Server, createMcpHandler } from "@modelcontextprotocol/server";
12
+ //#region src/adapters/mcp/stringify.ts
13
+ /**
14
+ * JSON-coercing serializer for MCP text payloads.
15
+ *
16
+ * MCP carries tool results and resource reads as plain text over a
17
+ * JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
18
+ * format the WS RPC transport falls back to for non-JSON values. Instead,
19
+ * we coerce common non-JSON types into JSON-friendly forms so the LLM
20
+ * client sees something useful instead of `[object Object]`.
21
+ *
22
+ * Coercions:
23
+ * - `BigInt` → `"123n"`
24
+ * - `Date` → ISO string (via the native `toJSON`)
25
+ * - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
26
+ * - `Set` → `{ __type: 'Set', entries: [v, …] }`
27
+ * - `Error` → `{ name, message, stack, cause? }` (cause recurses)
28
+ * - `Function` → `"[Function: name]"`
29
+ * - `Symbol` → `value.toString()`
30
+ * - cycles → `"[Circular]"`
31
+ */
32
+ function stringifyForMcp(value) {
33
+ if (value === void 0) return "undefined";
34
+ if (typeof value === "string") return value;
35
+ const seen = /* @__PURE__ */ new WeakSet();
36
+ return JSON.stringify(value, (_key, val) => {
37
+ if (typeof val === "bigint") return `${val}n`;
38
+ if (val instanceof Error) {
39
+ const out = {
40
+ name: val.name,
41
+ message: val.message,
42
+ stack: val.stack
43
+ };
44
+ if (val.cause !== void 0) out.cause = val.cause;
45
+ return out;
46
+ }
47
+ if (val instanceof Map) return {
48
+ __type: "Map",
49
+ entries: [...val.entries()]
50
+ };
51
+ if (val instanceof Set) return {
52
+ __type: "Set",
53
+ entries: [...val]
54
+ };
55
+ if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
56
+ if (typeof val === "symbol") return val.toString();
57
+ if (val !== null && typeof val === "object") {
58
+ if (seen.has(val)) return "[Circular]";
59
+ seen.add(val);
60
+ }
61
+ return val;
62
+ }, 2);
63
+ }
64
+ /**
65
+ * Format a thrown value for an MCP `isError` text payload.
66
+ *
67
+ * A nostics `Diagnostic` (every coded devframe error) becomes structured
68
+ * JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
69
+ * the actionable next step (`fix`) and the docs URL instead of a bare
70
+ * message string. Other errors surface `Error.name`/`message`, plus one
71
+ * level of `cause.message` so context isn't dropped silently.
72
+ */
73
+ function formatMcpError(error) {
74
+ if (error instanceof Diagnostic) return JSON.stringify({ error: {
75
+ code: error.code,
76
+ message: error.message,
77
+ ...error.fix ? { fix: error.fix } : {},
78
+ ...error.docs ? { docs: error.docs } : {}
79
+ } }, null, 2);
80
+ if (!(error instanceof Error)) return String(error);
81
+ const cause = error.cause;
82
+ const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
83
+ return `${error.name}: ${error.message}${causeText}`;
84
+ }
85
+ //#endregion
86
+ //#region src/adapters/mcp/to-json-schema.ts
87
+ const FALLBACK_OBJECT_SCHEMA = Object.freeze({
88
+ type: "object",
89
+ additionalProperties: true
90
+ });
91
+ /**
92
+ * Convert a Standard Schema to JSON Schema for the agent/MCP surface.
93
+ *
94
+ * Devframe stays validator-neutral, so conversion uses the schema's own
95
+ * [Standard JSON Schema](https://standardschema.dev/) converter
96
+ * (`~standard.jsonSchema`) when the validator provides one — zod 4 does,
97
+ * for example. Validators without a native converter (e.g. valibot) degrade
98
+ * to a permissive object schema rather than pulling in a converter library.
99
+ */
100
+ function safeToJsonSchema(schema) {
101
+ const standard = schema["~standard"];
102
+ if (standard.jsonSchema) try {
103
+ return standard.jsonSchema.input({ target: "draft-2020-12" });
104
+ } catch {
105
+ return FALLBACK_OBJECT_SCHEMA;
106
+ }
107
+ return FALLBACK_OBJECT_SCHEMA;
108
+ }
109
+ /**
110
+ * JSON Schema for an RPC return value on the agent/MCP surface.
111
+ * @internal
112
+ */
113
+ function returnToJsonSchema(schema) {
114
+ if (!schema) return void 0;
115
+ return safeToJsonSchema(schema);
116
+ }
117
+ /**
118
+ * JSON Schema for an RPC function's positional args on the agent/MCP
119
+ * surface. Each positional arg is advertised under `arg0` / `arg1` / … —
120
+ * matching how the agent bridge coerces the incoming object payload back
121
+ * into positional arguments.
122
+ *
123
+ * Returns `{ type: 'object', properties: {} }` when there are no args.
124
+ * @internal
125
+ */
126
+ function argsToJsonSchema(args) {
127
+ if (!args || args.length === 0) return {
128
+ schema: {
129
+ type: "object",
130
+ properties: {}
131
+ },
132
+ unwrapped: false
133
+ };
134
+ const properties = {};
135
+ const required = [];
136
+ for (let i = 0; i < args.length; i++) {
137
+ const key = `arg${i}`;
138
+ properties[key] = safeToJsonSchema(args[i]);
139
+ required.push(key);
140
+ }
141
+ return {
142
+ schema: {
143
+ type: "object",
144
+ properties,
145
+ required,
146
+ additionalProperties: false
147
+ },
148
+ unwrapped: false
149
+ };
150
+ }
151
+ //#endregion
152
+ //#region src/adapters/mcp/build-server.ts
153
+ /**
154
+ * Build a fresh MCP {@link Server} over a devframe context, registering its
155
+ * tool and resource handlers. This is a pure factory — it sets up no
156
+ * long-lived subscriptions and holds no per-connection state, so it is safe
157
+ * to call once per request under `createMcpHandler` or once per connection
158
+ * under `serveStdio`. Change notifications are published separately: over
159
+ * HTTP through the handler's `notify` bus (see `createMcpFetchHandler`), and
160
+ * on stdio through the connection's own `send*ListChanged` calls (see
161
+ * {@link bridgeListChanged}, wired by `serveStdio`).
162
+ *
163
+ * @internal
164
+ */
165
+ function buildMcpServerFromContext(ctx, options) {
166
+ const server = new Server({
167
+ name: options.serverName,
168
+ version: options.serverVersion
169
+ }, { capabilities: {
170
+ tools: { listChanged: true },
171
+ resources: { listChanged: true }
172
+ } });
173
+ registerToolHandlers(server, ctx, options.exposeSharedState);
174
+ registerResourceHandlers(server, ctx, options.exposeSharedState);
175
+ return server;
176
+ }
177
+ /**
178
+ * Publish devframe's `list_changed` events through a set of typed sinks:
179
+ * `tools()` for tool-list changes and `resources()` for resource-list
180
+ * changes (shared-state keys are surfaced as resources). Returns an
181
+ * unsubscribe function.
182
+ *
183
+ * The HTTP path passes the handler's `notify` bus sugar; the stdio path
184
+ * passes the pinned server's `send*ListChanged` methods, which `serveStdio`
185
+ * routes onto the connection's active `subscriptions/listen` streams.
186
+ *
187
+ * @internal
188
+ */
189
+ function bridgeListChanged(ctx, sinks) {
190
+ const offManifest = ctx.agent.events.on(DEVFRAME_EVENTS.bus.agentManifestChanged, () => {
191
+ sinks.tools();
192
+ sinks.resources();
193
+ });
194
+ const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
195
+ sinks.resources();
196
+ });
197
+ return () => {
198
+ offManifest();
199
+ offKeyAdded();
200
+ };
201
+ }
202
+ /**
203
+ * Build an MCP server over the agent surface of a devframe definition.
204
+ * Currently supports `stdio` transport only.
205
+ */
206
+ async function createMcpServer(definition, options = {}) {
207
+ const transport = options.transport ?? "stdio";
208
+ if (transport !== "stdio") throw diagnostics.DF0017({
209
+ transport,
210
+ reason: "Only stdio transport is supported in this release."
211
+ });
212
+ const ctx = await createHostContext({
213
+ cwd: process.cwd(),
214
+ mode: "dev",
215
+ host: {
216
+ mountStatic: () => {},
217
+ resolveOrigin: () => "mcp://devframe",
218
+ getStorageDir: (scope) => {
219
+ if (scope === "workspace") return join(process.cwd(), ".devframe");
220
+ if (scope === "project") return join(process.cwd(), `node_modules/.${definition.id}/devframe`);
221
+ return join(homedir(), `.${definition.id}/devframe`);
222
+ }
223
+ },
224
+ importMetaUrl: definition.importMetaUrl
225
+ });
226
+ for (const input of definition.services ?? []) ctx.services.install(input, { resolveFrom: definition.importMetaUrl });
227
+ await ctx.services.ready();
228
+ await definition.setup(ctx);
229
+ const buildOptions = {
230
+ serverName: options.serverName ?? `${definition.id} (devframe)`,
231
+ serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
232
+ exposeSharedState: options.exposeSharedState ?? true
233
+ };
234
+ let handle;
235
+ try {
236
+ const { serveStdio } = await import("@modelcontextprotocol/server/stdio");
237
+ handle = serveStdio(() => {
238
+ const server = buildMcpServerFromContext(ctx, buildOptions);
239
+ const unbridge = bridgeListChanged(ctx, {
240
+ tools: () => {
241
+ server.sendToolListChanged().catch(() => {});
242
+ },
243
+ resources: () => {
244
+ server.sendResourceListChanged().catch(() => {});
245
+ }
246
+ });
247
+ const priorOnClose = server.onclose;
248
+ server.onclose = () => {
249
+ unbridge();
250
+ priorOnClose?.();
251
+ };
252
+ return server;
253
+ });
254
+ } catch (error) {
255
+ const reason = error instanceof Error ? error.message : String(error);
256
+ throw diagnostics.DF0017({
257
+ transport,
258
+ reason,
259
+ cause: error
260
+ });
261
+ }
262
+ options.onReady?.({ transport: "stdio" });
263
+ return { async stop() {
264
+ await handle.close();
265
+ } };
266
+ }
267
+ /**
268
+ * Id of the built-in shared-state read tool — namespaced like every other
269
+ * built-in (`devframe:<area>:<fn>`). Tool-shaped access matters because many
270
+ * MCP clients only consume tools — the parallel `devframe://state/<key>`
271
+ * resource projection stays for the clients that do read resources.
272
+ */
273
+ const READ_STATE_TOOL = "devframe:state:read";
274
+ /** Wire name of the built-in shared-state read tool: `devframe_state_read`. */
275
+ const READ_STATE_NAME = toAgentToolName(READ_STATE_TOOL);
276
+ function sharedStateFilter(exposeSharedState) {
277
+ if (exposeSharedState === false) return void 0;
278
+ return typeof exposeSharedState === "function" ? exposeSharedState : () => true;
279
+ }
280
+ function readStateToolProjection() {
281
+ return {
282
+ name: READ_STATE_NAME,
283
+ title: "Read shared state",
284
+ 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.",
285
+ inputSchema: {
286
+ type: "object",
287
+ properties: { key: {
288
+ type: "string",
289
+ description: "A shared-state key from the key list. Omit to list all keys."
290
+ } }
291
+ },
292
+ annotations: {
293
+ title: "Read shared state",
294
+ readOnlyHint: true,
295
+ destructiveHint: false
296
+ }
297
+ };
298
+ }
299
+ async function readStateResult(ctx, filter, key) {
300
+ const keys = ctx.rpc.sharedState.keys().filter(filter);
301
+ if (key === void 0) return { keys };
302
+ if (!keys.includes(key)) throw diagnostics.DF0048({ key });
303
+ return {
304
+ key,
305
+ value: (await ctx.rpc.sharedState.get(key)).value()
306
+ };
307
+ }
308
+ function registerToolHandlers(server, ctx, exposeSharedState) {
309
+ const stateFilter = sharedStateFilter(exposeSharedState);
310
+ const warnedCollisions = /* @__PURE__ */ new Set();
311
+ /**
312
+ * Resolve a wire tool name back to the registered {@link AgentTool}.
313
+ * Wire-name matching runs first, in manifest order — the same tool the
314
+ * list projection advertises under that name — with a raw-id fallback so
315
+ * a colon-namespaced id keeps working as a call name.
316
+ */
317
+ const resolveTool = (name) => {
318
+ return ctx.agent.list().tools.find((tool) => toAgentToolName(tool.id) === name) ?? ctx.agent.getTool(name);
319
+ };
320
+ server.setRequestHandler("tools/list", async () => {
321
+ const byName = /* @__PURE__ */ new Map();
322
+ for (const tool of ctx.agent.list().tools) {
323
+ const name = toAgentToolName(tool.id);
324
+ const existing = byName.get(name);
325
+ if (existing) {
326
+ if (!warnedCollisions.has(`${name}|${tool.id}`)) {
327
+ warnedCollisions.add(`${name}|${tool.id}`);
328
+ diagnostics.DF0047({
329
+ name,
330
+ id: tool.id,
331
+ existing: existing.id
332
+ });
333
+ }
334
+ continue;
335
+ }
336
+ byName.set(name, tool);
337
+ }
338
+ const tools = [...byName.entries()].map(([name, tool]) => projectTool(name, tool, ctx));
339
+ if (stateFilter && !byName.has(READ_STATE_NAME)) tools.push(readStateToolProjection());
340
+ return { tools };
341
+ });
342
+ server.setRequestHandler("tools/call", async (request) => {
343
+ const { name, arguments: args } = request.params;
344
+ try {
345
+ const tool = resolveTool(name);
346
+ if (stateFilter && !tool && (name === READ_STATE_NAME || name === READ_STATE_TOOL)) {
347
+ const key = args?.key;
348
+ const result = await readStateResult(ctx, stateFilter, key);
349
+ return {
350
+ content: [{
351
+ type: "text",
352
+ text: stringifyForMcp(result)
353
+ }],
354
+ structuredContent: result
355
+ };
356
+ }
357
+ const outputSchema = tool ? usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx)) : void 0;
358
+ const result = await ctx.agent.invoke(tool?.id ?? name, args ?? {});
359
+ return {
360
+ content: [{
361
+ type: "text",
362
+ text: stringifyForMcp(result)
363
+ }],
364
+ ...outputSchema ? { structuredContent: result } : {}
365
+ };
366
+ } catch (error) {
367
+ return {
368
+ isError: true,
369
+ content: [{
370
+ type: "text",
371
+ text: `Error invoking "${name}": ${formatMcpError(error)}`
372
+ }]
373
+ };
374
+ }
375
+ });
376
+ }
377
+ function registerResourceHandlers(server, ctx, exposeSharedState) {
378
+ server.setRequestHandler("resources/list", async () => {
379
+ const resources = ctx.agent.list().resources.map((resource) => ({
380
+ uri: resource.uri,
381
+ name: resource.name,
382
+ description: resource.description,
383
+ mimeType: resource.mimeType
384
+ }));
385
+ if (exposeSharedState !== false) {
386
+ const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
387
+ for (const key of ctx.rpc.sharedState.keys()) {
388
+ if (!filter(key)) continue;
389
+ resources.push({
390
+ uri: `devframe://state/${encodeURIComponent(key)}`,
391
+ name: key,
392
+ description: `Shared state: ${key}`,
393
+ mimeType: "application/json"
394
+ });
395
+ }
396
+ }
397
+ return { resources };
398
+ });
399
+ server.setRequestHandler("resources/read", async (request) => {
400
+ const { uri } = request.params;
401
+ const parsed = parseResourceUri(uri);
402
+ if (parsed.kind === "resource") {
403
+ const content = await ctx.agent.read(parsed.id);
404
+ return { contents: [{
405
+ uri,
406
+ mimeType: content.mimeType ?? "application/json",
407
+ text: content.text ?? stringifyForMcp(content.json)
408
+ }] };
409
+ }
410
+ if (parsed.kind === "state") return { contents: [{
411
+ uri,
412
+ mimeType: "application/json",
413
+ text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
414
+ }] };
415
+ throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
416
+ });
417
+ }
418
+ /**
419
+ * MCP constrains a tool's `outputSchema` to a JSON Schema of `type:
420
+ * "object"` — clients (the SDK included) reject anything else. Non-object
421
+ * return schemas (e.g. a schema for `void` / a bare string) simply project
422
+ * no output schema; the text content still carries the result.
423
+ */
424
+ function usableOutputSchema(schema) {
425
+ return schema && typeof schema === "object" && schema.type === "object" ? schema : void 0;
426
+ }
427
+ function projectTool(name, tool, ctx) {
428
+ const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
429
+ const outputSchema = usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx));
430
+ return {
431
+ name,
432
+ title: tool.title,
433
+ description: tool.description,
434
+ inputSchema,
435
+ ...outputSchema ? { outputSchema } : {},
436
+ annotations: {
437
+ title: tool.title,
438
+ readOnlyHint: tool.safety === "read",
439
+ destructiveHint: tool.safety === "destructive"
440
+ }
441
+ };
442
+ }
443
+ function computeInputSchema(tool, ctx) {
444
+ if (tool.kind === "tool") return argsToJsonSchema(tool.args).schema;
445
+ if (tool.kind !== "rpc" || !tool.rpcName) return {
446
+ type: "object",
447
+ properties: {}
448
+ };
449
+ const def = ctx.rpc.definitions.get(tool.rpcName);
450
+ if (!def) return {
451
+ type: "object",
452
+ properties: {}
453
+ };
454
+ const args = def.args;
455
+ return argsToJsonSchema(args).schema;
456
+ }
457
+ function computeOutputSchema(tool, ctx) {
458
+ if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
459
+ const def = ctx.rpc.definitions.get(tool.rpcName);
460
+ if (!def) return void 0;
461
+ return returnToJsonSchema(def.returns);
462
+ }
463
+ function parseResourceUri(uri) {
464
+ const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
465
+ if (!match) return { kind: "unknown" };
466
+ const [, kind, rest] = match;
467
+ const decoded = decodeURIComponent(rest);
468
+ if (kind === "resource") return {
469
+ kind: "resource",
470
+ id: decoded
471
+ };
472
+ return {
473
+ kind: "state",
474
+ key: decoded
475
+ };
476
+ }
477
+ //#endregion
478
+ //#region src/adapters/mcp/fetch.ts
479
+ /**
480
+ * Build a framework-agnostic MCP endpoint over a devframe context: a
481
+ * web-standard `Request → Response` handler any host can mount — h3 (see
482
+ * `mountMcpHttp`), a Next.js App Router route, or any other fetch-shaped
483
+ * server.
484
+ *
485
+ * The endpoint is **stateless**: it serves the 2026-07-28 revision per request
486
+ * through the SDK's {@link createMcpHandler}, which builds a fresh MCP server
487
+ * (from the shared, live `ctx` via `buildMcpServerFromContext`) for each
488
+ * request — no `Mcp-Session-Id` registry, no session-local routing, no
489
+ * GET/DELETE teardown protocol. 2025-era clients are still served through the
490
+ * SDK's default stateless legacy path. `list_changed` events reach modern
491
+ * `subscriptions/listen` streams through the handler's `notify` bus.
492
+ *
493
+ * The origin gate guards every request: loopback-default DNS-rebinding
494
+ * protection that — unlike the WS upgrade's `isAllowedOrigin` — also rejects
495
+ * `Origin`-less requests, so a route-based endpoint isn't reachable by an
496
+ * arbitrary local process.
497
+ */
498
+ function createMcpFetchHandler(ctx, options) {
499
+ const allowedOrigins = options.allowedOrigins;
500
+ const handler = createMcpHandler(() => buildMcpServerFromContext(ctx, {
501
+ serverName: options.serverName,
502
+ serverVersion: options.serverVersion,
503
+ exposeSharedState: options.exposeSharedState
504
+ }));
505
+ const unbridge = bridgeListChanged(ctx, {
506
+ tools: () => {
507
+ handler.notify.toolsChanged();
508
+ },
509
+ resources: () => {
510
+ handler.notify.resourcesChanged();
511
+ }
512
+ });
513
+ async function handle(req) {
514
+ const origin = req.headers.get("origin") ?? void 0;
515
+ if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden: origin required", { status: 403 });
516
+ return handler.fetch(req);
517
+ }
518
+ return {
519
+ fetch: handle,
520
+ dispose: async () => {
521
+ unbridge();
522
+ await handler.close();
523
+ }
524
+ };
525
+ }
526
+ //#endregion
527
+ //#region src/adapters/mcp/http.ts
528
+ /**
529
+ * Mount a stateless MCP endpoint on an h3 app at `path` — the h3 binding over
530
+ * {@link createMcpFetchHandler}, which owns the per-request serving, the
531
+ * origin gate, and the transport plumbing.
532
+ *
533
+ * The handler is web-standard — it takes the h3 event's web `Request` and
534
+ * returns a web `Response` (an SSE `ReadableStream` body for a
535
+ * `subscriptions/listen` stream). We copy that response onto `event.res` and
536
+ * return its body rather than returning the `Response` object directly, so an
537
+ * MCP error response (e.g. a 4xx) isn't swallowed by h3's "Response-with-404
538
+ * falls through to the next handler" rule (which would otherwise hand the
539
+ * request to the SPA static catch-all).
540
+ */
541
+ function mountMcpHttp(app, ctx, path, options) {
542
+ const handler = createMcpFetchHandler(ctx, options);
543
+ app.use(path, defineHandler(async (event) => respond(event, await handler.fetch(event.req))));
544
+ return { dispose: handler.dispose };
545
+ }
546
+ /**
547
+ * Copy a web `Response` from the MCP transport onto the h3 event's response
548
+ * and return its body. Returning the body (a `ReadableStream` or `null`)
549
+ * rather than the `Response` object avoids h3's 404-fall-through behavior.
550
+ */
551
+ function respond(event, response) {
552
+ event.res.status = response.status;
553
+ event.res.statusText = response.statusText;
554
+ response.headers.forEach((value, key) => {
555
+ event.res.headers.set(key, value);
556
+ });
557
+ return response.body ?? "";
558
+ }
559
+ //#endregion
2
560
  export { createMcpFetchHandler, createMcpServer, mountMcpHttp };
package/dist/cli/main.mjs CHANGED
@@ -203,7 +203,7 @@ async function withInstanceClient(sdk, url, fn) {
203
203
  const client = new sdk.Client({
204
204
  name: "devframe-connect",
205
205
  version: "0.0.0"
206
- });
206
+ }, { versionNegotiation: { mode: "auto" } });
207
207
  await client.connect(transport);
208
208
  try {
209
209
  return await fn(client);
@@ -1,8 +1,8 @@
1
- import { f as RpcCacheManager, p as RpcCacheOptions } from "../index-DOg4S5wk.mjs";
2
- import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-BBLeGfpt.mjs";
3
- import { Q as DevframeServiceScopeOf, X as DevframeServiceMeta, _t as DevframeRpcServerFunctions, b as StreamSink, d as RpcSharedStateHost, dt as ScopedRpcFn, ft as ScopedServerFunctions, gt as DevframeRpcClientFunctions, k as SharedState, mt as SettingsForNamespace, nt as DevframeServicesState, ot as DevframeSettings, pt as ScopedSharedStates, t as ConnectionMeta, u as RpcSharedStateGetOptions, y as StreamReader, zt as EventEmitter } from "../context-_i51nYOs.mjs";
4
- import { t as SseRpcChannelOptions } from "../sse-client-CdGD_B8_.mjs";
5
- import { t as WsRpcChannelOptions } from "../ws-client-Bo2t6hES.mjs";
1
+ import { f as RpcCacheManager, p as RpcCacheOptions } from "../index-BzyopqxV.mjs";
2
+ import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-CMdPYhOx.mjs";
3
+ import { Q as DevframeServiceScopeOf, X as DevframeServiceMeta, _t as DevframeRpcServerFunctions, b as StreamSink, d as RpcSharedStateHost, dt as ScopedRpcFn, ft as ScopedServerFunctions, gt as DevframeRpcClientFunctions, k as SharedState, mt as SettingsForNamespace, nt as DevframeServicesState, ot as DevframeSettings, pt as ScopedSharedStates, t as ConnectionMeta, u as RpcSharedStateGetOptions, y as StreamReader, zt as EventEmitter } from "../context-BhBzOXVa.mjs";
4
+ import { t as SseRpcChannelOptions } from "../sse-client-DwX021ST.mjs";
5
+ import { t as WsRpcChannelOptions } from "../ws-client-7_lZi-B9.mjs";
6
6
  import { BirpcOptions, BirpcReturn } from "birpc";
7
7
  //#region src/client/connection.d.ts
8
8
  interface DevframeConnection {
@@ -1,3 +1,4 @@
1
+ import { t as DEVFRAME_EVENTS } from "../events-4daw0uK9.mjs";
1
2
  import { t as hash } from "../hash-DdPnc4k3.mjs";
2
3
  import { colors } from "../utils/colors.mjs";
3
4
  import { createEventEmitter } from "../utils/events.mjs";
@@ -8,70 +9,6 @@ import { defineDiagnostics } from "nostics";
8
9
  import { ansiFormatter } from "nostics/formatters/ansi";
9
10
  import { withBase, withProtocol } from "ufo";
10
11
  import { createBirpc } from "birpc";
11
- //#region src/events.ts
12
- /**
13
- * Centralized registry of the core devframe event names — the node-side host
14
- * bus events, the client RPC connection events, and the server→client
15
- * broadcast notifications — so these names live in one place instead of
16
- * scattered string literals.
17
- *
18
- * **Keep this in sync with [`docs/guide/events.md`](../../../docs/guide/events.md)**
19
- * (the "Core devframe events" section): every name here appears in that page's
20
- * tables, and every name there resolves to an entry here. Add, rename, or
21
- * remove a name in both places in the same change, and reference
22
- * `DEVFRAME_EVENTS.*` from call sites instead of re-typing a literal.
23
- *
24
- * This map covers **notifications** (events, broadcasts). The request/response
25
- * RPC endpoints of the shared-state, streaming, and auth-handshake protocols
26
- * (`devframe:rpc:server-state:*`, `devframe:streaming:subscribe`,
27
- * `anonymous:devframe:auth`, …) are defined at their handlers and typed in
28
- * `types/rpc-augments.ts`; they aren't events and stay out of this map.
29
- *
30
- * The `EventEmitter` maps (`RpcClientEvents`, `DevframeAgentHostEvents`) and the
31
- * `DevframeRpcClientFunctions` augmentation declare these names as type-level
32
- * keys (a literal is unavoidable in a type position); those declarations mirror
33
- * this map and move with it.
34
- */
35
- const DEVFRAME_EVENTS = {
36
- /**
37
- * Node-side host `EventEmitter` events. The agent host (`ctx.agent.events`)
38
- * emits these as its tool/resource surface changes; protocol adapters (e.g.
39
- * MCP) subscribe to re-publish their manifest.
40
- */
41
- bus: {
42
- agentManifestChanged: "agent:manifest:changed",
43
- agentToolRegistered: "agent:tool:registered",
44
- agentToolUnregistered: "agent:tool:unregistered",
45
- agentResourceRegistered: "agent:resource:registered",
46
- agentResourceUnregistered: "agent:resource:unregistered"
47
- },
48
- /**
49
- * Client-side RPC connection `EventEmitter` events (`rpc.events`) a UI
50
- * subscribes to for connection lifecycle and error surfacing.
51
- */
52
- client: {
53
- isTrustedUpdated: "rpc:is-trusted:updated",
54
- error: "rpc:error",
55
- connectionStatus: "connection:status",
56
- connectionError: "connection:error"
57
- },
58
- /**
59
- * Broadcast notifications the server pushes to clients (server → client),
60
- * `devframe:` prefix. The paired request methods (subscribe/get/set/…) are
61
- * RPC endpoints, not events, and are omitted deliberately.
62
- */
63
- broadcast: {
64
- authRevoked: "devframe:auth:revoked",
65
- clientStateUpdated: "devframe:rpc:client-state:updated",
66
- clientStatePatch: "devframe:rpc:client-state:patch",
67
- streamingChunk: "devframe:streaming:chunk",
68
- streamingEnd: "devframe:streaming:end",
69
- streamingUploadCancel: "devframe:streaming:upload-cancel"
70
- },
71
- /** `postMessage` channels the runtime posts across window boundaries. */
72
- postMessage: { remoteAssetsError: "devframe:remote-assets-error" }
73
- };
74
- //#endregion
75
12
  //#region src/constants.ts
76
13
  const DEVFRAME_CONNECTION_META_FILENAME = "__connection.json";
77
14
  /**
@@ -5,7 +5,7 @@
5
5
  * broadcast notifications — so these names live in one place instead of
6
6
  * scattered string literals.
7
7
  *
8
- * **Keep this in sync with [`docs/guide/events.md`](../../../docs/guide/events.md)**
8
+ * **Keep this in sync with [`docs/content/8.references/3.events.md`](../../../docs/content/8.references/3.events.md)**
9
9
  * (the "Core devframe events" section): every name here appears in that page's
10
10
  * tables, and every name there resolves to an entry here. Add, rename, or
11
11
  * remove a name in both places in the same change, and reference
@@ -58,9 +58,21 @@ declare const DEVFRAME_EVENTS: {
58
58
  readonly streamingEnd: "devframe:streaming:end";
59
59
  readonly streamingUploadCancel: "devframe:streaming:upload-cancel";
60
60
  };
61
+ /**
62
+ * In-page channel notifications the page script pushes to its panels
63
+ * (page script → panel), `devframe:` prefix. The paired request methods
64
+ * (`devframe:in-page:page-state:subscribe`/`set`/`patch`) are call
65
+ * endpoints, not events, and are defined at their handlers
66
+ * (`in-page-channel/state.ts`).
67
+ */
68
+ readonly inPageChannel: {
69
+ readonly panelStateUpdated: "devframe:in-page:panel-state:updated";
70
+ readonly panelStatePatch: "devframe:in-page:panel-state:patch";
71
+ };
61
72
  /** `postMessage` channels the runtime posts across window boundaries. */
62
73
  readonly postMessage: {
63
74
  readonly remoteAssetsError: "devframe:remote-assets-error";
75
+ readonly inPageChannel: "devframe:in-page-channel";
64
76
  };
65
77
  };
66
78
  //#endregion