devframe 0.7.16 → 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.
- package/bin/devframe.mjs +8 -0
- package/dist/adapters/build.d.mts +1 -1
- package/dist/adapters/build.mjs +4 -4
- package/dist/adapters/cac.d.mts +1 -1
- package/dist/adapters/cac.mjs +1 -1
- package/dist/adapters/cli.d.mts +1 -1
- package/dist/adapters/cli.mjs +1 -1
- package/dist/adapters/dev.d.mts +2 -2
- package/dist/adapters/dev.mjs +1 -1
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/mcp.d.mts +45 -3
- package/dist/adapters/mcp.mjs +2 -2
- package/dist/agent-tool-name-C3b5vEwJ.mjs +34 -0
- package/dist/{cac-CPaOy3LD.mjs → cac-B-R_tDGJ.mjs} +18 -13
- package/dist/cli/main.d.mts +11 -0
- package/dist/cli/main.mjs +283 -0
- package/dist/client/index.d.mts +4 -4
- package/dist/client/index.mjs +88 -4
- package/dist/{context-CzqPrJSz.d.mts → context-7BbaIUSI.d.mts} +1 -1
- package/dist/{context-DY72brRI.mjs → context-C9Cgm1hP.mjs} +1 -1
- package/dist/{dev-BCeaQuGE.mjs → dev-D4M7Veo7.mjs} +22 -11
- package/dist/{devframe-Ckhf3kFw.d.mts → devframe-Dsjn_Xtq.d.mts} +68 -15
- package/dist/diagnostics-B5-qHeqD.mjs +94 -0
- package/dist/{diagnostics-BXwBQmoN.mjs → diagnostics-hwjXp_UV.mjs} +8 -0
- package/dist/{dump-Cz7yVvsB.mjs → dump-CgZShDRB.mjs} +82 -6
- package/dist/{build-server-C7vTKjiQ.mjs → fetch-BZyK4v6W.mjs} +225 -51
- package/dist/helpers/vite.d.mts +1 -1
- package/dist/helpers/vite.mjs +2 -2
- package/dist/{host-h3-CgOetlNE.mjs → host-h3-Kz7t5Xab.mjs} +83 -28
- package/dist/http-BghOqfTD.mjs +38 -0
- package/dist/{index-8dndvIxR.d.mts → index-CeBbry0R.d.mts} +2 -2
- package/dist/{index-DgsLFhZg.d.mts → index-Dbw1p5ch.d.mts} +27 -3
- package/dist/{index-YDSJgsyG.d.mts → index-vAX6bYuc.d.mts} +1 -1
- package/dist/index.d.mts +5 -5
- package/dist/instance-registry-D6fxYu36.mjs +190 -0
- package/dist/node/auth.d.mts +2 -2
- package/dist/node/hub-internals.d.mts +2 -2
- package/dist/node/hub-internals.mjs +1 -1
- package/dist/node/index.d.mts +104 -6
- package/dist/node/index.mjs +5 -4
- package/dist/recipes/common-rpc-functions.d.mts +33 -33
- package/dist/recipes/common-rpc-functions.mjs +6 -6
- package/dist/recipes/interactive-auth.d.mts +2 -2
- package/dist/recipes/interactive-auth.mjs +13 -13
- package/dist/rpc/dump.d.mts +1 -1
- package/dist/rpc/dump.mjs +1 -1
- package/dist/rpc/index.d.mts +3 -3
- package/dist/rpc/index.mjs +4 -4
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-client.mjs +1 -1
- package/dist/rpc/transports/ws-server.d.mts +1 -1
- package/dist/rpc/transports/ws-server.mjs +1 -1
- package/dist/{serialization-DpLXCy13.mjs → serialization-C8Mnw9hK.mjs} +1 -1
- package/dist/{server-DykHCza_.mjs → server-CcAPxuoT.mjs} +2 -2
- package/dist/{server-B0wiP_Oi.d.mts → server-VLQJouOO.d.mts} +2 -2
- package/dist/simple-schema-BDzLeJDk.d.mts +102 -0
- package/dist/simple-schema-DQPZrAaZ.mjs +154 -0
- package/dist/{storage-gEnj9ASQ.mjs → storage-Dzoc3NVC.mjs} +2 -66
- package/dist/{transports-BmDVn4SF.mjs → transports-vhizgqXM.mjs} +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-CrzNxXKq.d.mts → types-CnJSgRVa.d.mts} +33 -19
- package/dist/utils/agent-tool-name.d.mts +27 -0
- package/dist/utils/agent-tool-name.mjs +34 -0
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/simple-schema.d.mts +2 -0
- package/dist/utils/simple-schema.mjs +154 -0
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/dist/{ws-client-DSXiI2h7.d.mts → ws-client-B6Tnr0ic.d.mts} +1 -1
- package/dist/{ws-server-9-wn7MNQ.d.mts → ws-server-D_Vjtums.d.mts} +1 -1
- package/package.json +18 -7
- package/skills/devframe/SKILL.md +1 -1
- package/dist/http-_pPbryTQ.mjs +0 -114
package/bin/devframe.mjs
ADDED
package/dist/adapters/build.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { t as collectStaticRpcDump } from "../dump-
|
|
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-
|
|
4
|
+
import { n as strictJsonStringify } from "../serialization-C8Mnw9hK.mjs";
|
|
5
5
|
import { n as structuredCloneStringify } from "../structured-clone-CbAV5rFI.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { n as createHostContext, t as createH3DevframeHost } from "../host-h3-Kz7t5Xab.mjs";
|
|
7
|
+
import { t as diagnostics } from "../diagnostics-B5-qHeqD.mjs";
|
|
8
8
|
import { n as resolveBasePath } from "../_shared-bWRzeSa0.mjs";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { dirname, resolve } from "pathe";
|
package/dist/adapters/cac.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ft as
|
|
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
|
package/dist/adapters/cac.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-
|
|
1
|
+
import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-B-R_tDGJ.mjs";
|
|
2
2
|
export { createCac, defineCliFlags, parseCliFlags };
|
package/dist/adapters/cli.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ft as
|
|
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. */
|
package/dist/adapters/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as defineCliFlags, r as parseCliFlags, t as createCac } from "../cac-
|
|
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;
|
package/dist/adapters/dev.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as ConnectionMeta, d as McpRouteOptions, p as DevframeAuthHandler, r as DevframeDefinition, u as DevframeWsOptions } from "../devframe-
|
|
2
|
-
import { n as StartedServer } from "../server-
|
|
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 {
|
package/dist/adapters/dev.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as resolveDevServerPort, r as resolveMcpConnectionMeta, t as createDevServer } from "../dev-
|
|
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-
|
|
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. */
|
package/dist/adapters/mcp.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as DevframeDefinition } from "../devframe-
|
|
2
|
-
import "@modelcontextprotocol/
|
|
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
|
-
|
|
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 };
|
package/dist/adapters/mcp.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as createMcpServer } from "../
|
|
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-
|
|
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
|
|
33
|
-
* corresponding CAC option takes a value.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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 =
|
|
63
|
-
if (result
|
|
64
|
-
|
|
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);
|
|
@@ -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 };
|
package/dist/client/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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,
|
|
2
|
-
import { _ as RpcFunctionDefinition, w as RpcFunctionsCollector } from "../types-
|
|
3
|
-
import {
|
|
4
|
-
import { t as WsRpcChannelOptions } from "../ws-client-
|
|
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
7
|
interface DevframeConnection {
|