foldkit-agent-webmcp 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,37 +1,82 @@
1
1
  # `foldkit-agent-webmcp`
2
2
 
3
- The browser-native adapter for [`foldkit-agent`](../agent). It projects exposed
4
- Foldkit Messages into Chrome's experimental WebMCP Imperative API.
3
+ Turns a [`foldkit-agent`](https://github.com/doeixd/foldkit-plus/tree/main/packages/agent)
4
+ contract into browser tools, so an agent running **inside the page** can use the
5
+ application the person in front of it is already using.
5
6
 
6
- WebMCP runs in the page itself, so a tool's `execute` dispatches directly into
7
- the same live Foldkit Runtime the human is already using — no DOM automation,
8
- and no external browser-session bridge.
7
+ [WebMCP](https://github.com/webmachinelearning/webmcp) is an experimental web
8
+ platform proposal: a page publishes tools on `document.modelContext`, and a
9
+ browser-resident agent calls them. Because the tool runs in the page, its
10
+ `execute` dispatches a Message straight into the live Foldkit Runtime. There is
11
+ no DOM automation, no scraping, no second copy of the application's logic, and
12
+ no browser-session bridge.
9
13
 
10
14
  ```text
11
15
  browser agent -> document.modelContext -> foldkit-agent-webmcp
12
16
  -> Foldkit AgentRuntime -> Message -> update -> Model + Commands
13
17
  ```
14
18
 
19
+ The agent can only do what the contract exposes, and the contract only exposes
20
+ Messages `update` already handles. Everything an agent may see or do is decided
21
+ in `foldkit-agent`; this package translates and nothing else.
22
+
15
23
  ## Install
16
24
 
17
25
  ```bash
18
26
  pnpm add foldkit-agent foldkit-agent-webmcp
19
27
  ```
20
28
 
29
+ `foldkit`, `effect`, and `foldkit-agent` are peer dependencies.
30
+
21
31
  ## Usage
22
32
 
33
+ The whole path, from a declared contract to live tools:
34
+
23
35
  ```ts
24
36
  import { AgentWebMcp } from 'foldkit-agent-webmcp'
25
37
 
26
- const registration = AgentWebMcp.register({ agent: agentRuntime })
38
+ // 1. Bind the contract to the running application. `TodoAgent` is
39
+ // `Agent.forApplication(App)`, or `Agent.forModel<Model>()` without one.
40
+ const agent = TodoAgent.bind({
41
+ definition: AppAgent,
42
+ host: {
43
+ model: mounted.model,
44
+ dispatch: (message: Message) => {
45
+ mounted.dispatch(message)
46
+ },
47
+ subscribe: mounted.subscribe, // lets tools appear and disappear with the Model
48
+ observe: mounted.observe, // required by a capability with a completion contract
49
+ },
50
+ })
51
+
52
+ // 2. Publish it, if this browser speaks WebMCP.
53
+ const modelContext = AgentWebMcp.documentModelContext()
54
+ if (modelContext !== undefined) {
55
+ const registration = AgentWebMcp.register({ agent, modelContext })
56
+ window.addEventListener('beforeunload', () => registration.unregister())
57
+ }
27
58
  ```
28
59
 
29
- [examples/todo](../../examples/todo) registers a real contract and executes the
30
- resulting tools.
60
+ **Check for support first.** WebMCP ships in no stable browser today, so
61
+ `document.modelContext` is usually absent. `documentModelContext()` returns it
62
+ or `undefined`, and is safe on a server, where there is no `document` at all.
63
+ Calling `register()` without a model context **throws**, on the grounds that
64
+ silently registering nothing would look like a working integration. Feature-detect,
65
+ or pass a `modelContext` of your own.
31
66
 
32
- `agent` is the `AgentRuntime` returned by `Agent.bind(...)`.
67
+ Registration begins immediately and in the background: `register` returns as
68
+ soon as the reconcile is scheduled, not when the browser has accepted the tools.
69
+ Pass `onError` to hear about a failure in that first pass, or `await
70
+ registration.refresh()` when you want to know the tools are live.
33
71
 
34
- Each currently available capability becomes one tool:
72
+ [`examples/todo`](https://github.com/doeixd/foldkit-plus/tree/main/examples/todo)
73
+ registers a real contract and executes the resulting tools;
74
+ [`examples/todo-app`](https://github.com/doeixd/foldkit-plus/tree/main/examples/todo-app)
75
+ does it in a real browser over a local-first replica.
76
+
77
+ ## What a capability becomes
78
+
79
+ Each **currently available** capability becomes one tool:
35
80
 
36
81
  ```text
37
82
  capability name -> tool name
@@ -40,30 +85,35 @@ derived JSON Schema -> inputSchema
40
85
  Runtime dispatch -> execute
41
86
  ```
42
87
 
43
- No schema or handler is repeated in this package.
88
+ No schema and no handler is written twice. `execute` decodes its input through
89
+ the capability's own Effect Schema, runs `available` and `authorize`, and
90
+ dispatches; a capability with a `completion` contract resolves when the
91
+ correlated Message is applied, so the agent learns the outcome rather than that
92
+ the call was received.
44
93
 
45
94
  ### Options
46
95
 
47
96
  | Option | Default | Purpose |
48
97
  | --- | --- | --- |
49
98
  | `agent` | — | The bound `AgentRuntime`. |
50
- | `modelContext` | `document.modelContext` | The WebMCP producer surface. |
51
- | `signal` | — | Unregisters everything when aborted. |
52
- | `followModel` | `true` | Reconcile registrations as the Model changes. |
99
+ | `modelContext` | `document.modelContext` | The WebMCP producer surface. Pass one to target a different surface, or to drive the adapter in a test. |
100
+ | `signal` | — | Unregisters everything when aborted. An already-aborted signal unregisters at once. |
101
+ | `followModel` | `true` | Reconcile registrations as the Model changes. Inert unless the host supports `subscribe`. |
53
102
  | `invocationId` | `crypto.randomUUID()` | Supplies the invocation id. |
54
- | `onError` | — | Reports a failure from a reconcile no caller is awaiting. |
103
+ | `onError` | — | Reports a failure from a reconcile no caller is awaiting: the initial registration, or one a Model change triggered. |
55
104
 
56
- ### Returned registration
105
+ ### The returned registration
57
106
 
58
107
  ```ts
59
- registration.refresh() // reconcile against the current Model
60
- registration.registered() // capability names currently registered
61
- registration.unregister() // abort every registration and stop following
108
+ registration.refresh() // Promise: reconcile against the current Model
109
+ registration.registered() // the capability names currently registered
110
+ registration.unregister() // abort every registration and stop following
62
111
  ```
63
112
 
64
- ## Dynamic availability
113
+ ## Tools appear and disappear with the Model
65
114
 
66
- Tool availability is a projection of Model state, which falls out of TEA:
115
+ Availability is a projection of Model state, which falls out of the
116
+ architecture rather than needing its own mechanism:
67
117
 
68
118
  ```ts
69
119
  RequestedDeleteTodo: {
@@ -79,6 +129,12 @@ user selects todo -> Model changes
79
129
  -> tools: create_todo, delete_todo
80
130
  ```
81
131
 
132
+ With `followModel` left on and a host that supports `subscribe`, every
133
+ transition reconciles the registrations: capabilities that are gone have their
134
+ registration signal aborted, and capabilities that have appeared are registered.
135
+ Availability is stronger than visibility. A capability the Model does not
136
+ currently offer is absent from the tool list **and** refused if called by name.
137
+
82
138
  The adapter holds one `AbortController` per registered tool and passes its
83
139
  signal in the options bag, where `registerTool` takes it:
84
140
 
@@ -88,18 +144,20 @@ await document.modelContext.registerTool(tool, { signal: controller.signal })
88
144
 
89
145
  Aborting that signal is what unregisters the tool, so it must not go on the
90
146
  descriptor. This is the registration signal, distinct from the execution signal
91
- passed to `execute`, which is forwarded as `Invocation.signal`.
147
+ passed to `execute`, which is forwarded as `Invocation.signal` so an agent can
148
+ cancel a call in flight.
92
149
 
93
150
  Reconciles are serialized, and a capability is recorded as registered only once
94
- `registerTool` resolves. A registration the browser refuses is not recorded and
95
- is retried on the next reconcile; `refresh()` rejects with the first failure
96
- after attempting the rest. Nothing is registered once `unregister()` has run,
97
- and a registration that was in flight when it ran is aborted.
151
+ `registerTool` resolves, so two overlapping reconciles cannot register one tool
152
+ twice. A registration the browser refuses is not recorded and is retried on the
153
+ next reconcile; `refresh()` rejects with the first failure after attempting the
154
+ rest. Nothing is registered once `unregister()` has run, and a registration that
155
+ was in flight when it ran is aborted.
98
156
 
99
157
  ## Errors
100
158
 
101
- A dispatch failure is returned as a tool error rather than thrown, so the
102
- calling agent can act on it:
159
+ A dispatch failure comes back as a tool error rather than a rejection, so the
160
+ calling agent can read it and decide what to do:
103
161
 
104
162
  ```text
105
163
  No such capability: delete_todo
@@ -110,11 +168,41 @@ Capability "create_todo" failed unexpectedly
110
168
  ```
111
169
 
112
170
  The last covers an unexpected defect, such as the host's `dispatch` throwing.
113
- `execute` never rejects and never echoes the underlying error back to the
114
- caller.
171
+ `execute` never rejects, and never echoes the underlying error back to the
172
+ caller: the detail stays in the application, and the agent gets a message
173
+ written for it.
174
+
175
+ ## Testing without a browser
176
+
177
+ `modelContext` is the only thing this package needs from the platform, so pass
178
+ your own object with a `registerTool` method to capture the descriptors and call
179
+ their `execute` directly. That is how this package's own suite and
180
+ [`examples/todo`](https://github.com/doeixd/foldkit-plus/tree/main/examples/todo)
181
+ exercise the real registration path with no browser involved.
182
+
183
+ ## Choosing an adapter
184
+
185
+ | Where the agent runs | Adapter |
186
+ | --- | --- |
187
+ | In the page, beside the user | `foldkit-agent-webmcp` |
188
+ | An external MCP client, over stdio or HTTP | [`foldkit-agent-mcp`](https://github.com/doeixd/foldkit-plus/tree/main/packages/agent-mcp) |
189
+ | Another agent, over A2A | [`foldkit-agent-a2a`](https://github.com/doeixd/foldkit-plus/tree/main/packages/agent-a2a) |
190
+ | An Agent Native host | [`foldkit-agent-native`](https://github.com/doeixd/foldkit-plus/tree/main/packages/agent-native) |
191
+
192
+ They share one contract, so serving two at once is two calls, not two
193
+ definitions.
115
194
 
116
195
  ## Why this stays an adapter
117
196
 
118
- WebMCP is experimental and evolving. The types it needs are declared locally in
119
- `src/webmcp.ts` rather than imported, so the browser API can change without the
120
- Foldkit agent contract changing with it.
197
+ WebMCP is experimental and still changing. The types it needs are declared
198
+ locally in `src/webmcp.ts` rather than imported, so the browser API can move
199
+ without the Foldkit agent contract moving with it. If the proposal changes
200
+ shape, this package changes and nothing else does.
201
+
202
+ ## See also
203
+
204
+ - [The agents guide](https://github.com/doeixd/foldkit-plus/blob/main/docs/agents.md)
205
+ — what an agent may see and do, and why a capability is a Message.
206
+ - [`foldkit-agent`](https://github.com/doeixd/foldkit-plus/tree/main/packages/agent)
207
+ — the contract this adapter serves.
208
+ - [WebMCP](https://github.com/webmachinelearning/webmcp) — the proposal.
package/dist/index.mjs CHANGED
@@ -52,9 +52,8 @@ const register = (options) => {
52
52
  signal: context.signal
53
53
  })));
54
54
  if (result._tag === "Failure") return textResult(result.failure.message, true);
55
- const { completion } = result.success;
56
- if (completion === void 0) return textResult(`Dispatched ${result.success.tag}`);
57
- return textResult(`${completion.status === "completed" ? "Completed" : "Failed"}: ${completion.message._tag}`, completion.status === "failed");
55
+ const summary = Agent.summarize(result.success);
56
+ return textResult(summary.text, !summary.ok);
58
57
  } catch {
59
58
  return textResult(`Capability "${name}" failed unexpectedly`, true);
60
59
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/webmcp.ts","../src/register.ts","../src/agentWebMcp.ts"],"sourcesContent":["/**\n * The slice of Chrome's experimental WebMCP Imperative API this adapter uses.\n *\n * These types are declared locally rather than imported: the API is still\n * experimental, and Foldkit should not be coupled to a browser proposal that is\n * still changing.\n */\n\n/** Metadata passed to a tool's `execute` function. */\nexport interface ToolExecutionContext {\n /** Cancellation for this invocation: is this call still wanted? */\n readonly signal?: AbortSignal | undefined\n}\n\n/** The value a WebMCP tool returns. */\nexport interface ToolResult {\n readonly content: ReadonlyArray<{ readonly type: 'text'; readonly text: string }>\n readonly isError?: boolean\n}\n\n/** A tool registration as accepted by `document.modelContext.registerTool`. */\nexport interface ToolDescriptor {\n readonly name: string\n readonly description: string\n readonly inputSchema: Record<string, unknown>\n readonly execute: (input: any, context: ToolExecutionContext) => Promise<ToolResult>\n}\n\n/**\n * Options accepted alongside a tool registration.\n *\n * The registration signal belongs here, not on the descriptor: aborting it is\n * how the documented API unregisters a tool.\n */\nexport interface RegisterToolOptions {\n readonly signal?: AbortSignal | undefined\n}\n\n/** The producer surface exposed on `document.modelContext`. */\nexport interface ModelContext {\n readonly registerTool: (\n tool: ToolDescriptor,\n options?: RegisterToolOptions,\n ) => void | Promise<unknown>\n}\n\n/** Reads `document.modelContext`, when the page provides it. */\nexport const documentModelContext = (): ModelContext | undefined => {\n if (typeof document === 'undefined') return undefined\n return (document as unknown as { modelContext?: ModelContext }).modelContext\n}\n","import { Agent } from 'foldkit-agent'\nimport { Effect } from 'effect'\nimport {\n type ModelContext,\n type ToolDescriptor,\n type ToolResult,\n documentModelContext,\n} from './webmcp.js'\n\n/** A live WebMCP registration. */\nexport interface Registration {\n /** Re-reads availability from the current Model and reconciles registrations. */\n readonly refresh: () => Promise<void>\n /** Unregisters every tool and stops following the Model. */\n readonly unregister: () => void\n /** The capability names currently registered with WebMCP. */\n readonly registered: () => ReadonlyArray<string>\n}\n\nexport interface RegisterOptions<Model, Context_, Principal, ByName, ByTag> {\n /** The agent contract bound to a live Foldkit Runtime. */\n readonly agent: Agent.AgentRuntime<Model, Context_, Principal, ByName, ByTag>\n /** Defaults to `document.modelContext`. */\n readonly modelContext?: ModelContext | undefined\n /** Unregisters everything when aborted. */\n readonly signal?: AbortSignal | undefined\n /**\n * Follow the live Model and reconcile registrations as `available(model)`\n * changes. Defaults to `true`, and is inert unless the host supports\n * subscription.\n */\n readonly followModel?: boolean | undefined\n /** Supplies an invocation id. Defaults to `crypto.randomUUID()`. */\n readonly invocationId?: (() => string) | undefined\n /**\n * Reports a failure from a reconcile that no caller is awaiting -- the\n * initial registration, or one triggered by a Model change.\n */\n readonly onError?: ((error: unknown) => void) | undefined\n}\n\nconst textResult = (text: string, isError = false): ToolResult => ({\n content: [{ type: 'text', text }],\n isError,\n})\n\n/**\n * Projects an agent contract into `document.modelContext.registerTool(...)`.\n *\n * Each currently available capability becomes one tool: the capability name\n * becomes the tool name, the variant description becomes the description, the\n * derived JSON Schema becomes `inputSchema`, and `execute` validates its input\n * and dispatches into the same live Foldkit Runtime the human is using.\n *\n * Tool availability is itself a projection of Model state. When\n * `available(model)` changes, registrations are reconciled: capabilities that\n * are gone have their registration signal aborted, and capabilities that have\n * appeared are registered.\n *\n * @example\n * ```ts\n * const registration = AgentWebMcp.register({ agent: agentRuntime })\n * ```\n */\nexport const register = <Model, Context_, Principal, ByName, ByTag>(\n options: RegisterOptions<Model, Context_, Principal, ByName, ByTag>,\n): Registration => {\n const modelContext = options.modelContext ?? documentModelContext()\n if (modelContext === undefined) {\n throw new Error(\n 'WebMCP is unavailable: no document.modelContext. Pass an explicit modelContext to register().',\n )\n }\n\n const { agent } = options\n const nextInvocationId = options.invocationId ?? Agent.newInvocationId\n\n /** One AbortController per registered tool: aborting it unregisters that tool. */\n const controllers = new Map<string, AbortController>()\n let disposed = false\n\n const toolFor = (\n name: string,\n description: string,\n inputSchema: Record<string, unknown>,\n ): { readonly controller: AbortController; readonly tool: ToolDescriptor } => {\n const controller = new AbortController()\n\n const execute = async (\n input: unknown,\n context: { readonly signal?: AbortSignal | undefined },\n ): Promise<ToolResult> => {\n try {\n // WebMCP hands the page a promise-returning callback, so this is the\n // edge where Effect meets the browser, not a run inside a service.\n const result = await Effect.runPromise(\n Effect.result(\n // The tool name and payload both come off the wire.\n agent.messages.dispatchUnknown(name, input, {\n id: nextInvocationId(),\n transport: 'webmcp',\n signal: context.signal,\n }),\n ),\n )\n\n // Every agent failure carries a message written for this audience.\n if (result._tag === 'Failure') return textResult(result.failure.message, true)\n\n // A declared completion contract has already resolved by the time\n // dispatch returns, so a failed completion is a failed tool call --\n // reported the same way the MCP adapter reports it.\n const { completion } = result.success\n if (completion === undefined) return textResult(`Dispatched ${result.success.tag}`)\n\n return textResult(\n `${completion.status === 'completed' ? 'Completed' : 'Failed'}: ${completion.message._tag}`,\n completion.status === 'failed',\n )\n } catch {\n // `Effect.result` captures expected failures but not defects, and a\n // rejected tool promise is not something a calling agent can act on.\n // Report it as a tool error without leaking the application's internals.\n return textResult(`Capability \"${name}\" failed unexpectedly`, true)\n }\n }\n\n return { controller, tool: { name, description, inputSchema, execute } }\n }\n\n const runReconcile = async (): Promise<void> => {\n if (disposed) return\n\n const available = await Effect.runPromise(agent.messages.available)\n const wanted = new Map(available.map(descriptor => [descriptor.name, descriptor]))\n\n // Unregister capabilities the Model no longer offers.\n for (const [name, controller] of [...controllers]) {\n if (!wanted.has(name)) {\n controller.abort()\n controllers.delete(name)\n }\n }\n\n // Register capabilities that have appeared.\n let failure: unknown\n for (const [name, descriptor] of wanted) {\n // unregister() can land while an await above is pending. Nothing more is\n // registered after that.\n if (disposed) return\n if (controllers.has(name)) continue\n\n const { controller, tool } = toolFor(name, descriptor.description, descriptor.inputSchema)\n\n try {\n // The signal goes in the options bag: aborting it is what unregisters\n // the tool.\n await modelContext.registerTool(tool, { signal: controller.signal })\n } catch (error) {\n // Nothing was registered, so nothing is recorded and nothing to abort.\n failure ??= error\n continue\n }\n\n // Disposal can also land while this registration is in flight, in which\n // case the tool is already registered and has to be taken back.\n if (disposed) {\n controller.abort()\n return\n }\n\n // Recorded only once the browser has accepted it.\n controllers.set(name, controller)\n }\n\n if (failure !== undefined) throw failure\n }\n\n /**\n * Reconciles are serialized.\n *\n * A capability is recorded only once the browser has accepted it, so two\n * overlapping reconciles would both see it as missing and register it twice.\n */\n let queue: Promise<void> = Promise.resolve()\n\n const reconcile = (): Promise<void> => {\n const next = queue.then(runReconcile, runReconcile)\n queue = next.then(\n () => {},\n () => {},\n )\n return next\n }\n\n let unsubscribe: () => void = () => {}\n\n const unregister = (): void => {\n disposed = true\n for (const controller of controllers.values()) controller.abort()\n controllers.clear()\n unsubscribe()\n }\n\n /**\n * Reconciles without a caller to await the result. A rejection here would\n * otherwise become an unhandled rejection, so it is reported through\n * `onError` and the registration is left as it was.\n */\n const reconcileInBackground = (): void => {\n reconcile().catch(error => {\n options.onError?.(error)\n })\n }\n\n reconcileInBackground()\n\n if (options.followModel !== false) {\n unsubscribe = agent.subscribe(reconcileInBackground)\n }\n\n if (options.signal?.aborted === true) {\n // An already-aborted signal never fires its listener.\n unregister()\n } else {\n options.signal?.addEventListener('abort', unregister, { once: true })\n }\n\n return {\n refresh: reconcile,\n unregister,\n registered: () => [...controllers.keys()],\n }\n}\n","export { register, type RegisterOptions, type Registration } from './register.js'\nexport { documentModelContext } from './webmcp.js'\n"],"mappings":";;;;;AA+CA,MAAa,6BAAuD;CAClE,IAAI,OAAO,aAAa,aAAa,OAAO,KAAA;CAC5C,OAAQ,SAAwD;AAClE;;;ACTA,MAAM,cAAc,MAAc,UAAU,WAAuB;CACjE,SAAS,CAAC;EAAE,MAAM;EAAQ;CAAK,CAAC;CAChC;AACF;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,YACX,YACiB;CACjB,MAAM,eAAe,QAAQ,gBAAgB,qBAAqB;CAClE,IAAI,iBAAiB,KAAA,GACnB,MAAM,IAAI,MACR,+FACF;CAGF,MAAM,EAAE,UAAU;CAClB,MAAM,mBAAmB,QAAQ,gBAAgB,MAAM;;CAGvD,MAAM,8BAAc,IAAI,IAA6B;CACrD,IAAI,WAAW;CAEf,MAAM,WACJ,MACA,aACA,gBAC4E;EAC5E,MAAM,aAAa,IAAI,gBAAgB;EAEvC,MAAM,UAAU,OACd,OACA,YACwB;GACxB,IAAI;IAGF,MAAM,SAAS,MAAM,OAAO,WAC1B,OAAO,OAEL,MAAM,SAAS,gBAAgB,MAAM,OAAO;KAC1C,IAAI,iBAAiB;KACrB,WAAW;KACX,QAAQ,QAAQ;IAClB,CAAC,CACH,CACF;IAGA,IAAI,OAAO,SAAS,WAAW,OAAO,WAAW,OAAO,QAAQ,SAAS,IAAI;IAK7E,MAAM,EAAE,eAAe,OAAO;IAC9B,IAAI,eAAe,KAAA,GAAW,OAAO,WAAW,cAAc,OAAO,QAAQ,KAAK;IAElF,OAAO,WACL,GAAG,WAAW,WAAW,cAAc,cAAc,SAAS,IAAI,WAAW,QAAQ,QACrF,WAAW,WAAW,QACxB;GACF,QAAQ;IAIN,OAAO,WAAW,eAAe,KAAK,wBAAwB,IAAI;GACpE;EACF;EAEA,OAAO;GAAE;GAAY,MAAM;IAAE;IAAM;IAAa;IAAa;GAAQ;EAAE;CACzE;CAEA,MAAM,eAAe,YAA2B;EAC9C,IAAI,UAAU;EAEd,MAAM,YAAY,MAAM,OAAO,WAAW,MAAM,SAAS,SAAS;EAClE,MAAM,SAAS,IAAI,IAAI,UAAU,KAAI,eAAc,CAAC,WAAW,MAAM,UAAU,CAAC,CAAC;EAGjF,KAAK,MAAM,CAAC,MAAM,eAAe,CAAC,GAAG,WAAW,GAC9C,IAAI,CAAC,OAAO,IAAI,IAAI,GAAG;GACrB,WAAW,MAAM;GACjB,YAAY,OAAO,IAAI;EACzB;EAIF,IAAI;EACJ,KAAK,MAAM,CAAC,MAAM,eAAe,QAAQ;GAGvC,IAAI,UAAU;GACd,IAAI,YAAY,IAAI,IAAI,GAAG;GAE3B,MAAM,EAAE,YAAY,SAAS,QAAQ,MAAM,WAAW,aAAa,WAAW,WAAW;GAEzF,IAAI;IAGF,MAAM,aAAa,aAAa,MAAM,EAAE,QAAQ,WAAW,OAAO,CAAC;GACrE,SAAS,OAAO;IAEd,YAAY;IACZ;GACF;GAIA,IAAI,UAAU;IACZ,WAAW,MAAM;IACjB;GACF;GAGA,YAAY,IAAI,MAAM,UAAU;EAClC;EAEA,IAAI,YAAY,KAAA,GAAW,MAAM;CACnC;;;;;;;CAQA,IAAI,QAAuB,QAAQ,QAAQ;CAE3C,MAAM,kBAAiC;EACrC,MAAM,OAAO,MAAM,KAAK,cAAc,YAAY;EAClD,QAAQ,KAAK,WACL,CAAC,SACD,CAAC,CACT;EACA,OAAO;CACT;CAEA,IAAI,oBAAgC,CAAC;CAErC,MAAM,mBAAyB;EAC7B,WAAW;EACX,KAAK,MAAM,cAAc,YAAY,OAAO,GAAG,WAAW,MAAM;EAChE,YAAY,MAAM;EAClB,YAAY;CACd;;;;;;CAOA,MAAM,8BAAoC;EACxC,UAAU,CAAC,CAAC,OAAM,UAAS;GACzB,QAAQ,UAAU,KAAK;EACzB,CAAC;CACH;CAEA,sBAAsB;CAEtB,IAAI,QAAQ,gBAAgB,OAC1B,cAAc,MAAM,UAAU,qBAAqB;CAGrD,IAAI,QAAQ,QAAQ,YAAY,MAE9B,WAAW;MAEX,QAAQ,QAAQ,iBAAiB,SAAS,YAAY,EAAE,MAAM,KAAK,CAAC;CAGtE,OAAO;EACL,SAAS;EACT;EACA,kBAAkB,CAAC,GAAG,YAAY,KAAK,CAAC;CAC1C;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/webmcp.ts","../src/register.ts","../src/agentWebMcp.ts"],"sourcesContent":["/**\n * The slice of Chrome's experimental WebMCP Imperative API this adapter uses.\n *\n * These types are declared locally rather than imported: the API is still\n * experimental, and Foldkit should not be coupled to a browser proposal that is\n * still changing.\n */\n\n/** Metadata passed to a tool's `execute` function. */\nexport interface ToolExecutionContext {\n /** Cancellation for this invocation: is this call still wanted? */\n readonly signal?: AbortSignal | undefined\n}\n\n/** The value a WebMCP tool returns. */\nexport interface ToolResult {\n readonly content: ReadonlyArray<{ readonly type: 'text'; readonly text: string }>\n readonly isError?: boolean\n}\n\n/** A tool registration as accepted by `document.modelContext.registerTool`. */\nexport interface ToolDescriptor {\n readonly name: string\n readonly description: string\n readonly inputSchema: Record<string, unknown>\n readonly execute: (input: any, context: ToolExecutionContext) => Promise<ToolResult>\n}\n\n/**\n * Options accepted alongside a tool registration.\n *\n * The registration signal belongs here, not on the descriptor: aborting it is\n * how the documented API unregisters a tool.\n */\nexport interface RegisterToolOptions {\n readonly signal?: AbortSignal | undefined\n}\n\n/** The producer surface exposed on `document.modelContext`. */\nexport interface ModelContext {\n readonly registerTool: (\n tool: ToolDescriptor,\n options?: RegisterToolOptions,\n ) => void | Promise<unknown>\n}\n\n/** Reads `document.modelContext`, when the page provides it. */\nexport const documentModelContext = (): ModelContext | undefined => {\n if (typeof document === 'undefined') return undefined\n return (document as unknown as { modelContext?: ModelContext }).modelContext\n}\n","import { Agent } from 'foldkit-agent'\nimport { Effect } from 'effect'\nimport {\n type ModelContext,\n type ToolDescriptor,\n type ToolResult,\n documentModelContext,\n} from './webmcp.js'\n\n/** A live WebMCP registration. */\nexport interface Registration {\n /** Re-reads availability from the current Model and reconciles registrations. */\n readonly refresh: () => Promise<void>\n /** Unregisters every tool and stops following the Model. */\n readonly unregister: () => void\n /** The capability names currently registered with WebMCP. */\n readonly registered: () => ReadonlyArray<string>\n}\n\nexport interface RegisterOptions<Model, Context_, Principal, ByName, ByTag> {\n /** The agent contract bound to a live Foldkit Runtime. */\n readonly agent: Agent.AgentRuntime<Model, Context_, Principal, ByName, ByTag>\n /** Defaults to `document.modelContext`. */\n readonly modelContext?: ModelContext | undefined\n /** Unregisters everything when aborted. */\n readonly signal?: AbortSignal | undefined\n /**\n * Follow the live Model and reconcile registrations as `available(model)`\n * changes. Defaults to `true`, and is inert unless the host supports\n * subscription.\n */\n readonly followModel?: boolean | undefined\n /** Supplies an invocation id. Defaults to `crypto.randomUUID()`. */\n readonly invocationId?: (() => string) | undefined\n /**\n * Reports a failure from a reconcile that no caller is awaiting -- the\n * initial registration, or one triggered by a Model change.\n */\n readonly onError?: ((error: unknown) => void) | undefined\n}\n\nconst textResult = (text: string, isError = false): ToolResult => ({\n content: [{ type: 'text', text }],\n isError,\n})\n\n/**\n * Projects an agent contract into `document.modelContext.registerTool(...)`.\n *\n * Each currently available capability becomes one tool: the capability name\n * becomes the tool name, the variant description becomes the description, the\n * derived JSON Schema becomes `inputSchema`, and `execute` validates its input\n * and dispatches into the same live Foldkit Runtime the human is using.\n *\n * Tool availability is itself a projection of Model state. When\n * `available(model)` changes, registrations are reconciled: capabilities that\n * are gone have their registration signal aborted, and capabilities that have\n * appeared are registered.\n *\n * @example\n * ```ts\n * const registration = AgentWebMcp.register({ agent: agentRuntime })\n * ```\n */\nexport const register = <Model, Context_, Principal, ByName, ByTag>(\n options: RegisterOptions<Model, Context_, Principal, ByName, ByTag>,\n): Registration => {\n const modelContext = options.modelContext ?? documentModelContext()\n if (modelContext === undefined) {\n throw new Error(\n 'WebMCP is unavailable: no document.modelContext. Pass an explicit modelContext to register().',\n )\n }\n\n const { agent } = options\n const nextInvocationId = options.invocationId ?? Agent.newInvocationId\n\n /** One AbortController per registered tool: aborting it unregisters that tool. */\n const controllers = new Map<string, AbortController>()\n let disposed = false\n\n const toolFor = (\n name: string,\n description: string,\n inputSchema: Record<string, unknown>,\n ): { readonly controller: AbortController; readonly tool: ToolDescriptor } => {\n const controller = new AbortController()\n\n const execute = async (\n input: unknown,\n context: { readonly signal?: AbortSignal | undefined },\n ): Promise<ToolResult> => {\n try {\n // WebMCP hands the page a promise-returning callback, so this is the\n // edge where Effect meets the browser, not a run inside a service.\n const result = await Effect.runPromise(\n Effect.result(\n // The tool name and payload both come off the wire.\n agent.messages.dispatchUnknown(name, input, {\n id: nextInvocationId(),\n transport: 'webmcp',\n signal: context.signal,\n }),\n ),\n )\n\n // Every agent failure carries a message written for this audience.\n if (result._tag === 'Failure') return textResult(result.failure.message, true)\n\n // A declared completion contract has already resolved by the time\n // dispatch returns, so a failed completion is a failed tool call.\n const summary = Agent.summarize(result.success)\n return textResult(summary.text, !summary.ok)\n } catch {\n // `Effect.result` captures expected failures but not defects, and a\n // rejected tool promise is not something a calling agent can act on.\n // Report it as a tool error without leaking the application's internals.\n return textResult(`Capability \"${name}\" failed unexpectedly`, true)\n }\n }\n\n return { controller, tool: { name, description, inputSchema, execute } }\n }\n\n const runReconcile = async (): Promise<void> => {\n if (disposed) return\n\n const available = await Effect.runPromise(agent.messages.available)\n const wanted = new Map(available.map(descriptor => [descriptor.name, descriptor]))\n\n // Unregister capabilities the Model no longer offers.\n for (const [name, controller] of [...controllers]) {\n if (!wanted.has(name)) {\n controller.abort()\n controllers.delete(name)\n }\n }\n\n // Register capabilities that have appeared.\n let failure: unknown\n for (const [name, descriptor] of wanted) {\n // unregister() can land while an await above is pending. Nothing more is\n // registered after that.\n if (disposed) return\n if (controllers.has(name)) continue\n\n const { controller, tool } = toolFor(name, descriptor.description, descriptor.inputSchema)\n\n try {\n // The signal goes in the options bag: aborting it is what unregisters\n // the tool.\n await modelContext.registerTool(tool, { signal: controller.signal })\n } catch (error) {\n // Nothing was registered, so nothing is recorded and nothing to abort.\n failure ??= error\n continue\n }\n\n // Disposal can also land while this registration is in flight, in which\n // case the tool is already registered and has to be taken back.\n if (disposed) {\n controller.abort()\n return\n }\n\n // Recorded only once the browser has accepted it.\n controllers.set(name, controller)\n }\n\n if (failure !== undefined) throw failure\n }\n\n /**\n * Reconciles are serialized.\n *\n * A capability is recorded only once the browser has accepted it, so two\n * overlapping reconciles would both see it as missing and register it twice.\n */\n let queue: Promise<void> = Promise.resolve()\n\n const reconcile = (): Promise<void> => {\n const next = queue.then(runReconcile, runReconcile)\n queue = next.then(\n () => {},\n () => {},\n )\n return next\n }\n\n let unsubscribe: () => void = () => {}\n\n const unregister = (): void => {\n disposed = true\n for (const controller of controllers.values()) controller.abort()\n controllers.clear()\n unsubscribe()\n }\n\n /**\n * Reconciles without a caller to await the result. A rejection here would\n * otherwise become an unhandled rejection, so it is reported through\n * `onError` and the registration is left as it was.\n */\n const reconcileInBackground = (): void => {\n reconcile().catch(error => {\n options.onError?.(error)\n })\n }\n\n reconcileInBackground()\n\n if (options.followModel !== false) {\n unsubscribe = agent.subscribe(reconcileInBackground)\n }\n\n if (options.signal?.aborted === true) {\n // An already-aborted signal never fires its listener.\n unregister()\n } else {\n options.signal?.addEventListener('abort', unregister, { once: true })\n }\n\n return {\n refresh: reconcile,\n unregister,\n registered: () => [...controllers.keys()],\n }\n}\n","export { register, type RegisterOptions, type Registration } from './register.js'\nexport { documentModelContext } from './webmcp.js'\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit-agent-webmcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "WebMCP adapter for foldkit-agent. Projects exposed Foldkit Messages into document.modelContext tools.",
5
5
  "keywords": [
6
6
  "foldkit",
@@ -39,7 +39,7 @@
39
39
  "peerDependencies": {
40
40
  "effect": "^4.0.0-rc.112",
41
41
  "foldkit": "^0.158.2",
42
- "foldkit-agent": "^0.1.0"
42
+ "foldkit-agent": "^0.2.1"
43
43
  },
44
44
  "dependencies": {},
45
45
  "devDependencies": {
@@ -47,7 +47,7 @@
47
47
  "foldkit": "0.158.2",
48
48
  "tsdown": "^0.23.0",
49
49
  "typescript": "^5.7.2",
50
- "foldkit-agent": "0.1.0"
50
+ "foldkit-agent": "0.2.1"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsdown",