foldkit-agent-webmcp 0.2.1 → 0.3.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 +147 -119
  2. package/package.json +3 -3
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
+ [WebMCP](https://github.com/webmachinelearning/webmcp) is an experimental web
35
+ platform proposal. No stable browser ships it today, so feature detection is
36
+ part of the normal integration path.
22
37
 
23
38
  ## Install
24
39
 
@@ -28,92 +43,83 @@ 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
32
47
 
33
- The whole path, from a declared contract to live tools:
48
+ Assume the application already declared a protocol-neutral contract:
49
+
50
+ ```ts
51
+ const AgentBuilder = Agent.forApplication(App).withPrincipal<Principal>()
52
+ const AssistantAgent = AgentBuilder.make({ ... })
53
+ ```
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, // required only by capabilities with completion contracts
49
68
  },
50
69
  })
51
70
 
52
- // 2. Publish it, if this browser speaks WebMCP.
53
71
  const modelContext = AgentWebMcp.documentModelContext()
72
+
54
73
  if (modelContext !== undefined) {
55
- const registration = AgentWebMcp.register({ agent, modelContext })
74
+ const registration = AgentWebMcp.register({
75
+ agent: agentRuntime,
76
+ modelContext,
77
+ })
78
+
56
79
  window.addEventListener('beforeunload', () => registration.unregister())
57
80
  }
58
81
  ```
59
82
 
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.
83
+ That is the whole architecture: the contract remains protocol-neutral, the
84
+ runtime remains the one authority for availability/authorization/dispatch, and
85
+ this package translates it to the browser API.
66
86
 
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.
87
+ `documentModelContext()` is safe during SSR and returns `undefined` when no
88
+ WebMCP producer surface exists. Calling `register()` without a model context
89
+ throws rather than silently pretending registration succeeded.
71
90
 
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.
91
+ Registration starts immediately, but browser registration is asynchronous.
92
+ `register()` returns after reconciliation is scheduled; use
93
+ `await registration.refresh()` when the caller needs to know that the browser
94
+ has accepted the current tool set, and `onError` for background reconcile
95
+ failures.
76
96
 
77
97
  ## What a capability becomes
78
98
 
79
- Each **currently available** capability becomes one tool:
99
+ Each **currently available** capability becomes one WebMCP tool:
80
100
 
81
101
  ```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.
102
+ foldkit-agent WebMCP
93
103
 
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. |
104
+ capability name -> tool name
105
+ variant description -> description
106
+ encoded input Schema -> inputSchema
107
+ AgentRuntime dispatch -> execute
108
+ completion contract -> resolved tool outcome
109
+ ```
104
110
 
105
- ### The returned registration
111
+ No input schema or handler is written again. `execute` delegates to the bound
112
+ runtime, which performs the same decode, availability check, authorization,
113
+ dispatch, cancellation, and completion handling as every other adapter.
106
114
 
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
- ```
115
+ A capability with a completion contract resolves when its correlated success or
116
+ failure Message is observed. Without one, validated dispatch is the completion
117
+ boundary.
112
118
 
113
- ## Tools appear and disappear with the Model
119
+ ## Tools follow the Model
114
120
 
115
- Availability is a projection of Model state, which falls out of the
116
- architecture rather than needing its own mechanism:
121
+ Availability is Model-dependent, so the advertised WebMCP tool set may change as
122
+ the application changes:
117
123
 
118
124
  ```ts
119
125
  RequestedDeleteTodo: {
@@ -124,40 +130,52 @@ RequestedDeleteTodo: {
124
130
  ```
125
131
 
126
132
  ```text
127
- Todo list tools: create_todo
128
- user selects todo -> Model changes
129
- -> tools: create_todo, delete_todo
133
+ no selection tools: create_todo
134
+ |
135
+ user selects todo
136
+ v
137
+ selected todo tools: create_todo, delete_todo
130
138
  ```
131
139
 
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.
140
+ With `followModel: true` (the default) and a host with `subscribe`, a Model
141
+ change reconciles registrations. A capability that disappears is unregistered;
142
+ a newly available capability is registered.
143
+
144
+ Availability is stronger than discoverability. A capability absent from the
145
+ tool list is also refused if a caller somehow invokes its known name.
146
+
147
+ Reconciliation is serialized, so overlapping Model changes cannot register the
148
+ same capability twice. A failed browser registration is not recorded as live and
149
+ is retried on a later reconcile.
137
150
 
