Skip to content

feat(webmcp): WebMCP support for the Internet Computer#9708

Open
dfinityianblenke wants to merge 14 commits intomasterfrom
ianblenke/webmcp
Open

feat(webmcp): WebMCP support for the Internet Computer#9708
dfinityianblenke wants to merge 14 commits intomasterfrom
ianblenke/webmcp

Conversation

@dfinityianblenke
Copy link
Copy Markdown
Contributor

@dfinityianblenke dfinityianblenke commented Apr 1, 2026

Summary

This PR adds WebMCP (Web Model Context Protocol) support for the Internet Computer, enabling IC canisters to expose their Candid interfaces as structured tools discoverable by AI agents via navigator.modelContext (Chrome 146+).

WebMCP is a W3C browser standard that lets websites register callable tools for AI agents. The IC is a natural fit: Candid interfaces already define structured schemas, certified queries provide verifiable responses, and Internet Identity enables scoped agent authentication.

Deliverables

  • rs/webmcp/codegen/ (ic-webmcp-codegen) — Rust CLI + library that parses .did files and generates webmcp.json manifests and webmcp.js registration scripts. Supports both single .did files and dfx.json project-level generation.
  • packages/ic-webmcp/ (@dfinity/webmcp) — TypeScript browser library bridging navigator.modelContext to @dfinity/agent. Handles manifest fetching, tool registration, JSON↔Candid encoding, and Internet Identity scoped delegation.
  • rs/webmcp/asset-middleware/ (ic-webmcp-asset-middleware) — Rust helpers for serving the manifest from any IC canister with correct CORS headers, plus .ic-assets.json5 generation for the asset canister.
  • rs/webmcp/demo/ — Runnable e-commerce demo canister (products, per-caller cart, checkout) with a complete dfx.json webmcp config.

How it works

ic-webmcp-codegen dfx --dfx-json dfx.json
  → .webmcp/backend.webmcp.json   (served at /.well-known/webmcp.json)
  → .webmcp/backend.webmcp.js     (served at /webmcp.js)

Browser (Chrome 146+ with WebMCP):
  navigator.modelContext ← @dfinity/webmcp ← IC canister

dfx.json integration

{
  "canisters": {
    "backend": {
      "type": "rust",
      "candid": "backend.did",
      "webmcp": {
        "name": "My DApp",
        "expose_methods": ["get_items", "checkout"],
        "require_auth": ["checkout"],
        "certified_queries": ["get_items"],
        "descriptions": {
          "get_items": "List available items",
          "checkout": "Complete the purchase"
        }
      }
    }
  }
}

Test plan

  • cargo test -p ic-webmcp-codegen — 25 unit tests + 5 integration tests against real .did files (ICP ledger, ICRC-1, NNS governance, SNS swap, CMC, ckBTC minter, management canister)
  • cargo test -p ic-webmcp-asset-middleware — 8 unit tests for HTTP handler and CORS header generation
  • cargo test -p demo-backend — 13 unit tests for the demo e-commerce canister
  • cd packages/ic-webmcp && npm test — 52 Vitest tests (manifest fetching, Candid↔JSON roundtrip for all IDL types, tool registry, ICWebMCP class lifecycle)
  • bazel query //rs/webmcp/... — confirms all 6 Bazel targets visible with correct data dependencies
  • ic-webmcp-codegen did --did rs/ledger_suite/icp/ledger.did --no-js — generates valid manifest
  • ic-webmcp-codegen dfx --dfx-json rs/webmcp/demo/dfx.json --out-dir /tmp/ — generates manifest from demo dfx.json

…st generation

Implements Phase 1 of the WebMCP integration:

a Rust codegen crate that parses .did files and generates WebMCP tool manifests (webmcp.json) and browser registration scripts (webmcp.js) for AI agent discovery.
…t coverage

Add a clap-based CLI for generating webmcp.json/webmcp.js from .did files.

Fix stack overflow on recursive Candid types (e.g., ICRC-1 Value) by tracking visited type names during schema generation.

