foldkit-agent-webmcp 0.2.1 → 0.4.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.
Files changed (2) hide show
  1. package/README.md +154 -120
  2. package/package.json +9 -8
package/README.md CHANGED
@@ -1,24 +1,39 @@
1
1
  # `foldkit-agent-webmcp`
2
2
 
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.
3
+ Publishes a [`foldkit-agent`](../agent) runtime as browser-native WebMCP tools.
6
4
 
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.
5
+ Use it when an agent runs **inside the page** and should operate the same live
6
+ Foldkit application the person is using. This package does not define new agent
7
+ capabilities and does not automate the DOM. It translates the capabilities your
8
+ agent contract already exposes into `document.modelContext` tools.
13
9
 
14
10
  ```text
15
- browser agent -> document.modelContext -> foldkit-agent-webmcp
16
- -> Foldkit AgentRuntime -> Message -> update -> Model + Commands
11
+ Foldkit application
12
+ |
13
+ v
14
+ AssistantAgent protocol-neutral contract
15
+ |
16
+ | AgentBuilder.bind(...)
17
+ v
18
+ AgentRuntime live Model + dispatch + policy
19
+ |
20
+ v
21
+ foldkit-agent-webmcp adapter only
22
+ |
23
+ v
24
+ document.modelContext browser tools
25
+ |
26
+ v
27
+ Message -> update -> Model + Commands
17
28
  ```
18
29
 
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.
30
+ The authority boundary stays in `foldkit-agent`: the contract decides what the
31
+ agent may observe and which Messages it may cause. WebMCP only makes those
32
+ capabilities available to a browser-resident agent.
33
+
34
+ The adapter uses the WebMCP producer API when the host exposes it. Feature
35
+ detect `document.modelContext` at runtime; tool registration is optional
36
+ for browsers that do not provide it.
22
37
 
23
38
  ## Install
24
39
 
@@ -28,92 +43,89 @@ pnpm add foldkit-agent foldkit-agent-webmcp
28
43
 
29
44
  `foldkit`, `effect`, and `foldkit-agent` are peer dependencies.
30
45
 