138
- The adapter holds one `AbortController` per registered tool and passes its
139
- signal in the options bag, where `registerTool` takes it:
151
+ ## Registration lifecycle
152
+
153
+ `AgentWebMcp.register` returns:
140
154
 
141
155
  ```ts
142
- await document.modelContext.registerTool(tool, { signal: controller.signal })
156
+ registration.registered() // capability names currently registered
157
+ await registration.refresh() // reconcile against the current Model
158
+ registration.unregister() // stop following and abort every registration
143
159
  ```
144
160
 
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.
161
+ The main options are:
149
162
 
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.
163
+ | Option | Default | Purpose |
164
+ | --- | --- | --- |
165
+ | `agent` | — | The bound `AgentRuntime`. |
166
+ | `modelContext` | `document.modelContext` | Producer surface to register against; pass a stand-in in tests. |
167
+ | `followModel` | `true` | Reconcile tools as capability availability changes. |
168
+ | `signal` | — | Unregister everything when aborted. |
169
+ | `invocationId` | `crypto.randomUUID()` | Supplies protocol invocation ids. |
170
+ | `onError` | — | Receives failures from background reconciliation. |
171
+
172
+ Each registered tool has its own registration `AbortController`. That signal is
173
+ for the WebMCP registration itself. It is distinct from a tool invocation's
174
+ execution signal, which is forwarded to `AgentRuntime` as `Invocation.signal`.
156
175
 
157
176
  ## Errors
158
177
 
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:
178
+ Application refusals become tool errors the calling agent can reason about:
161
179
 
162
180
  ```text
163
181
  No such capability: delete_todo
@@ -167,42 +185,52 @@ Invalid input for "create_todo"
167
185
  Capability "create_todo" failed unexpectedly
168
186
  ```
169
187
 
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.
188
+ `execute` does not expose underlying application errors to the caller. A host or
189
+ adapter defect is flattened to a generic failure; the detailed error stays in
190
+ the application.
174
191
 
175
192
  ## Testing without a browser
176
193
 
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.
194
+ `modelContext` is the only browser-specific seam this package needs. Pass an
195
+ object with `registerTool` to capture descriptors and invoke their `execute`
196
+ functions directly:
197
+
198
+ ```ts
199
+ const registration = AgentWebMcp.register({
200
+ agent: agentRuntime,
201
+ modelContext: fakeModelContext,
202
+ })
203
+
204
+ await registration.refresh()
205
+ ```
206
+
207
+ That is how the package test suite and [`examples/todo`](../../examples/todo)
208
+ exercise the real adapter path without requiring browser support.
209
+
210
+ [`examples/todo-app`](../../examples/todo-app) shows the same adapter attached to
211
+ a real local-first Foldkit application.
182
212
 
183
213
  ## Choosing an adapter
184
214
 
215
+ All four adapters serve the same `AssistantAgent` contract through a bound
216
+ runtime. Serving more than one does not duplicate capability declarations.
217
+
185
218
  | Where the agent runs | Adapter |
186
219
  | --- | --- |
187
220
  | 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.
221
+ | An external MCP client, over stdio or HTTP | [`foldkit-agent-mcp`](../agent-mcp) |
222
+ | Another agent, over A2A | [`foldkit-agent-a2a`](../agent-a2a) |
223
+ | An Agent Native host | [`foldkit-agent-native`](../agent-native) |
194
224
 
195
225
  ## Why this stays an adapter
196
226
 
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.
227
+ WebMCP is experimental and may change independently of Foldkit Plus. Its browser
228
+ shape is intentionally isolated here. If the proposal changes, this adapter can
229
+ change without changing the protocol-neutral `foldkit-agent` contract or the
230
+ application's Messages.
201
231
 
202
232
  ## See also
203
233
 
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.
234
+ - [Agents guide](../../docs/agents.md) — the full builder → contract → runtime mental model.
235
+ - [`foldkit-agent`](../agent) — declares and binds the contract this package serves.
236
+ - [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.3.0",
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.2.1"
42
+ "foldkit-agent": "^0.3.0"
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.2.1"
50
+ "foldkit-agent": "0.3.0"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsdown",