gesso-framework 0.4.2 → 0.5.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.
@@ -0,0 +1,317 @@
1
+ import { E as WorkerHandle, F as JsonSchema, a as resolveTarget, c as AgentResource, d as AgentSurfaceOptions, f as AgentTool, g as ServedChannel, h as resourceUri, i as outline, l as AgentSurface, m as agentSurface, n as UiRefs, o as uiSurface, p as AgentToolResult, r as UiSurfaceOptions, s as AgentConfirmation, t as UiHost, u as AgentSurfaceLike } from "../ui-U39HjNFA.js";
2
+ //#region src/agent/mcp.d.ts
3
+ /**
4
+ * An agent surface, served over the Model Context Protocol.
5
+ *
6
+ * Two layers. `handleMcpMessage` answers one JSON-RPC message and knows
7
+ * nothing about how it arrived, which is all a stdio transport or a
8
+ * relay needs. `mcpHandler` wraps it in MCP's Streamable HTTP
9
+ * transport as a `fetch`-style function, `(Request) => Response`,
10
+ * which is what `Bun.serve`, Deno, a service worker and every modern
11
+ * Node server framework take, so the framework imports no server.
12
+ *
13
+ * What it implements is the part a channel needs: `initialize`, `ping`,
14
+ * `tools/list`, `tools/call`, `resources/list` and `resources/read`.
15
+ * Responses are plain JSON rather than an event stream, which the
16
+ * transport allows, and the GET stream for server-initiated messages
17
+ * is declined with 405, which it also allows: nothing here sends one,
18
+ * so the capabilities say neither `listChanged` nor `subscribe`.
19
+ */
20
+ /** The protocol revisions this server speaks, newest first. */
21
+ declare const MCP_PROTOCOL_VERSIONS: readonly ['2025-11-25', '2025-06-18', '2025-03-26'];
22
+ interface McpServerInfo {
23
+ /** The name a client shows for this server. Defaults to `gesso`. */
24
+ name?: string;
25
+ version?: string;
26
+ /** Told to the agent on connecting: what this application is, in a sentence or two. */
27
+ instructions?: string;
28
+ }
29
+ type JsonRpcId = string | number;
30
+ type JsonRpcResponse = {
31
+ jsonrpc: '2.0';
32
+ id: JsonRpcId | null;
33
+ result: unknown;
34
+ } | {
35
+ jsonrpc: '2.0';
36
+ id: JsonRpcId | null;
37
+ error: {
38
+ code: number;
39
+ message: string;
40
+ };
41
+ };
42
+ /**
43
+ * Answers one JSON-RPC message. Returns null for a notification, which
44
+ * takes no answer, and never throws: a malformed message is answered
45
+ * with the JSON-RPC error that says what was wrong with it.
46
+ */
47
+ declare function handleMcpMessage(surface: AgentSurfaceLike, message: unknown, info?: McpServerInfo): Promise<JsonRpcResponse | null>;
48
+ interface McpHandlerOptions extends McpServerInfo {
49
+ /**
50
+ * Origins a browser may call from. A request carrying any other
51
+ * `Origin` is refused, as the transport requires: without it, a web
52
+ * page the person happens to have open could reach a server bound to
53
+ * their own machine and send commands to their application. Agents
54
+ * that are not browsers send no `Origin` and are unaffected.
55
+ */
56
+ allowedOrigins?: readonly string[];
57
+ /**
58
+ * A secret every request must carry as `Authorization: Bearer
59
+ * <token>`. Worth setting whenever the port is reachable by anything
60
+ * other than the person's own agents.
61
+ */
62
+ token?: string;
63
+ }
64
+ /**
65
+ * The Streamable HTTP transport, as a `fetch` handler.
66
+ *
67
+ * Bun.serve({ hostname: '127.0.0.1', port: 7310, fetch: mcpHandler(surface, { name: 'notes' }) });
68
+ *
69
+ * It answers every path; mount it where the server routes `/mcp`.
70
+ */
71
+ declare function mcpHandler(surface: AgentSurfaceLike, options?: McpHandlerOptions): (request: Request) => Promise<Response>;
72
+ //#endregion
73
+ //#region src/agent/validate.d.ts
74
+ /**
75
+ * Checks a value against the JSON Schema a channel description uses.
76
+ *
77
+ * Not a general validator. It understands the keywords
78
+ * `gesso-vite-plugin` writes (`type`, `enum`, `const`, `anyOf`,
79
+ * `items`, `prefixItems`, `minItems`, `properties`, `required`,
80
+ * `additionalProperties`, `not` and `$ref` into `$defs`), and passes
81
+ * anything else. That is enough to stop the case that matters: an
82
+ * agent sending `"3"` where a command takes a number, which the
83
+ * command would accept, store, and fail on much later and somewhere
84
+ * else.
85
+ *
86
+ * Returns the first problem as a sentence naming where it is, written
87
+ * for the agent that sent the value so it can correct itself, or null.
88
+ */
89
+ declare function validate(schema: JsonSchema, value: unknown, root?: JsonSchema, at?: string): string | null;
90
+ //#endregion
91
+ //#region src/agent/remote.d.ts
92
+ /**
93
+ * An agent surface across a thread.
94
+ *
95
+ * A web application's channels are served in workers, and an agent
96
+ * reaches the page, so the surface has to cross from one to the other.
97
+ * The worker that serves channels answers a `gesso:agent` port with its
98
+ * own surface (`serveAgentPort`); the page holds the other end as a
99
+ * surface of its own (`remoteSurface`); and a thread that knows several
100
+ * such ports, the render worker with its channel workers behind it,
101
+ * offers them as one (`combineSurfaces`).
102
+ *
103
+ * The four operations cross as they are. MCP itself is spoken only at
104
+ * the end that faces the agent, so nothing in a worker parses JSON-RPC.
105
+ */
106
+ /** The port key a thread answers with its agent surface. */
107
+ declare const AGENT_PORT = "gesso:agent";
108
+ /** What crosses an agent port, page to worker. */
109
+ type AgentPortRequest = {
110
+ id: number;
111
+ op: 'tools';
112
+ } | {
113
+ id: number;
114
+ op: 'resources';
115
+ } | {
116
+ id: number;
117
+ op: 'call';
118
+ name: string;
119
+ args?: Readonly<Record<string, unknown>>;
120
+ } | {
121
+ id: number;
122
+ op: 'read';
123
+ uri: string;
124
+ };
125
+ /** What crosses back. */
126
+ type AgentPortResponse = {
127
+ id: number;
128
+ result: unknown;
129
+ } | {
130
+ id: number;
131
+ error: string;
132
+ };
133
+ /** How a thread asks the person, wherever the person is. */
134
+ type AgentConfirm = (request: AgentConfirmation) => boolean | Promise<boolean>;
135
+ /** A `MessagePort`, or anything that posts and receives like one. */
136
+ type AgentPort = Pick<MessagePort, 'postMessage' | 'onmessage'>;
137
+ /**
138
+ * Answers a port with a surface.
139
+ *
140
+ * The surface is made on the first request rather than when the port
141
+ * arrives: a port is opened by a page that may never ask anything, and
142
+ * a surface subscribes to every view key once it is used. It is given
143
+ * a `confirm` that asks the far end of this port, so a command marked
144
+ * `@confirm` reaches the person wherever they are; the far end answers
145
+ * no when it has no way to ask.
146
+ */
147
+ declare function serveAgentPort(port: AgentPort, makeSurface: (confirm: AgentConfirm) => AgentSurfaceLike): () => void;
148
+ /**
149
+ * The surface at the other end of a port.
150
+ *
151
+ * A request nobody answers within `timeoutMs` (default 2000) is taken
152
+ * as a thread with nothing to offer: a worker that serves no channels
153
+ * never installs the answering side, and an agent asking the whole
154
+ * application should hear about the threads that do rather than wait
155
+ * on one that does not.
156
+ */
157
+ declare function remoteSurface(port: AgentPort, options?: {
158
+ timeoutMs?: number;
159
+ confirm?: AgentConfirm;
160
+ }): AgentSurfaceLike;
161
+ /**
162
+ * Several surfaces as one: tools and resources listed together, a call
163
+ * or a read sent to whichever surface listed it. When two list the same
164
+ * name, the first keeps it, as the order of the threads is the order
165
+ * the application registered them in.
166
+ */
167
+ declare function combineSurfaces(surfaces: readonly AgentSurfaceLike[]): AgentSurfaceLike;
168
+ //#endregion
169
+ //#region src/agent/dev.d.ts
170
+ /**
171
+ * The page's half of the development agent bridge.
172
+ *
173
+ * `gesso-vite-plugin` serves MCP at `/__gesso/mcp` on the dev server,
174
+ * but the channels are in the page's workers, and a server cannot
175
+ * reach a worker. The page can, and it already holds a socket to the
176
+ * server: Vite's HMR connection. So the server relays each JSON-RPC
177
+ * message down that socket, this answers it against the render
178
+ * worker's agent port, and the answer goes back up the same way.
179
+ *
180
+ * MCP is spoken here, in the page, rather than on the server, so the
181
+ * server needs nothing from the framework and the workers need nothing
182
+ * from MCP. The plugin injects the call in a dev server only; a build
183
+ * carries no reference to this module.
184
+ */
185
+ /** The part of `import.meta.hot` this uses. */
186
+ interface DevAgentHot {
187
+ send(event: string, data?: unknown): void;
188
+ on(event: string, listener: (data: never) => void): void;
189
+ }
190
+ /** What the bridge needs of an application: a way to reach its render worker. */
191
+ interface DevAgentApp {
192
+ openRenderPort(key: string): MessagePort | undefined;
193
+ }
194
+ /** Server to page. */
195
+ interface DevAgentRequest {
196
+ readonly id: number;
197
+ readonly message: unknown;
198
+ }
199
+ /** Page to server. */
200
+ interface DevAgentResponse {
201
+ readonly id: number;
202
+ readonly response: JsonRpcResponse | null;
203
+ }
204
+ /** The HMR events the bridge speaks. The plugin's half uses the same names. */
205
+ declare const DEV_AGENT_EVENTS: {
206
+ readonly ready: 'gesso:agent:ready';
207
+ readonly request: 'gesso:agent:request';
208
+ readonly response: 'gesso:agent:response';
209
+ };
210
+ declare function connectDevAgent(app: DevAgentApp, hot: DevAgentHot, info?: McpServerInfo): void;
211
+ //#endregion
212
+ //#region src/agent/webmcp.d.ts
213
+ /**
214
+ * An application's channels, offered to an agent in the browser.
215
+ *
216
+ * WebMCP (https://webmachinelearning.github.io/webmcp/) is a page
217
+ * registering tools with the browser, `document.modelContext.registerTool`,
218
+ * for an agent the browser runs or hosts to call. It is MCP without
219
+ * the server: no port, no process, and the tools live exactly as long
220
+ * as the page does. The channel surface is already a list of tools
221
+ * with JSON Schema inputs and an answer per call, so this registers
222
+ * each one and routes its calls back through the render worker.
223
+ *
224
+ * What the spec gives a tool that a channel does not, and the reverse:
225
+ *
226
+ * - `readOnlyHint` is a channel's view tool; `consequentialHint`, the
227
+ * spec's "significant, real-world, or non-reversible", is a command
228
+ * marked `@destructive`. `untrustedContentHint` is never set: a
229
+ * channel's view is the application's own data.
230
+ * - There is no output schema. The result of a call is serialized to
231
+ * JSON for the agent, so a call answers with the view it left.
232
+ * - There is no way to ask the person. A command marked `@confirm`
233
+ * asks through `confirm`, `window.confirm` unless told otherwise.
234
+ * - A failed call rejects, which the spec turns into a failed
235
+ * `executeTool` the agent reads, with the sentence that says why.
236
+ *
237
+ * The API is behind an origin trial in Chrome 149 to 156, and was
238
+ * `navigator.modelContext` until Chrome 150; the old name is read when
239
+ * the new one is missing. Where neither exists, nothing is registered
240
+ * and nothing fails: a page offering tools to a browser that takes
241
+ * none is an ordinary page.
242
+ */
243
+ /** The part of `ModelContext` this uses. */
244
+ interface ModelContextLike {
245
+ registerTool(tool: {
246
+ name: string;
247
+ title?: string;
248
+ description: string;
249
+ inputSchema?: object;
250
+ annotations?: {
251
+ readOnlyHint?: boolean;
252
+ consequentialHint?: boolean;
253
+ };
254
+ execute: (input: Record<string, unknown>, options?: {
255
+ signal?: AbortSignal;
256
+ }) => Promise<unknown>;
257
+ }, options?: {
258
+ signal?: AbortSignal;
259
+ }): Promise<void>;
260
+ }
261
+ interface WebMcpOptions {
262
+ /**
263
+ * Asks the person whether a `@confirm` command may be sent. Defaults to
264
+ * the browser's own `window.confirm`, which an application with its
265
+ * own dialogs will want to replace.
266
+ */
267
+ confirm?: AgentConfirm;
268
+ /** Where to register. Defaults to the page's own `modelContext`. */
269
+ modelContext?: ModelContextLike;
270
+ }
271
+ /** The page's model context, if the browser has one. */
272
+ declare function pageModelContext(): ModelContextLike | undefined;
273
+ /**
274
+ * Registers every tool a surface offers, and returns what removes them.
275
+ *
276
+ * A tool the browser refuses, a name already taken by something else on
277
+ * the page, is reported on the console and skipped rather than taking
278
+ * the rest down with it. Resolves to a no-op when there is no model
279
+ * context to register with.
280
+ */
281
+ declare function registerWebMcpTools(surface: AgentSurfaceLike, modelContext?: ModelContextLike | undefined): Promise<() => void>;
282
+ /**
283
+ * Registers an application's channels once it is mounted.
284
+ *
285
+ * Reaches them the way the development bridge does, through an agent
286
+ * port to the render worker, so every channel the page can reach is
287
+ * offered, wherever it is served.
288
+ */
289
+ declare function connectWebMcp(app: {
290
+ openRenderPort(key: string): MessagePort | undefined;
291
+ }, options?: WebMcpOptions): Promise<() => void>;
292
+ /** The browser's own dialog, worded for a person who did not ask for anything. */
293
+ declare function confirmInWindow(request: AgentConfirmation): boolean;
294
+ //#endregion
295
+ //#region src/agent/app.d.ts
296
+ /** What a thread that draws the application can offer an agent. */
297
+ interface ApplicationAgentParts {
298
+ /** The channels fed from this thread. */
299
+ readonly channels: () => readonly ServedChannel[];
300
+ /** The workers behind it, each asked over an agent port of its own. */
301
+ readonly workers: () => Iterable<WorkerHandle>;
302
+ /** The screen, once the application has started. */
303
+ readonly ui: () => UiHost | undefined;
304
+ }
305
+ /**
306
+ * Answers an agent port with the whole application: the channels this
307
+ * thread feeds, whatever each worker behind it serves, and the screen.
308
+ *
309
+ * The thread that draws is the one that knows all three, which is the
310
+ * render worker in the worker configuration and the page in the single
311
+ * thread one; both answer with this. The screen comes last, so a
312
+ * channel tool keeps its name.
313
+ */
314
+ declare function serveApplicationAgent(port: MessagePort, parts: ApplicationAgentParts): () => void;
315
+ //#endregion
316
+ export { AGENT_PORT, type AgentConfirmation, type AgentPortRequest, type AgentPortResponse, type AgentResource, type AgentSurface, type AgentSurfaceLike, type AgentSurfaceOptions, type AgentTool, type AgentToolResult, type ApplicationAgentParts, DEV_AGENT_EVENTS, type DevAgentApp, type DevAgentHot, type DevAgentRequest, type DevAgentResponse, type JsonRpcResponse, MCP_PROTOCOL_VERSIONS, type McpHandlerOptions, type McpServerInfo, type ModelContextLike, type UiHost, UiRefs, type UiSurfaceOptions, type WebMcpOptions, agentSurface, combineSurfaces, confirmInWindow, connectDevAgent, connectWebMcp, handleMcpMessage, mcpHandler, outline, pageModelContext, registerWebMcpTools, remoteSurface, resolveTarget, resourceUri, serveAgentPort, serveApplicationAgent, uiSurface, validate };
317
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,184 @@
1
+ import { a as agentSurface, i as serveAgentPort, n as combineSurfaces, o as resourceUri, r as remoteSurface, s as validate, t as AGENT_PORT } from "../remote-xxij8zXe.js";
2
+ import { a as resolveTarget, i as outline, n as serveApplicationAgent, o as uiSurface, r as UiRefs } from "../app-BvlIO1G9.js";
3
+ import { i as registerWebMcpTools, n as connectWebMcp, r as pageModelContext, t as confirmInWindow } from "../webmcp-CJVFvAHe.js";
4
+ //#region src/agent/mcp.ts
5
+ /**
6
+ * An agent surface, served over the Model Context Protocol.
7
+ *
8
+ * Two layers. `handleMcpMessage` answers one JSON-RPC message and knows
9
+ * nothing about how it arrived, which is all a stdio transport or a
10
+ * relay needs. `mcpHandler` wraps it in MCP's Streamable HTTP
11
+ * transport as a `fetch`-style function, `(Request) => Response`,
12
+ * which is what `Bun.serve`, Deno, a service worker and every modern
13
+ * Node server framework take, so the framework imports no server.
14
+ *
15
+ * What it implements is the part a channel needs: `initialize`, `ping`,
16
+ * `tools/list`, `tools/call`, `resources/list` and `resources/read`.
17
+ * Responses are plain JSON rather than an event stream, which the
18
+ * transport allows, and the GET stream for server-initiated messages
19
+ * is declined with 405, which it also allows: nothing here sends one,
20
+ * so the capabilities say neither `listChanged` nor `subscribe`.
21
+ */
22
+ /** The protocol revisions this server speaks, newest first. */
23
+ const MCP_PROTOCOL_VERSIONS = [
24
+ "2025-11-25",
25
+ "2025-06-18",
26
+ "2025-03-26"
27
+ ];
28
+ const PARSE_ERROR = -32700;
29
+ const INVALID_REQUEST = -32600;
30
+ const METHOD_NOT_FOUND = -32601;
31
+ const INVALID_PARAMS = -32602;
32
+ /** MCP's code for a resource that does not exist. */
33
+ const RESOURCE_NOT_FOUND = -32002;
34
+ /**
35
+ * Answers one JSON-RPC message. Returns null for a notification, which
36
+ * takes no answer, and never throws: a malformed message is answered
37
+ * with the JSON-RPC error that says what was wrong with it.
38
+ */
39
+ async function handleMcpMessage(surface, message, info = {}) {
40
+ if (!isRequest(message)) return error(null, INVALID_REQUEST, "Expected a JSON-RPC 2.0 request with a method.");
41
+ if (message.id === void 0) return null;
42
+ const { id } = message;
43
+ const params = message.params ?? {};
44
+ switch (message.method) {
45
+ case "initialize": {
46
+ const asked = params.protocolVersion;
47
+ return ok(id, {
48
+ protocolVersion: MCP_PROTOCOL_VERSIONS.find((supported) => supported === asked) ?? MCP_PROTOCOL_VERSIONS[0],
49
+ capabilities: {
50
+ tools: {},
51
+ resources: {}
52
+ },
53
+ serverInfo: {
54
+ name: info.name ?? "gesso",
55
+ version: info.version ?? "0.0.0"
56
+ },
57
+ ...info.instructions === void 0 ? {} : { instructions: info.instructions }
58
+ });
59
+ }
60
+ case "ping": return ok(id, {});
61
+ case "tools/list": return ok(id, { tools: await surface.tools() });
62
+ case "tools/call": {
63
+ if (typeof params.name !== "string") return error(id, INVALID_PARAMS, "tools/call needs the name of a tool.");
64
+ const args = params.arguments;
65
+ if (args !== void 0 && (args === null || typeof args !== "object" || Array.isArray(args))) return error(id, INVALID_PARAMS, "tools/call arguments must be an object.");
66
+ return ok(id, await surface.call(params.name, args));
67
+ }
68
+ case "resources/list": return ok(id, { resources: await surface.resources() });
69
+ case "resources/templates/list": return ok(id, { resourceTemplates: [] });
70
+ case "resources/read": {
71
+ const uri = params.uri;
72
+ const view = typeof uri === "string" ? await surface.read(uri) : void 0;
73
+ if (typeof uri !== "string" || view === void 0) return error(id, RESOURCE_NOT_FOUND, `No resource at ${String(uri)}.`);
74
+ return ok(id, { contents: [{
75
+ uri,
76
+ mimeType: "application/json",
77
+ text: JSON.stringify(view)
78
+ }] });
79
+ }
80
+ default: return error(id, METHOD_NOT_FOUND, `This server does not implement ${message.method}.`);
81
+ }
82
+ }
83
+ /**
84
+ * The Streamable HTTP transport, as a `fetch` handler.
85
+ *
86
+ * Bun.serve({ hostname: '127.0.0.1', port: 7310, fetch: mcpHandler(surface, { name: 'notes' }) });
87
+ *
88
+ * It answers every path; mount it where the server routes `/mcp`.
89
+ */
90
+ function mcpHandler(surface, options = {}) {
91
+ return async (request) => {
92
+ const origin = request.headers.get("origin");
93
+ if (origin !== null && !(options.allowedOrigins ?? []).includes(origin)) return text(403, `Requests from ${origin} are not allowed.`);
94
+ if (options.token !== void 0 && request.headers.get("authorization") !== `Bearer ${options.token}`) return text(401, "This server needs a bearer token.");
95
+ const version = request.headers.get("mcp-protocol-version");
96
+ if (version !== null && !MCP_PROTOCOL_VERSIONS.includes(version)) return text(400, `Unsupported MCP protocol version ${version}.`);
97
+ if (request.method !== "POST") return new Response(null, {
98
+ status: 405,
99
+ headers: { allow: "POST" }
100
+ });
101
+ let message;
102
+ try {
103
+ message = await request.json();
104
+ } catch {
105
+ return json(400, error(null, PARSE_ERROR, "The body is not JSON."));
106
+ }
107
+ const response = await handleMcpMessage(surface, message, options);
108
+ return response === null ? new Response(null, { status: 202 }) : json(200, response);
109
+ };
110
+ }
111
+ function isRequest(message) {
112
+ const candidate = message;
113
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && candidate.jsonrpc === "2.0" && typeof candidate.method === "string";
114
+ }
115
+ function ok(id, result) {
116
+ return {
117
+ jsonrpc: "2.0",
118
+ id,
119
+ result
120
+ };
121
+ }
122
+ function error(id, code, message) {
123
+ return {
124
+ jsonrpc: "2.0",
125
+ id,
126
+ error: {
127
+ code,
128
+ message
129
+ }
130
+ };
131
+ }
132
+ function json(status, body) {
133
+ return new Response(JSON.stringify(body), {
134
+ status,
135
+ headers: { "content-type": "application/json" }
136
+ });
137
+ }
138
+ function text(status, body) {
139
+ return new Response(body, {
140
+ status,
141
+ headers: { "content-type": "text/plain; charset=utf-8" }
142
+ });
143
+ }
144
+ //#endregion
145
+ //#region src/agent/dev.ts
146
+ /** The HMR events the bridge speaks. The plugin's half uses the same names. */
147
+ const DEV_AGENT_EVENTS = {
148
+ ready: "gesso:agent:ready",
149
+ request: "gesso:agent:request",
150
+ response: "gesso:agent:response"
151
+ };
152
+ function connectDevAgent(app, hot, info = {}) {
153
+ let surface;
154
+ /**
155
+ * Opened on the first request, because the render worker does not
156
+ * exist until the app mounts, and an agent may well ask first.
157
+ */
158
+ const reach = () => {
159
+ if (surface !== void 0) return surface;
160
+ const port = app.openRenderPort(AGENT_PORT);
161
+ if (port === void 0) return combineSurfaces([]);
162
+ surface = remoteSurface(port, { confirm: confirmInWindow });
163
+ return surface;
164
+ };
165
+ hot.on(DEV_AGENT_EVENTS.request, (request) => {
166
+ handleMcpMessage(reach(), request.message, {
167
+ name: info.name ?? (document.title || "gesso"),
168
+ ...info
169
+ }).then((response) => hot.send(DEV_AGENT_EVENTS.response, {
170
+ id: request.id,
171
+ response
172
+ }));
173
+ });
174
+ const announce = () => hot.send(DEV_AGENT_EVENTS.ready, {
175
+ title: document.title,
176
+ url: location.href
177
+ });
178
+ announce();
179
+ hot.on("vite:ws:connect", announce);
180
+ }
181
+ //#endregion
182
+ export { AGENT_PORT, DEV_AGENT_EVENTS, MCP_PROTOCOL_VERSIONS, UiRefs, agentSurface, combineSurfaces, confirmInWindow, connectDevAgent, connectWebMcp, handleMcpMessage, mcpHandler, outline, pageModelContext, registerWebMcpTools, remoteSurface, resolveTarget, resourceUri, serveAgentPort, serveApplicationAgent, uiSurface, validate };
183
+
184
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/agent/mcp.ts","../../src/agent/dev.ts"],"sourcesContent":["import type { AgentSurfaceLike } from './AgentSurface';\n\n/**\n * An agent surface, served over the Model Context Protocol.\n *\n * Two layers. `handleMcpMessage` answers one JSON-RPC message and knows\n * nothing about how it arrived, which is all a stdio transport or a\n * relay needs. `mcpHandler` wraps it in MCP's Streamable HTTP\n * transport as a `fetch`-style function, `(Request) => Response`,\n * which is what `Bun.serve`, Deno, a service worker and every modern\n * Node server framework take, so the framework imports no server.\n *\n * What it implements is the part a channel needs: `initialize`, `ping`,\n * `tools/list`, `tools/call`, `resources/list` and `resources/read`.\n * Responses are plain JSON rather than an event stream, which the\n * transport allows, and the GET stream for server-initiated messages\n * is declined with 405, which it also allows: nothing here sends one,\n * so the capabilities say neither `listChanged` nor `subscribe`.\n */\n\n/** The protocol revisions this server speaks, newest first. */\nexport const MCP_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26'] as const;\n\nexport interface McpServerInfo {\n /** The name a client shows for this server. Defaults to `gesso`. */\n name?: string;\n version?: string;\n /** Told to the agent on connecting: what this application is, in a sentence or two. */\n instructions?: string;\n}\n\ntype JsonRpcId = string | number;\n\ninterface JsonRpcRequest {\n readonly jsonrpc: '2.0';\n readonly id?: JsonRpcId;\n readonly method: string;\n readonly params?: Record<string, unknown>;\n}\n\nexport type JsonRpcResponse =\n | { jsonrpc: '2.0'; id: JsonRpcId | null; result: unknown }\n | { jsonrpc: '2.0'; id: JsonRpcId | null; error: { code: number; message: string } };\n\nconst PARSE_ERROR = -32700;\nconst INVALID_REQUEST = -32600;\nconst METHOD_NOT_FOUND = -32601;\nconst INVALID_PARAMS = -32602;\n/** MCP's code for a resource that does not exist. */\nconst RESOURCE_NOT_FOUND = -32002;\n\n/**\n * Answers one JSON-RPC message. Returns null for a notification, which\n * takes no answer, and never throws: a malformed message is answered\n * with the JSON-RPC error that says what was wrong with it.\n */\nexport async function handleMcpMessage(\n surface: AgentSurfaceLike,\n message: unknown,\n info: McpServerInfo = {}\n): Promise<JsonRpcResponse | null> {\n if (!isRequest(message)) {\n return error(null, INVALID_REQUEST, 'Expected a JSON-RPC 2.0 request with a method.');\n }\n if (message.id === undefined) {\n // `notifications/initialized` and `notifications/cancelled` are the\n // ones a client sends. Neither needs anything from a server whose\n // every call answers before the next is read.\n return null;\n }\n const { id } = message;\n const params = message.params ?? {};\n switch (message.method) {\n case 'initialize': {\n const asked = params.protocolVersion;\n const version = MCP_PROTOCOL_VERSIONS.find(supported => supported === asked) ?? MCP_PROTOCOL_VERSIONS[0];\n return ok(id, {\n protocolVersion: version,\n capabilities: { tools: {}, resources: {} },\n serverInfo: { name: info.name ?? 'gesso', version: info.version ?? '0.0.0' },\n ...(info.instructions === undefined ? {} : { instructions: info.instructions })\n });\n }\n case 'ping':\n return ok(id, {});\n case 'tools/list':\n return ok(id, { tools: await surface.tools() });\n case 'tools/call': {\n if (typeof params.name !== 'string') {\n return error(id, INVALID_PARAMS, 'tools/call needs the name of a tool.');\n }\n const args = params.arguments;\n if (args !== undefined && (args === null || typeof args !== 'object' || Array.isArray(args))) {\n return error(id, INVALID_PARAMS, 'tools/call arguments must be an object.');\n }\n return ok(id, await surface.call(params.name, args as Record<string, unknown> | undefined));\n }\n case 'resources/list':\n return ok(id, { resources: await surface.resources() });\n case 'resources/templates/list':\n return ok(id, { resourceTemplates: [] });\n case 'resources/read': {\n const uri = params.uri;\n const view = typeof uri === 'string' ? await surface.read(uri) : undefined;\n if (typeof uri !== 'string' || view === undefined) {\n return error(id, RESOURCE_NOT_FOUND, `No resource at ${String(uri)}.`);\n }\n return ok(id, { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(view) }] });\n }\n default:\n return error(id, METHOD_NOT_FOUND, `This server does not implement ${message.method}.`);\n }\n}\n\nexport interface McpHandlerOptions extends McpServerInfo {\n /**\n * Origins a browser may call from. A request carrying any other\n * `Origin` is refused, as the transport requires: without it, a web\n * page the person happens to have open could reach a server bound to\n * their own machine and send commands to their application. Agents\n * that are not browsers send no `Origin` and are unaffected.\n */\n allowedOrigins?: readonly string[];\n /**\n * A secret every request must carry as `Authorization: Bearer\n * <token>`. Worth setting whenever the port is reachable by anything\n * other than the person's own agents.\n */\n token?: string;\n}\n\n/**\n * The Streamable HTTP transport, as a `fetch` handler.\n *\n * Bun.serve({ hostname: '127.0.0.1', port: 7310, fetch: mcpHandler(surface, { name: 'notes' }) });\n *\n * It answers every path; mount it where the server routes `/mcp`.\n */\nexport function mcpHandler(\n surface: AgentSurfaceLike,\n options: McpHandlerOptions = {}\n): (request: Request) => Promise<Response> {\n return async request => {\n const origin = request.headers.get('origin');\n if (origin !== null && !(options.allowedOrigins ?? []).includes(origin)) {\n return text(403, `Requests from ${origin} are not allowed.`);\n }\n if (options.token !== undefined && request.headers.get('authorization') !== `Bearer ${options.token}`) {\n return text(401, 'This server needs a bearer token.');\n }\n const version = request.headers.get('mcp-protocol-version');\n if (version !== null && !(MCP_PROTOCOL_VERSIONS as readonly string[]).includes(version)) {\n return text(400, `Unsupported MCP protocol version ${version}.`);\n }\n if (request.method !== 'POST') {\n // GET is the optional stream for messages the server starts, and\n // DELETE ends a session. There are no such messages and no\n // sessions, and 405 is how the transport says so.\n return new Response(null, { status: 405, headers: { allow: 'POST' } });\n }\n let message: unknown;\n try {\n message = await request.json();\n } catch {\n return json(400, error(null, PARSE_ERROR, 'The body is not JSON.'));\n }\n const response = await handleMcpMessage(surface, message, options);\n return response === null ? new Response(null, { status: 202 }) : json(200, response);\n };\n}\n\nfunction isRequest(message: unknown): message is JsonRpcRequest {\n const candidate = message as Partial<JsonRpcRequest> | null;\n return (\n typeof candidate === 'object' &&\n candidate !== null &&\n !Array.isArray(candidate) &&\n candidate.jsonrpc === '2.0' &&\n typeof candidate.method === 'string'\n );\n}\n\nfunction ok(id: JsonRpcId, result: unknown): JsonRpcResponse {\n return { jsonrpc: '2.0', id, result };\n}\n\nfunction error(id: JsonRpcId | null, code: number, message: string): JsonRpcResponse {\n return { jsonrpc: '2.0', id, error: { code, message } };\n}\n\nfunction json(status: number, body: unknown): Response {\n return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n}\n\nfunction text(status: number, body: string): Response {\n return new Response(body, { status, headers: { 'content-type': 'text/plain; charset=utf-8' } });\n}\n","import { handleMcpMessage, type JsonRpcResponse, type McpServerInfo } from './mcp';\nimport { AGENT_PORT, combineSurfaces, remoteSurface } from './remote';\nimport { confirmInWindow } from './webmcp';\nimport type { AgentSurfaceLike } from './AgentSurface';\n\n/**\n * The page's half of the development agent bridge.\n *\n * `gesso-vite-plugin` serves MCP at `/__gesso/mcp` on the dev server,\n * but the channels are in the page's workers, and a server cannot\n * reach a worker. The page can, and it already holds a socket to the\n * server: Vite's HMR connection. So the server relays each JSON-RPC\n * message down that socket, this answers it against the render\n * worker's agent port, and the answer goes back up the same way.\n *\n * MCP is spoken here, in the page, rather than on the server, so the\n * server needs nothing from the framework and the workers need nothing\n * from MCP. The plugin injects the call in a dev server only; a build\n * carries no reference to this module.\n */\n\n/** The part of `import.meta.hot` this uses. */\nexport interface DevAgentHot {\n send(event: string, data?: unknown): void;\n on(event: string, listener: (data: never) => void): void;\n}\n\n/** What the bridge needs of an application: a way to reach its render worker. */\nexport interface DevAgentApp {\n openRenderPort(key: string): MessagePort | undefined;\n}\n\n/** Server to page. */\nexport interface DevAgentRequest {\n readonly id: number;\n readonly message: unknown;\n}\n\n/** Page to server. */\nexport interface DevAgentResponse {\n readonly id: number;\n readonly response: JsonRpcResponse | null;\n}\n\n/** The HMR events the bridge speaks. The plugin's half uses the same names. */\nexport const DEV_AGENT_EVENTS = {\n ready: 'gesso:agent:ready',\n request: 'gesso:agent:request',\n response: 'gesso:agent:response'\n} as const;\n\nexport function connectDevAgent(app: DevAgentApp, hot: DevAgentHot, info: McpServerInfo = {}): void {\n let surface: AgentSurfaceLike | undefined;\n /**\n * Opened on the first request, because the render worker does not\n * exist until the app mounts, and an agent may well ask first.\n */\n const reach = (): AgentSurfaceLike => {\n if (surface !== undefined) {\n return surface;\n }\n const port = app.openRenderPort(AGENT_PORT);\n if (port === undefined) {\n return combineSurfaces([]);\n }\n // A command marked `@confirm` is put to the person here, in the\n // page they are looking at. A browser's own dialog: in development\n // that is enough, and it cannot be mistaken for the application's.\n surface = remoteSurface(port, { confirm: confirmInWindow });\n return surface;\n };\n hot.on(DEV_AGENT_EVENTS.request, (request: DevAgentRequest) => {\n void handleMcpMessage(reach(), request.message, {\n name: info.name ?? (document.title || 'gesso'),\n ...info\n }).then(response => hot.send(DEV_AGENT_EVENTS.response, { id: request.id, response } satisfies DevAgentResponse));\n });\n const announce = () => hot.send(DEV_AGENT_EVENTS.ready, { title: document.title, url: location.href });\n announce();\n // A restarted dev server is a new one that has never heard of this\n // page, and the page reconnects to it without reloading. Saying so\n // again on every connection is what keeps an agent from being told\n // no page is open while one plainly is.\n hot.on('vite:ws:connect', announce);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,wBAAwB;CAAC;CAAc;CAAc;AAAY;AAuB9E,MAAM,cAAc;AACpB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AACzB,MAAM,iBAAiB;;AAEvB,MAAM,qBAAqB;;;;;;AAO3B,eAAsB,iBACpB,SACA,SACA,OAAsB,CAAC,GACU;CACjC,IAAI,CAAC,UAAU,OAAO,GACpB,OAAO,MAAM,MAAM,iBAAiB,gDAAgD;CAEtF,IAAI,QAAQ,OAAO,KAAA,GAIjB,OAAO;CAET,MAAM,EAAE,OAAO;CACf,MAAM,SAAS,QAAQ,UAAU,CAAC;CAClC,QAAQ,QAAQ,QAAhB;EACE,KAAK,cAAc;GACjB,MAAM,QAAQ,OAAO;GAErB,OAAO,GAAG,IAAI;IACZ,iBAFc,sBAAsB,MAAK,cAAa,cAAc,KAAK,KAAK,sBAAsB;IAGpG,cAAc;KAAE,OAAO,CAAC;KAAG,WAAW,CAAC;IAAE;IACzC,YAAY;KAAE,MAAM,KAAK,QAAQ;KAAS,SAAS,KAAK,WAAW;IAAQ;IAC3E,GAAI,KAAK,iBAAiB,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc,KAAK,aAAa;GAC/E,CAAC;EACH;EACA,KAAK,QACH,OAAO,GAAG,IAAI,CAAC,CAAC;EAClB,KAAK,cACH,OAAO,GAAG,IAAI,EAAE,OAAO,MAAM,QAAQ,MAAM,EAAE,CAAC;EAChD,KAAK,cAAc;GACjB,IAAI,OAAO,OAAO,SAAS,UACzB,OAAO,MAAM,IAAI,gBAAgB,sCAAsC;GAEzE,MAAM,OAAO,OAAO;GACpB,IAAI,SAAS,KAAA,MAAc,SAAS,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,IACxF,OAAO,MAAM,IAAI,gBAAgB,yCAAyC;GAE5E,OAAO,GAAG,IAAI,MAAM,QAAQ,KAAK,OAAO,MAAM,IAA2C,CAAC;EAC5F;EACA,KAAK,kBACH,OAAO,GAAG,IAAI,EAAE,WAAW,MAAM,QAAQ,UAAU,EAAE,CAAC;EACxD,KAAK,4BACH,OAAO,GAAG,IAAI,EAAE,mBAAmB,CAAC,EAAE,CAAC;EACzC,KAAK,kBAAkB;GACrB,MAAM,MAAM,OAAO;GACnB,MAAM,OAAO,OAAO,QAAQ,WAAW,MAAM,QAAQ,KAAK,GAAG,IAAI,KAAA;GACjE,IAAI,OAAO,QAAQ,YAAY,SAAS,KAAA,GACtC,OAAO,MAAM,IAAI,oBAAoB,kBAAkB,OAAO,GAAG,EAAE,EAAE;GAEvE,OAAO,GAAG,IAAI,EAAE,UAAU,CAAC;IAAE;IAAK,UAAU;IAAoB,MAAM,KAAK,UAAU,IAAI;GAAE,CAAC,EAAE,CAAC;EACjG;EACA,SACE,OAAO,MAAM,IAAI,kBAAkB,kCAAkC,QAAQ,OAAO,EAAE;CAC1F;AACF;;;;;;;;AA0BA,SAAgB,WACd,SACA,UAA6B,CAAC,GACW;CACzC,OAAO,OAAM,YAAW;EACtB,MAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;EAC3C,IAAI,WAAW,QAAQ,EAAE,QAAQ,kBAAkB,CAAC,EAAA,CAAG,SAAS,MAAM,GACpE,OAAO,KAAK,KAAK,iBAAiB,OAAO,kBAAkB;EAE7D,IAAI,QAAQ,UAAU,KAAA,KAAa,QAAQ,QAAQ,IAAI,eAAe,MAAM,UAAU,QAAQ,SAC5F,OAAO,KAAK,KAAK,mCAAmC;EAEtD,MAAM,UAAU,QAAQ,QAAQ,IAAI,sBAAsB;EAC1D,IAAI,YAAY,QAAQ,CAAE,sBAA4C,SAAS,OAAO,GACpF,OAAO,KAAK,KAAK,oCAAoC,QAAQ,EAAE;EAEjE,IAAI,QAAQ,WAAW,QAIrB,OAAO,IAAI,SAAS,MAAM;GAAE,QAAQ;GAAK,SAAS,EAAE,OAAO,OAAO;EAAE,CAAC;EAEvE,IAAI;EACJ,IAAI;GACF,UAAU,MAAM,QAAQ,KAAK;EAC/B,QAAQ;GACN,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,uBAAuB,CAAC;EACpE;EACA,MAAM,WAAW,MAAM,iBAAiB,SAAS,SAAS,OAAO;EACjE,OAAO,aAAa,OAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC,IAAI,KAAK,KAAK,QAAQ;CACrF;AACF;AAEA,SAAS,UAAU,SAA6C;CAC9D,MAAM,YAAY;CAClB,OACE,OAAO,cAAc,YACrB,cAAc,QACd,CAAC,MAAM,QAAQ,SAAS,KACxB,UAAU,YAAY,SACtB,OAAO,UAAU,WAAW;AAEhC;AAEA,SAAS,GAAG,IAAe,QAAkC;CAC3D,OAAO;EAAE,SAAS;EAAO;EAAI;CAAO;AACtC;AAEA,SAAS,MAAM,IAAsB,MAAc,SAAkC;CACnF,OAAO;EAAE,SAAS;EAAO;EAAI,OAAO;GAAE;GAAM;EAAQ;CAAE;AACxD;AAEA,SAAS,KAAK,QAAgB,MAAyB;CACrD,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;EAAE;EAAQ,SAAS,EAAE,gBAAgB,mBAAmB;CAAE,CAAC;AACvG;AAEA,SAAS,KAAK,QAAgB,MAAwB;CACpD,OAAO,IAAI,SAAS,MAAM;EAAE;EAAQ,SAAS,EAAE,gBAAgB,4BAA4B;CAAE,CAAC;AAChG;;;;ACvJA,MAAa,mBAAmB;CAC9B,OAAO;CACP,SAAS;CACT,UAAU;AACZ;AAEA,SAAgB,gBAAgB,KAAkB,KAAkB,OAAsB,CAAC,GAAS;CAClG,IAAI;;;;;CAKJ,MAAM,cAAgC;EACpC,IAAI,YAAY,KAAA,GACd,OAAO;EAET,MAAM,OAAO,IAAI,eAAe,UAAU;EAC1C,IAAI,SAAS,KAAA,GACX,OAAO,gBAAgB,CAAC,CAAC;EAK3B,UAAU,cAAc,MAAM,EAAE,SAAS,gBAAgB,CAAC;EAC1D,OAAO;CACT;CACA,IAAI,GAAG,iBAAiB,UAAU,YAA6B;EAC7D,iBAAsB,MAAM,GAAG,QAAQ,SAAS;GAC9C,MAAM,KAAK,SAAS,SAAS,SAAS;GACtC,GAAG;EACL,CAAC,CAAC,CAAC,MAAK,aAAY,IAAI,KAAK,iBAAiB,UAAU;GAAE,IAAI,QAAQ;GAAI;EAAS,CAA4B,CAAC;CAClH,CAAC;CACD,MAAM,iBAAiB,IAAI,KAAK,iBAAiB,OAAO;EAAE,OAAO,SAAS;EAAO,KAAK,SAAS;CAAK,CAAC;CACrG,SAAS;CAKT,IAAI,GAAG,mBAAmB,QAAQ;AACpC"}