|
| 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