devframe 0.9.5 → 0.9.7
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/README.md +1 -1
- package/dist/{_shared-CsBsnhuO.d.mts → _shared-BJ6AmwTu.d.mts} +1 -1
- package/dist/adapters/build.d.mts +1 -1
- package/dist/adapters/build.mjs +1 -1
- package/dist/adapters/cac.d.mts +1 -1
- package/dist/adapters/cac.mjs +3 -2
- package/dist/adapters/dev.d.mts +4 -4
- package/dist/adapters/dev.mjs +1 -1
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/initiate.d.mts +3 -3
- package/dist/adapters/initiate.mjs +1 -1
- package/dist/adapters/mcp.d.mts +30 -26
- package/dist/adapters/mcp.mjs +559 -1
- package/dist/cli/main.mjs +1 -1
- package/dist/client/index.d.mts +5 -5
- package/dist/client/index.mjs +1 -64
- package/dist/constants.d.mts +13 -1
- package/dist/constants.mjs +1 -1
- package/dist/{context-c0OlSc12.d.mts → context-BhBzOXVa.d.mts} +3 -3
- package/dist/{context-DO0Qu1O8.d.mts → context-HOnG-WvG.d.mts} +1 -1
- package/dist/{context-Cx9V9Grq.mjs → context-KUqAfawQ.mjs} +1 -1
- package/dist/{context-Bc5S40Qt.mjs → context-omlma_1n.mjs} +3 -150
- package/dist/{dev-BRQA42Lh.mjs → dev-Czit5IP4.mjs} +3 -3
- package/dist/{events-DzolaBEs.mjs → events-4daw0uK9.mjs} +16 -2
- package/dist/in-page-channel/index.d.mts +306 -0
- package/dist/in-page-channel/index.mjs +816 -0
- package/dist/{index-DOg4S5wk.d.mts → index-BzyopqxV.d.mts} +1 -1
- package/dist/{index-B_DPNIT4.d.mts → index-DcKxIMoI.d.mts} +2 -2
- package/dist/index.d.mts +4 -4
- package/dist/{instance-shell-CPuYY8pw.d.mts → instance-shell-Bh4l9Vz_.d.mts} +3 -3
- package/dist/{instance-shell-fB5PyVtR.mjs → instance-shell-CpVW4zG0.mjs} +15 -2
- package/dist/internal/index.d.mts +16 -6
- package/dist/internal/index.mjs +3 -3
- package/dist/node/auth.d.mts +2 -2
- package/dist/node/auth.mjs +1 -1
- package/dist/node/hub-internals.d.mts +2 -2
- package/dist/node/hub-internals.mjs +1 -1
- package/dist/node/index.d.mts +3 -3
- package/dist/node/index.mjs +1 -1
- package/dist/recipes/common-rpc-functions.d.mts +3 -3
- package/dist/recipes/interactive-auth.d.mts +13 -2
- package/dist/recipes/interactive-auth.mjs +5 -1
- package/dist/{revoke-PihtvVil.mjs → revoke-COLW0cOi.mjs} +1 -1
- package/dist/rpc/dump.d.mts +1 -1
- package/dist/rpc/index.d.mts +2 -2
- package/dist/rpc/transports/sse-client.d.mts +1 -1
- package/dist/rpc/transports/sse-server.d.mts +2 -2
- package/dist/rpc/transports/ws-bun.d.mts +2 -2
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-deno.d.mts +2 -2
- package/dist/rpc/transports/ws-server.d.mts +1 -1
- package/dist/{rpc-core-Oyz-cJBC.d.mts → rpc-core-BHUsDl7s.d.mts} +3 -3
- package/dist/{host-agent-DbPWo0Bl.mjs → services-install-CqqQVls5.mjs} +150 -2
- package/dist/{sse-client-CdGD_B8_.d.mts → sse-client-DwX021ST.d.mts} +1 -1
- package/dist/types/index.d.mts +3 -3
- package/dist/{types-BBLeGfpt.d.mts → types-CMdPYhOx.d.mts} +1 -1
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/serve-static.mjs +1 -1
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/dist/{ws-client-Bo2t6hES.d.mts → ws-client-7_lZi-B9.d.mts} +1 -1
- package/dist/{ws-server-DLtEoLdR.d.mts → ws-server-Di2oqr4q.d.mts} +1 -1
- package/package.json +2 -1
- package/skills/devframe/SKILL.md +86 -82
- package/dist/http-D7wWuy7-.mjs +0 -569
- package/dist/transports-vhizgqXM.mjs +0 -15
package/dist/http-D7wWuy7-.mjs
DELETED
|
@@ -1,569 +0,0 @@
|
|
|
1
|
-
import { n as __exportAll } from "./rolldown-runtime-B4iAMlE-.mjs";
|
|
2
|
-
import { t as Diagnostic } from "./nostics-CzECRXpE.mjs";
|
|
3
|
-
import { t as DEVFRAME_EVENTS } from "./events-DzolaBEs.mjs";
|
|
4
|
-
import { i as isAllowedOrigin } from "./ws-server-BdSLrhxE.mjs";
|
|
5
|
-
import { t as diagnostics } from "./diagnostics-BXWW3VK-.mjs";
|
|
6
|
-
import { t as createHostContext } from "./context-Bc5S40Qt.mjs";
|
|
7
|
-
import { t as toAgentToolName } from "./agent-tool-name-EgfoFO8C.mjs";
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
-
import process from "node:process";
|
|
10
|
-
import { join } from "pathe";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { defineHandler } from "h3";
|
|
13
|
-
import { Server, WebStandardStreamableHTTPServerTransport, isInitializeRequest } from "@modelcontextprotocol/server";
|
|
14
|
-
//#region src/adapters/mcp/stringify.ts
|
|
15
|
-
/**
|
|
16
|
-
* JSON-coercing serializer for MCP text payloads.
|
|
17
|
-
*
|
|
18
|
-
* MCP carries tool results and resource reads as plain text over a
|
|
19
|
-
* JSON-RPC transport, so we cannot use the `s:`-prefixed structured-clone
|
|
20
|
-
* format the WS RPC transport falls back to for non-JSON values. Instead,
|
|
21
|
-
* we coerce common non-JSON types into JSON-friendly forms so the LLM
|
|
22
|
-
* client sees something useful instead of `[object Object]`.
|
|
23
|
-
*
|
|
24
|
-
* Coercions:
|
|
25
|
-
* - `BigInt` → `"123n"`
|
|
26
|
-
* - `Date` → ISO string (via the native `toJSON`)
|
|
27
|
-
* - `Map` → `{ __type: 'Map', entries: [[k, v], …] }`
|
|
28
|
-
* - `Set` → `{ __type: 'Set', entries: [v, …] }`
|
|
29
|
-
* - `Error` → `{ name, message, stack, cause? }` (cause recurses)
|
|
30
|
-
* - `Function` → `"[Function: name]"`
|
|
31
|
-
* - `Symbol` → `value.toString()`
|
|
32
|
-
* - cycles → `"[Circular]"`
|
|
33
|
-
*/
|
|
34
|
-
function stringifyForMcp(value) {
|
|
35
|
-
if (value === void 0) return "undefined";
|
|
36
|
-
if (typeof value === "string") return value;
|
|
37
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
38
|
-
return JSON.stringify(value, (_key, val) => {
|
|
39
|
-
if (typeof val === "bigint") return `${val}n`;
|
|
40
|
-
if (val instanceof Error) {
|
|
41
|
-
const out = {
|
|
42
|
-
name: val.name,
|
|
43
|
-
message: val.message,
|
|
44
|
-
stack: val.stack
|
|
45
|
-
};
|
|
46
|
-
if (val.cause !== void 0) out.cause = val.cause;
|
|
47
|
-
return out;
|
|
48
|
-
}
|
|
49
|
-
if (val instanceof Map) return {
|
|
50
|
-
__type: "Map",
|
|
51
|
-
entries: [...val.entries()]
|
|
52
|
-
};
|
|
53
|
-
if (val instanceof Set) return {
|
|
54
|
-
__type: "Set",
|
|
55
|
-
entries: [...val]
|
|
56
|
-
};
|
|
57
|
-
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
|
|
58
|
-
if (typeof val === "symbol") return val.toString();
|
|
59
|
-
if (val !== null && typeof val === "object") {
|
|
60
|
-
if (seen.has(val)) return "[Circular]";
|
|
61
|
-
seen.add(val);
|
|
62
|
-
}
|
|
63
|
-
return val;
|
|
64
|
-
}, 2);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Format a thrown value for an MCP `isError` text payload.
|
|
68
|
-
*
|
|
69
|
-
* A nostics `Diagnostic` (every coded devframe error) becomes structured
|
|
70
|
-
* JSON — `{ error: { code, message, fix?, docs? } }` — so an agent receives
|
|
71
|
-
* the actionable next step (`fix`) and the docs URL instead of a bare
|
|
72
|
-
* message string. Other errors surface `Error.name`/`message`, plus one
|
|
73
|
-
* level of `cause.message` so context isn't dropped silently.
|
|
74
|
-
*/
|
|
75
|
-
function formatMcpError(error) {
|
|
76
|
-
if (error instanceof Diagnostic) return JSON.stringify({ error: {
|
|
77
|
-
code: error.code,
|
|
78
|
-
message: error.message,
|
|
79
|
-
...error.fix ? { fix: error.fix } : {},
|
|
80
|
-
...error.docs ? { docs: error.docs } : {}
|
|
81
|
-
} }, null, 2);
|
|
82
|
-
if (!(error instanceof Error)) return String(error);
|
|
83
|
-
const cause = error.cause;
|
|
84
|
-
const causeText = cause instanceof Error ? ` (cause: ${cause.message})` : cause !== void 0 ? ` (cause: ${String(cause)})` : "";
|
|
85
|
-
return `${error.name}: ${error.message}${causeText}`;
|
|
86
|
-
}
|
|
87
|
-
//#endregion
|
|
88
|
-
//#region src/adapters/mcp/to-json-schema.ts
|
|
89
|
-
const FALLBACK_OBJECT_SCHEMA = Object.freeze({
|
|
90
|
-
type: "object",
|
|
91
|
-
additionalProperties: true
|
|
92
|
-
});
|
|
93
|
-
/**
|
|
94
|
-
* Convert a Standard Schema to JSON Schema for the agent/MCP surface.
|
|
95
|
-
*
|
|
96
|
-
* Devframe stays validator-neutral, so conversion uses the schema's own
|
|
97
|
-
* [Standard JSON Schema](https://standardschema.dev/) converter
|
|
98
|
-
* (`~standard.jsonSchema`) when the validator provides one — zod 4 does,
|
|
99
|
-
* for example. Validators without a native converter (e.g. valibot) degrade
|
|
100
|
-
* to a permissive object schema rather than pulling in a converter library.
|
|
101
|
-
*/
|
|
102
|
-
function safeToJsonSchema(schema) {
|
|
103
|
-
const standard = schema["~standard"];
|
|
104
|
-
if (standard.jsonSchema) try {
|
|
105
|
-
return standard.jsonSchema.input({ target: "draft-2020-12" });
|
|
106
|
-
} catch {
|
|
107
|
-
return FALLBACK_OBJECT_SCHEMA;
|
|
108
|
-
}
|
|
109
|
-
return FALLBACK_OBJECT_SCHEMA;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* JSON Schema for an RPC return value on the agent/MCP surface.
|
|
113
|
-
* @internal
|
|
114
|
-
*/
|
|
115
|
-
function returnToJsonSchema(schema) {
|
|
116
|
-
if (!schema) return void 0;
|
|
117
|
-
return safeToJsonSchema(schema);
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* JSON Schema for an RPC function's positional args on the agent/MCP
|
|
121
|
-
* surface. Each positional arg is advertised under `arg0` / `arg1` / … —
|
|
122
|
-
* matching how the agent bridge coerces the incoming object payload back
|
|
123
|
-
* into positional arguments.
|
|
124
|
-
*
|
|
125
|
-
* Returns `{ type: 'object', properties: {} }` when there are no args.
|
|
126
|
-
* @internal
|
|
127
|
-
*/
|
|
128
|
-
function argsToJsonSchema(args) {
|
|
129
|
-
if (!args || args.length === 0) return {
|
|
130
|
-
schema: {
|
|
131
|
-
type: "object",
|
|
132
|
-
properties: {}
|
|
133
|
-
},
|
|
134
|
-
unwrapped: false
|
|
135
|
-
};
|
|
136
|
-
const properties = {};
|
|
137
|
-
const required = [];
|
|
138
|
-
for (let i = 0; i < args.length; i++) {
|
|
139
|
-
const key = `arg${i}`;
|
|
140
|
-
properties[key] = safeToJsonSchema(args[i]);
|
|
141
|
-
required.push(key);
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
schema: {
|
|
145
|
-
type: "object",
|
|
146
|
-
properties,
|
|
147
|
-
required,
|
|
148
|
-
additionalProperties: false
|
|
149
|
-
},
|
|
150
|
-
unwrapped: false
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
//#endregion
|
|
154
|
-
//#region src/adapters/mcp/build-server.ts
|
|
155
|
-
/**
|
|
156
|
-
* Wire an MCP {@link Server} to a devframe context. Returns the server
|
|
157
|
-
* plus a disposal function for the subscriptions it sets up. The
|
|
158
|
-
* transport is the caller's responsibility — `createMcpServer` connects
|
|
159
|
-
* stdio; tests can connect an {@link InMemoryTransport} instead.
|
|
160
|
-
*
|
|
161
|
-
* @internal
|
|
162
|
-
*/
|
|
163
|
-
function buildMcpServerFromContext(ctx, options) {
|
|
164
|
-
const server = new Server({
|
|
165
|
-
name: options.serverName,
|
|
166
|
-
version: options.serverVersion
|
|
167
|
-
}, { capabilities: {
|
|
168
|
-
tools: { listChanged: true },
|
|
169
|
-
resources: { listChanged: true }
|
|
170
|
-
} });
|
|
171
|
-
registerToolHandlers(server, ctx, options.exposeSharedState);
|
|
172
|
-
registerResourceHandlers(server, ctx, options.exposeSharedState);
|
|
173
|
-
const notify = (method) => {
|
|
174
|
-
server.notification({ method }).catch(() => {});
|
|
175
|
-
};
|
|
176
|
-
const offManifest = ctx.agent.events.on(DEVFRAME_EVENTS.bus.agentManifestChanged, () => {
|
|
177
|
-
notify("notifications/tools/list_changed");
|
|
178
|
-
notify("notifications/resources/list_changed");
|
|
179
|
-
});
|
|
180
|
-
const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
|
|
181
|
-
notify("notifications/resources/list_changed");
|
|
182
|
-
});
|
|
183
|
-
return {
|
|
184
|
-
server,
|
|
185
|
-
dispose: () => {
|
|
186
|
-
offManifest();
|
|
187
|
-
offKeyAdded();
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Build an MCP server over the agent surface of a devframe definition.
|
|
193
|
-
* Currently supports `stdio` transport only.
|
|
194
|
-
*/
|
|
195
|
-
async function createMcpServer(definition, options = {}) {
|
|
196
|
-
const transport = options.transport ?? "stdio";
|
|
197
|
-
if (transport !== "stdio") throw diagnostics.DF0017({
|
|
198
|
-
transport,
|
|
199
|
-
reason: "Only stdio transport is supported in this release."
|
|
200
|
-
});
|
|
201
|
-
const ctx = await createHostContext({
|
|
202
|
-
cwd: process.cwd(),
|
|
203
|
-
mode: "dev",
|
|
204
|
-
host: {
|
|
205
|
-
mountStatic: () => {},
|
|
206
|
-
resolveOrigin: () => "mcp://devframe",
|
|
207
|
-
getStorageDir: (scope) => {
|
|
208
|
-
if (scope === "workspace") return join(process.cwd(), ".devframe");
|
|
209
|
-
if (scope === "project") return join(process.cwd(), `node_modules/.${definition.id}/devframe`);
|
|
210
|
-
return join(homedir(), `.${definition.id}/devframe`);
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
importMetaUrl: definition.importMetaUrl
|
|
214
|
-
});
|
|
215
|
-
for (const input of definition.services ?? []) ctx.services.install(input, { resolveFrom: definition.importMetaUrl });
|
|
216
|
-
await ctx.services.ready();
|
|
217
|
-
await definition.setup(ctx);
|
|
218
|
-
const { server, dispose } = buildMcpServerFromContext(ctx, {
|
|
219
|
-
serverName: options.serverName ?? `${definition.id} (devframe)`,
|
|
220
|
-
serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
|
|
221
|
-
exposeSharedState: options.exposeSharedState ?? true
|
|
222
|
-
});
|
|
223
|
-
const { startStdioTransport } = await import("./transports-vhizgqXM.mjs");
|
|
224
|
-
let stop;
|
|
225
|
-
try {
|
|
226
|
-
stop = await startStdioTransport(server);
|
|
227
|
-
} catch (error) {
|
|
228
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
229
|
-
throw diagnostics.DF0017({
|
|
230
|
-
transport,
|
|
231
|
-
reason,
|
|
232
|
-
cause: error
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
options.onReady?.({ transport: "stdio" });
|
|
236
|
-
return { async stop() {
|
|
237
|
-
dispose();
|
|
238
|
-
await stop();
|
|
239
|
-
} };
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Id of the built-in shared-state read tool — namespaced like every other
|
|
243
|
-
* built-in (`devframe:<area>:<fn>`). Tool-shaped access matters because many
|
|
244
|
-
* MCP clients only consume tools — the parallel `devframe://state/<key>`
|
|
245
|
-
* resource projection stays for the clients that do read resources.
|
|
246
|
-
*/
|
|
247
|
-
const READ_STATE_TOOL = "devframe:state:read";
|
|
248
|
-
/** Wire name of the built-in shared-state read tool: `devframe_state_read`. */
|
|
249
|
-
const READ_STATE_NAME = toAgentToolName(READ_STATE_TOOL);
|
|
250
|
-
function sharedStateFilter(exposeSharedState) {
|
|
251
|
-
if (exposeSharedState === false) return void 0;
|
|
252
|
-
return typeof exposeSharedState === "function" ? exposeSharedState : () => true;
|
|
253
|
-
}
|
|
254
|
-
function readStateToolProjection() {
|
|
255
|
-
return {
|
|
256
|
-
name: READ_STATE_NAME,
|
|
257
|
-
title: "Read shared state",
|
|
258
|
-
description: "Read this devtool's live shared state. Call without arguments to list the available keys, then with a key to get that value as JSON. Safe to call freely.",
|
|
259
|
-
inputSchema: {
|
|
260
|
-
type: "object",
|
|
261
|
-
properties: { key: {
|
|
262
|
-
type: "string",
|
|
263
|
-
description: "A shared-state key from the key list. Omit to list all keys."
|
|
264
|
-
} }
|
|
265
|
-
},
|
|
266
|
-
annotations: {
|
|
267
|
-
title: "Read shared state",
|
|
268
|
-
readOnlyHint: true,
|
|
269
|
-
destructiveHint: false
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
async function readStateResult(ctx, filter, key) {
|
|
274
|
-
const keys = ctx.rpc.sharedState.keys().filter(filter);
|
|
275
|
-
if (key === void 0) return { keys };
|
|
276
|
-
if (!keys.includes(key)) throw diagnostics.DF0048({ key });
|
|
277
|
-
return {
|
|
278
|
-
key,
|
|
279
|
-
value: (await ctx.rpc.sharedState.get(key)).value()
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
function registerToolHandlers(server, ctx, exposeSharedState) {
|
|
283
|
-
const stateFilter = sharedStateFilter(exposeSharedState);
|
|
284
|
-
const warnedCollisions = /* @__PURE__ */ new Set();
|
|
285
|
-
/**
|
|
286
|
-
* Resolve a wire tool name back to the registered {@link AgentTool}.
|
|
287
|
-
* Wire-name matching runs first, in manifest order — the same tool the
|
|
288
|
-
* list projection advertises under that name — with a raw-id fallback so
|
|
289
|
-
* a colon-namespaced id keeps working as a call name.
|
|
290
|
-
*/
|
|
291
|
-
const resolveTool = (name) => {
|
|
292
|
-
return ctx.agent.list().tools.find((tool) => toAgentToolName(tool.id) === name) ?? ctx.agent.getTool(name);
|
|
293
|
-
};
|
|
294
|
-
server.setRequestHandler("tools/list", async () => {
|
|
295
|
-
const byName = /* @__PURE__ */ new Map();
|
|
296
|
-
for (const tool of ctx.agent.list().tools) {
|
|
297
|
-
const name = toAgentToolName(tool.id);
|
|
298
|
-
const existing = byName.get(name);
|
|
299
|
-
if (existing) {
|
|
300
|
-
if (!warnedCollisions.has(`${name}|${tool.id}`)) {
|
|
301
|
-
warnedCollisions.add(`${name}|${tool.id}`);
|
|
302
|
-
diagnostics.DF0047({
|
|
303
|
-
name,
|
|
304
|
-
id: tool.id,
|
|
305
|
-
existing: existing.id
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
byName.set(name, tool);
|
|
311
|
-
}
|
|
312
|
-
const tools = [...byName.entries()].map(([name, tool]) => projectTool(name, tool, ctx));
|
|
313
|
-
if (stateFilter && !byName.has(READ_STATE_NAME)) tools.push(readStateToolProjection());
|
|
314
|
-
return { tools };
|
|
315
|
-
});
|
|
316
|
-
server.setRequestHandler("tools/call", async (request) => {
|
|
317
|
-
const { name, arguments: args } = request.params;
|
|
318
|
-
try {
|
|
319
|
-
const tool = resolveTool(name);
|
|
320
|
-
if (stateFilter && !tool && (name === READ_STATE_NAME || name === READ_STATE_TOOL)) {
|
|
321
|
-
const key = args?.key;
|
|
322
|
-
const result = await readStateResult(ctx, stateFilter, key);
|
|
323
|
-
return {
|
|
324
|
-
content: [{
|
|
325
|
-
type: "text",
|
|
326
|
-
text: stringifyForMcp(result)
|
|
327
|
-
}],
|
|
328
|
-
structuredContent: result
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
const outputSchema = tool ? usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx)) : void 0;
|
|
332
|
-
const result = await ctx.agent.invoke(tool?.id ?? name, args ?? {});
|
|
333
|
-
return {
|
|
334
|
-
content: [{
|
|
335
|
-
type: "text",
|
|
336
|
-
text: stringifyForMcp(result)
|
|
337
|
-
}],
|
|
338
|
-
...outputSchema ? { structuredContent: result } : {}
|
|
339
|
-
};
|
|
340
|
-
} catch (error) {
|
|
341
|
-
return {
|
|
342
|
-
isError: true,
|
|
343
|
-
content: [{
|
|
344
|
-
type: "text",
|
|
345
|
-
text: `Error invoking "${name}": ${formatMcpError(error)}`
|
|
346
|
-
}]
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
function registerResourceHandlers(server, ctx, exposeSharedState) {
|
|
352
|
-
server.setRequestHandler("resources/list", async () => {
|
|
353
|
-
const resources = ctx.agent.list().resources.map((resource) => ({
|
|
354
|
-
uri: resource.uri,
|
|
355
|
-
name: resource.name,
|
|
356
|
-
description: resource.description,
|
|
357
|
-
mimeType: resource.mimeType
|
|
358
|
-
}));
|
|
359
|
-
if (exposeSharedState !== false) {
|
|
360
|
-
const filter = typeof exposeSharedState === "function" ? exposeSharedState : () => true;
|
|
361
|
-
for (const key of ctx.rpc.sharedState.keys()) {
|
|
362
|
-
if (!filter(key)) continue;
|
|
363
|
-
resources.push({
|
|
364
|
-
uri: `devframe://state/${encodeURIComponent(key)}`,
|
|
365
|
-
name: key,
|
|
366
|
-
description: `Shared state: ${key}`,
|
|
367
|
-
mimeType: "application/json"
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return { resources };
|
|
372
|
-
});
|
|
373
|
-
server.setRequestHandler("resources/read", async (request) => {
|
|
374
|
-
const { uri } = request.params;
|
|
375
|
-
const parsed = parseResourceUri(uri);
|
|
376
|
-
if (parsed.kind === "resource") {
|
|
377
|
-
const content = await ctx.agent.read(parsed.id);
|
|
378
|
-
return { contents: [{
|
|
379
|
-
uri,
|
|
380
|
-
mimeType: content.mimeType ?? "application/json",
|
|
381
|
-
text: content.text ?? stringifyForMcp(content.json)
|
|
382
|
-
}] };
|
|
383
|
-
}
|
|
384
|
-
if (parsed.kind === "state") return { contents: [{
|
|
385
|
-
uri,
|
|
386
|
-
mimeType: "application/json",
|
|
387
|
-
text: stringifyForMcp((await ctx.rpc.sharedState.get(parsed.key)).value())
|
|
388
|
-
}] };
|
|
389
|
-
throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* MCP constrains a tool's `outputSchema` to a JSON Schema of `type:
|
|
394
|
-
* "object"` — clients (the SDK included) reject anything else. Non-object
|
|
395
|
-
* return schemas (e.g. a schema for `void` / a bare string) simply project
|
|
396
|
-
* no output schema; the text content still carries the result.
|
|
397
|
-
*/
|
|
398
|
-
function usableOutputSchema(schema) {
|
|
399
|
-
return schema && typeof schema === "object" && schema.type === "object" ? schema : void 0;
|
|
400
|
-
}
|
|
401
|
-
function projectTool(name, tool, ctx) {
|
|
402
|
-
const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx);
|
|
403
|
-
const outputSchema = usableOutputSchema(tool.outputSchema ?? computeOutputSchema(tool, ctx));
|
|
404
|
-
return {
|
|
405
|
-
name,
|
|
406
|
-
title: tool.title,
|
|
407
|
-
description: tool.description,
|
|
408
|
-
inputSchema,
|
|
409
|
-
...outputSchema ? { outputSchema } : {},
|
|
410
|
-
annotations: {
|
|
411
|
-
title: tool.title,
|
|
412
|
-
readOnlyHint: tool.safety === "read",
|
|
413
|
-
destructiveHint: tool.safety === "destructive"
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
function computeInputSchema(tool, ctx) {
|
|
418
|
-
if (tool.kind === "tool") return argsToJsonSchema(tool.args).schema;
|
|
419
|
-
if (tool.kind !== "rpc" || !tool.rpcName) return {
|
|
420
|
-
type: "object",
|
|
421
|
-
properties: {}
|
|
422
|
-
};
|
|
423
|
-
const def = ctx.rpc.definitions.get(tool.rpcName);
|
|
424
|
-
if (!def) return {
|
|
425
|
-
type: "object",
|
|
426
|
-
properties: {}
|
|
427
|
-
};
|
|
428
|
-
const args = def.args;
|
|
429
|
-
return argsToJsonSchema(args).schema;
|
|
430
|
-
}
|
|
431
|
-
function computeOutputSchema(tool, ctx) {
|
|
432
|
-
if (tool.kind !== "rpc" || !tool.rpcName) return void 0;
|
|
433
|
-
const def = ctx.rpc.definitions.get(tool.rpcName);
|
|
434
|
-
if (!def) return void 0;
|
|
435
|
-
return returnToJsonSchema(def.returns);
|
|
436
|
-
}
|
|
437
|
-
function parseResourceUri(uri) {
|
|
438
|
-
const match = uri.match(/^devframe:\/\/(resource|state)\/(.+)$/);
|
|
439
|
-
if (!match) return { kind: "unknown" };
|
|
440
|
-
const [, kind, rest] = match;
|
|
441
|
-
const decoded = decodeURIComponent(rest);
|
|
442
|
-
if (kind === "resource") return {
|
|
443
|
-
kind: "resource",
|
|
444
|
-
id: decoded
|
|
445
|
-
};
|
|
446
|
-
return {
|
|
447
|
-
kind: "state",
|
|
448
|
-
key: decoded
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
//#endregion
|
|
452
|
-
//#region src/adapters/mcp/fetch.ts
|
|
453
|
-
/**
|
|
454
|
-
* Build a framework-agnostic MCP Streamable-HTTP endpoint over a devframe
|
|
455
|
-
* context: a web-standard `Request → Response` handler any host can mount —
|
|
456
|
-
* h3 (see `mountMcpHttp`), a Next.js App Router route, or any other
|
|
457
|
-
* fetch-shaped server.
|
|
458
|
-
*
|
|
459
|
-
* Each MCP session gets its own {@link WebStandardStreamableHTTPServerTransport}
|
|
460
|
-
* and MCP server (built from the shared, live `ctx` via
|
|
461
|
-
* `buildMcpServerFromContext`), correlated by the `Mcp-Session-Id` header: an
|
|
462
|
-
* `initialize` POST spins up a session; later requests route to it; a `DELETE`
|
|
463
|
-
* (or client disconnect) tears it down. The origin gate guards every request:
|
|
464
|
-
* loopback-default DNS-rebinding protection that — unlike the WS upgrade's
|
|
465
|
-
* `isAllowedOrigin` — also rejects `Origin`-less requests, so a route-based
|
|
466
|
-
* endpoint isn't reachable by an arbitrary local process.
|
|
467
|
-
*/
|
|
468
|
-
function createMcpFetchHandler(ctx, options) {
|
|
469
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
470
|
-
const allowedOrigins = options.allowedOrigins;
|
|
471
|
-
function drop(sessionId) {
|
|
472
|
-
const session = sessions.get(sessionId);
|
|
473
|
-
if (!session) return;
|
|
474
|
-
sessions.delete(sessionId);
|
|
475
|
-
session.dispose();
|
|
476
|
-
}
|
|
477
|
-
async function createSession() {
|
|
478
|
-
let session;
|
|
479
|
-
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
480
|
-
sessionIdGenerator: () => randomUUID(),
|
|
481
|
-
onsessioninitialized: (id) => {
|
|
482
|
-
sessions.set(id, session);
|
|
483
|
-
},
|
|
484
|
-
onsessionclosed: (id) => {
|
|
485
|
-
drop(id);
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
const { server, dispose } = buildMcpServerFromContext(ctx, {
|
|
489
|
-
serverName: options.serverName,
|
|
490
|
-
serverVersion: options.serverVersion,
|
|
491
|
-
exposeSharedState: options.exposeSharedState
|
|
492
|
-
});
|
|
493
|
-
session = {
|
|
494
|
-
transport,
|
|
495
|
-
dispose: async () => {
|
|
496
|
-
dispose();
|
|
497
|
-
await server.close();
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
transport.onclose = () => {
|
|
501
|
-
if (transport.sessionId) drop(transport.sessionId);
|
|
502
|
-
};
|
|
503
|
-
await server.connect(transport);
|
|
504
|
-
return session;
|
|
505
|
-
}
|
|
506
|
-
async function handle(req) {
|
|
507
|
-
const origin = req.headers.get("origin") ?? void 0;
|
|
508
|
-
if (allowedOrigins !== false && (origin === void 0 || !isAllowedOrigin(origin, allowedOrigins ?? []))) return new Response("Forbidden: origin required", { status: 403 });
|
|
509
|
-
const sessionId = req.headers.get("mcp-session-id") ?? void 0;
|
|
510
|
-
let session = sessionId ? sessions.get(sessionId) : void 0;
|
|
511
|
-
if (!session && req.method === "POST") {
|
|
512
|
-
let body;
|
|
513
|
-
try {
|
|
514
|
-
body = await req.json();
|
|
515
|
-
} catch {
|
|
516
|
-
body = void 0;
|
|
517
|
-
}
|
|
518
|
-
if (!sessionId && isInitializeRequest(body)) session = await createSession();
|
|
519
|
-
else return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: no valid session ID and not an initialize request", { status: sessionId ? 404 : 400 });
|
|
520
|
-
return session.transport.handleRequest(req, { parsedBody: body });
|
|
521
|
-
}
|
|
522
|
-
if (!session) return new Response(sessionId ? "Not Found: unknown MCP session" : "Bad Request: missing MCP session ID", { status: sessionId ? 404 : 400 });
|
|
523
|
-
return session.transport.handleRequest(req);
|
|
524
|
-
}
|
|
525
|
-
return {
|
|
526
|
-
fetch: handle,
|
|
527
|
-
dispose: async () => {
|
|
528
|
-
const live = [...sessions.values()];
|
|
529
|
-
sessions.clear();
|
|
530
|
-
await Promise.all(live.map((session) => session.dispose()));
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
//#endregion
|
|
535
|
-
//#region src/adapters/mcp/http.ts
|
|
536
|
-
var http_exports = /* @__PURE__ */ __exportAll({ mountMcpHttp: () => mountMcpHttp });
|
|
537
|
-
/**
|
|
538
|
-
* Mount an MCP Streamable-HTTP endpoint on an h3 app at `path` — the h3
|
|
539
|
-
* binding over {@link createMcpFetchHandler}, which owns the sessions, the
|
|
540
|
-
* origin gate, and the transport plumbing.
|
|
541
|
-
*
|
|
542
|
-
* The handler is web-standard — it takes the h3 event's web `Request` and
|
|
543
|
-
* returns a web `Response` (an SSE `ReadableStream` body for the
|
|
544
|
-
* server→client stream). We copy that response onto `event.res` and return
|
|
545
|
-
* its body rather than returning the `Response` object directly, so a
|
|
546
|
-
* legitimate MCP 404 (unknown session) isn't swallowed by h3's
|
|
547
|
-
* "Response-with-404 falls through to the next handler" rule (which would
|
|
548
|
-
* otherwise hand the request to the SPA static catch-all).
|
|
549
|
-
*/
|
|
550
|
-
function mountMcpHttp(app, ctx, path, options) {
|
|
551
|
-
const handler = createMcpFetchHandler(ctx, options);
|
|
552
|
-
app.use(path, defineHandler(async (event) => respond(event, await handler.fetch(event.req))));
|
|
553
|
-
return { dispose: handler.dispose };
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* Copy a web `Response` from the MCP transport onto the h3 event's response
|
|
557
|
-
* and return its body. Returning the body (a `ReadableStream` or `null`)
|
|
558
|
-
* rather than the `Response` object avoids h3's 404-fall-through behavior.
|
|
559
|
-
*/
|
|
560
|
-
function respond(event, response) {
|
|
561
|
-
event.res.status = response.status;
|
|
562
|
-
event.res.statusText = response.statusText;
|
|
563
|
-
response.headers.forEach((value, key) => {
|
|
564
|
-
event.res.headers.set(key, value);
|
|
565
|
-
});
|
|
566
|
-
return response.body ?? "";
|
|
567
|
-
}
|
|
568
|
-
//#endregion
|
|
569
|
-
export { createMcpServer as i, mountMcpHttp as n, createMcpFetchHandler as r, http_exports as t };
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
2
|
-
//#region src/adapters/mcp/transports.ts
|
|
3
|
-
/**
|
|
4
|
-
* Start the MCP server on stdio. Returns a stop function.
|
|
5
|
-
* @internal
|
|
6
|
-
*/
|
|
7
|
-
async function startStdioTransport(server) {
|
|
8
|
-
const transport = new StdioServerTransport();
|
|
9
|
-
await server.connect(transport);
|
|
10
|
-
return async () => {
|
|
11
|
-
await server.close();
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
//#endregion
|
|
15
|
-
export { startStdioTransport };
|