foldkit-agent-webmcp 0.1.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/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/index.d.mts +101 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +147 -0
- package/dist/index.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Glenn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# `foldkit-agent-webmcp`
|
|
2
|
+
|
|
3
|
+
The browser-native adapter for [`foldkit-agent`](../agent). It projects exposed
|
|
4
|
+
Foldkit Messages into Chrome's experimental WebMCP Imperative API.
|
|
5
|
+
|
|
6
|
+
WebMCP runs in the page itself, so a tool's `execute` dispatches directly into
|
|
7
|
+
the same live Foldkit Runtime the human is already using — no DOM automation,
|
|
8
|
+
and no external browser-session bridge.
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
browser agent -> document.modelContext -> foldkit-agent-webmcp
|
|
12
|
+
-> Foldkit AgentRuntime -> Message -> update -> Model + Commands
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add foldkit-agent foldkit-agent-webmcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { AgentWebMcp } from 'foldkit-agent-webmcp'
|
|
25
|
+
|
|
26
|
+
const registration = AgentWebMcp.register({ agent: agentRuntime })
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
[examples/todo](../../examples/todo) registers a real contract and executes the
|
|
30
|
+
resulting tools.
|
|
31
|
+
|
|
32
|
+
`agent` is the `AgentRuntime` returned by `Agent.bind(...)`.
|
|
33
|
+
|
|
34
|
+
Each currently available capability becomes one tool:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
capability name -> tool name
|
|
38
|
+
variant description -> description
|
|
39
|
+
derived JSON Schema -> inputSchema
|
|
40
|
+
Runtime dispatch -> execute
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
No schema or handler is repeated in this package.
|
|
44
|
+
|
|
45
|
+
### Options
|
|
46
|
+
|
|
47
|
+
| Option | Default | Purpose |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `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. |
|
|
53
|
+
| `invocationId` | `crypto.randomUUID()` | Supplies the invocation id. |
|
|
54
|
+
| `onError` | — | Reports a failure from a reconcile no caller is awaiting. |
|
|
55
|
+
|
|
56
|
+
### Returned registration
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
registration.refresh() // reconcile against the current Model
|
|
60
|
+
registration.registered() // capability names currently registered
|
|
61
|
+
registration.unregister() // abort every registration and stop following
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Dynamic availability
|
|
65
|
+
|
|
66
|
+
Tool availability is a projection of Model state, which falls out of TEA:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
RequestedDeleteTodo: {
|
|
70
|
+
name: 'delete_todo',
|
|
71
|
+
description: 'Delete the selected todo',
|
|
72
|
+
available: model => Option.isSome(model.selectedTodoId),
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
Todo list tools: create_todo
|
|
78
|
+
user selects todo -> Model changes
|
|
79
|
+
-> tools: create_todo, delete_todo
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The adapter holds one `AbortController` per registered tool and passes its
|
|
83
|
+
signal in the options bag, where `registerTool` takes it:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
await document.modelContext.registerTool(tool, { signal: controller.signal })
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Aborting that signal is what unregisters the tool, so it must not go on the
|
|
90
|
+
descriptor. This is the registration signal, distinct from the execution signal
|
|
91
|
+
passed to `execute`, which is forwarded as `Invocation.signal`.
|
|
92
|
+
|
|
93
|
+
Reconciles are serialized, and a capability is recorded as registered only once
|
|
94
|
+
`registerTool` resolves. A registration the browser refuses is not recorded and
|
|
95
|
+
is retried on the next reconcile; `refresh()` rejects with the first failure
|
|
96
|
+
after attempting the rest. Nothing is registered once `unregister()` has run,
|
|
97
|
+
and a registration that was in flight when it ran is aborted.
|
|
98
|
+
|
|
99
|
+
## Errors
|
|
100
|
+
|
|
101
|
+
A dispatch failure is returned as a tool error rather than thrown, so the
|
|
102
|
+
calling agent can act on it:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
No such capability: delete_todo
|
|
106
|
+
Capability "delete_todo" is not available right now
|
|
107
|
+
Not authorized to invoke "delete_todo"
|
|
108
|
+
Invalid input for "create_todo"
|
|
109
|
+
Capability "create_todo" failed unexpectedly
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
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.
|
|
115
|
+
|
|
116
|
+
## Why this stays an adapter
|
|
117
|
+
|
|
118
|
+
WebMCP is experimental and evolving. The types it needs are declared locally in
|
|
119
|
+
`src/webmcp.ts` rather than imported, so the browser API can change without the
|
|
120
|
+
Foldkit agent contract changing with it.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Agent } from "foldkit-agent";
|
|
2
|
+
//#region src/webmcp.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The slice of Chrome's experimental WebMCP Imperative API this adapter uses.
|
|
5
|
+
*
|
|
6
|
+
* These types are declared locally rather than imported: the API is still
|
|
7
|
+
* experimental, and Foldkit should not be coupled to a browser proposal that is
|
|
8
|
+
* still changing.
|
|
9
|
+
*/
|
|
10
|
+
/** Metadata passed to a tool's `execute` function. */
|
|
11
|
+
interface ToolExecutionContext {
|
|
12
|
+
/** Cancellation for this invocation: is this call still wanted? */
|
|
13
|
+
readonly signal?: AbortSignal | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** The value a WebMCP tool returns. */
|
|
16
|
+
interface ToolResult {
|
|
17
|
+
readonly content: ReadonlyArray<{
|
|
18
|
+
readonly type: 'text';
|
|
19
|
+
readonly text: string;
|
|
20
|
+
}>;
|
|
21
|
+
readonly isError?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** A tool registration as accepted by `document.modelContext.registerTool`. */
|
|
24
|
+
interface ToolDescriptor {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly description: string;
|
|
27
|
+
readonly inputSchema: Record<string, unknown>;
|
|
28
|
+
readonly execute: (input: any, context: ToolExecutionContext) => Promise<ToolResult>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Options accepted alongside a tool registration.
|
|
32
|
+
*
|
|
33
|
+
* The registration signal belongs here, not on the descriptor: aborting it is
|
|
34
|
+
* how the documented API unregisters a tool.
|
|
35
|
+
*/
|
|
36
|
+
interface RegisterToolOptions {
|
|
37
|
+
readonly signal?: AbortSignal | undefined;
|
|
38
|
+
}
|
|
39
|
+
/** The producer surface exposed on `document.modelContext`. */
|
|
40
|
+
interface ModelContext {
|
|
41
|
+
readonly registerTool: (tool: ToolDescriptor, options?: RegisterToolOptions) => void | Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
/** Reads `document.modelContext`, when the page provides it. */
|
|
44
|
+
declare const documentModelContext: () => ModelContext | undefined;
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/register.d.ts
|
|
47
|
+
/** A live WebMCP registration. */
|
|
48
|
+
interface Registration {
|
|
49
|
+
/** Re-reads availability from the current Model and reconciles registrations. */
|
|
50
|
+
readonly refresh: () => Promise<void>;
|
|
51
|
+
/** Unregisters every tool and stops following the Model. */
|
|
52
|
+
readonly unregister: () => void;
|
|
53
|
+
/** The capability names currently registered with WebMCP. */
|
|
54
|
+
readonly registered: () => ReadonlyArray<string>;
|
|
55
|
+
}
|
|
56
|
+
interface RegisterOptions<Model, Context_, Principal, ByName, ByTag> {
|
|
57
|
+
/** The agent contract bound to a live Foldkit Runtime. */
|
|
58
|
+
readonly agent: Agent.AgentRuntime<Model, Context_, Principal, ByName, ByTag>;
|
|
59
|
+
/** Defaults to `document.modelContext`. */
|
|
60
|
+
readonly modelContext?: ModelContext | undefined;
|
|
61
|
+
/** Unregisters everything when aborted. */
|
|
62
|
+
readonly signal?: AbortSignal | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Follow the live Model and reconcile registrations as `available(model)`
|
|
65
|
+
* changes. Defaults to `true`, and is inert unless the host supports
|
|
66
|
+
* subscription.
|
|
67
|
+
*/
|
|
68
|
+
readonly followModel?: boolean | undefined;
|
|
69
|
+
/** Supplies an invocation id. Defaults to `crypto.randomUUID()`. */
|
|
70
|
+
readonly invocationId?: (() => string) | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Reports a failure from a reconcile that no caller is awaiting -- the
|
|
73
|
+
* initial registration, or one triggered by a Model change.
|
|
74
|
+
*/
|
|
75
|
+
readonly onError?: ((error: unknown) => void) | undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Projects an agent contract into `document.modelContext.registerTool(...)`.
|
|
79
|
+
*
|
|
80
|
+
* Each currently available capability becomes one tool: the capability name
|
|
81
|
+
* becomes the tool name, the variant description becomes the description, the
|
|
82
|
+
* derived JSON Schema becomes `inputSchema`, and `execute` validates its input
|
|
83
|
+
* and dispatches into the same live Foldkit Runtime the human is using.
|
|
84
|
+
*
|
|
85
|
+
* Tool availability is itself a projection of Model state. When
|
|
86
|
+
* `available(model)` changes, registrations are reconciled: capabilities that
|
|
87
|
+
* are gone have their registration signal aborted, and capabilities that have
|
|
88
|
+
* appeared are registered.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const registration = AgentWebMcp.register({ agent: agentRuntime })
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare const register: <Model, Context_, Principal, ByName, ByTag>(options: RegisterOptions<Model, Context_, Principal, ByName, ByTag>) => Registration;
|
|
96
|
+
declare namespace agentWebMcp_d_exports {
|
|
97
|
+
export { RegisterOptions, Registration, documentModelContext, register };
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
export { agentWebMcp_d_exports as AgentWebMcp, type ModelContext, type RegisterToolOptions, type ToolDescriptor, type ToolExecutionContext, type ToolResult };
|
|
101
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
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;
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/register.ts
|
|
12
|
+
const textResult = (text, isError = false) => ({
|
|
13
|
+
content: [{
|
|
14
|
+
type: "text",
|
|
15
|
+
text
|
|
16
|
+
}],
|
|
17
|
+
isError
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Projects an agent contract into `document.modelContext.registerTool(...)`.
|
|
21
|
+
*
|
|
22
|
+
* Each currently available capability becomes one tool: the capability name
|
|
23
|
+
* becomes the tool name, the variant description becomes the description, the
|
|
24
|
+
* derived JSON Schema becomes `inputSchema`, and `execute` validates its input
|
|
25
|
+
* and dispatches into the same live Foldkit Runtime the human is using.
|
|
26
|
+
*
|
|
27
|
+
* Tool availability is itself a projection of Model state. When
|
|
28
|
+
* `available(model)` changes, registrations are reconciled: capabilities that
|
|
29
|
+
* are gone have their registration signal aborted, and capabilities that have
|
|
30
|
+
* appeared are registered.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const registration = AgentWebMcp.register({ agent: agentRuntime })
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
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().");
|
|
40
|
+
const { agent } = options;
|
|
41
|
+
const nextInvocationId = options.invocationId ?? Agent.newInvocationId;
|
|
42
|
+
/** One AbortController per registered tool: aborting it unregisters that tool. */
|
|
43
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
44
|
+
let disposed = false;
|
|
45
|
+
const toolFor = (name, description, inputSchema) => {
|
|
46
|
+
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 { completion } = result.success;
|
|
56
|
+
if (completion === void 0) return textResult(`Dispatched ${result.success.tag}`);
|
|
57
|
+
return textResult(`${completion.status === "completed" ? "Completed" : "Failed"}: ${completion.message._tag}`, completion.status === "failed");
|
|
58
|
+
} catch {
|
|
59
|
+
return textResult(`Capability "${name}" failed unexpectedly`, true);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
controller,
|
|
64
|
+
tool: {
|
|
65
|
+
name,
|
|
66
|
+
description,
|
|
67
|
+
inputSchema,
|
|
68
|
+
execute
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const runReconcile = async () => {
|
|
73
|
+
if (disposed) return;
|
|
74
|
+
const available = await Effect.runPromise(agent.messages.available);
|
|
75
|
+
const wanted = new Map(available.map((descriptor) => [descriptor.name, descriptor]));
|
|
76
|
+
for (const [name, controller] of [...controllers]) if (!wanted.has(name)) {
|
|
77
|
+
controller.abort();
|
|
78
|
+
controllers.delete(name);
|
|
79
|
+
}
|
|
80
|
+
let failure;
|
|
81
|
+
for (const [name, descriptor] of wanted) {
|
|
82
|
+
if (disposed) return;
|
|
83
|
+
if (controllers.has(name)) continue;
|
|
84
|
+
const { controller, tool } = toolFor(name, descriptor.description, descriptor.inputSchema);
|
|
85
|
+
try {
|
|
86
|
+
await modelContext.registerTool(tool, { signal: controller.signal });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
failure ??= error;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (disposed) {
|
|
92
|
+
controller.abort();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
controllers.set(name, controller);
|
|
96
|
+
}
|
|
97
|
+
if (failure !== void 0) throw failure;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Reconciles are serialized.
|
|
101
|
+
*
|
|
102
|
+
* A capability is recorded only once the browser has accepted it, so two
|
|
103
|
+
* overlapping reconciles would both see it as missing and register it twice.
|
|
104
|
+
*/
|
|
105
|
+
let queue = Promise.resolve();
|
|
106
|
+
const reconcile = () => {
|
|
107
|
+
const next = queue.then(runReconcile, runReconcile);
|
|
108
|
+
queue = next.then(() => {}, () => {});
|
|
109
|
+
return next;
|
|
110
|
+
};
|
|
111
|
+
let unsubscribe = () => {};
|
|
112
|
+
const unregister = () => {
|
|
113
|
+
disposed = true;
|
|
114
|
+
for (const controller of controllers.values()) controller.abort();
|
|
115
|
+
controllers.clear();
|
|
116
|
+
unsubscribe();
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Reconciles without a caller to await the result. A rejection here would
|
|
120
|
+
* otherwise become an unhandled rejection, so it is reported through
|
|
121
|
+
* `onError` and the registration is left as it was.
|
|
122
|
+
*/
|
|
123
|
+
const reconcileInBackground = () => {
|
|
124
|
+
reconcile().catch((error) => {
|
|
125
|
+
options.onError?.(error);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
reconcileInBackground();
|
|
129
|
+
if (options.followModel !== false) unsubscribe = agent.subscribe(reconcileInBackground);
|
|
130
|
+
if (options.signal?.aborted === true) unregister();
|
|
131
|
+
else options.signal?.addEventListener("abort", unregister, { once: true });
|
|
132
|
+
return {
|
|
133
|
+
refresh: reconcile,
|
|
134
|
+
unregister,
|
|
135
|
+
registered: () => [...controllers.keys()]
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/agentWebMcp.ts
|
|
140
|
+
var agentWebMcp_exports = /* @__PURE__ */ __exportAll({
|
|
141
|
+
documentModelContext: () => documentModelContext,
|
|
142
|
+
register: () => register
|
|
143
|
+
});
|
|
144
|
+
//#endregion
|
|
145
|
+
export { agentWebMcp_exports as AgentWebMcp };
|
|
146
|
+
|
|
147
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "foldkit-agent-webmcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WebMCP adapter for foldkit-agent. Projects exposed Foldkit Messages into document.modelContext tools.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"foldkit",
|
|
7
|
+
"effect",
|
|
8
|
+
"agents",
|
|
9
|
+
"webmcp",
|
|
10
|
+
"browser"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Patrick Glenn <doeixd@gmail.com>",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/doeixd/foldkit-plus.git",
|
|
18
|
+
"directory": "packages/agent-webmcp"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/doeixd/foldkit-plus/tree/main/packages/agent-webmcp#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/doeixd/foldkit-plus/issues"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"import": "./dist/index.mjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"effect": "^4.0.0-rc.112",
|
|
41
|
+
"foldkit": "^0.158.2",
|
|
42
|
+
"foldkit-agent": "^0.1.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"effect": "4.0.0-rc.112",
|
|
47
|
+
"foldkit": "0.158.2",
|
|
48
|
+
"tsdown": "^0.23.0",
|
|
49
|
+
"typescript": "^5.7.2",
|
|
50
|
+
"foldkit-agent": "0.1.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsdown",
|
|
54
|
+
"typecheck": "tsc -b"
|
|
55
|
+
}
|
|
56
|
+
}
|