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 +121 -33
- package/dist/index.mjs +2 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,37 +1,82 @@
|
|
|
1
1
|
# `foldkit-agent-webmcp`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
###
|
|
105
|
+
### The returned registration
|
|
57
106
|
|
|
58
107
|
```ts
|
|
59
|
-
registration.refresh()
|
|
60
|
-
registration.registered()
|
|
61
|
-
registration.unregister()
|
|
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
|
-
##
|
|
113
|
+
## Tools appear and disappear with the Model
|
|
65
114
|
|
|
66
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
102
|
-
calling agent can
|
|
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
|
|
119
|
-
`src/webmcp.ts` rather than imported, so the browser API can
|
|
120
|
-
Foldkit agent contract
|
|
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
|
|
56
|
-
|
|
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
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -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":"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"}
|
|
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
|
|
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
|
|
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
|
|
50
|
+
"foldkit-agent": "0.2.1"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsdown",
|