From 7154e1f5a5e3007a7d0ef1255f260216ede5867b Mon Sep 17 00:00:00 2001 From: Ian Blenke Date: Thu, 2 Apr 2026 10:24:25 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20WebMCP=20skill=20=E2=80=94=20?= =?UTF-8?q?AI=20agent=20tool=20protocol=20for=20IC=20canisters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 https://github.com/dfinity/ic/pull/9708 --- evaluations/webmcp.json | 69 ++++++++++ skills/webmcp/SKILL.md | 291 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 evaluations/webmcp.json create mode 100644 skills/webmcp/SKILL.md diff --git a/evaluations/webmcp.json b/evaluations/webmcp.json new file mode 100644 index 0000000..e178eea --- /dev/null +++ b/evaluations/webmcp.json @@ -0,0 +1,69 @@ +{ + "skill": "webmcp", + "description": "Evaluates agent ability to expose IC canister methods as AI-accessible tools via WebMCP", + "output_evals": [ + { + "name": "Add WebMCP to an existing canister", + "prompt": "I have a Rust canister with a .did file. How do I make it discoverable by AI agents?", + "expected_behaviors": [ + "Adds a webmcp section to dfx.json with name, description, expose_methods", + "Runs ic-webmcp-codegen to generate webmcp.json and webmcp.js", + "Configures CORS headers via .ic-assets.json5 for /.well-known/webmcp.json", + "Includes the tag or manual ICWebMCP initialization", + "Does NOT reference esm.sh or any external CDN for @dfinity/webmcp" + ] + }, + { + "name": "Add auth-required tools", + "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?", + "expected_behaviors": [ + "Adds the method to require_auth in dfx.json webmcp config", + "Configures onAuthRequired callback in ICWebMCP constructor", + "Uses AuthClient or Internet Identity for the login flow", + "Creates a scoped delegation via createAgentDelegation with non-empty targets", + "Does NOT create an unrestricted delegation with empty targets array" + ] + }, + { + "name": "Use polyfill for non-Chrome environments", + "prompt": "I want my IC canister tools to work with Claude and OpenAI, not just Chrome. How?", + "expected_behaviors": [ + "Calls installPolyfill() before ICWebMCP.registerAll()", + "Uses getOpenAITools() or getAnthropicTools() to export in framework-specific format", + "Uses dispatchToolCall() to route tool call responses back to the canister", + "Does NOT assume navigator.modelContext is always available natively" + ] + }, + { + "name": "Generate manifest from .did file", + "prompt": "Generate a WebMCP manifest from my ledger.did file for the ICP ledger canister", + "expected_behaviors": [ + "Uses ic-webmcp-codegen did subcommand with --did flag", + "Sets --canister-id to the correct ICP ledger principal", + "Uses --expose to select specific methods", + "Uses --require-auth for methods that modify state", + "Generates valid JSON with schema_version 1.0" + ] + } + ], + "trigger_evals": { + "should_trigger": [ + "How do I expose my canister to AI agents?", + "WebMCP integration for Internet Computer", + "Make my dapp work with navigator.modelContext", + "Generate a tool manifest from a .did file", + "How do AI agents call IC canisters?", + "Add OpenAI function calling to my IC canister", + "Anthropic tool use with Internet Computer", + "LangChain tools for IC canisters" + ], + "should_not_trigger": [ + "How do I deploy a canister?", + "Write a Motoko counter canister", + "How does Internet Identity work?", + "What is certified data?", + "How do I use ckBTC?", + "Set up a custom domain for my canister" + ] + } +} diff --git a/skills/webmcp/SKILL.md b/skills/webmcp/SKILL.md new file mode 100644 index 0000000..d873132 --- /dev/null +++ b/skills/webmcp/SKILL.md @@ -0,0 +1,291 @@ +--- +name: webmcp +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." +license: Apache-2.0 +compatibility: "icp-cli >= 0.2.2" +metadata: + title: WebMCP — AI Agent Tool Protocol + category: Integration +--- + +# WebMCP for the Internet Computer + +## What This Is + +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. + +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). + +## Prerequisites + +- Rust: `ic-webmcp-codegen` crate (CLI tool) for manifest generation +- TypeScript: `@dfinity/webmcp` package for browser/agent integration +- `@dfinity/agent` >= 1.0.0, `@dfinity/candid` >= 1.0.0, `@dfinity/principal` >= 1.0.0 +- Chrome 146+ for native `navigator.modelContext` (polyfill available for other environments) + +## Canister IDs + +No fixed canister IDs. WebMCP is a protocol layer applied to YOUR canister. The manifest embeds your canister's principal ID. + +## Mistakes That Break Your Build + +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. + +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. + +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`. + +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. + +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. + +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. + +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()`. + +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. + +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. + +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. + +## Pipeline Overview + +``` +dfx.json (with webmcp section) + + backend.did + │ + ▼ +ic-webmcp-codegen dfx --dfx-json dfx.json --out-dir assets/ + │ + ├── backend.webmcp.json → /.well-known/webmcp.json + └── backend.webmcp.js → /webmcp.js + │ + ▼ +Asset canister serves manifest + script with CORS headers + │ + ▼ +Browser loads webmcp.js → @dfinity/webmcp → navigator.modelContext + │ + ▼ +AI agent discovers tools → calls execute() → @dfinity/agent → canister +``` + +## Step 1: Add `webmcp` to dfx.json + +```json +{ + "canisters": { + "backend": { + "type": "rust", + "candid": "backend.did", + "webmcp": { + "enabled": true, + "name": "My DApp", + "description": "Description for AI agents", + "expose_methods": ["get_items", "add_to_cart", "checkout"], + "require_auth": ["add_to_cart", "checkout"], + "certified_queries": ["get_items"], + "descriptions": { + "get_items": "List available products with prices", + "add_to_cart": "Add a product to the shopping cart", + "checkout": "Complete purchase with current cart contents" + }, + "param_descriptions": { + "add_to_cart.product_id": "The unique product identifier", + "add_to_cart.quantity": "Number of items to add (default 1)" + } + } + }, + "frontend": { + "type": "assets", + "source": ["assets"], + "dependencies": ["backend"] + } + } +} +``` + +Config fields: +- `enabled` (bool, default true): whether to generate for this canister +- `name` (string): human-readable name shown to AI agents +- `description` (string): what the canister does, in agent-friendly terms +- `expose_methods` (string[]): which service methods to include. Omit to include all. +- `require_auth` (string[]): methods requiring Internet Identity authentication +- `certified_queries` (string[]): query methods with certified responses +- `descriptions` (object): per-method descriptions (key: method name) +- `param_descriptions` (object): per-parameter descriptions (key: `"method.param"`) + +## Step 2: Generate the Manifest + +```bash +# Install (from the IC repo) +cargo install --path rs/webmcp/codegen + +# Generate from dfx.json (all WebMCP-enabled canisters) +ic-webmcp-codegen dfx --dfx-json dfx.json --out-dir assets/ + +# Or from a single .did file +ic-webmcp-codegen did \ + --did backend.did \ + --canister-id ryjl3-tyaaa-aaaaa-aaaba-cai \ + --name "My DApp" \ + --expose get_items,add_to_cart,checkout \ + --require-auth add_to_cart,checkout \ + --out-manifest assets/.well-known/webmcp.json \ + --out-js assets/webmcp.js +``` + +## Step 3: Configure CORS Headers + +Add to `assets/.ic-assets.json5`: + +```json5 +[ + { + "match": "/.well-known/webmcp.json", + "headers": [ + { "name": "Content-Type", "value": "application/json" }, + { "name": "Access-Control-Allow-Origin", "value": "*" }, + { "name": "Access-Control-Allow-Methods", "value": "GET, OPTIONS" }, + { "name": "Access-Control-Allow-Headers", "value": "Content-Type" }, + { "name": "Cache-Control", "value": "public, max-age=300" } + ], + "allow_raw_access": true + }, + { + "match": "/webmcp.js", + "headers": [ + { "name": "Content-Type", "value": "application/javascript; charset=utf-8" }, + { "name": "Access-Control-Allow-Origin", "value": "*" }, + { "name": "Cache-Control", "value": "public, max-age=300" } + ], + "allow_raw_access": true + } +] +``` + +Or generate it programmatically: + +```rust +use ic_webmcp_asset_middleware::ic_assets_config; +std::fs::write("assets/.ic-assets.json5", ic_assets_config()).unwrap(); +``` + +## Step 4: Browser Integration + +**Automatic (zero-code):** Include the generated script in your HTML: + +```html + +``` + +**Manual (with auth support):** + +```typescript +import { ICWebMCP } from '@dfinity/webmcp'; +import { AuthClient } from '@dfinity/auth-client'; + +const authClient = await AuthClient.create(); + +const webmcp = new ICWebMCP({ + manifestUrl: '/.well-known/webmcp.json', + host: 'https://icp-api.io', + onAuthRequired: async () => { + await authClient.login({ identityProvider: 'https://identity.ic0.app' }); + return authClient.getIdentity(); + }, +}); + +await webmcp.registerAll(); +``` + +## Step 5: Non-Chrome / Server-Side Agent Integration + +Use the polyfill to expose tools to any AI framework: + +```typescript +import { ICWebMCP, installPolyfill, getOpenAITools, getAnthropicTools, dispatchToolCall } from '@dfinity/webmcp'; + +// Install shim (no-op if navigator.modelContext exists natively) +installPolyfill(); + +const webmcp = new ICWebMCP({ manifestUrl: '/.well-known/webmcp.json' }); +await webmcp.registerAll(); + +// OpenAI function calling +const tools = getOpenAITools(); +const completion = await openai.chat.completions.create({ + model: 'gpt-4o', + tools, + messages, +}); + +// Anthropic tool use +const anthropicTools = getAnthropicTools(); +const message = await anthropic.messages.create({ + model: 'claude-sonnet-4-5-20241022', + tools: anthropicTools, + messages, +}); + +// Dispatch a tool call result back to the IC canister +const result = await dispatchToolCall('get_items', {}); +``` + +## Step 6: Scoped Delegation for Agent Auth + +Create short-lived, canister-scoped delegations for AI agents: + +```typescript +import { ICWebMCP, createScopedDelegation, getDelegationTargets } from '@dfinity/webmcp'; + +const webmcp = new ICWebMCP({ identity: iiIdentity }); +await webmcp.registerAll(); + +// 1-hour delegation scoped to this canister only +const agentIdentity = await webmcp.createAgentDelegation({ + maxTtlSeconds: 3600, +}); + +webmcp.setIdentity(agentIdentity); +``` + +## Candid → JSON Schema Type Mapping + +| Candid Type | JSON Schema | +|---|---| +| `nat` | `{ "type": "string", "pattern": "^[0-9]+$" }` | +| `int` | `{ "type": "string", "pattern": "^-?[0-9]+$" }` | +| `nat8/16/32` | `{ "type": "integer", "minimum": 0, "maximum": N }` | +| `nat64` | `{ "type": "string", "pattern": "^[0-9]+$" }` | +| `text` | `{ "type": "string" }` | +| `bool` | `{ "type": "boolean" }` | +| `blob` | `{ "type": "string", "contentEncoding": "base64" }` | +| `principal` | `{ "type": "string" }` | +| `opt T` | `{ "oneOf": [schema(T), { "type": "null" }] }` | +| `vec T` | `{ "type": "array", "items": schema(T) }` | +| `record { a: T }` | `{ "type": "object", "properties": { "a": schema(T) } }` | +| `variant { A; B: T }` | `{ "oneOf": [{ "const": "A" }, { "type": "object", ... }] }` | + +## Verification + +```bash +# Check manifest is served correctly +curl -s https://.icp0.io/.well-known/webmcp.json | jq .schema_version +# Expected: "1.0" + +# Check CORS headers +curl -sI https://.icp0.io/.well-known/webmcp.json | grep -i access-control +# Expected: Access-Control-Allow-Origin: * + +# Generate and verify manifest from .did file +ic-webmcp-codegen did --did backend.did --no-js --out-manifest /dev/stdout | jq '.tools | length' +# Expected: number of exposed methods + +# Run codegen tests +cargo test -p ic-webmcp-codegen + +# Run TypeScript tests +cd packages/ic-webmcp && npm test +``` From b314eba5442255fb5667a35255422e2930d5fb3d Mon Sep 17 00:00:00 2001 From: Ian Blenke Date: Thu, 2 Apr 2026 12:16:10 -0400 Subject: [PATCH 2/2] refactor: update WebMCP skill to prefer icp-cli and @icp-sdk/core Address team feedback (Marco Walz): - Prerequisites: @icp-sdk/core >= 5.0.0 instead of @dfinity/agent/candid/principal - Auth: @icp-sdk/auth instead of @dfinity/auth-client - Added pitfall #11: using deprecated @dfinity/* packages - Step 1: notes icp.yaml compatibility is planned, dfx.json used for now - Step 4: AuthClient import from @icp-sdk/auth - Pipeline diagram: references icp.yaml alongside dfx.json - Verification: uses icp deploy - Evaluation: added expected behavior for @icp-sdk/auth Co-Authored-By: Claude Opus 4.6 (1M context) --- evaluations/webmcp.json | 3 ++- skills/webmcp/SKILL.md | 29 ++++++++++++++++------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/evaluations/webmcp.json b/evaluations/webmcp.json index e178eea..60aa652 100644 --- a/evaluations/webmcp.json +++ b/evaluations/webmcp.json @@ -19,7 +19,8 @@ "expected_behaviors": [ "Adds the method to require_auth in dfx.json webmcp config", "Configures onAuthRequired callback in ICWebMCP constructor", - "Uses AuthClient or Internet Identity for the login flow", + "Uses @icp-sdk/auth AuthClient or Internet Identity for the login flow", + "Does NOT import from deprecated @dfinity/auth-client", "Creates a scoped delegation via createAgentDelegation with non-empty targets", "Does NOT create an unrestricted delegation with empty targets array" ] diff --git a/skills/webmcp/SKILL.md b/skills/webmcp/SKILL.md index d873132..166139e 100644 --- a/skills/webmcp/SKILL.md +++ b/skills/webmcp/SKILL.md @@ -14,13 +14,14 @@ metadata: 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. -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). +The IC WebMCP stack generates tool manifests from `.did` files, serves them from asset canisters, and bridges `navigator.modelContext` to canister calls via `@icp-sdk/core`. A polyfill extends this to non-Chrome browsers and server-side agent frameworks (Claude, OpenAI, LangChain). ## Prerequisites - Rust: `ic-webmcp-codegen` crate (CLI tool) for manifest generation - TypeScript: `@dfinity/webmcp` package for browser/agent integration -- `@dfinity/agent` >= 1.0.0, `@dfinity/candid` >= 1.0.0, `@dfinity/principal` >= 1.0.0 +- `@icp-sdk/core` >= 5.0.0 (provides agent, candid, identity, principal modules) +- `@icp-sdk/auth` for Internet Identity login flows - Chrome 146+ for native `navigator.modelContext` (polyfill available for other environments) ## Canister IDs @@ -37,11 +38,11 @@ No fixed canister IDs. WebMCP is a protocol layer applied to YOUR canister. The 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. -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. +5. **Omitting methods from `expose_methods` but listing them in other config sections.** If `expose_methods` is set, only those methods appear in the manifest. Methods listed in `require_auth`, `certified_queries`, or `descriptions` but NOT in `expose_methods` are silently dropped. 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. -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()`. +7. **Assuming `certified: true` means full BLS verification.** The `certified` flag in the manifest indicates the query supports certified responses. `@icp-sdk/core/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()`. 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. @@ -49,10 +50,12 @@ No fixed canister IDs. WebMCP is a protocol layer applied to YOUR canister. The 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. +11. **Using deprecated `@dfinity/*` packages.** The `@dfinity/agent`, `@dfinity/candid`, `@dfinity/identity`, and `@dfinity/principal` packages are deprecated. Use `@icp-sdk/core` with subpath imports instead (e.g., `@icp-sdk/core/agent`). Use `@icp-sdk/auth` instead of `@dfinity/auth-client`. + ## Pipeline Overview ``` -dfx.json (with webmcp section) +icp.yaml or dfx.json (with webmcp section) + backend.did │ ▼ @@ -68,10 +71,12 @@ Asset canister serves manifest + script with CORS headers Browser loads webmcp.js → @dfinity/webmcp → navigator.modelContext │ ▼ -AI agent discovers tools → calls execute() → @dfinity/agent → canister +AI agent discovers tools → calls execute() → @icp-sdk/core/agent → canister ``` -## Step 1: Add `webmcp` to dfx.json +## Step 1: Configure WebMCP + +The codegen currently reads a `webmcp` section from `dfx.json`. If your project uses `icp.yaml`, add a `dfx.json` alongside it for the WebMCP config (native `icp.yaml` support is planned). ```json { @@ -96,11 +101,6 @@ AI agent discovers tools → calls execute() → @dfinity/agent → canister "add_to_cart.quantity": "Number of items to add (default 1)" } } - }, - "frontend": { - "type": "assets", - "source": ["assets"], - "dependencies": ["backend"] } } } @@ -184,7 +184,7 @@ std::fs::write("assets/.ic-assets.json5", ic_assets_config()).unwrap(); ```typescript import { ICWebMCP } from '@dfinity/webmcp'; -import { AuthClient } from '@dfinity/auth-client'; +import { AuthClient } from '@icp-sdk/auth'; const authClient = await AuthClient.create(); @@ -271,6 +271,9 @@ webmcp.setIdentity(agentIdentity); ## Verification ```bash +# Deploy with icp-cli +icp deploy + # Check manifest is served correctly curl -s https://.icp0.io/.well-known/webmcp.json | jq .schema_version # Expected: "1.0"