foldkit-agent-webmcp 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -69,7 +69,7 @@ const agentRuntime = AgentBuilder.bind({
69
69
  },
70
70
  })
71
71
 
72
- const modelContext = AgentWebMcp.documentModelContext()
72
+ const modelContext = AgentWebMcp.pageModelContext()
73
73
 
74
74
  if (modelContext !== undefined) {
75
75
  const registration = AgentWebMcp.register({
@@ -90,8 +90,10 @@ That is the whole architecture: the contract remains protocol-neutral, the
90
90
  runtime remains the one authority for availability/authorization/dispatch, and
91
91
  this package translates it to the browser API.
92
92
 
93
- `documentModelContext()` is safe during SSR and returns `undefined` when no
94
- WebMCP producer surface exists. Calling `register()` without a model context
93
+ `pageModelContext()` is `document.modelContext`, where the spec and Chromium
94
+ 150 put it, or else `navigator.modelContext`, the older name that earlier
95
+ Chromium and agent extensions providing WebMCP still expose. It is safe during
96
+ SSR and returns `undefined` when neither exists. Calling `register()` without a model context
95
97
  throws rather than silently pretending registration succeeded.
96
98
 
97
99
  Registration starts immediately, but browser registration is asynchronous.
@@ -154,6 +156,52 @@ Reconciliation is serialized, so overlapping Model changes cannot register the
154
156
  same capability twice. A failed browser registration is not recorded as live and
155
157
  is retried on a later reconcile.
156
158
 
159
+ ## Forms as declarative tools
160
+
161
+ WebMCP also proposes a declarative API: a form carrying `toolname` and
162
+ `tooldescription`, with fields named by `name` and described by
163
+ `toolparamdescription`, is a tool the browser derives from the markup. The
164
+ browser fills the fields and submits; the person can see the form as it is
165
+ filled. `formTool` draws a capability that way, from the same contract, so the
166
+ form and the registered tool cannot name or describe it differently:
167
+
168
+ ```ts
169
+ const CreateTodoForm = AgentWebMcp.formTool(AppAgent, 'requested_create_todo')
170
+
171
+ // In the view: the form and each field the input names. `DraftSubmitted` and
172
+ // `model.draft` are the application's own submit and draft, as before.
173
+ h.form([...CreateTodoForm.form(h), h.OnSubmit(Message.DraftSubmitted())], [
174
+ h.input([...CreateTodoForm.field('title', h), h.Value(model.draft)]),
175
+ ])
176
+
177
+ // Beside `register`, which then answers the form's agent submissions.
178
+ AgentWebMcp.register({ agent, forms: [CreateTodoForm] })
179
+ ```
180
+
181
+ `formTool` only writes attributes; it registers nothing and listens to nothing.
182
+ `register({ forms })` listens for `submit` on the document in the capture phase,
183
+ ahead of the application's own handler. A submission the browser marks
184
+ `agentInvoked` is answered as the tool call would be: the fields the input names
185
+ are read from the form as text, decoded, authorized and dispatched as the
186
+ capability, and the result goes back through `respondWith`. The application's
187
+ `OnSubmit` does not run for it, so an agent's submission never depends on the
188
+ Model having seen the browser fill the fields. A person's submission is the
189
+ application's as before.
190
+
191
+ In a browser whose `SubmitEvent` has `agentInvoked`
192
+ (`AgentWebMcp.declarativeTools()`), a form's capability is not also registered
193
+ imperatively, since the form is the tool. Elsewhere it is registered as usual.
194
+ A field's value arrives as text, as every form submits it: give such a
195
+ capability text fields, or a transforming Schema (`Schema.NumberFromString`).
196
+ The declarative API is an early proposal whose form-to-schema rules are still
197
+ open, so treat it as a second way in, not the only one.
198
+
199
+ `formTool(definition, name, { autosubmit: true })` adds `toolautosubmit`, which
200
+ lets the agent submit without the person's review. It throws for a name the
201
+ contract does not expose and for an input that is not an object of fields;
202
+ `field` throws for a key the input lacks, and its type takes only the input's
203
+ keys.
204
+
157
205
  ## Registration lifecycle
158
206
 
159
207
  `AgentWebMcp.register` returns:
@@ -169,11 +217,12 @@ The main options are:
169
217
  | Option | Default | Purpose |
170
218
  | --- | --- | --- |
171
219
  | `agent` | — | The bound `AgentRuntime`. |
172
- | `modelContext` | `document.modelContext` | Producer surface to register against; pass a stand-in in tests. |
220
+ | `modelContext` | `pageModelContext()` | Producer surface to register against; pass a stand-in in tests. |
173
221
  | `followModel` | `true` | Reconcile tools as capability availability changes. |
174
222
  | `signal` | — | Unregister everything when aborted. |
175
223
  | `invocationId` | `crypto.randomUUID()` | Supplies protocol invocation ids. |
176
224
  | `onError` | — | Receives failures from background reconciliation. |
225
+ | `forms` | — | Capabilities drawn as forms (`formTool`): agent submissions are answered, and declarative browsers get no second, imperative tool. |
177
226
 
178
227
  Each registered tool has its own registration `AbortController`. That signal is
179
228
  for the WebMCP registration itself. It is distinct from a tool invocation's
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Agent } from "foldkit-agent";
2
+ import { Attribute, HtmlBuilder } from "foldkit/html";
2
3
  //#region src/webmcp.d.ts
3
4
  /**
4
5
  * The slice of Chrome's experimental WebMCP Imperative API this adapter uses.
@@ -40,8 +41,44 @@ interface RegisterToolOptions {
40
41
  interface ModelContext {
41
42
  readonly registerTool: (tool: ToolDescriptor, options?: RegisterToolOptions) => void | Promise<unknown>;
42
43
  }
43
- /** Reads `document.modelContext`, when the page provides it. */
44
- declare const documentModelContext: () => ModelContext | undefined;
44
+ /**
45
+ * The page's model context, when it has one: `document.modelContext`, where
46
+ * the spec puts it, or else `navigator.modelContext`, the name Chromium used
47
+ * before 150 and agent extensions that provide WebMCP still use. Undefined on
48
+ * a server, or in a browser with neither.
49
+ */
50
+ declare const pageModelContext: () => ModelContext | undefined;
51
+ //#endregion
52
+ //#region src/forms.d.ts
53
+ /**
54
+ * One capability drawn as a WebMCP declarative tool: a form whose `toolname`
55
+ * and `tooldescription`, and whose fields' `name` and `toolparamdescription`,
56
+ * come from the agent contract, so the form and the registered tool cannot
57
+ * disagree. `register({ forms })` answers its agent submissions.
58
+ */
59
+ interface FormTool<Name extends string = string, Input = unknown> {
60
+ readonly name: Name;
61
+ /** The form's attributes: `toolname`, `tooldescription`, and `toolautosubmit` when asked. */
62
+ readonly form: <Message>(h: HtmlBuilder<Message>) => ReadonlyArray<Attribute<Message>>;
63
+ /**
64
+ * A field's attributes: `name`, the input's key, and `toolparamdescription`
65
+ * when the input's Schema describes it.
66
+ */
67
+ readonly field: <Message>(key: keyof Input & string, h: HtmlBuilder<Message>) => ReadonlyArray<Attribute<Message>>;
68
+ /** The input's keys, which an agent's submission is read from. */
69
+ readonly keys: ReadonlyArray<string>;
70
+ }
71
+ /**
72
+ * Draws the capability `name` of an agent contract as a declarative tool: see
73
+ * the README's "Forms as declarative tools". It throws for a name the contract
74
+ * does not expose, or whose input is not an object of fields, which no form
75
+ * could fill.
76
+ */
77
+ declare const formTool: <ByName extends Record<string, unknown>, const Name extends keyof ByName & string>(definition: Agent.Definition<any, any, any, ByName, any>, name: Name, options?: {
78
+ readonly autosubmit?: boolean;
79
+ }) => FormTool<Name, ByName[Name]>;
80
+ /** Whether this browser draws forms as tools: its `SubmitEvent` says when an agent submitted. */
81
+ declare const declarativeTools: () => boolean;
45
82
  //#endregion
46
83
  //#region src/register.d.ts
47
84
  /** A live WebMCP registration. */
@@ -56,7 +93,7 @@ interface Registration {
56
93
  interface RegisterOptions<Model, Context_, Principal, ByName, ByTag> {
57
94
  /** The agent contract bound to a live Foldkit Runtime. */
58
95
  readonly agent: Agent.AgentRuntime<Model, Context_, Principal, ByName, ByTag>;
59
- /** Defaults to `document.modelContext`. */
96
+ /** Defaults to `pageModelContext()`: `document.modelContext`, or the older `navigator.modelContext`. */
60
97
  readonly modelContext?: ModelContext | undefined;
61
98
  /** Unregisters everything when aborted. */
62
99
  readonly signal?: AbortSignal | undefined;
@@ -73,6 +110,12 @@ interface RegisterOptions<Model, Context_, Principal, ByName, ByTag> {
73
110
  * initial registration, or one triggered by a Model change.
74
111
  */
75
112
  readonly onError?: ((error: unknown) => void) | undefined;
113
+ /**
114
+ * Capabilities the page draws as forms (`formTool`). An agent's submission
115
+ * of one is answered as its tool call would be. In a browser that draws
116
+ * forms as tools these are not registered too, as the form is the tool.
117
+ */
118
+ readonly forms?: ReadonlyArray<FormTool<keyof ByName & string>> | undefined;
76
119
  }
77
120
  /**
78
121
  * Projects an agent contract into `document.modelContext.registerTool(...)`.
@@ -94,7 +137,7 @@ interface RegisterOptions<Model, Context_, Principal, ByName, ByTag> {
94
137
  */
95
138
  declare const register: <Model, Context_, Principal, ByName, ByTag>(options: RegisterOptions<Model, Context_, Principal, ByName, ByTag>) => Registration;
96
139
  declare namespace agentWebMcp_d_exports {
97
- export { RegisterOptions, Registration, documentModelContext, register };
140
+ export { FormTool, RegisterOptions, Registration, declarativeTools, formTool, pageModelContext, register };
98
141
  }
99
142
  //#endregion
100
143
  export { agentWebMcp_d_exports as AgentWebMcp, type ModelContext, type RegisterToolOptions, type ToolDescriptor, type ToolExecutionContext, type ToolResult };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/webmcp.ts","../src/register.ts","../src/agentWebMcp.ts"],"mappings":";;;;;;;;;;UASiB;;WAEN,SAAS;;;UAIH;WACN,SAAS;aAAyB;aAAuB;;WACzD;;;UAIM;WACN;WACA;WACA,aAAa;WACb,UAAU,YAAY,SAAS,yBAAyB,QAAQ;;;;;;;;UAS1D;WACN,SAAS;;;UAIH;WACN,eACP,MAAM,gBACN,UAAU,+BACA;;;cAID,4BAA2B;;;;UCrCvB;;WAEN,eAAe;;WAEf;;WAEA,kBAAkB;;UAGZ,gBAAgB,OAAO,UAAU,WAAW,QAAQ;;WAE1D,OAAO,MAAM,aAAa,OAAO,UAAU,WAAW,QAAQ;;WAE9D,eAAe;;WAEf,SAAS;;;;;;WAMT;;WAEA;;;;;WAKA,YAAY;;;;;;;;;;;;;;;;;;;;cA0BV,WAAY,OAAO,UAAU,WAAW,QAAQ,OAC3D,SAAS,gBAAgB,OAAO,UAAU,WAAW,QAAQ,WAC5D"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/webmcp.ts","../src/forms.ts","../src/register.ts","../src/agentWebMcp.ts"],"mappings":";;;;;;;;;;;UASiB;;WAEN,SAAS;;;UAIH;WACN,SAAS;aAAyB;aAAuB;;WACzD;;;UAIM;WACN;WACA;WACA,aAAa;WACb,UAAU,YAAY,SAAS,yBAAyB,QAAQ;;;;;;;;UAS1D;WACN,SAAS;;;UAIH;WACN,eACP,MAAM,gBACN,UAAU,+BACA;;;;;;;;cASD,wBAAuB;;;;;;;;;UCzCnB,SAAS,8BAA8B;WAC7C,MAAM;;WAEN,OAAO,SAAS,GAAG,YAAY,aAAa,cAAc,UAAU;;;;;WAKpE,QAAQ,SACf,WAAW,gBACX,GAAG,YAAY,aACZ,cAAc,UAAU;;WAEpB,MAAM;;;;;;;;cAkBJ,WACX,eAAe,+BACT,mBAAmB,iBAEzB,YAAY,MAAM,0BAA0B,cAC5C,MAAM,MACN;WAAoB;MACnB,SAAS,MAAM,OAAO;;cA0CZ;;;;UCrFI;;WAEN,eAAe;;WAEf;;WAEA,kBAAkB;;UAGZ,gBAAgB,OAAO,UAAU,WAAW,QAAQ;;WAE1D,OAAO,MAAM,aAAa,OAAO,UAAU,WAAW,QAAQ;;WAE9D,eAAe;;WAEf,SAAS;;;;;;WAMT;;WAEA;;;;;WAKA,YAAY;;;;;;WAMZ,QAAQ,cAAc,eAAe;;;;;;;;;;;;;;;;;;;;cAqBnC,WAAY,OAAO,UAAU,WAAW,QAAQ,OAC3D,SAAS,gBAAgB,OAAO,UAAU,WAAW,QAAQ,WAC5D"}
package/dist/index.mjs CHANGED
@@ -1,14 +1,71 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
2
  import { Agent } from "foldkit-agent";
3
- import { Effect } from "effect";
4
- //#region src/webmcp.ts
5
- /** Reads `document.modelContext`, when the page provides it. */
6
- const documentModelContext = () => {
7
- if (typeof document === "undefined") return void 0;
8
- return document.modelContext;
3
+ import { Effect, Option } from "effect";
4
+ //#region src/forms.ts
5
+ /** The description a property of the input's JSON Schema carries, if any. */
6
+ const describedBy = (property) => typeof property === "object" && property !== null && "description" in property && typeof property.description === "string" ? Option.some(property.description) : Option.none();
7
+ /**
8
+ * Draws the capability `name` of an agent contract as a declarative tool: see
9
+ * the README's "Forms as declarative tools". It throws for a name the contract
10
+ * does not expose, or whose input is not an object of fields, which no form
11
+ * could fill.
12
+ */
13
+ const formTool = (definition, name, options = {}) => {
14
+ const descriptor = Agent.messages(definition).find((each) => each.name === name);
15
+ if (descriptor === void 0) throw new Error(`AgentWebMcp.formTool: the contract exposes no capability "${name}"`);
16
+ const properties = descriptor.inputSchema["properties"];
17
+ if (descriptor.inputSchema["type"] !== "object" || typeof properties !== "object" || properties === null) throw new Error(`AgentWebMcp.formTool: "${name}" takes no object of fields, which no form can fill`);
18
+ const fields = new Map(Object.entries(properties).map(([key, property]) => [key, describedBy(property)]));
19
+ return {
20
+ name,
21
+ keys: [...fields.keys()],
22
+ form: (h) => [
23
+ h.Attribute("toolname", name),
24
+ h.Attribute("tooldescription", descriptor.description),
25
+ ...options.autosubmit === true ? [h.Attribute("toolautosubmit", "")] : []
26
+ ],
27
+ field: (key, h) => {
28
+ const described = fields.get(key);
29
+ if (described === void 0) throw new Error(`AgentWebMcp.formTool: "${name}" has no field "${key}"`);
30
+ return [h.Attribute("name", key), ...Option.match(described, {
31
+ onNone: () => [],
32
+ onSome: (description) => [h.Attribute("toolparamdescription", description)]
33
+ })];
34
+ }
35
+ };
9
36
  };
10
- //#endregion
11
- //#region src/register.ts
37
+ /** Whether this browser draws forms as tools: its `SubmitEvent` says when an agent submitted. */
38
+ const declarativeTools = () => typeof SubmitEvent !== "undefined" && "agentInvoked" in SubmitEvent.prototype;
39
+ /**
40
+ * Answers an agent's submission of a form tool on `root`: in the capture phase,
41
+ * before the application's own submit handler, the form's fields named by the
42
+ * input are dispatched as the capability through `dispatch`, and the result
43
+ * goes back through `respondWith`. A person's submission is left alone.
44
+ */
45
+ const answerForms = (root, forms, dispatch) => {
46
+ const byName = new Map(forms.map((form) => [form.name, form]));
47
+ const onSubmit = (event) => {
48
+ const form = event.target;
49
+ if (!("agentInvoked" in event) || event.agentInvoked !== true) return;
50
+ if (!(form instanceof HTMLFormElement) || !("respondWith" in event)) return;
51
+ const { respondWith } = event;
52
+ if (typeof respondWith !== "function") return;
53
+ const tool = byName.get(form.getAttribute("toolname") ?? "");
54
+ if (tool === void 0) return;
55
+ event.preventDefault();
56
+ event.stopImmediatePropagation();
57
+ const data = new FormData(form);
58
+ const input = Object.create(null);
59
+ for (const key of tool.keys) {
60
+ const value = data.get(key);
61
+ if (typeof value === "string") input[key] = value;
62
+ }
63
+ respondWith.call(event, dispatch(tool.name, input));
64
+ };
65
+ root.addEventListener("submit", onSubmit, { capture: true });
66
+ return () => root.removeEventListener("submit", onSubmit, { capture: true });
67
+ };
68
+ /** Text a tool returns, which an agent reads. */
12
69
  const textResult = (text, isError = false) => ({
13
70
  content: [{
14
71
  type: "text",
@@ -16,6 +73,37 @@ const textResult = (text, isError = false) => ({
16
73
  }],
17
74
  isError
18
75
  });
76
+ /** Dispatches a capability for an agent, with the result as a tool returns it; it never rejects. */
77
+ const dispatchFor = (agent, invocationId) => async (name, input, signal) => {
78
+ try {
79
+ const result = await Effect.runPromise(Effect.result(agent.messages.dispatchUnknown(name, input, {
80
+ id: invocationId(),
81
+ transport: "webmcp",
82
+ signal
83
+ })));
84
+ if (result._tag === "Failure") return textResult(result.failure.message, true);
85
+ const summary = Agent.summarize(result.success);
86
+ return textResult(summary.text, !summary.ok);
87
+ } catch {
88
+ return textResult(`Capability "${name}" failed unexpectedly`, true);
89
+ }
90
+ };
91
+ //#endregion
92
+ //#region src/webmcp.ts
93
+ /**
94
+ * The page's model context, when it has one: `document.modelContext`, where
95
+ * the spec puts it, or else `navigator.modelContext`, the name Chromium used
96
+ * before 150 and agent extensions that provide WebMCP still use. Undefined on
97
+ * a server, or in a browser with neither.
98
+ */
99
+ const pageModelContext = () => {
100
+ if (typeof document === "undefined") return void 0;
101
+ const current = document.modelContext;
102
+ if (current !== void 0) return current;
103
+ return typeof navigator === "undefined" ? void 0 : navigator.modelContext;
104
+ };
105
+ //#endregion
106
+ //#region src/register.ts
19
107
  /**
20
108
  * Projects an agent contract into `document.modelContext.registerTool(...)`.
21
109
  *
@@ -35,29 +123,19 @@ const textResult = (text, isError = false) => ({
35
123
  * ```
36
124
  */
37
125
  const register = (options) => {
38
- const modelContext = options.modelContext ?? documentModelContext();
39
- if (modelContext === void 0) throw new Error("WebMCP is unavailable: no document.modelContext. Pass an explicit modelContext to register().");
126
+ const modelContext = options.modelContext ?? pageModelContext();
127
+ if (modelContext === void 0) throw new Error("WebMCP is unavailable: no document.modelContext or navigator.modelContext. Pass an explicit modelContext to register().");
40
128
  const { agent } = options;
41
- const nextInvocationId = options.invocationId ?? Agent.newInvocationId;
129
+ const dispatch = dispatchFor(agent, options.invocationId ?? Agent.newInvocationId);
130
+ const forms = options.forms ?? [];
131
+ const asForms = new Set(declarativeTools() ? forms.map((form) => form.name) : []);
132
+ const stopAnswering = forms.length === 0 || typeof document === "undefined" ? () => {} : answerForms(document, forms, dispatch);
42
133
  /** One AbortController per registered tool: aborting it unregisters that tool. */
43
134
  const controllers = /* @__PURE__ */ new Map();
44
135
  let disposed = false;
45
136
  const toolFor = (name, description, inputSchema) => {
46
137
  const controller = new AbortController();
47
- const execute = async (input, context) => {
48
- try {
49
- const result = await Effect.runPromise(Effect.result(agent.messages.dispatchUnknown(name, input, {
50
- id: nextInvocationId(),
51
- transport: "webmcp",
52
- signal: context.signal
53
- })));
54
- if (result._tag === "Failure") return textResult(result.failure.message, true);
55
- const summary = Agent.summarize(result.success);
56
- return textResult(summary.text, !summary.ok);
57
- } catch {
58
- return textResult(`Capability "${name}" failed unexpectedly`, true);
59
- }
60
- };
138
+ const execute = (input, context) => dispatch(name, input, context.signal);
61
139
  return {
62
140
  controller,
63
141
  tool: {
@@ -71,7 +149,7 @@ const register = (options) => {
71
149
  const runReconcile = async () => {
72
150
  if (disposed) return;
73
151
  const available = await Effect.runPromise(agent.messages.available);
74
- const wanted = new Map(available.map((descriptor) => [descriptor.name, descriptor]));
152
+ const wanted = new Map(available.flatMap((descriptor) => asForms.has(descriptor.name) ? [] : [[descriptor.name, descriptor]]));
75
153
  for (const [name, controller] of [...controllers]) if (!wanted.has(name)) {
76
154
  controller.abort();
77
155
  controllers.delete(name);
@@ -113,6 +191,7 @@ const register = (options) => {
113
191
  for (const controller of controllers.values()) controller.abort();
114
192
  controllers.clear();
115
193
  unsubscribe();
194
+ stopAnswering();
116
195
  };
117
196
  /**
118
197
  * Reconciles without a caller to await the result. A rejection here would
@@ -137,7 +216,9 @@ const register = (options) => {
137
216
  //#endregion
138
217
  //#region src/agentWebMcp.ts
139
218
  var agentWebMcp_exports = /* @__PURE__ */ __exportAll({
140
- documentModelContext: () => documentModelContext,
219
+ declarativeTools: () => declarativeTools,
220
+ formTool: () => formTool,
221
+ pageModelContext: () => pageModelContext,
141
222
  register: () => register
142
223
  });
143
224
  //#endregion
@@ -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 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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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/forms.ts","../src/webmcp.ts","../src/register.ts","../src/agentWebMcp.ts"],"sourcesContent":["import { Agent } from 'foldkit-agent'\nimport { Effect, Option } from 'effect'\nimport type { Attribute, HtmlBuilder } from 'foldkit/html'\nimport type { ToolResult } from './webmcp.js'\n\n/**\n * One capability drawn as a WebMCP declarative tool: a form whose `toolname`\n * and `tooldescription`, and whose fields' `name` and `toolparamdescription`,\n * come from the agent contract, so the form and the registered tool cannot\n * disagree. `register({ forms })` answers its agent submissions.\n */\nexport interface FormTool<Name extends string = string, Input = unknown> {\n readonly name: Name\n /** The form's attributes: `toolname`, `tooldescription`, and `toolautosubmit` when asked. */\n readonly form: <Message>(h: HtmlBuilder<Message>) => ReadonlyArray<Attribute<Message>>\n /**\n * A field's attributes: `name`, the input's key, and `toolparamdescription`\n * when the input's Schema describes it.\n */\n readonly field: <Message>(\n key: keyof Input & string,\n h: HtmlBuilder<Message>,\n ) => ReadonlyArray<Attribute<Message>>\n /** The input's keys, which an agent's submission is read from. */\n readonly keys: ReadonlyArray<string>\n}\n\n/** The description a property of the input's JSON Schema carries, if any. */\nconst describedBy = (property: unknown): Option.Option<string> =>\n typeof property === 'object' &&\n property !== null &&\n 'description' in property &&\n typeof property.description === 'string'\n ? Option.some(property.description)\n : Option.none()\n\n/**\n * Draws the capability `name` of an agent contract as a declarative tool: see\n * the README's \"Forms as declarative tools\". It throws for a name the contract\n * does not expose, or whose input is not an object of fields, which no form\n * could fill.\n */\nexport const formTool = <\n ByName extends Record<string, unknown>,\n const Name extends keyof ByName & string,\n>(\n definition: Agent.Definition<any, any, any, ByName, any>,\n name: Name,\n options: { readonly autosubmit?: boolean } = {},\n): FormTool<Name, ByName[Name]> => {\n const descriptor = Agent.messages(definition).find(each => each.name === name)\n if (descriptor === undefined)\n throw new Error(`AgentWebMcp.formTool: the contract exposes no capability \"${name}\"`)\n const properties = descriptor.inputSchema['properties']\n if (\n descriptor.inputSchema['type'] !== 'object' ||\n typeof properties !== 'object' ||\n properties === null\n )\n throw new Error(\n `AgentWebMcp.formTool: \"${name}\" takes no object of fields, which no form can fill`,\n )\n const fields = new Map(\n Object.entries(properties).map(([key, property]) => [key, describedBy(property)] as const),\n )\n return {\n name,\n keys: [...fields.keys()],\n form: h => [\n h.Attribute('toolname', name),\n h.Attribute('tooldescription', descriptor.description),\n ...(options.autosubmit === true ? [h.Attribute('toolautosubmit', '')] : []),\n ],\n field: (key, h) => {\n const described = fields.get(key)\n if (described === undefined)\n throw new Error(`AgentWebMcp.formTool: \"${name}\" has no field \"${key}\"`)\n return [\n // An attribute, not `h.Name`, which Foldkit sets only in the browser:\n // a server-rendered form names its fields too.\n h.Attribute('name', key),\n ...Option.match(described, {\n onNone: () => [],\n onSome: description => [h.Attribute('toolparamdescription', description)],\n }),\n ]\n },\n }\n}\n\n/** Whether this browser draws forms as tools: its `SubmitEvent` says when an agent submitted. */\nexport const declarativeTools = (): boolean =>\n typeof SubmitEvent !== 'undefined' && 'agentInvoked' in SubmitEvent.prototype\n\n/**\n * Answers an agent's submission of a form tool on `root`: in the capture phase,\n * before the application's own submit handler, the form's fields named by the\n * input are dispatched as the capability through `dispatch`, and the result\n * goes back through `respondWith`. A person's submission is left alone.\n */\nexport const answerForms = (\n root: Document,\n forms: ReadonlyArray<FormTool>,\n dispatch: (name: string, input: unknown) => Promise<ToolResult>,\n): (() => void) => {\n const byName = new Map(forms.map(form => [form.name, form]))\n const onSubmit = (event: Event) => {\n // `agentInvoked` and `respondWith` are what a declarative WebMCP browser adds to the event.\n const form = event.target\n if (!('agentInvoked' in event) || event.agentInvoked !== true) return\n if (!(form instanceof HTMLFormElement) || !('respondWith' in event)) return\n const { respondWith } = event\n if (typeof respondWith !== 'function') return\n const tool = byName.get(form.getAttribute('toolname') ?? '')\n if (tool === undefined) return\n event.preventDefault()\n event.stopImmediatePropagation()\n // Text, as a form submits it; a field the input does not name is not read.\n const data = new FormData(form)\n const input: Record<string, string> = Object.create(null)\n for (const key of tool.keys) {\n const value = data.get(key)\n if (typeof value === 'string') input[key] = value\n }\n respondWith.call(event, dispatch(tool.name, input))\n }\n root.addEventListener('submit', onSubmit, { capture: true })\n return () => root.removeEventListener('submit', onSubmit, { capture: true })\n}\n\n/** Text a tool returns, which an agent reads. */\nconst textResult = (text: string, isError = false): ToolResult => ({\n content: [{ type: 'text', text }],\n isError,\n})\n\n/** Dispatches a capability for an agent, with the result as a tool returns it; it never rejects. */\nexport const dispatchFor =\n (agent: Agent.AgentRuntime<any, any, any, any, any>, invocationId: () => string) =>\n async (name: string, input: unknown, signal?: AbortSignal): 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: invocationId(),\n transport: 'webmcp',\n signal,\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 // 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 * 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/**\n * The page's model context, when it has one: `document.modelContext`, where\n * the spec puts it, or else `navigator.modelContext`, the name Chromium used\n * before 150 and agent extensions that provide WebMCP still use. Undefined on\n * a server, or in a browser with neither.\n */\nexport const pageModelContext = (): ModelContext | undefined => {\n if (typeof document === 'undefined') return undefined\n const current = (document as unknown as { modelContext?: ModelContext }).modelContext\n if (current !== undefined) return current\n return typeof navigator === 'undefined'\n ? undefined\n : (navigator as unknown as { modelContext?: ModelContext }).modelContext\n}\n","import { Agent } from 'foldkit-agent'\nimport { Effect } from 'effect'\nimport { answerForms, declarativeTools, dispatchFor, type FormTool } from './forms.js'\nimport { type ModelContext, type ToolDescriptor, pageModelContext } 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 `pageModelContext()`: `document.modelContext`, or the older `navigator.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 * Capabilities the page draws as forms (`formTool`). An agent's submission\n * of one is answered as its tool call would be. In a browser that draws\n * forms as tools these are not registered too, as the form is the tool.\n */\n readonly forms?: ReadonlyArray<FormTool<keyof ByName & string>> | undefined\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 ?? pageModelContext()\n if (modelContext === undefined) {\n throw new Error(\n 'WebMCP is unavailable: no document.modelContext or navigator.modelContext. Pass an explicit modelContext to register().',\n )\n }\n\n const { agent } = options\n const dispatch = dispatchFor(agent, options.invocationId ?? Agent.newInvocationId)\n const forms = options.forms ?? []\n // Drawn as forms where the browser makes them tools, so registered only elsewhere.\n const asForms = new Set<string>(declarativeTools() ? forms.map(form => form.name) : [])\n const stopAnswering =\n forms.length === 0 || typeof document === 'undefined'\n ? () => {}\n : answerForms(document, forms, dispatch)\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 const execute = (input: unknown, context: { readonly signal?: AbortSignal | undefined }) =>\n dispatch(name, input, context.signal)\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(\n available.flatMap(descriptor =>\n asForms.has(descriptor.name) ? [] : [[descriptor.name, descriptor] as const],\n ),\n )\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 stopAnswering()\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 { pageModelContext } from './webmcp.js'\nexport { declarativeTools, formTool, type FormTool } from './forms.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.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "WebMCP adapter for foldkit-agent. Projects exposed Foldkit Messages into document.modelContext tools.",
5
5
  "keywords": [
6
6
  "foldkit",
@@ -40,7 +40,7 @@
40
40
  "peerDependencies": {
41
41
  "effect": "^4.0.0",
42
42
  "foldkit": "^0.165.0",
43
- "foldkit-agent": "^0.6.0"
43
+ "foldkit-agent": "^0.6.1"
44
44
  },
45
45
  "dependencies": {},
46
46
  "devDependencies": {
@@ -49,7 +49,7 @@
49
49
  "foldkit": "0.165.0",
50
50
  "tsdown": "^0.23.0",
51
51
  "typescript": "5.9.3",
52
- "foldkit-agent": "0.6.0"
52
+ "foldkit-agent": "0.6.1"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "tsdown",