31
- ## Usage
46
+ ## Sixty seconds: publish a bound runtime
47
+
48
+ Start with the [Agent contract and host guide](../agent/README.md#usage).
49
+ The integration below assumes `AssistantAgent` is your declared contract and
50
+ `AgentBuilder` is its application-specialized builder. `mounted` below
51
+ is an application host (for example, the value returned by `Sync.mount`),
52
+ not an arbitrary Foldkit runtime handle.
32
53
 
33
- The whole path, from a declared contract to live tools:
54
+
55
+ Bind that contract to the running application, then register its currently
56
+ available capabilities with WebMCP:
34
57
 
35
58
  ```ts
36
59
  import { AgentWebMcp } from 'foldkit-agent-webmcp'
37
60
 
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,
61
+ const agentRuntime = AgentBuilder.bind({
62
+ definition: AssistantAgent,
42
63
  host: {
43
64
  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
65
+ dispatch: message => mounted.dispatch(message),
66
+ subscribe: mounted.subscribe,
67
+ observe: mounted.observe, // Message-based completion
68
+ principal: () => currentPrincipal, // your authenticated application principal
49
69
  },
50
70
  })
51
71
 
52
- // 2. Publish it, if this browser speaks WebMCP.
53
72
  const modelContext = AgentWebMcp.documentModelContext()
73
+
54
74
  if (modelContext !== undefined) {
55
- const registration = AgentWebMcp.register({ agent, modelContext })
56
- window.addEventListener('beforeunload', () => registration.unregister())
75
+ const registration = AgentWebMcp.register({
76
+ agent: agentRuntime,
77
+ modelContext,
78
+ })
79
+
80
+ await registration.refresh()
81
+ window.addEventListener('pagehide', () => registration.unregister())
57
82
  }
58
83
  ```
59
84
 
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.
85
+ Also call `registration.unregister()` when the owning component or runtime
86
+ is disposed; a page unload listener alone does not handle in-page teardown.
87
+ `currentPrincipal` above comes from your application, not tool arguments.
88
+
89
+ That is the whole architecture: the contract remains protocol-neutral, the
90
+ runtime remains the one authority for availability/authorization/dispatch, and
91
+ this package translates it to the browser API.
66
92
 
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.
93
+ `documentModelContext()` is safe during SSR and returns `undefined` when no
94
+ WebMCP producer surface exists. Calling `register()` without a model context
95
+ throws rather than silently pretending registration succeeded.
71
96
 
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.
97
+ Registration starts immediately, but browser registration is asynchronous.
98
+ `register()` returns after reconciliation is scheduled; use
99
+ `await registration.refresh()` when the caller needs to know that the browser
100
+ has accepted the current tool set, and `onError` for background reconcile
101
+ failures.
76
102
 
77
103
  ## What a capability becomes
78
104
 
79
- Each **currently available** capability becomes one tool:
105
+ Each **currently available** capability becomes one WebMCP tool:
80
106
 
81
107
  ```text
82
- capability name -> tool name
83
- variant description -> description
84
- derived JSON Schema -> inputSchema
85
- Runtime dispatch -> execute
86
- ```
87
-
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.
108
+ foldkit-agent WebMCP
93
109
 
94
- ### Options
95
-
96
- | Option | Default | Purpose |
97
- | --- | --- | --- |
98
- | `agent` | — | The bound `AgentRuntime`. |
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`. |
102
- | `invocationId` | `crypto.randomUUID()` | Supplies the invocation id. |
103
- | `onError` | — | Reports a failure from a reconcile no caller is awaiting: the initial registration, or one a Model change triggered. |
110
+ capability name -> tool name
111
+ variant description -> description
112
+ encoded input Schema -> inputSchema
113
+ AgentRuntime dispatch -> execute
114
+ completion contract -> resolved tool outcome
115
+ ```
104
116
 
105
- ### The returned registration
117
+ No input schema or handler is written again. `execute` delegates to the bound
118
+ runtime, which performs the same decode, availability check, authorization,
119
+ dispatch, cancellation, and completion handling as every other adapter.
106
120
 
107
- ```ts
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
111
- ```
121
+ A capability with a completion contract resolves when its correlated success or
122
+ failure Message is observed. Without one, validated dispatch is the completion
123
+ boundary.
112
124
 
113
- ## Tools appear and disappear with the Model
125
+ ## Tools follow the Model
114
126
 
115
- Availability is a projection of Model state, which falls out of the
116
- architecture rather than needing its own mechanism:
127
+ Availability is Model-dependent, so the advertised WebMCP tool set may change as
128
+ the application changes:
117
129
 
118
130
  ```ts
119
131
  RequestedDeleteTodo: {
@@ -124,40 +136,52 @@ RequestedDeleteTodo: {
124
136
  ```
125
137
 
126
138
  ```text
127
- Todo list tools: create_todo
128
- user selects todo -> Model changes
129
- -> tools: create_todo, delete_todo
139
+ no selection tools: create_todo
140
+ |
141
+ user selects todo
142
+ v
143
+ selected todo tools: create_todo, delete_todo
130
144
  ```
131
145
 
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.
146
+ With `followModel: true` (the default) and a host with `subscribe`, a Model
147
+ change reconciles registrations. A capability that disappears is unregistered;
148
+ a newly available capability is registered.
149
+
150
+ Availability is stronger than discoverability. A capability absent from the
151
+ tool list is also refused if a caller somehow invokes its known name.
137
152
 
138
- The adapter holds one `AbortController` per registered tool and passes its
139
- signal in the options bag, where `registerTool` takes it:
153
+ Reconciliation is serialized, so overlapping Model changes cannot register the
154
+ same capability twice. A failed browser registration is not recorded as live and
155
+ is retried on a later reconcile.
156
+
157
+ ## Registration lifecycle
158
+
159
+ `AgentWebMcp.register` returns:
140
160
 
141
161
  ```ts
142
- await document.modelContext.registerTool(tool, { signal: controller.signal })
162
+ registration.registered() // capability names currently registered
163
+ await registration.refresh() // reconcile against the current Model
164
+ registration.unregister() // stop following and abort every registration
143
165
  ```
144
166
 
145
- Aborting that signal is what unregisters the tool, so it must not go on the
146
- descriptor. This is the registration signal, distinct from the execution signal
147
- passed to `execute`, which is forwarded as `Invocation.signal` so an agent can
148
- cancel a call in flight.
167
+ The main options are:
168
+
169
+ | Option | Default | Purpose |
170
+ | --- | --- | --- |
171
+ | `agent` | — | The bound `AgentRuntime`. |
172
+ | `modelContext` | `document.modelContext` | Producer surface to register against; pass a stand-in in tests. |
173
+ | `followModel` | `true` | Reconcile tools as capability availability changes. |
174
+ | `signal` | — | Unregister everything when aborted. |
175
+ | `invocationId` | `crypto.randomUUID()` | Supplies protocol invocation ids. |
176
+ | `onError` | — | Receives failures from background reconciliation. |
149
177
 
150
- Reconciles are serialized, and a capability is recorded as registered only once
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.
178
+ Each registered tool has its own registration `AbortController`. That signal is
179
+ for the WebMCP registration itself. It is distinct from a tool invocation's
180
+ execution signal, which is forwarded to `AgentRuntime` as `Invocation.signal`.
156
181
 
157
182
  ## Errors
158
183
 
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:
184
+ Application refusals become tool errors the calling agent can reason about:
161
185
 
162
186
  ```text
163
187
  No such capability: delete_todo
@@ -167,42 +191,52 @@ Invalid input for "create_todo"
167
191
  Capability "create_todo" failed unexpectedly
168
192
  ```
169
193
 
170
- The last covers an unexpected defect, such as the host's `dispatch` throwing.
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.
194
+ `execute` does not expose underlying application errors to the caller. A host or
195
+ adapter defect is flattened to a generic failure; the detailed error stays in
196
+ the application.
174
197
 
175
198
  ## Testing without a browser
176
199
 
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.
200
+ `modelContext` is the only browser-specific seam this package needs. Pass an
201
+ object with `registerTool` to capture descriptors and invoke their `execute`
202
+ functions directly:
203
+
204
+ ```ts
205
+ const registration = AgentWebMcp.register({
206
+ agent: agentRuntime,
207
+ modelContext: fakeModelContext,
208
+ })
209
+
210
+ await registration.refresh()
211
+ ```
212
+
213
+ That is how the package test suite and [`examples/todo`](../../examples/todo)
214
+ exercise the real adapter path without requiring browser support.
215
+
216
+ [`examples/todo-app`](../../examples/todo-app) shows the same adapter attached to
217
+ a real local-first Foldkit application.
182
218
 
183
219
  ## Choosing an adapter
184
220
 
221
+ All four adapters serve the same `AssistantAgent` contract through a bound
222
+ runtime. Serving more than one does not duplicate capability declarations.
223
+
185
224
  | Where the agent runs | Adapter |
186
225
  | --- | --- |
187
226
  | 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.
227
+ | An external MCP client, over stdio or HTTP | [`foldkit-agent-mcp`](../agent-mcp) |
228
+ | Another agent, over A2A | [`foldkit-agent-a2a`](../agent-a2a) |
229
+ | An Agent Native host | [`foldkit-agent-native`](../agent-native) |
194
230
 
195
231
  ## Why this stays an adapter
196
232
 
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.
233
+ WebMCP is experimental and may change independently of Foldkit Plus. Its browser
234
+ shape is intentionally isolated here. If the proposal changes, this adapter can
235
+ change without changing the protocol-neutral `foldkit-agent` contract or the
236
+ application's Messages.
201
237
 
202
238
  ## See also
203
239
 
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.
240
+ - [Agents guide](../../docs/agents.md) — the full builder → contract → runtime mental model.
241
+ - [`foldkit-agent`](../agent) — declares and binds the contract this package serves.
242
+ - [WebMCP](https://github.com/webmachinelearning/webmcp) — the browser proposal.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit-agent-webmcp",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "WebMCP adapter for foldkit-agent. Projects exposed Foldkit Messages into document.modelContext tools.",
5
5
  "keywords": [
6
6
  "foldkit",
@@ -37,17 +37,18 @@
37
37
  "LICENSE"
38
38
  ],
39
39
  "peerDependencies": {
40
- "effect": "^4.0.0-rc.112",
41
- "foldkit": "^0.158.2",
42
- "foldkit-agent": "^0.2.1"
40
+ "effect": "^4.0.0-rc.116",
41
+ "foldkit": "^0.163.0",
42
+ "foldkit-agent": "^0.4.0"
43
43
  },
44
44
  "dependencies": {},
45
45
  "devDependencies": {
46
- "effect": "4.0.0-rc.112",
47
- "foldkit": "0.158.2",
46
+ "@effect/platform-browser": "4.0.0-rc.116",
47
+ "effect": "4.0.0-rc.116",
48
+ "foldkit": "0.163.0",
48
49
  "tsdown": "^0.23.0",
49
- "typescript": "^5.7.2",
50
- "foldkit-agent": "0.2.1"
50
+ "typescript": "5.9.3",
51
+ "foldkit-agent": "0.4.0"
51
52
  },
52
53
  "scripts": {
53
54
  "build": "tsdown",