diff --git a/evaluations/webmcp.json b/evaluations/webmcp.json new file mode 100644 index 0000000..60aa652 --- /dev/null +++ b/evaluations/webmcp.json @@ -0,0 +1,70 @@ +{ + "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 @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" + ] + }, + { + "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..166139e --- /dev/null +++ b/skills/webmcp/SKILL.md @@ -0,0 +1,294 @@ +--- +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 `@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 +- `@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 + +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, 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. `@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. + +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. + +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 + +``` +icp.yaml or 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() → @icp-sdk/core/agent → canister +``` + +## 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 +{ + "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)" + } + } + } + } +} +``` + +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 '@icp-sdk/auth'; + +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 +# 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" + +# 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 +```