Skip to content

Commit 43b654a

Browse files
authored
feat(client): expose agent-flagged client RPC functions over WebMCP (#366)
1 parent 3ce993f commit 43b654a

18 files changed

Lines changed: 1366 additions & 89 deletions

File tree

‎docs/content/1.guide/15.agent-native.md‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
title: 'Agent-Native Devframe'
33
navigation:
44
icon: i-lucide-bot
5-
description: 'Devframe exposes its browser-side API (RPC functions, resources, shared state) to coding agents over MCP, opt-in per function.'
5+
description: 'Devframe exposes its API (RPC functions, resources, shared state) to agents, over MCP on the node side and WebMCP on the browser side, opt-in per function.'
66
---
77

8-
Devframe exposes its browser-side API (RPC functions, resources, shared state) to coding agents over MCP, opt-in per function.
8+
Devframe exposes its API (RPC functions, resources, shared state) to agents, over MCP on the node side and [WebMCP](#browser-side-tools-over-webmcp) on the browser side, opt-in per function.
99

1010
## How it works
1111

12-
Three pieces: the **`agent` field** on `defineRpcFunction`, **`ctx.agent`** (non-RPC tools + resources), and the **MCP adapter** (`devframe/adapters/mcp`) serving an [MCP](https://modelcontextprotocol.io) server.
12+
Three pieces: the **`agent` field** on `defineRpcFunction`, **`ctx.agent`** (non-RPC tools + resources), and the **MCP adapter** (`devframe/adapters/mcp`) serving an [MCP](https://modelcontextprotocol.io) server. The same `agent` field on a *client* RPC function surfaces it [over WebMCP](#browser-side-tools-over-webmcp) instead.
1313

1414
## Exposing an RPC function
1515

@@ -137,6 +137,29 @@ In `claude_desktop_config.json`:
137137

138138
Restart; tools appear in the drawer, resources as `devframe://resource/<id>` / `devframe://state/<key>` URIs.
139139

140+
## Browser-side tools over WebMCP
141+
142+
The same `agent` signature works on the browser side: a client RPC function (a function the node side calls on the browser, registered on `rpc.client` or through a scoped `client.scope('my-plugin').rpc.register(...)`) carrying an `agent` field is mirrored onto the page's [WebMCP](https://github.com/webmachinelearning/webmcp) model context (`document.modelContext` / `navigator.modelContext`) as a callable tool, so in-page and browser-integrated agents can drive browser-side functionality directly. Wire names, `arg0`/`arg1`/… input schemas, and safety annotations match the MCP projection above.
143+
144+
```ts
145+
const rpc = await connectDevframe()
146+
147+
rpc.client.register({
148+
name: 'my-plugin:highlight-node',
149+
type: 'action',
150+
jsonSerializable: true,
151+
agent: {
152+
description: 'Highlight a node in the open inspector view. Use it to point the user at a finding.',
153+
},
154+
handler: (id: string) => highlightNode(id),
155+
})
156+
```
157+
158+
`connectDevframe()` wires this on its own when the browser provides a model context; `webmcp: false` keeps the browser side off the WebMCP surface. `registerWebMcpTools(collector)` (from `devframe/client`) applies the same projection to a hand-built collector and returns a dispose that unregisters every tool.
159+
160+
> [!WARNING]
161+
> WebMCP is an experimental proposal; `registerWebMcpTools` tracks the current draft (`AbortSignal`-based unregistration) and earlier handle-returning drafts, but the browser API may still change.
162+
140163
## Writing descriptions agents act on
141164

142165
Describe *when* to use a tool, not just its return:

‎docs/content/5.add-ons/1.devframes/2.inspect.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ _History panels_
2424
## What it does
2525

2626
- **Functions**: type, flags, JSON Schema, agent exposure; read-only `query` / `static` invokable inline.
27+
- **Client**: the browser side of the connection: client RPC functions (invoked locally in the page) and the page's WebMCP tools, live from the model context's `getTools()` when the browser supports discovery, otherwise projected from `agent`-flagged client functions.
2728
- **State**: shared-state keys in a live JSON tree that flashes changes.
2829
- **Agent**: tools and resources for agents.
2930
- **History**: a timeline of RPC calls and shared-state updates.

‎docs/content/8.references/5.browser-api.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The options of `connectDevframe()` / `getDevframeRpcClient()`: [Client](/guide/c
2121
| `wsOptions` | Transport overrides: `onConnected` / `onError` / `onDisconnected` hooks, socket URL. |
2222
| `rpcOptions` | Forwarded to `birpc`. |
2323
| `connectionMeta` | Descriptor that skips the `__connection.json` fetch. |
24+
| `webmcp` | Mirror `agent`-flagged client RPC functions onto the page's WebMCP model context as tools; `false` opts out. Default `true` (applies only when the browser provides one). See [Agent-Native](/guide/agent-native#browser-side-tools-over-webmcp). |
2425

2526
## RPC client events
2627

‎packages/devframe/src/client/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export * from './rpc-streaming'
99
export { resolveWsUrl, type WsUrlLocation } from './rpc-ws'
1010
export * from './scope'
1111
export * from './settings'
12+
export * from './webmcp'
1213

1314
export const connectDevframe = getDevframeRpcClient

‎packages/devframe/src/client/rpc.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { createStaticRpcClientMode } from './rpc-static'
2121
import { createRpcStreamingClientHost } from './rpc-streaming'
2222
import { createWsRpcClientMode } from './rpc-ws'
2323
import { createScopedClientContext } from './scope'
24+
import { registerWebMcpTools } from './webmcp'
2425

2526
export interface DevframeRpcContext {
2627
/**
@@ -99,6 +100,18 @@ export interface DevframeRpcClientOptions extends SetupDevframeConnectionOptions
99100
sseOptions?: Partial<SseRpcChannelOptions>
100101
rpcOptions?: Partial<BirpcOptions<DevframeRpcServerFunctions, DevframeRpcClientFunctions, boolean>>
101102
cacheOptions?: boolean | Partial<RpcCacheOptions>
103+
/**
104+
* Mirror `agent`-flagged client RPC functions (functions registered on
105+
* `rpc.client` with an `agent` field) onto the page's WebMCP model
106+
* context (`document.modelContext` / `navigator.modelContext`) as
107+
* callable tools, so in-page and browser-integrated agents can invoke
108+
* them; see `registerWebMcpTools`. Applies only when the browser
109+
* provides a model context. Set `false` to keep the browser side off
110+
* the WebMCP surface.
111+
*
112+
* @default true
113+
*/
114+
webmcp?: boolean
102115
/**
103116
* Reject a pending `rpc.call(...)` if the server hasn't answered within this
104117
* many milliseconds, with a {@link DevframeConnectionError} of kind
@@ -332,6 +345,8 @@ export async function getDevframeRpcClient(
332345
rpc: undefined!,
333346
}
334347
const clientRpc: DevframeClientRpcHost = new RpcFunctionsCollectorBase<DevframeRpcClientFunctions, DevframeRpcContext>(context)
348+
// No-op when the browser provides no WebMCP model context.
349+
const disposeWebMcp = options.webmcp === false ? undefined : registerWebMcpTools(clientRpc)
335350

336351
async function fetchJsonFromBases(path: string): Promise<any> {
337352
const candidates = [
@@ -470,7 +485,10 @@ export async function getDevframeRpcClient(
470485
streaming: undefined!,
471486
cacheManager,
472487
scope: undefined!,
473-
close: () => mode.close?.(),
488+
close: () => {
489+
disposeWebMcp?.()
490+
mode.close?.()
491+
},
474492
}
475493

476494
rpc.sharedState = createRpcSharedStateClientHost(rpc)

0 commit comments

Comments
 (0)