devframe 0.7.16 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/bin/devframe.mjs +8 -0
  2. package/dist/adapters/build.d.mts +1 -1
  3. package/dist/adapters/build.mjs +4 -4
  4. package/dist/adapters/cac.d.mts +1 -1
  5. package/dist/adapters/cac.mjs +1 -1
  6. package/dist/adapters/cli.d.mts +1 -1
  7. package/dist/adapters/cli.mjs +1 -1
  8. package/dist/adapters/dev.d.mts +2 -2
  9. package/dist/adapters/dev.mjs +1 -1
  10. package/dist/adapters/embedded.d.mts +1 -1
  11. package/dist/adapters/mcp.d.mts +51 -3
  12. package/dist/adapters/mcp.mjs +2 -2
  13. package/dist/agent-tool-name-C3b5vEwJ.mjs +34 -0
  14. package/dist/{cac-CPaOy3LD.mjs → cac-B1LM3zit.mjs} +18 -13
  15. package/dist/cli/main.d.mts +11 -0
  16. package/dist/cli/main.mjs +284 -0
  17. package/dist/client/index.d.mts +40 -11
  18. package/dist/client/index.mjs +129 -19
  19. package/dist/constants.d.mts +13 -6
  20. package/dist/constants.mjs +13 -6
  21. package/dist/{context-DY72brRI.mjs → context-CQefP2jR.mjs} +3 -2
  22. package/dist/{context-CzqPrJSz.d.mts → context-DFzmxCLa.d.mts} +1 -1
  23. package/dist/{revoke-BtQDKTp7.mjs → crypto-token-XCqTSMg9.mjs} +1 -35
  24. package/dist/{dev-BCeaQuGE.mjs → dev-BSHFZGZr.mjs} +22 -11
  25. package/dist/{devframe-Ckhf3kFw.d.mts → devframe-qgCKL683.d.mts} +77 -15
  26. package/dist/diagnostics-B5-qHeqD.mjs +94 -0
  27. package/dist/{diagnostics-BXwBQmoN.mjs → diagnostics-hwjXp_UV.mjs} +8 -0
  28. package/dist/{dump-Cz7yVvsB.mjs → dump-CgZShDRB.mjs} +82 -6
  29. package/dist/fetch-CK0S253E.mjs +534 -0
  30. package/dist/helpers/vite.d.mts +15 -14
  31. package/dist/helpers/vite.mjs +8 -8
  32. package/dist/{host-h3-CgOetlNE.mjs → host-h3-Kz7t5Xab.mjs} +83 -28
  33. package/dist/http-Dvgxujcu.mjs +38 -0
  34. package/dist/{index-8dndvIxR.d.mts → index-B7RgLw--.d.mts} +11 -6
  35. package/dist/{index-DgsLFhZg.d.mts → index-Dbw1p5ch.d.mts} +27 -3
  36. package/dist/{index-YDSJgsyG.d.mts → index-vAX6bYuc.d.mts} +1 -1
  37. package/dist/index.d.mts +5 -5
  38. package/dist/instance-registry-D6fxYu36.mjs +190 -0
  39. package/dist/node/auth.d.mts +2 -2
  40. package/dist/node/auth.mjs +2 -2
  41. package/dist/node/hub-internals.d.mts +2 -2
  42. package/dist/node/hub-internals.mjs +1 -1
  43. package/dist/node/index.d.mts +124 -6
  44. package/dist/node/index.mjs +5 -4
  45. package/dist/recipes/common-rpc-functions.d.mts +33 -33
  46. package/dist/recipes/common-rpc-functions.mjs +6 -6
  47. package/dist/recipes/interactive-auth.d.mts +2 -2
  48. package/dist/recipes/interactive-auth.mjs +23 -15
  49. package/dist/revoke-DZbJ7Cl0.mjs +35 -0
  50. package/dist/rpc/dump.d.mts +1 -1
  51. package/dist/rpc/dump.mjs +1 -1
  52. package/dist/rpc/index.d.mts +3 -3
  53. package/dist/rpc/index.mjs +4 -4
  54. package/dist/rpc/transports/ws-client.d.mts +1 -1
  55. package/dist/rpc/transports/ws-client.mjs +1 -1
  56. package/dist/rpc/transports/ws-server.d.mts +2 -2
  57. package/dist/rpc/transports/ws-server.mjs +50 -3
  58. package/dist/{serialization-DpLXCy13.mjs → serialization-C8Mnw9hK.mjs} +1 -1
  59. package/dist/{server-DykHCza_.mjs → server-CXaDDUl0.mjs} +2 -2
  60. package/dist/{server-B0wiP_Oi.d.mts → server-E9z4ZWbd.d.mts} +4 -3
  61. package/dist/simple-schema-BDzLeJDk.d.mts +102 -0
  62. package/dist/simple-schema-DQPZrAaZ.mjs +154 -0
  63. package/dist/{state-WX3HT5a3.mjs → state-BeUHDDjk.mjs} +13 -6
  64. package/dist/{storage-gEnj9ASQ.mjs → storage-Dzoc3NVC.mjs} +2 -66
  65. package/dist/{transports-BmDVn4SF.mjs → transports-vhizgqXM.mjs} +1 -1
  66. package/dist/types/index.d.mts +4 -4
  67. package/dist/{types-CrzNxXKq.d.mts → types-CnJSgRVa.d.mts} +33 -19
  68. package/dist/utils/agent-tool-name.d.mts +27 -0
  69. package/dist/utils/agent-tool-name.mjs +34 -0
  70. package/dist/utils/events.d.mts +1 -1
  71. package/dist/utils/shared-state.d.mts +1 -1
  72. package/dist/utils/simple-schema.d.mts +2 -0
  73. package/dist/utils/simple-schema.mjs +154 -0
  74. package/dist/utils/streaming-channel.d.mts +1 -1
  75. package/dist/{ws-client-DSXiI2h7.d.mts → ws-client-B6Tnr0ic.d.mts} +1 -1
  76. package/dist/{ws-server-9-wn7MNQ.d.mts → ws-server-J-zkiOyp.d.mts} +24 -3
  77. package/package.json +18 -7
  78. package/skills/devframe/SKILL.md +4 -3
  79. package/dist/build-server-C7vTKjiQ.mjs +0 -359
  80. package/dist/http-_pPbryTQ.mjs +0 -114