Add integration tests covering 6 complex .did files (ICRC-1, NNS Governance, SNS Swap, CMC, Management Canister, ckBTC Minter).
Implements Phase 2: browser-side library bridging navigator.modelContext
to Internet Computer canisters via @dfinity/agent.

Modules:
- types.ts: shared interfaces + navigator.modelContext type declarations
- manifest.ts: fetch and validate /.well-known/webmcp.json
- candid-json.ts: JSON <-> Candid encoding/decoding for all IDL types
- agent-bridge.ts: map tool execute() to agent query/call
- tool-registry.ts: register/unregister tools with navigator.modelContext
- auth.ts: scoped Internet Identity delegation for agent auth
- ic-webmcp.ts: ICWebMCP class tying all modules together
- index.ts: public API barrel export
…ckage

52 tests across 4 modules: manifest fetch/validation, candid-json type roundtrips (opt, blob, record, variant, bigint), tool-registry mock of navigator.modelContext, ICWebMCP class lifecycle.

Also fixes opt/variant JSON decoding to be type-aware, correctly unwrapping Candid array representation of opt into null/value.
…e, demo dapp

dfx.json config parsing (rs/webmcp/codegen/src/dfx_config.rs):
- configs_from_dfx_json(): parse webmcp sections from dfx.json into Configs
- load_canister_ids(): load .dfx/<network>/canister_ids.json for principal injection
- 8 unit tests covering parsing, defaults, multi-canister, ID injection

CLI dfx subcommand (main.rs):
- ic-webmcp-codegen dfx --dfx-json dfx.json --out-dir .webmcp/
- Auto-discovers canister_ids.json at conventional dfx paths
- Generates <canister>.webmcp.json + <canister>.webmcp.js per enabled canister

Asset canister middleware (rs/webmcp/asset-middleware/):
- handle_webmcp_request(): HTTP handler producing correct CORS headers
- ic_assets_config(): generate .ic-assets.json5 for the asset canister
- Handles GET, OPTIONS, query strings, case-insensitive methods
- 8 unit tests

Demo dapp (rs/webmcp/demo/):
- backend.did: e-commerce canister (products, cart, checkout)
- dfx.json: full webmcp config with descriptions, auth, certified queries
README.md files (IC convention for tools and npm packages):
- rs/webmcp/README.md: architecture overview, pipeline diagram, all components
- rs/webmcp/codegen/README.md: CLI usage, both subcommands, dfx.json config
  reference, Candid→JSON Schema mapping table, library API examples
- packages/ic-webmcp/README.md: quick start, config reference, auth/delegation,
  full API reference, manifest format, browser requirements

Rust inline docs (IC convention for library crates):
- codegen/src/lib.rs: expanded //! crate docs with two runnable examples
  (from .did and from dfx.json), module listing
- codegen/src/config.rs: /// on every field of Config, doc example on
  from_did_file(), explaining fallbacks and agent interaction
- asset-middleware/src/lib.rs: expanded //! docs with runnable doctest showing
  handle_webmcp_request() usage

All doctests compile and pass (3 in codegen, 2 in asset-middleware).
… crates

