devframe 0.7.15 → 0.8.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 (75) hide show
  1. package/bin/devframe.mjs +8 -0
  2. package/dist/adapters/build.d.mts +8 -1
  3. package/dist/adapters/build.mjs +5 -3
  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 +45 -3
  12. package/dist/adapters/mcp.mjs +2 -2
  13. package/dist/agent-tool-name-C3b5vEwJ.mjs +34 -0
  14. package/dist/{cac-CjF0haJ6.mjs → cac-B-R_tDGJ.mjs} +19 -14
  15. package/dist/cli/main.d.mts +11 -0
  16. package/dist/cli/main.mjs +283 -0
  17. package/dist/client/index.d.mts +45 -9
  18. package/dist/client/index.mjs +266 -97
  19. package/dist/constants.d.mts +7 -1
  20. package/dist/constants.mjs +7 -1
  21. package/dist/{context-C_bqRR0i.d.mts → context-7BbaIUSI.d.mts} +1 -1
  22. package/dist/{context-C1Gr2BGB.mjs → context-C9Cgm1hP.mjs} +1 -1
  23. package/dist/{dev-OJFr4rx8.mjs → dev-D4M7Veo7.mjs} +22 -11
  24. package/dist/{devframe-BADsX91-.d.mts → devframe-Dsjn_Xtq.d.mts} +78 -15
  25. package/dist/diagnostics-B5-qHeqD.mjs +94 -0
  26. package/dist/{diagnostics-BXwBQmoN.mjs → diagnostics-hwjXp_UV.mjs} +8 -0
  27. package/dist/{dump-Cz7yVvsB.mjs → dump-CgZShDRB.mjs} +82 -6
  28. package/dist/{build-server-cANnMwg0.mjs → fetch-BZyK4v6W.mjs} +225 -51
  29. package/dist/helpers/vite.d.mts +1 -1
  30. package/dist/helpers/vite.mjs +2 -2
  31. package/dist/{host-h3-SjwRTwkE.mjs → host-h3-Kz7t5Xab.mjs} +83 -28
  32. package/dist/http-BghOqfTD.mjs +38 -0
  33. package/dist/{index-Brcd2V4t.d.mts → index-CeBbry0R.d.mts} +2 -2
  34. package/dist/{index-DgsLFhZg.d.mts → index-Dbw1p5ch.d.mts} +27 -3
  35. package/dist/{index-YDSJgsyG.d.mts → index-vAX6bYuc.d.mts} +1 -1
  36. package/dist/index.d.mts +5 -5
  37. package/dist/instance-registry-D6fxYu36.mjs +190 -0
  38. package/dist/node/auth.d.mts +2 -2
  39. package/dist/node/hub-internals.d.mts +2 -2
  40. package/dist/node/hub-internals.mjs +1 -1
  41. package/dist/node/index.d.mts +104 -6
  42. package/dist/node/index.mjs +5 -4
  43. package/dist/recipes/common-rpc-functions.d.mts +33 -33
  44. package/dist/recipes/common-rpc-functions.mjs +6 -6
  45. package/dist/recipes/interactive-auth.d.mts +2 -2
  46. package/dist/recipes/interactive-auth.mjs +13 -13
  47. package/dist/rpc/dump.d.mts +1 -1
  48. package/dist/rpc/dump.mjs +1 -1
  49. package/dist/rpc/index.d.mts +3 -3
  50. package/dist/rpc/index.mjs +4 -4
  51. package/dist/rpc/transports/ws-client.d.mts +1 -1
  52. package/dist/rpc/transports/ws-client.mjs +1 -1
  53. package/dist/rpc/transports/ws-server.d.mts +1 -1
  54. package/dist/rpc/transports/ws-server.mjs +1 -1
  55. package/dist/{serialization-DpLXCy13.mjs → serialization-C8Mnw9hK.mjs} +1 -1
  56. package/dist/{server-BFsuZI9e.mjs → server-CcAPxuoT.mjs} +2 -2
  57. package/dist/{server-Bu5iB3aV.d.mts → server-VLQJouOO.d.mts} +2 -2
  58. package/dist/simple-schema-BDzLeJDk.d.mts +102 -0
  59. package/dist/simple-schema-DQPZrAaZ.mjs +154 -0
  60. package/dist/{storage-D_Xy9v1l.mjs → storage-Dzoc3NVC.mjs} +2 -62
  61. package/dist/{transports-BmDVn4SF.mjs → transports-vhizgqXM.mjs} +1 -1
  62. package/dist/types/index.d.mts +4 -4
  63. package/dist/{types-CrzNxXKq.d.mts → types-CnJSgRVa.d.mts} +33 -19
  64. package/dist/utils/agent-tool-name.d.mts +27 -0
  65. package/dist/utils/agent-tool-name.mjs +34 -0
  66. package/dist/utils/events.d.mts +1 -1
  67. package/dist/utils/shared-state.d.mts +1 -1
  68. package/dist/utils/simple-schema.d.mts +2 -0
  69. package/dist/utils/simple-schema.mjs +154 -0
  70. package/dist/utils/streaming-channel.d.mts +1 -1
  71. package/dist/{ws-client-DSXiI2h7.d.mts → ws-client-B6Tnr0ic.d.mts} +1 -1
  72. package/dist/{ws-server-9-wn7MNQ.d.mts → ws-server-D_Vjtums.d.mts} +1 -1
  73. package/package.json +18 -34
  74. package/skills/devframe/SKILL.md +1 -1
  75. package/dist/http-B2P55ZXg.mjs +0 -114
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import process from 'node:process'
3
+ import { runDevframeCli } from '../dist/cli/main.mjs'
4
+
5
+ runDevframeCli().catch((error) => {
6
+ console.error(error)
7
+ process.exit(1)
8
+ })
@@ -1,4 +1,4 @@
1
- import { r as DevframeDefinition } from "../devframe-BADsX91-.mjs";
1
+ import { r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
2
2
  //#region src/adapters/build.d.ts
3
3
  interface CreateBuildOptions {
4
4
  /** Output directory. Defaults to `dist-static`. */
@@ -17,6 +17,13 @@ interface CreateBuildOptions {
17
17
  * minified. Set `true` when you need to diff / read the dumps by hand.
18
18
  */
19
19
  pretty?: boolean;
20
+ /**
21
+ * Proceed even when the definition declares `capabilities.build: false`.
22
+ * `createCac` already skips registering the `build` subcommand for such
23
+ * a definition — this only matters for a caller invoking `createBuild`
24
+ * directly, bypassing the CLI.
25
+ */
26
+ force?: boolean;
20
27
  }
21
28
  /**
22
29
  * Produce a self-contained static deploy of a devframe:
@@ -1,9 +1,10 @@
1
- import { t as collectStaticRpcDump } from "../dump-Cz7yVvsB.mjs";
1
+ import { t as collectStaticRpcDump } from "../dump-CgZShDRB.mjs";
2
2
  import { n as colors } from "../diagnostics-reporter-CsIG85Q5.mjs";
3
3
  import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME } from "../constants.mjs";
4
- import { n as strictJsonStringify } from "../serialization-DpLXCy13.mjs";
4
+ import { n as strictJsonStringify } from "../serialization-C8Mnw9hK.mjs";
5
5
  import { n as structuredCloneStringify } from "../structured-clone-CbAV5rFI.mjs";
6
- import { n as createHostContext, t as createH3DevframeHost } from "../host-h3-SjwRTwkE.mjs";
6
+ import { n as createHostContext, t as createH3DevframeHost } from "../host-h3-Kz7t5Xab.mjs";
7
+ import { t as diagnostics } from "../diagnostics-B5-qHeqD.mjs";
7
8
  import { n as resolveBasePath } from "../_shared-bWRzeSa0.mjs";
8
9
  import { existsSync } from "node:fs";
9
10
  import { dirname, resolve } from "pathe";
@@ -24,6 +25,7 @@ import fs$1 from "node:fs/promises";
24
25
  * works at `/`, `/devframe/`, or any base, no rewriting required.
25
26
  */
26
27
  async function createBuild(d, options = {}) {
28
+ if (d.capabilities?.build === false && !options.force) throw diagnostics.DF0042({ id: d.id });
27
29
  const outDir = resolve(options.outDir ?? "dist-static");
28
30
  const distDir = options.distDir ?? d.cli?.distDir;
29
31
  if (!distDir) throw new Error(`[devframe] createBuild: no distDir for "${d.id}". Set \`cli.distDir\` on the definition or pass it as an option.`);
@@ -1,4 +1,4 @@
1
- import { Ft as parseCliFlags, Mt as CliFlagsSchema, Nt as InferCliFlags, Pt as defineCliFlags, r as DevframeDefinition } from "../devframe-BADsX91-.mjs";
1
+ import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema, r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
2
2
  import { CAC } from "cac";
3
3
  import { H3 } from "h3";
4
4
  //#region src/adapters/cac.d.ts
@@ -1,2 +1,2 @@
1
- import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-CjF0haJ6.mjs";
1
+ import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-B-R_tDGJ.mjs";
2
2
  export { createCac, defineCliFlags, parseCliFlags };
@@ -1,4 +1,4 @@
1
- import { Ft as parseCliFlags, Mt as CliFlagsSchema, Nt as InferCliFlags, Pt as defineCliFlags } from "../devframe-BADsX91-.mjs";
1
+ import { Ft as InferCliFlags, It as defineCliFlags, Lt as parseCliFlags, Pt as CliFlagsSchema } from "../devframe-Dsjn_Xtq.mjs";
2
2
  import { CacHandle, CreateCacOptions, createCac } from "./cac.mjs";
3
3
  //#region src/adapters/cli.d.ts
4
4
  /** @deprecated Use `createCac` from `devframe/adapters/cac` instead. */
@@ -1,4 +1,4 @@
1
- import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-CjF0haJ6.mjs";
1
+ import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-B-R_tDGJ.mjs";
2
2
  //#region src/adapters/cli.ts
3
3
  /** @deprecated Use `createCac` from `devframe/adapters/cac` instead. */
4
4
  const createCli = createCac;
@@ -1,5 +1,5 @@
1
- import { _ as ConnectionMeta, d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition, u as DevframeWsOptions } from "../devframe-BADsX91-.mjs";
2
- import { n as StartedServer } from "../server-Bu5iB3aV.mjs";
1
+ import { _ as ConnectionMeta, d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition, u as DevframeWsOptions } from "../devframe-Dsjn_Xtq.mjs";
2
+ import { n as StartedServer } from "../server-VLQJouOO.mjs";
3
3
  import { H3 } from "h3";
4
4
  //#region src/adapters/dev.d.ts
5
5
  interface CreateDevServerOptions {
@@ -1,2 +1,2 @@
1
- import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-OJFr4rx8.mjs";
1
+ import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-D4M7Veo7.mjs";
2
2
  export { createDevServer, resolveDevServerPort, resolveMcpConnectionMeta };
@@ -1,4 +1,4 @@
1
- import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-BADsX91-.mjs";
1
+ import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
2
2
  //#region src/adapters/embedded.d.ts
3
3
  interface CreateEmbeddedOptions {
4
4
  /** Target context the devframe is registered into. Required. */
@@ -1,5 +1,5 @@
1
- import { r as DevframeDefinition } from "../devframe-BADsX91-.mjs";
2
- import "@modelcontextprotocol/sdk/server/index.js";
1
+ import { b as DevframeNodeContext, r as DevframeDefinition } from "../devframe-Dsjn_Xtq.mjs";
2
+ import "@modelcontextprotocol/server";
3
3
  //#region src/adapters/mcp/build-server.d.ts
4
4
  interface CreateMcpServerOptions {
5
5
  /**
@@ -37,4 +37,46 @@ interface McpServerHandle {
37
37
  */
38
38
  declare function createMcpServer(definition: DevframeDefinition, options?: CreateMcpServerOptions): Promise<McpServerHandle>;
39
39
  //#endregion
40
- export { type CreateMcpServerOptions, type McpServerHandle, createMcpServer };
40
+ //#region src/adapters/mcp/fetch.d.ts
41
+ interface CreateMcpFetchHandlerOptions {
42
+ /** Name reported in the MCP handshake. */
43
+ serverName: string;
44
+ /** Version reported in the MCP handshake. */
45
+ serverVersion: string;
46
+ /** Expose shared-state keys as MCP resources — see `buildMcpServerFromContext`. */
47
+ exposeSharedState: boolean | ((key: string) => boolean);
48
+ /**
49
+ * Origin allow-list beyond the loopback default. `false` disables the
50
+ * origin gate entirely. Default: loopback-only (mirrors the WS transport).
51
+ */
52
+ allowedOrigins?: readonly string[] | false;
53
+ }
54
+ interface McpFetchHandler {
55
+ /**
56
+ * WHATWG-`fetch` handler for the MCP Streamable-HTTP endpoint. Hand every
57
+ * method (POST/GET/DELETE) on the endpoint's path to it — routing by path
58
+ * is the host's job.
59
+ */
60
+ fetch: (request: Request) => Promise<Response>;
61
+ /** Tear down every live MCP session (closes servers, drops subscriptions). */
62
+ dispose: () => Promise<void>;
63
+ }
64
+ /**
65
+ * Build a framework-agnostic MCP Streamable-HTTP endpoint over a devframe
66
+ * context: a web-standard `Request → Response` handler any host can mount —
67
+ * h3 (see `mountMcpHttp`), a Next.js App Router route, or any other
68
+ * fetch-shaped server.
69
+ *
70
+ * Each MCP session gets its own {@link WebStandardStreamableHTTPServerTransport}
71
+ * and MCP server (built from the shared, live `ctx` via
72
+ * `buildMcpServerFromContext`), correlated by the `Mcp-Session-Id` header: an
73
+ * `initialize` POST spins up a session; later requests route to it; a `DELETE`
74
+ * (or client disconnect) tears it down. The origin gate applies devframe's
75
+ * loopback-default DNS-rebinding protection (identical semantics to the WS
76
+ * upgrade's `isAllowedOrigin`).
77
+ *
78
+ * @experimental
79
+ */
80
+ declare function createMcpFetchHandler(ctx: DevframeNodeContext, options: CreateMcpFetchHandlerOptions): McpFetchHandler;
81
+ //#endregion
82
+ export { type CreateMcpFetchHandlerOptions, type CreateMcpServerOptions, type McpFetchHandler, type McpServerHandle, createMcpFetchHandler, createMcpServer };
@@ -1,2 +1,2 @@
1
- import { n as createMcpServer } from "../build-server-cANnMwg0.mjs";
2
- export { createMcpServer };
1
+ import { n as createMcpServer, t as createMcpFetchHandler } from "../fetch-BZyK4v6W.mjs";
2
+ export { createMcpFetchHandler, createMcpServer };
@@ -0,0 +1,34 @@
1
+ //#region src/utils/agent-tool-name.ts
2
+ /**
3
+ * Maximum tool-name length several MCP clients enforce (the Anthropic API
4
+ * pattern is `^[a-zA-Z0-9_-]{1,128}$`).
5
+ */
6
+ const MAX_TOOL_NAME_LENGTH = 128;
7
+ /**
8
+ * Derive the wire-safe agent tool name for an internal tool id.
9
+ *
10
+ * Devframe tool ids are colon-namespaced — `devframe:<area>:<fn>` for
11
+ * built-ins, `devframes:plugin:<slug>:<fn>` for plugin RPCs, and hub
12
+ * command ids for command-derived tools. MCP clients constrain tool names
13
+ * to `^[a-zA-Z0-9_-]{1,128}$`, so the agent/MCP boundary derives the wire
14
+ * name automatically: every run of characters outside `[a-zA-Z0-9_-]`
15
+ * becomes a single `_`, truncated to 128 characters. Internal ids never
16
+ * change — resolution back to the id happens at the boundary.
17
+ *
18
+ * ```
19
+ * devframe:state:read → devframe_state_read
20
+ * devframes:plugin:git:status → devframes_plugin_git_status
21
+ * ```
22
+ *
23
+ * A plain string transform with no node dependency, so browser-side UIs
24
+ * that display a tool's id (e.g. the inspect plugin's agent view) can
25
+ * import it too and show the name a client actually calls.
26
+ *
27
+ * @experimental The agent-native surface is experimental and may change
28
+ * without a major version bump until it stabilizes.
29
+ */
30
+ function toAgentToolName(id) {
31
+ return id.replace(/[^\w-]+/g, "_").slice(0, MAX_TOOL_NAME_LENGTH);
32
+ }
33
+ //#endregion
34
+ export { toAgentToolName as t };
@@ -1,9 +1,8 @@
1
1
  import { n as colors } from "./diagnostics-reporter-CsIG85Q5.mjs";
2
2
  import { createBuild } from "./adapters/build.mjs";
3
- import { n as resolveDevServerPort, t as createDevServer } from "./dev-OJFr4rx8.mjs";
3
+ import { n as resolveDevServerPort, t as createDevServer } from "./dev-D4M7Veo7.mjs";
4
4
  import process from "node:process";
5
- import cac from "cac";
6
- import { safeParse } from "valibot";
5
+ import cac$1 from "cac";
7
6
  //#region src/adapters/flags.ts
8
7
  /**
9
8
  * Identity helper that preserves the literal schema-map type — use this
@@ -29,10 +28,12 @@ function defineCliFlags(flags) {
29
28
  return flags;
30
29
  }
31
30
  /**
32
- * Best-effort probe of a valibot schema to decide whether the
33
- * corresponding CAC option takes a value. Unwraps `optional` / `nullable`
34
- * / `nullish` / `default` / `pipe` wrappers then matches on the inner
35
- * type's kind.
31
+ * Best-effort, dependency-free probe of a schema to decide whether the
32
+ * corresponding CAC option takes a value. Duck-types the `type` /
33
+ * `wrapped` / `inner` / `pipe` fields exposed by valibot and by devframe's
34
+ * built-in `s` builder, unwrapping `optional` / `nullable` / `nullish` /
35
+ * `pipe` wrappers then matching on the inner kind. Validators that don't
36
+ * expose these fields (e.g. zod) fall through to a value-taking option.
36
37
  */
37
38
  function getSchemaKind(schema) {
38
39
  let current = schema;
@@ -46,7 +47,7 @@ function getSchemaKind(schema) {
46
47
  current = current.pipe[0];
47
48
  continue;
48
49
  }
49
- return kind;
50
+ return kind ?? "unknown";
50
51
  }
51
52
  return "unknown";
52
53
  }
@@ -54,14 +55,18 @@ function getSchemaKind(schema) {
54
55
  function isBooleanFlag(schema) {
55
56
  return getSchemaKind(schema) === "boolean";
56
57
  }
57
- /** Validate and coerce the raw cac-parsed bag against a {@link CliFlagsSchema}. */
58
+ /** Validate the raw cac-parsed bag against a {@link CliFlagsSchema}. */
58
59
  function parseCliFlags(schema, raw) {
59
60
  const flags = {};
60
61
  const issues = [];
61
62
  for (const [key, fieldSchema] of Object.entries(schema)) {
62
- const result = safeParse(fieldSchema, raw[key]);
63
- if (result.success) flags[key] = result.output;
64
- else issues.push(`--${toKebab(key)}: ${result.issues.map((i) => i.message).join(", ")}`);
63
+ const result = fieldSchema["~standard"].validate(raw[key]);
64
+ if (result instanceof Promise) {
65
+ issues.push(`--${toKebab(key)}: async flag validation is not supported`);
66
+ continue;
67
+ }
68
+ if (result.issues) issues.push(`--${toKebab(key)}: ${result.issues.map((i) => i.message).join(", ")}`);
69
+ else flags[key] = result.value;
65
70
  }
66
71
  for (const [key, value] of Object.entries(raw)) if (!(key in schema) && !(key in flags)) flags[key] = value;
67
72
  return issues.length ? {
@@ -87,7 +92,7 @@ function flagKeyToOption(camel) {
87
92
  function createCac(d, options = {}) {
88
93
  const defaultPort = options.defaultPort ?? d.cli?.port ?? 9999;
89
94
  const defaultHost = d.cli?.host ?? "localhost";
90
- const cli = cac(d.cli?.command ?? d.id);
95
+ const cli = cac$1(d.cli?.command ?? d.id);
91
96
  const devCommand = cli.command("[...args]", "Start a local dev server").option("--port <port>", "Port to listen on").option("--host <host>", "Host to bind to", { default: defaultHost }).option("--open", "Open the browser on start").option("--no-open", "Do not open the browser").option("--no-auth", "Disable the interactive authentication gate").option("--mcp", "Expose an MCP server over HTTP at /__mcp (use --no-mcp to disable) [experimental]");
92
97
  if (d.cli?.flags) for (const [key, schema] of Object.entries(d.cli.flags)) {
93
98
  const optionName = flagKeyToOption(key);
@@ -111,7 +116,7 @@ function createCac(d, options = {}) {
111
116
  onReady: options.onReady
112
117
  });
113
118
  });
114
- cli.command("build", "Build a self-contained static deploy of the devframe").option("--out-dir <outDir>", "Output directory", { default: "dist-static" }).option("--base <base>", "URL base", { default: "/" }).option("--pretty", "Pretty-print dump JSON (larger on disk)").action(async (flags) => {
119
+ if (d.capabilities?.build !== false) cli.command("build", "Build a self-contained static deploy of the devframe").option("--out-dir <outDir>", "Output directory", { default: "dist-static" }).option("--base <base>", "URL base", { default: "/" }).option("--pretty", "Pretty-print dump JSON (larger on disk)").action(async (flags) => {
115
120
  await createBuild(d, {
116
121
  outDir: flags.outDir,
117
122
  base: flags.base,
@@ -0,0 +1,11 @@
1
+ //#region src/cli/main.d.ts
2
+ /**
3
+ * The `devframe` bin — the framework's own CLI, distinct from the per-app
4
+ * CLI shells authors build with `createCac(definition)`. It hosts the
5
+ * app-independent commands; today that is `connect`, the MCP connector.
6
+ *
7
+ * @experimental
8
+ */
9
+ declare function runDevframeCli(argv?: string[]): Promise<void>;
10
+ //#endregion
11
+ export { runDevframeCli };
@@ -0,0 +1,283 @@
1
+ import { t as diagnostics } from "../diagnostics-B5-qHeqD.mjs";
2
+ import { n as probeDevframeOrigin, t as listLiveDevframeInstances } from "../instance-registry-D6fxYu36.mjs";
3
+ import { t as toAgentToolName } from "../agent-tool-name-C3b5vEwJ.mjs";
4
+ import { Diagnostic } from "nostics";
5
+ import process from "node:process";
6
+ import { joinURL } from "ufo";
7
+ import { cac } from "cac";
8
+ //#region src/cli/connect.ts
9
+ const INDEX_TOOL = toAgentToolName("devframe:connect:list-instances");
10
+ const CALL_TOOL = toAgentToolName("devframe:connect:call-tool");
11
+ const MCP_DISABLED_HINT = "This instance runs without an MCP route. Restart it with the --mcp flag (or set `cli.mcp: true` on its definition) to expose its tools, then list instances again.";
12
+ const GATEWAY_TOOLS = [{
13
+ name: INDEX_TOOL,
14
+ title: "Discover running devframes",
15
+ description: "Discover every running devframe dev server on this machine and list each one's MCP tools. Call this FIRST, before assuming which devtools are available — the result names the instance (id, project root, origin) and the port to pass to the call tool. Safe to call freely.",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {}
19
+ },
20
+ annotations: {
21
+ readOnlyHint: true,
22
+ destructiveHint: false
23
+ }
24
+ }, {
25
+ name: CALL_TOOL,
26
+ title: "Call a devframe tool",
27
+ description: "Invoke one MCP tool on one running devframe instance discovered via the list-instances tool. Pass the instance's port, the tool name, and the tool's arguments object.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ port: {
32
+ type: "number",
33
+ description: "The instance's port, from the list-instances tool."
34
+ },
35
+ tool: {
36
+ type: "string",
37
+ description: "Tool name, from the instance's tool list."
38
+ },
39
+ args: {
40
+ type: "object",
41
+ description: "Arguments object for the tool. Omit for zero-argument tools."
42
+ }
43
+ },
44
+ required: ["port", "tool"],
45
+ additionalProperties: false
46
+ }
47
+ }];
48
+ /**
49
+ * Start the devframe MCP connector on stdio: a thin discovery + proxy server
50
+ * in the shape Vercel's next-devtools-mcp (https://github.com/vercel/next-devtools-mcp)
51
+ * validated — credit due there for the architecture this connector follows.
52
+ * It exposes two gateway tools —
53
+ * `devframe_connect_list-instances` (discover running devframe instances via
54
+ * the instance registry and list each one's MCP tools) and
55
+ * `devframe_connect_call-tool` (invoke one tool on one instance over its
56
+ * Streamable-HTTP endpoint) — and holds no domain knowledge of its own.
57
+ *
58
+ * @experimental
59
+ */
60
+ async function startConnectServer(options = {}) {
61
+ const sdk = await importSdk();
62
+ const server = new sdk.Server({
63
+ name: "devframe-connect",
64
+ version: "0.0.0"
65
+ }, { capabilities: { tools: {} } });
66
+ server.setRequestHandler("tools/list", async () => ({ tools: GATEWAY_TOOLS }));
67
+ server.setRequestHandler("tools/call", async (request) => {
68
+ const { name, arguments: args } = request.params;
69
+ try {
70
+ if (name === INDEX_TOOL) return textResult(await index(sdk, options));
71
+ if (name === CALL_TOOL) return textResult(await call(sdk, options, args ?? {}));
72
+ return errorResult({
73
+ message: `unknown tool "${name}"`,
74
+ fix: `Call ${INDEX_TOOL} or ${CALL_TOOL}.`
75
+ });
76
+ } catch (error) {
77
+ return errorResult(toErrorPayload(error));
78
+ }
79
+ });
80
+ const transport = new sdk.StdioServerTransport();
81
+ await server.connect(transport);
82
+ return { stop: async () => {
83
+ await server.close();
84
+ } };
85
+ }
86
+ async function importSdk() {
87
+ try {
88
+ const [serverMod, stdioMod, clientMod] = await Promise.all([
89
+ import("@modelcontextprotocol/server"),
90
+ import("@modelcontextprotocol/server/stdio"),
91
+ import("@modelcontextprotocol/client")
92
+ ]);
93
+ return {
94
+ Server: serverMod.Server,
95
+ StdioServerTransport: stdioMod.StdioServerTransport,
96
+ Client: clientMod.Client,
97
+ StreamableHTTPClientTransport: clientMod.StreamableHTTPClientTransport
98
+ };
99
+ } catch (error) {
100
+ const reason = error instanceof Error ? error.message : String(error);
101
+ throw diagnostics.DF0046({
102
+ reason,
103
+ cause: error
104
+ });
105
+ }
106
+ }
107
+ /** Discover instances: registry (prune-on-read) + explicit port probes. */
108
+ async function index(sdk, options) {
109
+ const { live } = await listLiveDevframeInstances({
110
+ instancesDir: options.instancesDir,
111
+ timeoutMs: options.timeoutMs
112
+ });
113
+ const records = [...live];
114
+ for (const port of options.ports ?? []) {
115
+ if (records.some((r) => r.port === port)) continue;
116
+ const probed = await probePort(port, options.timeoutMs);
117
+ if (probed) records.push(probed);
118
+ }
119
+ const instances = await Promise.all(records.map(async (record) => {
120
+ const { mcp, ...rest } = record;
121
+ const entry = {
122
+ ...rest,
123
+ mcp: null
124
+ };
125
+ if (!mcp) {
126
+ entry.hint = MCP_DISABLED_HINT;
127
+ return entry;
128
+ }
129
+ const url = `${record.origin}${mcp.path}`;
130
+ try {
131
+ entry.mcp = {
132
+ url,
133
+ tools: await listInstanceTools(sdk, url)
134
+ };
135
+ } catch (error) {
136
+ entry.mcp = {
137
+ url,
138
+ error: error instanceof Error ? error.message : String(error)
139
+ };
140
+ }
141
+ return entry;
142
+ }));
143
+ return {
144
+ instances,
145
+ ...instances.length === 0 ? { hint: "No running devframe instances found. Start a devframe dev server (with --mcp for tools), or pass --port <n> to devframe connect if the instance predates the registry." } : {}
146
+ };
147
+ }
148
+ /**
149
+ * Probe an explicit port for a devframe serving `__connection.json` at `/`,
150
+ * reusing the registry's origin-candidate probe (a `localhost`-bound server
151
+ * may listen on either address family).
152
+ */
153
+ async function probePort(port, timeoutMs) {
154
+ const probed = await probeDevframeOrigin(`http://localhost:${port}`, "/", timeoutMs);
155
+ if (!probed) return null;
156
+ const mcpPath = probed.meta.mcp ? joinURL("/", probed.meta.mcp.path) : null;
157
+ return {
158
+ pid: -1,
159
+ port,
160
+ origin: probed.origin,
161
+ basePath: "/",
162
+ id: `port-${port}`,
163
+ rootDir: "",
164
+ mcp: mcpPath ? { path: mcpPath } : null,
165
+ startedAt: 0
166
+ };
167
+ }
168
+ async function listInstanceTools(sdk, url) {
169
+ return withInstanceClient(sdk, url, async (client) => {
170
+ return (await client.listTools()).tools.map((tool) => ({
171
+ name: tool.name,
172
+ description: tool.description
173
+ }));
174
+ });
175
+ }
176
+ async function call(sdk, options, args) {
177
+ if (typeof args.port !== "number" || typeof args.tool !== "string") throw diagnostics.DF0049();
178
+ const { live } = await listLiveDevframeInstances({
179
+ instancesDir: options.instancesDir,
180
+ timeoutMs: options.timeoutMs
181
+ });
182
+ const record = live.find((r) => r.port === args.port) ?? await probePort(args.port, options.timeoutMs);
183
+ if (!record) throw diagnostics.DF0050({ port: args.port });
184
+ if (!record.mcp) throw diagnostics.DF0051({ port: args.port });
185
+ return withInstanceClient(sdk, `${record.origin}${record.mcp.path}`, async (client) => {
186
+ const result = await client.callTool({
187
+ name: args.tool,
188
+ arguments: args.args ?? {}
189
+ });
190
+ return {
191
+ instance: {
192
+ id: record.id,
193
+ port: record.port
194
+ },
195
+ tool: args.tool,
196
+ isError: result.isError ?? false,
197
+ content: result.content,
198
+ ...result.structuredContent ? { structuredContent: result.structuredContent } : {}
199
+ };
200
+ });
201
+ }
202
+ async function withInstanceClient(sdk, url, fn) {
203
+ const transport = new sdk.StreamableHTTPClientTransport(new URL(url));
204
+ const client = new sdk.Client({
205
+ name: "devframe-connect",
206
+ version: "0.0.0"
207
+ });
208
+ await client.connect(transport);
209
+ try {
210
+ return await fn(client);
211
+ } finally {
212
+ await client.close().catch(() => {});
213
+ }
214
+ }
215
+ function textResult(value) {
216
+ return { content: [{
217
+ type: "text",
218
+ text: JSON.stringify(value, null, 2)
219
+ }] };
220
+ }
221
+ /**
222
+ * Project a thrown value into the connector's structured error payload. A
223
+ * nostics `Diagnostic` carries its code, `fix`, and docs URL across so the
224
+ * calling agent gets the actionable next step.
225
+ */
226
+ function toErrorPayload(error) {
227
+ if (error instanceof Diagnostic) return {
228
+ code: error.code,
229
+ message: error.message,
230
+ ...error.fix ? { fix: error.fix } : {},
231
+ ...error.docs ? { docs: error.docs } : {}
232
+ };
233
+ return {
234
+ message: error instanceof Error ? error.message : String(error),
235
+ ...error && typeof error === "object" && "fix" in error && typeof error.fix === "string" ? { fix: error.fix } : {}
236
+ };
237
+ }
238
+ function errorResult(error) {
239
+ return {
240
+ isError: true,
241
+ content: [{
242
+ type: "text",
243
+ text: JSON.stringify({ error }, null, 2)
244
+ }]
245
+ };
246
+ }
247
+ /** Parse the repeatable `--port` flag value(s) from cac into numbers. */
248
+ function parsePortsFlag(value) {
249
+ return (Array.isArray(value) ? value : value === void 0 ? [] : [value]).map((v) => Number(v)).filter((n) => Number.isInteger(n) && n > 0 && n < 65536);
250
+ }
251
+ /** Keep the connector process alive until the stdio transport closes it. */
252
+ function keepAlive() {
253
+ process.stdin.resume();
254
+ }
255
+ //#endregion
256
+ //#region src/cli/main.ts
257
+ /**
258
+ * The `devframe` bin — the framework's own CLI, distinct from the per-app
259
+ * CLI shells authors build with `createCac(definition)`. It hosts the
260
+ * app-independent commands; today that is `connect`, the MCP connector.
261
+ *
262
+ * @experimental
263
+ */
264
+ async function runDevframeCli(argv = process.argv) {
265
+ const cli = cac("devframe");
266
+ cli.command("connect", "Run the devframe MCP connector on stdio (discovers running devframe dev servers and proxies their tools)").option("--port <port>", "Probe an explicit port besides the instance registry (repeatable)").option("--instances-dir <dir>", "Override the instance registry directory (default: ~/.devframe/instances, or $DEVFRAME_INSTANCES_DIR)").option("--timeout <ms>", "Probe timeout per instance in milliseconds", { default: 1e3 }).action(async (options) => {
267
+ await startConnectServer({
268
+ ports: parsePortsFlag(options.port),
269
+ instancesDir: options.instancesDir,
270
+ timeoutMs: options.timeout
271
+ });
272
+ keepAlive();
273
+ });
274
+ cli.help();
275
+ cli.parse(argv, { run: false });
276
+ if (!cli.matchedCommand) {
277
+ if (!cli.options.help) cli.outputHelp();
278
+ return;
279
+ }
280
+ await cli.runMatchedCommand();
281
+ }
282
+ //#endregion
283
+ export { runDevframeCli };
@@ -1,9 +1,42 @@
1
- import { $ as DevframeRpcServerFunctions, F as ScopedSharedStates, I as SettingsForNamespace, J as RpcSharedStateHost, N as ScopedRpcFn, O as DevframeSettings, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, _ as ConnectionMeta, at as StreamReader, ht as SharedState, kt as EventEmitter, ot as StreamSink, q as RpcSharedStateGetOptions } from "../devframe-BADsX91-.mjs";
2
- import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-CrzNxXKq.mjs";
3
- import { C as RpcCacheManager, w as RpcCacheOptions } from "../index-DgsLFhZg.mjs";
4
- import { t as WsRpcChannelOptions } from "../ws-client-DSXiI2h7.mjs";
1
+ import { $ as DevframeRpcServerFunctions, F as ScopedSharedStates, I as SettingsForNamespace, J as RpcSharedStateHost, N as ScopedRpcFn, O as DevframeSettings, P as ScopedServerFunctions, Q as DevframeRpcClientFunctions, _ as ConnectionMeta, at as StreamReader, ht as SharedState, jt as EventEmitter, ot as StreamSink, q as RpcSharedStateGetOptions } from "../devframe-Dsjn_Xtq.mjs";
2
+ import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-CnJSgRVa.mjs";
3
+ import { E as RpcCacheOptions, T as RpcCacheManager } from "../index-Dbw1p5ch.mjs";
4
+ import { t as WsRpcChannelOptions } from "../ws-client-B6Tnr0ic.mjs";
5
5
  import { BirpcOptions, BirpcReturn } from "birpc";
6
6
  //#region src/client/connection.d.ts
7
+ interface DevframeConnection {
8
+ /** Server-advertised transport and serialization metadata. */
9
+ connectionMeta: ConnectionMeta;
10
+ /**
11
+ * Absolute URL of the `__connection.json` that produced
12
+ * {@link connectionMeta}. Relative transport paths and side-car ports are
13
+ * resolved from this URL, rather than from an external viewer's location.
14
+ */
15
+ metaBaseUrl: string;
16
+ /** Previously issued bearer token, when the connection is already trusted. */
17
+ authToken?: string;
18
+ }
19
+ interface SetupDevframeConnectionOptions {
20
+ /** Reuse a complete connection prepared in another viewer or JavaScript realm. */
21
+ connection?: DevframeConnection;
22
+ /** Use a pre-known descriptor while deriving its source URL from `baseURL`. */
23
+ connectionMeta?: ConnectionMeta;
24
+ /** Base URL, or fallback list, used to locate `__connection.json`. */
25
+ baseURL?: string | string[];
26
+ /** Override the locally stored auth token. */
27
+ authToken?: string;
28
+ }
29
+ /**
30
+ * Return connection information previously prepared in this window or an
31
+ * accessible parent window.
32
+ */
33
+ declare function getDevframeConnection(): DevframeConnection | undefined;
34
+ /**
35
+ * Prepare the connection information shared by a devframe client and external
36
+ * viewers. Reuses an explicit or previously prepared connection before
37
+ * fetching `__connection.json` from the configured base URLs.
38
+ */
39
+ declare function setupDevframeConnection(options?: SetupDevframeConnectionOptions): Promise<DevframeConnection>;
7
40
  /**
8
41
  * The connection lifecycle of a devframe client, as a single value a UI can
9
42
  * render from. Derived from the transport (WebSocket open/close/error) and the
@@ -183,9 +216,7 @@ interface RpcClientEvents {
183
216
  */
184
217
  'rpc:error': (error: Error, method: string) => void;
185
218
  }
186
- interface DevframeRpcClientOptions {
187
- connectionMeta?: ConnectionMeta;
188
- baseURL?: string | string[];
219
+ interface DevframeRpcClientOptions extends SetupDevframeConnectionOptions {
189
220
  /**
190
221
  * The auth token to use for the client
191
222
  */
@@ -250,7 +281,12 @@ interface DevframeRpcClient {
250
281
  */
251
282
  readonly connectionError: Error | null;
252
283
  /**
253
- * The connection meta
284
+ * The complete connection used by this client, including the metadata source
285
+ * URL external viewers use to resolve relative resources.
286
+ */
287
+ readonly connection: DevframeConnection;
288
+ /**
289
+ * The server-advertised connection metadata.
254
290
  */
255
291
  readonly connectionMeta: ConnectionMeta;
256
292
  /**
@@ -376,4 +412,4 @@ declare function createClientSettings<T extends Record<string, any> = Record<str
376
412
  //#region src/client/index.d.ts
377
413
  declare const connectDevframe: typeof getDevframeRpcClient;
378
414
  //#endregion
379
- export { DevframeClientRpcHost, DevframeConnectionError, DevframeConnectionErrorKind, DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientCall, DevframeRpcClientCallEvent, DevframeRpcClientCallOptional, DevframeRpcClientMode, DevframeRpcClientOptions, DevframeRpcContext, DevframeScopedClientContext, DevframeScopedClientRpc, DevframeScopedClientStreamingHost, RpcClientEvents, RpcStreamingClientHost, StreamingSubscribeOptions, authenticateWithUrlOtp, connectDevframe, consumeOtpFromUrl, createClientSettings, createRpcStreamingClientHost, createScopedClientContext, getDevframeRpcClient, isCallableStatus, readOtpFromUrl };
415
+ export { DevframeClientRpcHost, DevframeConnection, DevframeConnectionError, DevframeConnectionErrorKind, DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientCall, DevframeRpcClientCallEvent, DevframeRpcClientCallOptional, DevframeRpcClientMode, DevframeRpcClientOptions, DevframeRpcContext, DevframeScopedClientContext, DevframeScopedClientRpc, DevframeScopedClientStreamingHost, RpcClientEvents, RpcStreamingClientHost, SetupDevframeConnectionOptions, StreamingSubscribeOptions, authenticateWithUrlOtp, connectDevframe, consumeOtpFromUrl, createClientSettings, createRpcStreamingClientHost, createScopedClientContext, getDevframeConnection, getDevframeRpcClient, isCallableStatus, readOtpFromUrl, setupDevframeConnection };