@@ -0,0 +1,154 @@
1
+ //#region src/utils/simple-schema.ts
2
+ function ok(value) {
3
+ return { value };
4
+ }
5
+ function fail(message, path) {
6
+ return { issues: [path ? {
7
+ message,
8
+ path
9
+ } : { message }] };
10
+ }
11
+ function make(type, validate, extra) {
12
+ return {
13
+ type,
14
+ ...extra,
15
+ "~standard": {
16
+ version: 1,
17
+ vendor: "devframe",
18
+ validate
19
+ }
20
+ };
21
+ }
22
+ /** Run a Standard Schema synchronously, rejecting async validators. */
23
+ function runSync(schema, value) {
24
+ const result = schema["~standard"].validate(value);
25
+ if (result instanceof Promise) throw new TypeError("[devframe/utils/simple-schema] async validators are not supported inside object()/optional()/nullable()");
26
+ return result;
27
+ }
28
+ /** Any string. */
29
+ function string() {
30
+ return make("string", (v) => typeof v === "string" ? ok(v) : fail("Expected a string"));
31
+ }
32
+ /** A finite number (rejects `NaN`). */
33
+ function number() {
34
+ return make("number", (v) => typeof v === "number" && !Number.isNaN(v) ? ok(v) : fail("Expected a number"));
35
+ }
36
+ /** A boolean. */
37
+ function boolean() {
38
+ return make("boolean", (v) => typeof v === "boolean" ? ok(v) : fail("Expected a boolean"));
39
+ }
40
+ /** `undefined` — mirrors valibot's `void`. */
41
+ function voidType() {
42
+ return make("void", (v) => v === void 0 ? ok(void 0) : fail("Expected undefined"));
43
+ }
44
+ /** `null`. */
45
+ function nullType() {
46
+ return make("null", (v) => v === null ? ok(null) : fail("Expected null"));
47
+ }
48
+ /** One of a fixed set of literal values. */
49
+ function picklist(values) {
50
+ const set = new Set(values);
51
+ return make("picklist", (v) => set.has(v) ? ok(v) : fail(`Expected one of: ${values.join(", ")}`), { values });
52
+ }
53
+ /** A single literal value (string / number / boolean). */
54
+ function literal(value) {
55
+ return make("literal", (v) => v === value ? ok(v) : fail(`Expected ${JSON.stringify(value)}`), { value });
56
+ }
57
+ /** A value matching any one of the given schemas. */
58
+ function union(options) {
59
+ return make("union", (v) => {
60
+ const issues = [];
61
+ for (const option of options) {
62
+ const result = runSync(option, v);
63
+ if (!result.issues) return ok(v);
64
+ issues.push(...result.issues);
65
+ }
66
+ return { issues };
67
+ }, { options });
68
+ }
69
+ /** A record with string keys whose values each satisfy the value schema. */
70
+ function record(_key, value) {
71
+ return make("record", (v) => {
72
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return fail("Expected an object");
73
+ const obj = v;
74
+ const issues = [];
75
+ for (const key of Object.keys(obj)) {
76
+ const result = runSync(value, obj[key]);
77
+ if (result.issues) for (const issue of result.issues) issues.push({
78
+ message: issue.message,
79
+ path: [key, ...issue.path ?? []]
80
+ });
81
+ }
82
+ return issues.length ? { issues } : ok(v);
83
+ });
84
+ }
85
+ /** An array whose every element satisfies the item schema. */
86
+ function array(item) {
87
+ return make("array", (v) => {
88
+ if (!Array.isArray(v)) return fail("Expected an array");
89
+ const issues = [];
90
+ for (let i = 0; i < v.length; i++) {
91
+ const result = runSync(item, v[i]);
92
+ if (result.issues) for (const issue of result.issues) issues.push({
93
+ message: issue.message,
94
+ path: [i, ...issue.path ?? []]
95
+ });
96
+ }
97
+ return issues.length ? { issues } : ok(v);
98
+ });
99
+ }
100
+ /** An object whose known keys each satisfy their schema (extra keys are kept). */
101
+ function object(shape) {
102
+ const entries = Object.entries(shape);
103
+ return make("object", (v) => {
104
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return fail("Expected an object");
105
+ const obj = v;
106
+ const issues = [];
107
+ for (const [key, schema] of entries) {
108
+ const result = runSync(schema, obj[key]);
109
+ if (result.issues) for (const issue of result.issues) issues.push({
110
+ message: issue.message,
111
+ path: [key, ...issue.path ?? []]
112
+ });
113
+ }
114
+ return issues.length ? { issues } : ok(v);
115
+ });
116
+ }
117
+ /** Allow `undefined` in addition to the inner schema. */
118
+ function optional(inner) {
119
+ return make("optional", (v) => v === void 0 ? ok(void 0) : runSync(inner, v), { wrapped: inner });
120
+ }
121
+ /** Allow `null` in addition to the inner schema. */
122
+ function nullable(inner) {
123
+ return make("nullable", (v) => v === null ? ok(null) : runSync(inner, v), { wrapped: inner });
124
+ }
125
+ /** Attach a human-readable description (used for CLI option help). */
126
+ function describe(schema, description) {
127
+ return {
128
+ ...schema,
129
+ description
130
+ };
131
+ }
132
+ /**
133
+ * Grouped access to every builder — `s.string()`, `s.object({ ... })`,
134
+ * `s.void()`, etc. Handy for a valibot-like `import { s } from
135
+ * 'devframe/utils/simple-schema'` call site.
136
+ */
137
+ const s = {
138
+ string,
139
+ number,
140
+ boolean,
141
+ void: voidType,
142
+ null: nullType,
143
+ literal,
144
+ picklist,
145
+ union,
146
+ record,
147
+ array,
148
+ object,
149
+ optional,
150
+ nullable,
151
+ describe
152
+ };
153
+ //#endregion
154
+ export { array, boolean, describe, literal, nullType, nullable, number, object, optional, picklist, record, s, string, union, voidType };
@@ -1,2 +1,2 @@
1
- import { at as StreamReader, ct as createStreamReader, it as StreamErrorPayload, lt as createStreamSink, nt as CreateStreamReaderOptions, ot as StreamSink, rt as CreateStreamSinkOptions, st as StreamSinkEvents, tt as BufferedChunk } from "../devframe-Ckhf3kFw.mjs";
1
+ import { at as StreamReader, ct as createStreamReader, it as StreamErrorPayload, lt as createStreamSink, nt as CreateStreamReaderOptions, ot as StreamSink, rt as CreateStreamSinkOptions, st as StreamSinkEvents, tt as BufferedChunk } from "../devframe-qgCKL683.mjs";
2
2
  export { BufferedChunk, CreateStreamReaderOptions, CreateStreamSinkOptions, StreamErrorPayload, StreamReader, StreamSink, StreamSinkEvents, createStreamReader, createStreamSink };