rs/webmcp/codegen/BUILD.bazel:
- rust_library :ic_webmcp_codegen (src/ excluding main.rs)
- rust_binary   :ic-webmcp-codegen (src/main.rs)
- rust_test     :unit_tests (inline #[cfg(test)] via crate =)
- rust_test     :integration_tests (tests/integration_test.rs) with data entries for all 7 .did fixtures from across the IC repo

rs/webmcp/asset-middleware/BUILD.bazel:
- rust_library :ic_webmcp_asset_middleware
- rust_test    :unit_tests (inline #[cfg(test)] via crate =)

Also updates integration_test.rs to detect Bazel's TEST_SRCDIR env var and resolve runfiles paths correctly, while keeping the existing CARGO_MANIFEST_DIR fallback for cargo test.

bazel query //rs/webmcp/... confirms all 6 targets visible and all 7 .did data dependencies resolve.
Implements the backend canister for rs/webmcp/demo/ matching the Candid interface in backend.did. Per-caller cart state using Principal as key; products seeded at init time.

src/lib.rs:
- Product, CartItem, Cart, AddToCartResult, CheckoutResult types
- Thread-local State (products, carts BTreeMap, order counter)
- init_products(): seeds 4 products (1 out-of-stock to demonstrate)
- list_products(), get_product(), get_cart(caller), add_to_cart(caller, item),
  remove_from_cart(caller, id), checkout(caller)
- Functions take explicit Principal to avoid ic0 host dependency in tests
- 13 unit tests covering all methods, error paths, and per-caller isolation

src/main.rs:
- #[ic_cdk::init/query/update] entry points passing msg_caller() through

Also:
- dfx.json updated to type: rust with package: demo-backend
- assets/index.html minimal frontend loading webmcp.js
- README.md with deploy and test instructions
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end WebMCP (Web Model Context Protocol) support for the Internet Computer: Rust codegen to turn Candid into WebMCP manifests + registration scripts, a browser TypeScript bridge to navigator.modelContext, an asset-serving helper for correct CORS/headers, and a runnable demo canister.

Changes:

  • Introduces ic-webmcp-codegen (Rust) to parse .did / dfx.json and generate webmcp.json + webmcp.js.
  • Adds ic-webmcp-asset-middleware (Rust) helpers to serve WebMCP endpoints with required headers / .ic-assets.json5 generation.
  • Adds @dfinity/webmcp (TypeScript) to fetch manifests and register/call canister tools via navigator.modelContext, plus a demo canister and docs.

Reviewed changes

Copilot reviewed 44 out of 46 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
rs/webmcp/README.md Top-level WebMCP documentation and component overview.
rs/webmcp/IMPLEMENTATION_PLAN.md Detailed architecture/plan and intended deliverables.
rs/webmcp/demo/src/main.rs Demo canister entry points wiring msg_caller() to backend logic.
rs/webmcp/demo/src/lib.rs Demo e-commerce business logic + unit tests.
rs/webmcp/demo/README.md Demo usage instructions and local run steps.
rs/webmcp/demo/dfx.json Demo dfx.json including webmcp config block.
rs/webmcp/demo/Cargo.toml Demo crate configuration (bin + lib).
rs/webmcp/demo/backend.did Demo Candid interface used by codegen.
rs/webmcp/demo/assets/index.html Minimal demo frontend that loads /webmcp.js.
rs/webmcp/codegen/tests/integration_test.rs Integration tests against real .did fixtures (ledger/NNS/etc).
rs/webmcp/codegen/src/schema_mapper.rs Candid → JSON Schema mapping implementation + unit tests.
rs/webmcp/codegen/src/manifest.rs Manifest generation (webmcp.json) from parsed interfaces + config.
rs/webmcp/codegen/src/main.rs CLI (did / dfx) to write manifests and JS scripts.
rs/webmcp/codegen/src/lib.rs Library exports and crate-level documentation.
rs/webmcp/codegen/src/js_emitter.rs Generator for webmcp.js registration script.
rs/webmcp/codegen/src/did_parser.rs .did parsing into method definitions.
rs/webmcp/codegen/src/dfx_config.rs dfx.json parsing into per-canister codegen Config.
rs/webmcp/codegen/src/config.rs Config struct for controlling generation.
rs/webmcp/codegen/README.md Codegen usage docs and config reference.
rs/webmcp/codegen/Cargo.toml ic-webmcp-codegen crate definition/deps.
rs/webmcp/codegen/BUILD.bazel Bazel targets for codegen lib/bin/tests.
rs/webmcp/asset-middleware/src/lib.rs HTTP handler + .ic-assets.json5 generator + tests.
rs/webmcp/asset-middleware/Cargo.toml ic-webmcp-asset-middleware crate definition/deps.
rs/webmcp/asset-middleware/BUILD.bazel Bazel targets for asset middleware.
packages/ic-webmcp/vitest.config.ts Vitest configuration for the TS package.
packages/ic-webmcp/tsconfig.test.json TS config for tests/typechecking.
packages/ic-webmcp/tsconfig.json TS build configuration for the library.
packages/ic-webmcp/src/types.ts Shared types for manifest/tools + navigator.modelContext typings.
packages/ic-webmcp/src/tool-registry.ts Tool registration/unregistration against navigator.modelContext.
packages/ic-webmcp/src/tests/tool-registry.test.ts Tests for tool registry behaviors including auth gating.
packages/ic-webmcp/src/tests/manifest.test.ts Tests for manifest fetch/validation.
packages/ic-webmcp/src/tests/ic-webmcp.test.ts Tests for ICWebMCP lifecycle (register/unregister/etc).
packages/ic-webmcp/src/tests/candid-json.test.ts Tests for JSON↔Candid conversions.
packages/ic-webmcp/src/manifest.ts Manifest fetching and basic validation.
packages/ic-webmcp/src/index.ts Public exports surface.
packages/ic-webmcp/src/ic-webmcp.ts High-level ICWebMCP class orchestrating manifest/agent/tool registration.
packages/ic-webmcp/src/candid-json.ts JSON params ↔ Candid binary encode/decode utilities.
packages/ic-webmcp/src/auth.ts Scoped delegation helpers (Internet Identity delegation chain).
packages/ic-webmcp/src/agent-bridge.ts Tool execution bridge (Actor/raw agent call paths).
packages/ic-webmcp/README.md Package docs and usage guidance.
packages/ic-webmcp/package.json NPM package definition, scripts, deps/peers.
packages/ic-webmcp/package-lock.json Lockfile for the TS package dependencies.
packages/ic-webmcp/.gitignore Ignores build outputs and node_modules.
Cargo.toml Adds new Rust crates to workspace members.
Cargo.lock Adds lock entries for new Rust crates.
.claude/instructions/webmcp.md Branch-specific implementation context notes.
Files not reviewed (1)
  • packages/ic-webmcp/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@dfinityianblenke dfinityianblenke force-pushed the ianblenke/webmcp branch 4 times, most recently from 71d5d91 to c443134 Compare April 1, 2026 18:47
…ddress Copilot PR review feedback, align with IC repo conventions, fix four security issues identified in branch audit, certified query verification and navigator.modelContext polyfill, certified query verification and navigator.modelContext polyfill, address CI failures

BUILD.bazel (rs/webmcp/demo/):
- rust_library :demo_backend_lib (src/lib.rs — testable without IC host)
- rust_canister :demo_backend_canister (src/main.rs, wasm32, with .did)
- rust_test :unit_tests (inline #[cfg(test)])
- bazel query //rs/webmcp/demo/... confirms wasm artifact + didfile targets

Stable memory (pre_upgrade / post_upgrade):
- StableState { carts, next_order_id } serialised via Candid to stable memory
- pre_upgrade: ic_cdk::storage::stable_save((stable,))
- post_upgrade: ic_cdk::storage::stable_restore() then restore_stable_state()
- Products are re-seeded from constants; only mutable state persists
- All 13 unit tests still pass, clippy clean

manifest.rs — positional args now have required + additionalProperties: false
  Multi-arg methods generated arg0/arg1/... but omitted required[], making
  agents think all args were optional. Now marks non-opt args as required
  and adds additionalProperties: false.

js_emitter.rs — fix auto-init for ES module script tags
  document.currentScript is null for <script type="module">, so the
  previous guard never fired. Replaced with unconditional top-level await
  which runs when the module is first evaluated.

agent-bridge.ts — fix executeRawCall ignoring params
  Previously encoded IDL.encode([], []) regardless of params, silently
  sending wrong Candid for any tool with inputs. Now throws a clear error
  requiring an idlFactory for methods that take arguments.

agent-bridge.ts — fix buildActorArgs passing [{}] for zero-arg methods
  Empty params object {} was treated as a single record argument, causing
  Actor calls to fail with an argument-count mismatch. Now returns [] for
  empty params so zero-arg methods call correctly.

agent-bridge.ts — accurately report certified: false
  The certified flag was described but never backed by BLS verification.
  Now explicitly returns certified: false with a TODO comment noting where
  readState()-based verification should be implemented.

demo/dfx.json — add remove_from_cart to expose_methods
  remove_from_cart was in require_auth, descriptions, and param_descriptions
  but absent from expose_methods, so codegen silently dropped it.

Cargo.toml (all three crates):
- version/authors/edition/description/documentation now use workspace
  inheritance (version.workspace = true etc.) matching the ic-crypto-sha2
  and icrc1-ledger patterns
- serde_json = "1" and anyhow = "1" changed to { workspace = true }
- clap, pretty_assertions, tempfile similarly moved to workspace = true

lib.rs (codegen + asset-middleware):
- Added #![forbid(unsafe_code)] and #![deny(clippy::unwrap_used)] matching
  the lint attributes used in rs/crypto/* and other IC library crates

BUILD.bazel (all three crates):
- Added ALIASES = {} and aliases = ALIASES, to all rust_library /
  rust_binary / rust_test / rust_canister rules (matches rs/crypto/sha2,
  rs/nns/cmc and other crates)
- Added compile_data = [":backend.did"] and proc_macro_deps = [] to
  the demo rust_canister target

Cargo Lint Linux — clippy::option_as_ref_deref:
  dfx_config.rs:222: use .as_deref() instead of
  .as_ref().map(|v| v.as_slice())

DFINITY-capitalization-check:
  demo/src/lib.rs: 'Dfinity Sticker Pack' -> 'DFINITY Sticker Pack'

Bazel visibility error:
  //rs/types/management_canister_types:tests/ic.did is not publicly
  exported; removed from integration_tests data list. The test already
  has a graceful skip for missing fixtures.

auth.ts — empty delegation targets (High)
  createScopedDelegation now throws if targets is empty. An empty
  targets list produces an unrestricted IC delegation valid for ALL
  canisters, not just the intended one. getDelegationTargets always
  populates targets from the canister ID + manifest, so legitimate
  callers are unaffected.

candid-json.ts — prototype pollution (Medium)
  toJsonValue (untyped fallback) now skips __proto__, constructor,
  and prototype keys when iterating decoded Candid records to prevent
  Object.prototype mutation.

candid-json.ts — integer range validation (Medium)
  Fixed-width integer types (Nat8/16/32/64, Int8/16/32/64) now throw
  RangeError when values fall outside the valid range. Previously,
  Nat32(4294967296) silently accepted out-of-range inputs.

js_emitter.rs — esm.sh CDN supply chain risk (High)
  Replaced hardcoded 'https://esm.sh/@dfinity/webmcp' with a local
  package import '@dfinity/webmcp' and added a prominent security
  warning in the generated script explaining the CDN risk and
  requiring developers to use their own bundled copy in production.

Also adds security.test.ts: 14 tests covering all four fixes.

Certified query verification (certified-response.ts, agent-bridge.ts):
- wrapCertifiedResponse(): extracts NodeSignature metadata from query
  responses. @dfinity/agent verifies signatures by default
  (verifyQuerySignatures: true), so if execution reaches this function
  the response is already cryptographically verified by the subnet nodes.
- readCertifiedData(): full BLS certificate path via agent.readState() +
  Certificate.create() for canisters using ic0.certified_data_set().
- executeToolCall now returns CertifiedQueryResult (certified: true +
  signatures[]) for tools marked certified: true instead of certified: false.
- executeCertifiedQuery(): raw agent.query() path for certified tools to
  preserve the signatures array that Actor calls would discard.

navigator.modelContext polyfill (polyfill.ts):
- installPolyfill(): installs in-memory shim when native API is absent
  (non-Chrome browsers, Node.js, test runners). No-op if already present.
- clearRegistry(): clears registered tools (useful in tests).
- getOpenAITools(): exports tools in OpenAI function calling format.
- getAnthropicTools(): exports tools in Anthropic tool use format.
- getLangChainTools(): exports tools as LangChain DynamicStructuredTool defs
  with func() that JSON-stringifies results.
- dispatchToolCall(): routes a named tool call from any framework back to
  the registered execute() callback.

Certified query verification (certified-response.ts, agent-bridge.ts):
- wrapCertifiedResponse(): extracts NodeSignature metadata from query
  responses. @dfinity/agent verifies signatures by default
  (verifyQuerySignatures: true), so if execution reaches this function
  the response is already cryptographically verified by the subnet nodes.
- readCertifiedData(): full BLS certificate path via agent.readState() +
  Certificate.create() for canisters using ic0.certified_data_set().
- executeToolCall now returns CertifiedQueryResult (certified: true +
  signatures[]) for tools marked certified: true instead of certified: false.
- executeCertifiedQuery(): raw agent.query() path for certified tools to
  preserve the signatures array that Actor calls would discard.

navigator.modelContext polyfill (polyfill.ts):
- installPolyfill(): installs in-memory shim when native API is absent
  (non-Chrome browsers, Node.js, test runners). No-op if already present.
- clearRegistry(): clears registered tools (useful in tests).
- getOpenAITools(): exports tools in OpenAI function calling format.
- getAnthropicTools(): exports tools in Anthropic tool use format.
- getLangChainTools(): exports tools as LangChain DynamicStructuredTool defs
  with func() that JSON-stringifies results.
- dispatchToolCall(): routes a named tool call from any framework back to
  the registered execute() callback.

Tests: 78 passing (was 66). New: polyfill.test.ts (12 tests).
The integration test calls serde_json::to_string_pretty() directly.
Cargo resolves it transitively, but Bazel requires explicit deps.
Fixes Bazel Test arm64-darwin/linux compilation failure.
@dfinityianblenke dfinityianblenke marked this pull request as ready for review April 1, 2026 19:49
@dfinityianblenke dfinityianblenke requested review from a team as code owners April 1, 2026 19:49
dfinityianblenke added a commit to dfinity/icskills that referenced this pull request Apr 2, 2026
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
Update user-facing READMEs to reference the current IC tooling:
- demo/README.md: dfx start/deploy → icp start/deploy
- packages/ic-webmcp/README.md: @dfinity/agent → @icp-sdk/core,
  @dfinity/auth-client → @icp-sdk/auth
- rs/webmcp/README.md: @dfinity/agent → @icp-sdk/core/agent,
  fix stale esm.sh import and document.currentScript in example

Does NOT rename the codegen dfx subcommand or dfx_config.rs module —
those describe the config file format the tool actually reads today.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…/core

All TypeScript imports migrated to the current IC SDK packages:
- @dfinity/agent → @icp-sdk/core/agent
- @dfinity/candid → @icp-sdk/core/candid
- @dfinity/principal → @icp-sdk/core/principal
- @dfinity/identity → @icp-sdk/core/identity

API changes in @icp-sdk/core v5 addressed:
- IDL.encode() returns Uint8Array (was ArrayBuffer)
- IDL.decode() takes Uint8Array (was ArrayBuffer)
- Certificate.lookup → Certificate.lookup_path
- Certificate.create: canisterId → principal: { canisterId }
- lookupResultToBuffer returns Uint8Array | undefined
- readState paths take Uint8Array[][] (was ArrayBuffer[][])

package.json:
- dependencies: @icp-sdk/core ^5.2.1, @icp-sdk/auth ^5.0.0
- removed: @dfinity/agent, @dfinity/candid, @dfinity/identity, @dfinity/principal
- peerDependencies: @icp-sdk/core >= 5.0.0

All 78 tests pass. Changes strictly within packages/ic-webmcp/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants