Skip to content

Commit 7154e1f

Browse files
feat: add WebMCP skill — AI agent tool protocol for IC canisters
Adds a new skill covering the full WebMCP stack: - Candid-to-JSON-Schema codegen via ic-webmcp-codegen - Browser tool registration via navigator.modelContext - dfx.json webmcp config section - CORS headers via .ic-assets.json5 - Internet Identity scoped delegation for agents - Polyfill with OpenAI/Anthropic/LangChain adapters - 10 common pitfalls Evaluation: 4 output evals (add to canister, auth tools, polyfill, manifest gen) + 8 should-trigger / 6 should-not-trigger queries. This is related to the ic repo PR dfinity/ic#9708
1 parent 384eb3d commit 7154e1f

File tree

2 files changed

+360
-0
lines changed

2 files changed

+360
-0
lines changed

evaluations/webmcp.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"skill": "webmcp",
3+
"description": "Evaluates agent ability to expose IC canister methods as AI-accessible tools via WebMCP",
4+
"output_evals": [
5+
{
6+
"name": "Add WebMCP to an existing canister",
7+
"prompt": "I have a Rust canister with a .did file. How do I make it discoverable by AI agents?",
8+
"expected_behaviors": [
9+
"Adds a webmcp section to dfx.json with name, description, expose_methods",
10+
"Runs ic-webmcp-codegen to generate webmcp.json and webmcp.js",
11+
"Configures CORS headers via .ic-assets.json5 for /.well-known/webmcp.json",
12+
"Includes the <script type='module' src='/webmcp.js'></script> tag or manual ICWebMCP initialization",
13+
"Does NOT reference esm.sh or any external CDN for @dfinity/webmcp"
14+
]
15+
},
16+
{
17+
"name": "Add auth-required tools",
18+
"prompt": "My canister has a transfer method that should require login before AI agents can call it. How do I set this up with WebMCP?",
19+
"expected_behaviors": [
20+
"Adds the method to require_auth in dfx.json webmcp config",
21+
"Configures onAuthRequired callback in ICWebMCP constructor",
22+
"Uses AuthClient or Internet Identity for the login flow",
23+
"Creates a scoped delegation via createAgentDelegation with non-empty targets",
24+
"Does NOT create an unrestricted delegation with empty targets array"
25+
]
26+
},
27+
{
28+
"name": "Use polyfill for non-Chrome environments",
29+
"prompt": "I want my IC canister tools to work with Claude and OpenAI, not just Chrome. How?",
30+
"expected_behaviors": [
31+
"Calls installPolyfill() before ICWebMCP.registerAll()",
32+
"Uses getOpenAITools() or getAnthropicTools() to export in framework-specific format",
33+
"Uses dispatchToolCall() to route tool call responses back to the canister",
34+
"Does NOT assume navigator.modelContext is always available natively"
35+
]
36+
},
37+
{
38+
"name": "Generate manifest from .did file",
39+
"prompt": "Generate a WebMCP manifest from my ledger.did file for the ICP ledger canister",
40+
"expected_behaviors": [
41+
"Uses ic-webmcp-codegen did subcommand with --did flag",
42+
"Sets --canister-id to the correct ICP ledger principal",
43+
"Uses --expose to select specific methods",
44+
"Uses --require-auth for methods that modify state",
45+
"Generates valid JSON with schema_version 1.0"
46+
]
47+
}
48+
],
49+
"trigger_evals": {
50+
"should_trigger": [
51+
"How do I expose my canister to AI agents?",
52+
"WebMCP integration for Internet Computer",
53+
"Make my dapp work with navigator.modelContext",
54+
"Generate a tool manifest from a .did file",
55+
"How do AI agents call IC canisters?",
56+
"Add OpenAI function calling to my IC canister",
57+
"Anthropic tool use with Internet Computer",
58+
"LangChain tools for IC canisters"
59+
],
60+
"should_not_trigger": [
61+
"How do I deploy a canister?",
62+
"Write a Motoko counter canister",
63+
"How does Internet Identity work?",
64+
"What is certified data?",
65+
"How do I use ckBTC?",
66+
"Set up a custom domain for my canister"
67+
]
68+
}
69+
}

skills/webmcp/SKILL.md

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
---
2+
name: webmcp
3+
description: "Expose Internet Computer canister methods as AI agent tools via WebMCP (Web Model Context Protocol). Covers Candid-to-JSON-Schema codegen, browser tool registration via navigator.modelContext, certified query verification, Internet Identity scoped delegation for agents, and framework adapters for OpenAI/Anthropic/LangChain. Use when building AI-accessible dapps, agent tooling, or canister discovery."
4+
license: Apache-2.0
5+
compatibility: "icp-cli >= 0.2.2"
6+
metadata:
7+
title: WebMCP — AI Agent Tool Protocol
8+
category: Integration
9+
---
10+
11+
# WebMCP for the Internet Computer
12+
13+
## What This Is
14+
15+
WebMCP (Web Model Context Protocol) is a W3C browser standard (Chrome 146+) that lets websites register callable tools for AI agents via `navigator.modelContext`. The Internet Computer is uniquely suited for WebMCP: Candid interfaces already define structured tool schemas, certified queries provide verifiable responses, and Internet Identity enables scoped agent authentication via delegation chains.
16+
17+
The IC WebMCP stack generates tool manifests from `.did` files, serves them from asset canisters, and bridges `navigator.modelContext` to canister calls via `@dfinity/agent`. A polyfill extends this to non-Chrome browsers and server-side agent frameworks (Claude, OpenAI, LangChain).
18+
19+
## Prerequisites
20+
21+
- Rust: `ic-webmcp-codegen` crate (CLI tool) for manifest generation
22+
- TypeScript: `@dfinity/webmcp` package for browser/agent integration
23+
- `@dfinity/agent` >= 1.0.0, `@dfinity/candid` >= 1.0.0, `@dfinity/principal` >= 1.0.0
24+
- Chrome 146+ for native `navigator.modelContext` (polyfill available for other environments)
25+
26+
## Canister IDs
27+
28+
No fixed canister IDs. WebMCP is a protocol layer applied to YOUR canister. The manifest embeds your canister's principal ID.
29+
30+
## Mistakes That Break Your Build
31+
32+
1. **Not generating the manifest before deploying.** The `webmcp.json` must be generated from your `.did` file using `ic-webmcp-codegen` and placed in your asset canister's assets directory. Without it, no AI agent can discover your tools.
33+
34+
2. **Forgetting CORS headers on the manifest.** The `/.well-known/webmcp.json` endpoint must return `Access-Control-Allow-Origin: *` and `Content-Type: application/json`. Use `ic-webmcp-asset-middleware` or add an `.ic-assets.json5` config. Without CORS, cross-origin browser requests fail silently.
35+
36+
3. **Using `document.currentScript` in module scripts.** The generated `webmcp.js` is an ES module — `document.currentScript` is always `null` for module scripts. The codegen uses top-level `await` instead. If you write custom init code, don't rely on `document.currentScript`.
37+
38+
4. **Passing empty `targets` to `createScopedDelegation`.** An empty targets array produces an unrestricted delegation valid for ALL IC canisters, not just yours. Always pass at least your canister ID. Use `getDelegationTargets(canisterId, manifest.authentication)` to build the correct list.
39+
40+
5. **Omitting methods from `expose_methods` but listing them in other config sections.** If `expose_methods` is set in `dfx.json`, only those methods appear in the manifest. Methods listed in `require_auth`, `certified_queries`, or `descriptions` but NOT in `expose_methods` are silently dropped.
41+
42+
6. **Sending parameters without an IDL factory.** Without `setIdlFactory()`, the bridge can only call zero-argument methods. For methods with parameters, the bridge throws: `"requires an idlFactory to encode parameters"`. Always provide the generated IDL factory for full tool execution.
43+
44+
7. **Assuming `certified: true` means full BLS verification.** The `certified` flag in the manifest indicates the query supports certified responses. `@dfinity/agent` verifies node signatures automatically (`verifyQuerySignatures: true` by default), but this is node-level verification. For full BLS threshold certificate verification of canister-managed certified data, use `readCertifiedData()` with `readState()`.
45+
46+
8. **Loading `@dfinity/webmcp` from a CDN without integrity checking.** The generated `webmcp.js` imports `@dfinity/webmcp`. In production, this must come from your own bundled copy, not a third-party CDN. CDN imports without Subresource Integrity (SRI) are a supply chain attack vector.
47+
48+
9. **Not handling `onAuthRequired` for update methods.** Tools marked `requires_auth: true` will throw if the user is anonymous and no `onAuthRequired` callback is configured. Always provide an auth callback that triggers Internet Identity login.
49+
50+
10. **Ignoring recursive Candid types.** Types like `type Value = variant { Array: vec Value }` (common in ICRC-3) are handled by the codegen, which emits `{ "description": "Recursive type: Value" }` at the recursion point. Agents should treat these as opaque — they cannot be fully validated by JSON Schema alone.
51+
52+
## Pipeline Overview
53+
54+
```
55+
dfx.json (with webmcp section)
56+
+ backend.did
57+
58+
59+
ic-webmcp-codegen dfx --dfx-json dfx.json --out-dir assets/
60+
61+
├── backend.webmcp.json → /.well-known/webmcp.json
62+
└── backend.webmcp.js → /webmcp.js
63+
64+
65+
Asset canister serves manifest + script with CORS headers
66+
67+
68+
Browser loads webmcp.js → @dfinity/webmcp → navigator.modelContext
69+
70+
71+
AI agent discovers tools → calls execute() → @dfinity/agent → canister
72+
```
73+
74+
## Step 1: Add `webmcp` to dfx.json
75+
76+
```json
77+
{
78+
"canisters": {
79+
"backend": {
80+
"type": "rust",
81+
"candid": "backend.did",
82+
"webmcp": {
83+
"enabled": true,
84+
"name": "My DApp",
85+
"description": "Description for AI agents",
86+
"expose_methods": ["get_items", "add_to_cart", "checkout"],
87+
"require_auth": ["add_to_cart", "checkout"],
88+
"certified_queries": ["get_items"],
89+
"descriptions": {
90+
"get_items": "List available products with prices",
91+
"add_to_cart": "Add a product to the shopping cart",
92+
"checkout": "Complete purchase with current cart contents"
93+
},
94+
"param_descriptions": {
95+
"add_to_cart.product_id": "The unique product identifier",
96+
"add_to_cart.quantity": "Number of items to add (default 1)"
97+
}
98+
}
99+
},
100+
"frontend": {
101+
"type": "assets",
102+
"source": ["assets"],
103+
"dependencies": ["backend"]
104+
}
105+
}
106+
}
107+
```
108+
109+
Config fields:
110+
- `enabled` (bool, default true): whether to generate for this canister
111+
- `name` (string): human-readable name shown to AI agents
112+
- `description` (string): what the canister does, in agent-friendly terms
113+
- `expose_methods` (string[]): which service methods to include. Omit to include all.
114+
- `require_auth` (string[]): methods requiring Internet Identity authentication
115+
- `certified_queries` (string[]): query methods with certified responses
116+
- `descriptions` (object): per-method descriptions (key: method name)
117+
- `param_descriptions` (object): per-parameter descriptions (key: `"method.param"`)
118+
119+
## Step 2: Generate the Manifest
120+
121+
```bash
122+
# Install (from the IC repo)
123+
cargo install --path rs/webmcp/codegen
124+
125+
# Generate from dfx.json (all WebMCP-enabled canisters)
126+
ic-webmcp-codegen dfx --dfx-json dfx.json --out-dir assets/
127+
128+
# Or from a single .did file
129+
ic-webmcp-codegen did \
130+
--did backend.did \
131+
--canister-id ryjl3-tyaaa-aaaaa-aaaba-cai \
132+
--name "My DApp" \
133+
--expose get_items,add_to_cart,checkout \
134+
--require-auth add_to_cart,checkout \
135+
--out-manifest assets/.well-known/webmcp.json \
136+
--out-js assets/webmcp.js
137+
```
138+
139+
## Step 3: Configure CORS Headers
140+
141+
Add to `assets/.ic-assets.json5`:
142+
143+
```json5
144+
[
145+
{
146+
"match": "/.well-known/webmcp.json",
147+
"headers": [
148+
{ "name": "Content-Type", "value": "application/json" },
149+
{ "name": "Access-Control-Allow-Origin", "value": "*" },
150+
{ "name": "Access-Control-Allow-Methods", "value": "GET, OPTIONS" },
151+
{ "name": "Access-Control-Allow-Headers", "value": "Content-Type" },
152+
{ "name": "Cache-Control", "value": "public, max-age=300" }
153+
],
154+
"allow_raw_access": true
155+
},
156+
{
157+
"match": "/webmcp.js",
158+
"headers": [
159+
{ "name": "Content-Type", "value": "application/javascript; charset=utf-8" },
160+
{ "name": "Access-Control-Allow-Origin", "value": "*" },
161+
{ "name": "Cache-Control", "value": "public, max-age=300" }
162+
],
163+
"allow_raw_access": true
164+
}
165+
]
166+
```
167+
168+
Or generate it programmatically:
169+
170+
```rust
171+
use ic_webmcp_asset_middleware::ic_assets_config;
172+
std::fs::write("assets/.ic-assets.json5", ic_assets_config()).unwrap();
173+
```
174+
175+
## Step 4: Browser Integration
176+
177+
**Automatic (zero-code):** Include the generated script in your HTML:
178+
179+
```html
180+
<script type="module" src="/webmcp.js"></script>
181+
```
182+
183+
**Manual (with auth support):**
184+
185+
```typescript
186+
import { ICWebMCP } from '@dfinity/webmcp';
187+
import { AuthClient } from '@dfinity/auth-client';
188+
189+
const authClient = await AuthClient.create();
190+
191+
const webmcp = new ICWebMCP({
192+
manifestUrl: '/.well-known/webmcp.json',
193+
host: 'https://icp-api.io',
194+
onAuthRequired: async () => {
195+
await authClient.login({ identityProvider: 'https://identity.ic0.app' });
196+
return authClient.getIdentity();
197+
},
198+
});
199+
200+
await webmcp.registerAll();
201+
```
202+
203+
## Step 5: Non-Chrome / Server-Side Agent Integration
204+
205+
Use the polyfill to expose tools to any AI framework:
206+
207+
```typescript
208+
import { ICWebMCP, installPolyfill, getOpenAITools, getAnthropicTools, dispatchToolCall } from '@dfinity/webmcp';
209+
210+
// Install shim (no-op if navigator.modelContext exists natively)
211+
installPolyfill();
212+
213+
const webmcp = new ICWebMCP({ manifestUrl: '/.well-known/webmcp.json' });
214+
await webmcp.registerAll();
215+
216+
// OpenAI function calling
217+
const tools = getOpenAITools();
218+
const completion = await openai.chat.completions.create({
219+
model: 'gpt-4o',
220+
tools,
221+
messages,
222+
});
223+
224+
// Anthropic tool use
225+
const anthropicTools = getAnthropicTools();
226+
const message = await anthropic.messages.create({
227+
model: 'claude-sonnet-4-5-20241022',
228+
tools: anthropicTools,
229+
messages,
230+
});
231+
232+
// Dispatch a tool call result back to the IC canister
233+
const result = await dispatchToolCall('get_items', {});
234+
```
235+
236+
## Step 6: Scoped Delegation for Agent Auth
237+
238+
Create short-lived, canister-scoped delegations for AI agents:
239+
240+
```typescript
241+
import { ICWebMCP, createScopedDelegation, getDelegationTargets } from '@dfinity/webmcp';
242+
243+
const webmcp = new ICWebMCP({ identity: iiIdentity });
244+
await webmcp.registerAll();
245+
246+
// 1-hour delegation scoped to this canister only
247+
const agentIdentity = await webmcp.createAgentDelegation({
248+
maxTtlSeconds: 3600,
249+
});
250+
251+
webmcp.setIdentity(agentIdentity);
252+
```
253+
254+
## Candid → JSON Schema Type Mapping
255+
256+
| Candid Type | JSON Schema |
257+
|---|---|
258+
| `nat` | `{ "type": "string", "pattern": "^[0-9]+$" }` |
259+
| `int` | `{ "type": "string", "pattern": "^-?[0-9]+$" }` |
260+
| `nat8/16/32` | `{ "type": "integer", "minimum": 0, "maximum": N }` |
261+
| `nat64` | `{ "type": "string", "pattern": "^[0-9]+$" }` |
262+
| `text` | `{ "type": "string" }` |
263+
| `bool` | `{ "type": "boolean" }` |
264+
| `blob` | `{ "type": "string", "contentEncoding": "base64" }` |
265+
| `principal` | `{ "type": "string" }` |
266+
| `opt T` | `{ "oneOf": [schema(T), { "type": "null" }] }` |
267+
| `vec T` | `{ "type": "array", "items": schema(T) }` |
268+
| `record { a: T }` | `{ "type": "object", "properties": { "a": schema(T) } }` |
269+
| `variant { A; B: T }` | `{ "oneOf": [{ "const": "A" }, { "type": "object", ... }] }` |
270+
271+
## Verification
272+
273+
```bash
274+
# Check manifest is served correctly
275+
curl -s https://<canister-id>.icp0.io/.well-known/webmcp.json | jq .schema_version
276+
# Expected: "1.0"
277+
278+
# Check CORS headers
279+
curl -sI https://<canister-id>.icp0.io/.well-known/webmcp.json | grep -i access-control
280+
# Expected: Access-Control-Allow-Origin: *
281+
282+
# Generate and verify manifest from .did file
283+
ic-webmcp-codegen did --did backend.did --no-js --out-manifest /dev/stdout | jq '.tools | length'
284+
# Expected: number of exposed methods
285+
286+
# Run codegen tests
287+
cargo test -p ic-webmcp-codegen
288+
289+
# Run TypeScript tests
290+
cd packages/ic-webmcp && npm test
291+
```

0 commit comments

Comments
 (0)