@@ -1,4 +1,4 @@
1
- import { v as RpcFunctionDefinitionAny } from "./types-CrzNxXKq.mjs";
1
+ import { v as RpcFunctionDefinitionAny } from "./types-CnJSgRVa.mjs";
2
2
  import { ChannelOptions } from "birpc";
3
3
  //#region src/rpc/transports/ws-client.d.ts
4
4
  interface WsRpcChannelOptions {
@@ -1,4 +1,4 @@
1
- import { v as RpcFunctionDefinitionAny } from "./types-CrzNxXKq.mjs";
1
+ import { v as RpcFunctionDefinitionAny } from "./types-CnJSgRVa.mjs";
2
2
  import { Peer } from "crossws";
3
3
  import { BirpcGroup, ChannelOptions } from "birpc";
4
4
  import { NodeAdapter } from "crossws/adapters/node";
@@ -62,7 +62,7 @@ interface WsRpcTransportOptions {
62
62
  * Pass `false` to disable origin checking entirely (not recommended).
63
63
  * Default: loopback-only.
64
64
  */
65
- allowedOrigins?: readonly string[] | false;
65
+ allowedOrigins?: readonly string[] | WsOriginRegistry | false;
66
66
  /**
67
67
  * RPC function definitions, used by the per-call wire serializer to
68
68
  * dispatch between strict-JSON and structured-clone encoding based
@@ -79,6 +79,27 @@ interface WsRpcTransportOptions {
79
79
  /** Override the default per-call deserializer. Most callers should leave this unset. */
80
80
  deserialize?: ChannelOptions['deserialize'];
81
81
  }
82
+ interface CreateWsOriginRegistryOptions {
83
+ /** Origins allowed before any external viewers are registered. */
84
+ allowedOrigins?: readonly string[];
85
+ /** Additional validation to run after the registration token is verified. */
86
+ validateOrigin?: (origin: string) => boolean;
87
+ }
88
+ interface WsOriginRegistry {
89
+ /** Registration token to include in connection metadata. */
90
+ readonly token: string;
91
+ /** Read and register an origin from a connection bootstrap URL. */
92
+ registerFromUrl: (url: string) => string | undefined;
93
+ /** Check whether an origin is currently allowed. */
94
+ isAllowed: (origin: string | undefined) => boolean;
95
+ }
96
+ /**
97
+ * Create a live, token-protected origin allowlist for external browser
98
+ * viewers. Pass it to {@link WsRpcTransportOptions.allowedOrigins}, then use
99
+ * `registerFromUrl()` in the connection metadata handler to authorize a
100
+ * viewer without sharing a mutable array or disabling DNS-rebinding protection.
101
+ */
102
+ declare function createWsOriginRegistry(options?: CreateWsOriginRegistryOptions): WsOriginRegistry;
82
103
  interface WsRpcTransport {
83
104
  /**
84
105
  * The crossws node adapter driving the socket — exposes the connected
@@ -115,4 +136,4 @@ declare function isAllowedOrigin(origin: string | undefined, allowedOrigins: rea
115
136
  */
116
137
  declare function attachWsRpcTransport<ClientFunctions extends object, ServerFunctions extends object>(rpcGroup: BirpcGroup<ClientFunctions, ServerFunctions, false>, options?: WsRpcTransportOptions): WsRpcTransport;
117
138
  //#endregion
118
- export { isAllowedOrigin as a, attachWsRpcTransport as i, WsRpcTransport as n, isLoopbackHostname as o, WsRpcTransportOptions as r, DevframeNodeRpcSessionMeta as t };
139
+ export { WsRpcTransportOptions as a, isAllowedOrigin as c, WsRpcTransport as i, isLoopbackHostname as l, DevframeNodeRpcSessionMeta as n, attachWsRpcTransport as o, WsOriginRegistry as r, createWsOriginRegistry as s, CreateWsOriginRegistryOptions as t };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devframe",
3
3
  "type": "module",
4
- "version": "0.7.16",
4
+ "version": "0.8.1",
5
5
  "description": "Framework for building generic devframes",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -42,6 +42,7 @@
42
42
  "./rpc/transports/ws-client": "./dist/rpc/transports/ws-client.mjs",
43
43
  "./rpc/transports/ws-server": "./dist/rpc/transports/ws-server.mjs",
44
44
  "./types": "./dist/types/index.mjs",
45
+ "./utils/agent-tool-name": "./dist/utils/agent-tool-name.mjs",
45
46
  "./utils/colors": "./dist/utils/colors.mjs",
46
47
  "./utils/crypto-token": "./dist/utils/crypto-token.mjs",
47
48
  "./utils/events": "./dist/utils/events.mjs",
@@ -50,6 +51,7 @@
50
51
  "./utils/nanoid": "./dist/utils/nanoid.mjs",
51
52
  "./utils/open": "./dist/utils/open.mjs",
52
53
  "./utils/promise": "./dist/utils/promise.mjs",
54
+ "./utils/simple-schema": "./dist/utils/simple-schema.mjs",
53
55
  "./utils/scope": "./dist/utils/scope.mjs",
54
56
  "./utils/serve-static": "./dist/utils/serve-static.mjs",
55
57
  "./utils/shared-state": "./dist/utils/shared-state.mjs",
@@ -59,16 +61,24 @@
59
61
  "./package.json": "./package.json"
60
62
  },
61
63
  "types": "./dist/index.d.mts",
64
+ "bin": {
65
+ "devframe": "./bin/devframe.mjs"
66
+ },
62
67
  "files": [
68
+ "bin",
63
69
  "dist",
64
70
  "skills"
65
71
  ],
66
72
  "peerDependencies": {
67
- "@modelcontextprotocol/sdk": "^1.0.0",
73
+ "@modelcontextprotocol/client": "^2.0.0",
74
+ "@modelcontextprotocol/server": "^2.0.0",
68
75
  "cac": "^7.0.0"
69
76
  },
70
77
  "peerDependenciesMeta": {
71
- "@modelcontextprotocol/sdk": {
78
+ "@modelcontextprotocol/client": {
79
+ "optional": true
80
+ },
81
+ "@modelcontextprotocol/server": {
72
82
  "optional": true
73
83
  },
74
84
  "cac": {
@@ -76,7 +86,7 @@
76
86
  }
77
87
  },
78
88
  "dependencies": {
79
- "@valibot/to-json-schema": "^1.7.1",
89
+ "@standard-schema/spec": "^1.1.0",
80
90
  "birpc": "^4.0.0",
81
91
  "crossws": "^0.4.10",
82
92
  "destr": "^2.0.5",
@@ -84,11 +94,11 @@
84
94
  "mrmime": "^2.0.1",
85
95
  "nostics": "^1.2.0",
86
96
  "pathe": "^2.0.3",
87
- "ufo": "^1.6.4",
88
- "valibot": "^1.4.2"
97
+ "ufo": "^1.6.4"
89
98
  },
90
99
  "devDependencies": {
91
- "@modelcontextprotocol/sdk": "^1.30.0",
100
+ "@modelcontextprotocol/client": "^2.0.0",
101
+ "@modelcontextprotocol/server": "^2.0.0",
92
102
  "cac": "^7.0.0",
93
103
  "get-port-please": "^3.2.0",
94
104
  "immer": "^11.1.15",
@@ -103,6 +113,7 @@
103
113
  "tinyglobby": "^0.2.17",
104
114
  "tsdown": "^0.22.14",
105
115
  "ua-parser-modern": "^0.1.1",
116
+ "valibot": "^1.4.2",
106
117
  "whenexpr": "^0.1.2",
107
118
  "ws": "^8.21.1"
108
119
  },
@@ -485,7 +485,7 @@ import { createMcpServer } from 'devframe/adapters/mcp'
485
485
  await createMcpServer(devframe, { transport: 'stdio' })
486
486
  ```
487
487
 
488
- `@modelcontextprotocol/sdk` is a peer dependency. The CLI adapter also exposes `my-devframe mcp` — route host logs to stderr (stdout is the MCP transport). Safety classifications (`'read' | 'action' | 'destructive'`) drive MCP hint annotations that agent clients use to prompt for confirmation.
488
+ `@modelcontextprotocol/server` is a peer dependency. The CLI adapter also exposes `my-devframe mcp` — route host logs to stderr (stdout is the MCP transport). Safety classifications (`'read' | 'action' | 'destructive'`) drive MCP hint annotations that agent clients use to prompt for confirmation.
489
489
 
490
490
  ## Author SPA
491
491
 
@@ -569,10 +569,11 @@ RPC handlers run with the full privileges of the host process, so the boundary t
569
569
  - **`auth` defaults to `true`** — dev-mode connections must authenticate before calls are accepted. Devframe ships the node primitives (`exchangeTempAuthCode`, `verifyAuthToken` in `devframe/node/auth`); the host adapter (e.g. Vite DevTools) provides the interactive `devframe:anonymous:auth` + `devframe:auth:exchange` handlers and auth UI.
570
570
  - **`auth: false` trusts every reachable connection.** Use it only for single-user `localhost` tools. Never combine it with a non-loopback bind host, a tunnel, or a shared/CI environment. The default bind host is already `localhost`.
571
571
  - **Authentication** exchanges a 6-digit one-time code (shown in the developer's terminal) for a node-issued bearer token via `requestTrustWithCode(code)`. The code is single-use, expires in 5 min, compared in constant time, and rotates after repeated failures — show it only in the terminal, never over the network.
572
- - **Magic-link (optional):** print `buildOtpAuthUrl(origin)` — `<origin>/?devframe_otp=<code>`. `connectDevframe` reads the code, exchanges it, and strips it from the URL. Integrations can opt out (`otpParam: false`) and drive it via the exposed `authenticateWithUrlOtp(rpc)` / `consumeOtpFromUrl()` client utilities. Only the single-use code rides the URL, never the bearer; treat the printed link like the code itself. The standalone CLI's `--open` does this automatically via `DevframeAuthHandler.buildOpenUrl` — the launched tab already carries the OTP, no prompt needed.
572
+ - **Magic-link (optional):** print `buildOtpAuthUrl(origin)` — `<origin>/#devframe_otp=<code>`. The code rides the URL **fragment**, which the browser never sends to the server, so it stays out of access logs and `Referer`. `connectDevframe` reads the code, exchanges it, and strips it from the URL. Integrations can opt out (`otpParam: false`) and drive it via the exposed `authenticateWithUrlOtp(rpc)` / `consumeOtpFromUrl()` client utilities. Only the single-use code rides the URL, never the bearer; treat the printed link like the code itself. The standalone CLI's `--open` does this automatically via `DevframeAuthHandler.buildOpenUrl` — the launched tab already carries the OTP, no prompt needed.
573
573
  - **Tokens are secrets.** The bearer token rides the WS URL (`?devframe_auth_token=…`) — serve over `wss://`/`https://` beyond loopback. Never log the token or code, never bake them into build output. Revoke via `revokeAuthToken(...)`; clients drop to untrusted on `devframe:auth:revoked`.
574
574
  - **Authorize handlers.** Any trusted client can call any registered function — validate inputs, and mark state-changing functions `type: 'destructive'` so MCP/agent clients prompt first.
575
- - **Origin-lock remote docks** (`originLock`) so a dock token is honored only from its expected origin.
575
+ - **Origin-lock remote docks** (`originLock`, on by default) so a dock's session token is honored only on a connection whose `Origin` matches the dock — the connect-time gate enforces it.
576
+ - **The MCP route requires an Origin.** The route-based MCP server (`cli.mcp`, `viteDevBridge`/Next handler `mcp`, `createMcpFetchHandler`) rejects `Origin`-less requests — a request must carry a loopback (or allow-listed) `Origin`, so it isn't reachable by an arbitrary local process. `devframe connect` sends each instance's own loopback origin automatically.
576
577
 
577
578
  See [Security](https://devfra.me/security) for the full reference.
578
579
 
@@ -1,359 +0,0 @@
1
- import { a as diagnostics } from "./storage-gEnj9ASQ.mjs";
2
- import { n as createHostContext } from "./host-h3-CgOetlNE.mjs";
3
- import { Diagnostic } from "nostics";
4
- import { join } from "pathe";
5
- import process from "node:process";
6
- import { homedir } from "node:os";
7
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
9
- import { toJsonSchema } from "@valibot/to-json-schema";
10
- //#region src/adapters/mcp/stringify.ts
11
- /**
12
- * JSON-coercing serializer for MCP text payloads.
13
- *
14
- * MCP carries tool results and resource reads as plain text over a
15
- * JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
16
- * format the WS RPC transport falls back to for non-JSON values. Instead,
17
- * we coerce common non-JSON types into JSON-friendly forms so the LLM
18
- * client sees something useful instead of `[object Object]`.
19
- *
20
- * Coercions:
21
- * - `BigInt` → `"123n"`
22
- * - `Date` → ISO string (via the native `toJSON`)
23
- * - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
24
- * - `Set` → `{ __type: 'Set', entries: [v, …] }`
25
- * - `Error` → `{ name, message, stack, cause? }` (cause recurses)
26
- * - `Function` → `"[Function: name]"`
27
- * - `Symbol` → `value.toString()`
28
- * - cycles → `"[Circular]"`
29
- */
30
- function stringifyForMcp(value) {
31
- if (value === void 0) return "undefined";
32
- if (typeof value === "string") return value;
33
- const seen = /* @__PURE__ */ new WeakSet();
34
- return JSON.stringify(value, (_key, val) => {
35
- if (typeof val === "bigint") return `${val}n`;
36
- if (val instanceof Error) {
37
- const out = {
38
- name: val.name,
39
- message: val.message,
40
- stack: val.stack
41
- };
42
- if (val.cause !== void 0) out.cause = val.cause;
43
- return out;
44
- }
45
- if (val instanceof Map) return {
46
- __type: "Map",
47
- entries: [...val.entries()]
48
- };
49
- if (val instanceof Set) return {
50
- __type: "Set",
51
- entries: [...val]
52
- };
53
- if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
54
- if (typeof val === "symbol") return val.toString();
55
- if (val !== null && typeof val === "object") {
56
- if (seen.has(val)) return "[Circular]";
57
- seen.add(val);
58
- }
59
- return val;
60
- }, 2);
61
- }
62
- /**
63
- * Format a thrown value for an MCP `isError` text payload.
64
- *
65
- * A nostics `Diagnostic` (every coded devframe error) becomes structured
66
- * JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
67
- * the actionable next step (`fix`) and the docs URL instead of a bare
68
- * message string. Other errors surface `Error.name`/`message`, plus one
69
- * level of `cause.message` so context isn't dropped silently.
70
- */
71
- function formatMcpError(error) {
72
- if (error instanceof Diagnostic) return JSON.stringify({ error: {
73
- code: error.code,
74
- message: error.message,
75
- ...error.fix ? { fix: error.fix } : {},
76
- ...error.docs ? { docs: error.docs } : {}
77
- } }, null, 2);
78
- if (!(error instanceof Error)) return String(error);
79
- const cause = error.cause;
80
- const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
81
- return `${error.name}: ${error.message}${causeText}`;
82
- }
83
- //#endregion
84
- //#region src/adapters/mcp/to-json-schema.ts
85
- const FALLBACK_OBJECT_SCHEMA = Object.freeze({
86
- type: "object",
87
- additionalProperties: true
88
- });
89
- /**
90
- * Convert a valibot return schema to JSON Schema.
91
- * @internal
92
- */
93
- function valibotReturnToJsonSchema(schema) {
94
- if (!schema) return void 0;
95
- try {
96
- return toJsonSchema(schema);
97
- } catch {
98
- return FALLBACK_OBJECT_SCHEMA;
99
- }
100
- }
101
- /**
102
- * Convert positional RPC args schemas to a single MCP-friendly object
103
- * schema. When the RPC declares `args: [v.object(...)]`, unwrap the
104
- * single-object schema directly (nicer agent UX than `{ arg0: {...} }`).
105
- *
106
- * Returns `undefined` when there are no args (the MCP SDK treats this
107
- * as `{ type: 'object', properties: {} }`).
108
- * @internal
109
- */
110
- function valibotArgsToJsonSchema(args) {
111
- if (!args || args.length === 0) return {
112
- schema: {
113
- type: "object",
114
- properties: {}
115
- },
116
- unwrapped: false
117
- };
118
- if (args.length === 1) {
119
- const inner = safeToJsonSchema(args[0]);
120
- if (isObjectJsonSchema(inner)) return {
121
- schema: inner,
122
- unwrapped: true
123
- };
124
- }
125
- const properties = {};
126
- const required = [];
127
- for (let i = 0; i < args.length; i++) {
128
- const key = `arg${i}`;
129
- properties[key] = safeToJsonSchema(args[i]);
130
- required.push(key);
131
- }
132
- return {
133
- schema: {
134
- type: "object",
135
- properties,
136
- required,
137
- additionalProperties: false
138
- },
139
- unwrapped: false
140
- };
141
- }
142
- function safeToJsonSchema(schema) {
143
- try {
144
- return toJsonSchema(schema);
145
- } catch {
146
- return FALLBACK_OBJECT_SCHEMA;
147
- }
148
- }
149
- function isObjectJsonSchema(value) {
150
- return !!value && typeof value === "object" && value.type === "object";
151
- }
152
- //#endregion
153
- //#region src/adapters/mcp/build-server.ts
154
- /**
155
- * Wire an MCP {@link Server} to a devframe context. Returns the server
156
- * plus a disposal function for the subscriptions it sets up. The
157
- * transport is the caller's responsibility — `createMcpServer` connects
158
- * stdio; tests can connect an {@link InMemoryTransport} instead.
159
- *
160
- * @internal
161
- */
162
- function buildMcpServerFromContext(ctx, options) {
163
- const server = new Server({
164
- name: options.serverName,
165
- version: options.serverVersion
166
- }, { capabilities: {
167
- tools: { listChanged: true },
168
- resources: { listChanged: true }
169
- } });
170
- registerToolHandlers(server, ctx);
171
- registerResourceHandlers(server, ctx, options.exposeSharedState);
172
- const notify = (method) => {
173
- server.notification({ method }).catch(() => {});
174
- };
175
- const offManifest = ctx.agent.events.on("agent:manifest:changed", () => {
176
- notify("notifications/tools/list_changed");
177
- notify("notifications/resources/list_changed");
178
- });
179
- const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
180
- notify("notifications/resources/list_changed");
181
- });
182
- return {
183
- server,
184
- dispose: () => {
185
- offManifest();
186
- offKeyAdded();
187
- }
188
- };
189
- }
190
- /**
191
- * Build an MCP server over the agent surface of a devframe definition.
192
- * Currently supports `stdio` transport only.
193
- *
194
- * @experimental The agent-native surface is experimental and may change
195
- * without a major version bump until it stabilizes.
196
- */
197
- async function createMcpServer(definition, options = {}) {
198
- const transport = options.transport ?? "stdio";
199
- if (transport !== "stdio") throw diagnostics.DF0017({
200
- transport,
201
- reason: "Only stdio transport is supported in this release."
202
- });
203
- const ctx = await createHostContext({
204
- cwd: process.cwd(),
205
- mode: "dev",
206
- host: {
207
- mountStatic: () => {},
208
- resolveOrigin: () => "mcp://devframe",
209
- getStorageDir: (scope) => {
210
- if (scope === "workspace") return join(process.cwd(), ".devframe");
211
- if (scope === "project") return join(process.cwd(), `node_modules/.${definition.id}/devframe`);
212
- return join(homedir(), `.${definition.id}/devframe`);
213
- }
214
- }
215
- });
216
- await definition.setup(ctx);
217
- const { server, dispose } = buildMcpServerFromContext(ctx, {
218
- serverName: options.serverName ?? `${definition.id} (devframe)`,
219
- serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
220
- exposeSharedState: options.exposeSharedState ?? true
221
- });
222
- const { startStdioTransport } = await import("./transports-BmDVn4SF.mjs");
223
- let stop;
224
- try {
225
- stop = await startStdioTransport(server);
226
- } catch (error) {
227
- const reason = error instanceof Error ? error.message : String(error);
228
- throw diagnostics.DF0017({
229
- transport,
230
- reason,
231
- cause: error
232
- });
233
- }
234
- options.onReady?.({ transport: "stdio" });
235
- return { async stop() {
236
- dispose();
237
- await stop();
238
- } };
239
- }
240
- function registerToolHandlers(server, ctx) {
241
- server.setRequestHandler(ListToolsRequestSchema, async () => {
242
- return { tools: ctx.agent.list().tools.map((tool) => projectTool(tool, ctx)) };
243
- });
244
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
245
- const { name, arguments: args } = request.params;
246
- try {
247
- const tool = ctx.agent.getTool(name);
248
- const outputSchema = tool ? tool.outputSchema ?? computeOutputSchema(tool, ctx) : void 0;
249
- const result = await ctx.agent.invoke(name, args ?? {});
250
- return {
251
- content: [{
252
- type: "text",
253
- text: stringifyForMcp(result)
254
- }],
255
- ...outputSchema ? { structuredContent: result } : {}
256
- };
257
- } catch (error) {
258
- return {
259
- isError: true,
260
- content: [{
261
- type: "text",
262
- text: `Error invoking "${name}": ${formatMcpError(error)}`
263
- }]
264
- };
265
- }
266
- });
267
- }
268
- function registerResourceHandlers(server, ctx, exposeSharedState) {
269
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
270
- const resources = ctx.agent.list().resources.map((resource) => ({
271
- uri: resource.uri,
272
- name: resource.name,
273
- description: resource.description,
274
- mimeType: resource.mimeType
275
- }));
276
- if (exposeSharedState !== false) {
277
- const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
278
- for (const key of ctx.rpc.sharedState.keys()) {
279
- if (!filter(key)) continue;
280
- resources.push({
281
- uri: `devframe://state/${encodeURIComponent(key)}`,
282
- name: key,
283
- description: `Shared state: ${key}`,
284
- mimeType: "application/json"
285
- });
286
- }
287
- }
288
- return { resources };
289
- });
290
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
291
- const { uri } = request.params;
292
- const parsed = parseResourceUri(uri);
293
- if (parsed.kind === "resource") {
294
- const content = await ctx.agent.read(parsed.id);
295
- return { contents: [{
296
- uri,
297
- mimeType: content.mimeType ?? "application/json",
298
- text: content.text ?? stringifyForMcp(content.json)
299
- }] };
300
- }
301
- if (parsed.kind === "state") return { contents: [{
302
- uri,
303
- mimeType: "application/json",
304
- text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
305
- }] };
306
- throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
307
- });
308
- }
309
- function projectTool(tool, ctx) {
310
- const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
311
- const outputSchema = tool.outputSchema ?? computeOutputSchema(tool, ctx);
312
- return {
313
- name: tool.id,
314
- title: tool.title,
315
- description: tool.description,
316
- inputSchema,
317
- ...outputSchema ? { outputSchema } : {},
318
- annotations: {
319
- title: tool.title,
320
- readOnlyHint: tool.safety === "read",
321
- destructiveHint: tool.safety === "destructive"
322
- }
323
- };
324
- }
325
- function computeInputSchema(tool, ctx) {
326
- if (tool.kind !== "rpc" || !tool.rpcName) return {
327
- type: "object",
328
- properties: {}
329
- };
330
- const def = ctx.rpc.definitions.get(tool.rpcName);
331
- if (!def) return {
332
- type: "object",
333
- properties: {}
334
- };
335
- const args = def.args;
336
- return valibotArgsToJsonSchema(args).schema;
337
- }
338
- function computeOutputSchema(tool, ctx) {
339
- if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
340
- const def = ctx.rpc.definitions.get(tool.rpcName);
341
- if (!def) return void 0;
342
- return valibotReturnToJsonSchema(def.returns);
343
- }
344
- function parseResourceUri(uri) {
345
- const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
346
- if (!match) return { kind: "unknown" };
347
- const [, kind, rest] = match;
348
- const decoded = decodeURIComponent(rest);
349
- if (kind === "resource") return {
350
- kind: "resource",
351
- id: decoded
352
- };
353
- return {
354
- kind: "state",
355
- key: decoded
356
- };
357
- }
358
- //#endregion
359
- export { createMcpServer as n, buildMcpServerFromContext as t };