From d0f0108d9825ada6f5118460b61d9e33f7afc803 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 15:06:23 +0000 Subject: [PATCH 01/14] Defer IcProtocolTools from the served surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default IcTools composition now serves only IcCanisterTools — the 10 app/canister tools. The icp_-prefixed protocol/meta group (dashboard lookups, the skills tools, the OQL guide tool, and canister creation/management) is deferred: every reference in the served text, the README, the landing page, and the submission docs now says we anticipate this will come in a future version. The type, its 16 tools, and their tests stay in the library — IcProtocolTools::new and its pub tool_router let an embedder serve the group today, and re-enabling it in IcTools is one line. What still works without the tools: the OQL dialect guide and the official IC skills remain served as MCP resources (oql://usage, skill://), and served texts point there; name-guess guidance routes through open_app's known-app resolution; the compliance refusal for cycles-ledger creation spends points directly at the user-run icp CLI (install pointer included). The financial posture gets simpler still: no funding or management tools are served at all, and the canister_update_call guard stays. Tests: the served surface is pinned at 10 tools with no icp_ prefix (instructions say the deferred group is coming); the deferred router is pinned at 16 with all contracts (annotations, output schemas, instructions-only descriptions) still guarded; imcp2-local's live MCP round-trip now sees the 12-tool merged surface. Both routers are generated pub (vis = "pub") so the deferred half stays live code for the library. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- README.md | 133 +++++++------- crates/imcp2-core/src/calls.rs | 14 +- crates/imcp2-core/src/compliance.rs | 20 +-- crates/imcp2-core/src/tools.rs | 231 ++++++++++++++----------- crates/imcp2-local/src/server.rs | 10 +- docs/anthropic-directory-submission.md | 51 +++--- docs/openai-directory-submission.md | 17 +- src/assets/index.html | 2 +- 8 files changed, 245 insertions(+), 233 deletions(-) diff --git a/README.md b/README.md index 935a196..24c67eb 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,15 @@ encoding/decoding and signing against the IC via building, and operating canisters — it is not a wallet or trading tool, and financial operations (token transfers, spending approvals, payments, trades) are unsupported by policy. The standardized ledger surface is refused -mechanically: `canister_update_call` rejects the ICRC-1/ICRC-2 (and related -ledger-standard) transfer/approval methods on every canister, and -`icp_top_up_canister` and `icp_create_canister` return `icp` CLI instructions -for the user to run themselves instead of executing anything — those tools -create nothing and move no funds. For financial operations, users act -themselves in a wallet or frontend they control (e.g. -[oisy.com](https://oisy.com)), in their own browser. +mechanically (`canister_update_call` rejects the ICRC-1/ICRC-2 and related +ledger-standard transfer/approval methods on every canister), and this version +ships no funding or management tools at all: creating, funding, and topping up +canisters is done by the user with the +[`icp` CLI](https://github.com/dfinity/icp-cli) in their own terminal +(instructions-only helper tools are deferred — we anticipate these will come +in a future version). For financial operations, users act themselves in a +wallet or frontend they control (e.g. [oisy.com](https://oisy.com)), in their +own browser. ## Use as a library @@ -147,27 +149,26 @@ results). |------|------|---------| | `open_app` | `app` (name **or** URL) | **One-call entry point** when a user names/links an app: resolves the Internet Identity `derivation_origin` *and* discovers the canisters behind it, together. A name or bare host is matched to the known-app registry first (so a wrong-TLD guess repairs to the canonical URL); an explicit `https://` URL is resolved as given. An unknown bare name, or a URL with no IC evidence, is *refused* (never guessed). Also probes the app's own canisters and reports per-canister `oql`/`api_doc_available` capability flags plus a caller-gated data-access note (which canister holds the app data, and the origin to read it as the user). Wraps `resolve_app` + `discover_app_canisters`; no auth | | `discover_app_canisters` | `domain` | Canister ids behind a web domain — app-declared App Connect metadata first (`/ai-connect.html`'s `ic:canister-id` meta, `/.well-known/ic-app.json` manifest), then the frontend via `x-ic-canister-id` and backend candidates via `/env.json` + JS-bundle mining — each with provenance, its IC dashboard label/type where known, and (for the app's own canisters) `oql`/`api_doc_available` capability flags from a one-shot Candid probe | -| `icp_find_canister_by_name` | `query` | Canister ids matching a name/symbol, searched in the IC dashboard's service registries — ICRC token ledgers (e.g. `ckUSDC`) and the SNS project catalog | -| `icp_find_app_by_name` | `name` | A well-known app's front-end URL + `derivation_origin`, for a small built-in set of well-known IC apps. The **first stop** whenever only an app *name* is known — never guess a domain from a name. Any other name returns no match and a `note` directing a web search for the app's URL (there's no on-chain name→URL directory) | -| `icp_lookup_canister_info_by_id` | `canister_id` | What a canister IS, per the IC dashboard: label/name, type, controllers, subnet, module hash, latest upgrade proposal | -| `get_canister_candid` | `canister_id` | The canister's `candid:service` interface (`.did` text), plus two capability flags: `oql` (`true` when it exposes an OQL query surface — a `schema` + `execute` pair — with a pointer to `icp_oql_guide`) and `api_doc_available` (`true` when it declares a `getApiDoc`/`get_api_doc` method, gating `get_canister_api_doc`) | +| `get_canister_candid` | `canister_id` | The canister's `candid:service` interface (`.did` text), plus two capability flags: `oql` (`true` when it exposes an OQL query surface — a `schema` + `execute` pair — with a pointer to the `oql://usage` resource) and `api_doc_available` (`true` when it declares a `getApiDoc`/`get_api_doc` method, gating `get_canister_api_doc`) | | `get_canister_api_doc` | `canister_id` | The canister's own prose API guide ("how this app behaves" — units, auth, lifecycle, mutation safety, polling, gotchas), from its `getApiDoc`/`get_api_doc` method. Call **only** when `get_canister_candid`/`open_app` report `api_doc_available`. Returns a **structured** result in every case — `available` + the doc on success, else `available:false` with `expected`/`retry`/`next` so an expected absence is distinct from an unreachable canister | | `canister_query` | `canister_id`, `method?` **or** `oql?`, `args?` (textual Candid), `derivation_origin?`, `account?`, `candid?` | READ a canister — provide EITHER a Candid `query` `method` (with `args`) OR an `oql` query (a JSON object string, run against `execute`). A Candid `method` query may be anonymous or as your account and returns textual Candid; an `oql` query **requires** `derivation_origin` and returns `columns` + `rows` (a table) with `has_more`, validating `start` against the schema on an empty result. On an OQL canister a Candid `method` query is rejected — use `oql`. `candid` is a fallback: the `.did` interface text to encode/decode against when the canister exposes no `candid:service` metadata. Echoes `derived_for_origin` / `requested` / `acted_as_principal` | | `canister_update_call` | `canister_id`, `method`, `args` (textual Candid), `derivation_origin?`, `account?`, `candid?` | Make an UPDATE (state-changing) call; reply as textual Candid; anonymous, or as your account at an app (identified by its canonical II `derivation_origin`, obtained once from `open_app`/`resolve_app`). **Financial transactions are refused**: the ICRC-standard transfer/approval methods (ICRC-1/ICRC-2 and the ICRC-4/-7/-37 equivalents) are disallowed on every canister, and the ICP and cycles ledgers' own value-moving methods (the legacy `transfer`, `withdraw`, the `create_canister` spends) on those ledgers, to protect the user — the refusal directs the user to act themselves — in a wallet they control (e.g. [oisy.com](https://oisy.com)), or, for canister creation, with the [icp CLI](https://github.com/dfinity/icp-cli) in their own terminal. `candid` is the same `.did` fallback as on `canister_query`, used when the interface isn't published on-chain. Echoes `derived_for_origin` / `requested` / `acted_as_principal` | | `get_app_principal` | `derivation_origin`, `account?` | The principal you act as at an app, without a call. Identify the app by its `derivation_origin` (from `open_app`/`resolve_app`). Echoes `derived_for_origin` / `requested` so an origin mismatch is visible | | `list_app_accounts` | `derivation_origin` | The user's Internet Identity accounts at an app — the default account plus any named ones — with name, number, last-used, and the derivation origin they were listed for. Identify the app by its `derivation_origin` (from `open_app`/`resolve_app`) | | `resolve_app` | `app_url` | Resolve an app URL to its Internet Identity derivation context: `application_origin`, the `derivation_origin` to use (declared in `/.well-known/ic-app.json`, else a built-in known-app value, else assumed = app origin — flagged via `derivation_origin_source`: `declared`/`known`/`app_url_default`, with `application_is_ic` echoing the gateway evidence), and the app's `alternative_origins` (informational). An origin with **no IC evidence** that would need the `app_url_default` assumption is **refused** (guessed-domain guard, with a "did you mean" repair when the host resembles a well-known app). Does not return a principal (no account chosen) or require auth — pass the `derivation_origin` to `get_app_principal`/`list_app_accounts` | -| `icp_list_skills` | — | The official [IC skills](https://skills.internetcomputer.org) (Motoko, mops/icp CLIs, cycles, stable memory, security, …), grouped by category | -| `icp_get_skill` | `name` | The full `SKILL.md` instructions for one skill (e.g. `writing-motoko`, `icp-cli`, `cycles-management`) | -| `icp_oql_guide` | — | The OQL query-surface dialect guide (for canisters where `get_canister_candid` reports `oql: true`): the JSON query object, predicate grammar, edges, and paged result shape. The entity/field names come from `get_canister_oql_schema` and queries run through `canister_query` (the `oql` argument) | | `get_canister_oql_schema` | `canister_id`, `derivation_origin`, `account?` | The canister's OQL schema catalogue (entities, primary keys, fields, edges) as JSON — wraps its `schema` method — plus a ready-to-run `canister_query` example per entity. **`derivation_origin` is required**: the schema is caller-gated, so an anonymous read is rejected (for now) with guidance, rather than returning an empty list | -| `icp_cycles_balance` | — | Your cycles-ledger balance and management principal — the principal to add as a controller (via the icp CLI) so the management tools can operate canisters you create | -| `icp_create_canister` | `cycles?` / `icp?` | **How to** create + fund a new canister: returns `icp` CLI steps for the USER to run themselves (create, fund, and add your management principal as a controller) — executes nothing and moves no funds | -| `icp_install_code` | `canister_id`, `wasm_base64` / `wasm_hex`, `mode?`, `arg?` | Install/reinstall/upgrade a Wasm module (single-shot, or via the chunk store for large modules) | -| `icp_canister_status` | `canister_id` | Run state, cycle balance, module hash, memory, controllers, allocations | -| `icp_update_canister_settings` | `canister_id`, `controllers?`, allocations, `freezing_threshold?`, `log_visibility?`, … | Update a canister's settings | -| `icp_start_canister` / `icp_stop_canister` / `icp_uninstall_code` / `icp_delete_canister` | `canister_id` | Canister lifecycle | -| `icp_top_up_canister` | `canister_id`, `cycles?` / `icp?` | **Instructions only — nothing is executed and no funds move.** Returns the step-by-step [`icp` CLI](https://github.com/dfinity/icp-cli) commands — including where to get the CLI — for the user to add cycles to a canister themselves; `cycles`/`icp` are substituted into the printed commands. Provided for completeness, to reflect the platform capability | + +> **Deferred in this version.** The `icp_`-prefixed protocol/meta tools — the +> dashboard name/id lookups (`icp_find_canister_by_name`, `icp_find_app_by_name`, +> `icp_lookup_canister_info_by_id`), the skills tools (`icp_list_skills`, +> `icp_get_skill`), the OQL guide tool (`icp_oql_guide`), and the +> canister-management group (`icp_cycles_balance`, the instructions-only +> `icp_create_canister` / `icp_top_up_canister`, `icp_install_code`, +> `icp_canister_status`, `icp_update_canister_settings`, and the lifecycle +> tools) — are not served in this version; **we anticipate this will come in a +> future version.** The OQL dialect guide and the official IC skills remain +> available as MCP resources (`oql://usage`, `skill://`), and the types +> stay in the library (`IcProtocolTools`) for embedders and for the return. `open_app` (its `app` argument takes a name **or** a URL) is the one-call entry point when the user names or links an app: it resolves the Internet Identity @@ -196,8 +197,8 @@ Acting **for the user** at an app: gateway header, no `ic-app.json` derivation origin) is refused too; when the host resembles a known app the error names it and gives the real URL (a "did you mean" repair). For a single step, the narrower tools remain: - **`icp_find_app_by_name`** (name→URL only), **`resolve_app(url)`** (origin only), - **`discover_app_canisters(url)`** (canisters only). + **`resolve_app(url)`** (origin only), **`discover_app_canisters(url)`** + (canisters only). 3. **`list_app_accounts`** — if there's more than one account, ask which to use (and remember it). 4. **`get_app_principal`** — only when you need the principal *value* itself; @@ -271,15 +272,11 @@ above). It is the only authoritative way for `open_app` / `resolve_app` to learn so an app that uses one should declare it here; otherwise the connector assumes the derivation origin equals the application origin and flags that assumption. -When the user names a **token, project, or service** (e.g. `ckUSDC`) rather than a -website or id, `icp_find_canister_by_name` resolves it via -[`dashboard.internetcomputer.org`](https://dashboard.internetcomputer.org)'s public -APIs — the ICRC token registry and the SNS catalog — to the matching canister id(s). -`icp_lookup_canister_info_by_id` goes the other way: given a bare id, it returns the dashboard's -label, type, controllers, subnet, and module hash, so a raw principal becomes an -identified service. (`discover_app_canisters` results are annotated with these labels -inline.) There is no public name-search over arbitrary canisters, so `icp_find_canister_by_name` -covers the IC's labelled services, which is where the meaningful ones live. +When the user names a **token, project, or service** rather than a website or +id, the dedicated dashboard lookup tools are not part of this version — we +anticipate they will come in a future version. Until then, web search the +canister id or ask the user for it. (`discover_app_canisters` results are still +annotated with the dashboard's labels inline.) `canister_query` and `canister_update_call` run anonymously by default; pass a `derivation_origin` to call as @@ -335,15 +332,17 @@ token (see Auth). ### Skills awareness -`icp_list_skills` / `icp_get_skill` expose the official Internet Computer +The official Internet Computer [skills](https://skills.internetcomputer.org) — authoritative, current how-to guides for authoring and shipping IC apps (the Motoko language, the `mops` and `icp` CLIs, cycles management, stable memory & upgrades, canister security, -auth, …). The catalogue is fetched live from the registry's manifest -(`/api/skills.json`, cached ~15 min) and each skill's `SKILL.md` on demand; -nothing is bundled, so the agent always sees the current skills. They are also -listed as MCP **resources** (`skill://`) alongside the `candid://` -references. Override the registry origin with `SKILLS_URL`. +auth, …) — are served as MCP **resources** (`skill://`) alongside the +`candid://` references. The catalogue is fetched live from the registry's +manifest (`/api/skills.json`, cached ~15 min) and each skill's `SKILL.md` on +demand; nothing is bundled, so the agent always sees the current skills. +Dedicated browsing tools (`icp_list_skills` / `icp_get_skill`) are deferred — +we anticipate these will come in a future version. Override the registry +origin with `SKILLS_URL`. ### OQL query surfaces @@ -366,7 +365,7 @@ individual layer is small. The OQL query path is JSON rather than Candid, so it shares the same 1 MiB size cap but is parsed by the JSON parser, not this structural guard. Rather than inline the whole dialect into every interface read, `get_canister_candid` emits only that flag plus a -one-line pointer; the full guide is served on demand by `icp_oql_guide` and as +one-line pointer; the full guide is served on demand as the `oql://usage` MCP **resource**. Two tools drive the surface: **`get_canister_oql_schema`** returns the entity/field @@ -391,33 +390,21 @@ conventions. ### Creating & managing canisters -The management tools let the agent act **on chain as your standing Internet -Identity principal** — a stable per-connection identity (the one returned when you -authenticate, printed by `icp_cycles_balance`). - -**Creation and top-ups are instructions-only.** `icp_create_canister` and -`icp_top_up_canister` execute nothing and move no funds: each returns the -[`icp` CLI](https://github.com/dfinity/icp-cli) steps (including where to get -the CLI) for the user to run the operation themselves, in their own terminal, -with keys they control. The tools exist for completeness — to reflect the -Internet Computer platform capabilities — not to perform the operations on the -user's behalf. The creation steps end with `icp canister settings update … ---add-controller `: adding the management principal as a controller -is what lets the connector's lifecycle tools operate the new canister. - -Lifecycle calls -(`icp_install_code`, `icp_canister_status`, `icp_update_canister_settings`, -`start`/`stop`/`uninstall`/`delete`) go to the management canister (`aaaaa-aa`) -with the effective canister id set to the target. `icp_install_code` takes the -compiled Wasm as base64/hex and uploads it via the chunk store automatically when -it exceeds the single-message limit. - -Together these make the end-to-end flow work: *"create a Motoko canister that does -X and deploy it"* → the agent reads the relevant skills, writes and **builds** the -Wasm in its own environment, the user creates and funds the canister with the -`icp` CLI (`icp_create_canister` prints the exact steps), then `icp_install_code` -puts the Wasm on chain. (Compiling Motoko/Rust to Wasm happens in the agent's -environment, not in this server.) +Canister creation, funding, deployment, and lifecycle management are done by +the **user** with the [`icp` CLI](https://github.com/dfinity/icp-cli) in their +own terminal, guided by the official skills (`skill://icp-cli`, +`skill://cycles-management`). The dedicated management tool group — a +cycles-balance read, instructions-only creation and top-up helpers, +`icp_install_code`, status/settings, and the lifecycle operations, acting as a +standing per-connection management principal — is deferred from this version; +**we anticipate this will come in a future version** (the code remains in the +library as `IcProtocolTools`). + +The end-to-end flow still works: *"create a Motoko canister that does X and +deploy it"* → the agent reads the relevant skills (`skill://` resources), +writes and **builds** the Wasm in its own environment, and the user creates, +funds, and installs it with the `icp` CLI. (Compiling Motoko/Rust to Wasm +happens in the agent's environment, not in this server.) ## Connect from an MCP client @@ -434,9 +421,10 @@ the tools become available. All clients use the same **authorization-code + PKCE flow. > II's consent screen makes you choose an access level: **"Questions only"** or -> **"Actions & questions"**. Choose **Actions & questions** if you want to create -> or manage canisters — a Questions-only session makes every management tool -> inert (see [Read-only sessions](#read-only-sessions) below). +> **"Actions & questions"**. Choose **Actions & questions** if you want the +> agent to make state-changing calls (`canister_update_call`) — a +> Questions-only session rejects them at ingress (see +> [Read-only sessions](#read-only-sessions) below). ## Run @@ -848,9 +836,10 @@ can cost in memory and disk, not the request rate. Put a rate limiter in front II's consent screen requires an explicit access-level choice: **"Questions only"** or **"Actions & questions"**. A user who picks Questions only gets a session whose per-app delegations are `permissions = "queries"`, -and the IC **rejects update calls made through them at ingress**. That makes the -entire canister-management surface inert — `create`/`install`/`start`/`stop`/ -`uninstall`/`delete`, and even `icp_canister_status`, are update calls. To handle this +and the IC **rejects update calls made through them at ingress**. That makes +`canister_update_call` inert — and, when the deferred canister-management +tools return in a future version, that whole surface too (even a status read +is an update call there). To handle this without opaque low-level errors: - The `mcp_register_v2` reply carries `permissions: "queries" | "all"`, so the diff --git a/crates/imcp2-core/src/calls.rs b/crates/imcp2-core/src/calls.rs index e54ed0b..712047d 100644 --- a/crates/imcp2-core/src/calls.rs +++ b/crates/imcp2-core/src/calls.rs @@ -41,8 +41,8 @@ pub struct GetCandidOutput { /// The Candid (`.did`) interface text. pub candid: String, /// True when the interface exposes the standard OQL query surface (both a - /// `schema` and an `execute` method). When set, load `icp_oql_guide` (or the - /// `oql://usage` resource) to learn the JSON query dialect, then call + /// `schema` and an `execute` method). When set, load the `oql://usage` + /// resource to learn the JSON query dialect, then call /// `get_canister_oql_schema` for the entity/field names and `canister_query` /// (its `oql` argument) to run the query. pub oql: bool, @@ -244,8 +244,8 @@ pub struct CanisterQueryArgs { /// An OQL query as a JSON object string — passed straight to the canister's /// `execute` method, so NO Candid escaping is needed (write plain JSON). E.g. /// `{"start":"employee","where":{"icontains":{"field":"lastName","value":"smith"}},"select":["firstName","lastName"],"limit":10}`. - /// Provide EITHER `oql` OR `method` — not both. See icp_oql_guide for the dialect - /// and get_canister_oql_schema for the entity/field names. + /// Provide EITHER `oql` OR `method` — not both. See the `oql://usage` resource for + /// the dialect and get_canister_oql_schema for the entity/field names. /// The OQL path REQUIRES `derivation_origin` (anonymous per-app reads are disabled). #[serde(default)] pub oql: Option, @@ -823,8 +823,8 @@ pub fn oql_query_redirect(did: Option<&str>) -> Option { if did.is_some_and(has_oql) { Some( "this canister exposes an OQL query surface, so its data is READ with an OQL query, \ - NOT a raw Candid `method` query call. Do this instead, in order: (1) `icp_oql_guide` \ - for the JSON dialect (once), (2) `get_canister_oql_schema` for the entity and field \ + NOT a raw Candid `method` query call. Do this instead, in order: (1) the `oql://usage` \ + resource for the JSON dialect (once), (2) `get_canister_oql_schema` for the entity and field \ names, (3) call `canister_query` again with the `oql` argument (a JSON query object) \ instead of `method`. This canister gates data by the caller's principal, so pass the \ app's `derivation_origin` (from open_app / resolve_app) — an anonymous read is rejected. \ @@ -1854,7 +1854,7 @@ mod tests { // Candid `method` query on an OQL canister → redirected, and the message // names the full guide→schema→query path (#5) plus the auth hint. let msg = oql_query_redirect(Some(oql)).expect("query on OQL canister must be redirected"); - assert!(msg.contains("icp_oql_guide"), "message must point to the OQL guide: {msg}"); + assert!(msg.contains("oql://usage"), "message must point to the OQL guide: {msg}"); assert!(msg.contains("get_canister_oql_schema"), "message must point to the OQL schema tool: {msg}"); assert!(msg.contains("canister_query"), "message must point to canister_query's oql path: {msg}"); assert!(msg.contains("`oql`"), "message must name the oql argument: {msg}"); diff --git a/crates/imcp2-core/src/compliance.rs b/crates/imcp2-core/src/compliance.rs index b2abffb..2166895 100644 --- a/crates/imcp2-core/src/compliance.rs +++ b/crates/imcp2-core/src/compliance.rs @@ -91,9 +91,8 @@ const DISALLOWED_CANISTER_METHODS: &[(&str, &str, &str)] = &[ // The cycles ledger's value movement beyond ICRC-1/-2 (which group one // already covers): withdrawals and the canister-creation spends. The // creation refusal points the user at running the operation themselves - // with the icp CLI; icp_create_canister is instructions-only (it prints - // those steps and executes nothing), so naming it does not offer an - // executing route (per review). + // with the icp CLI directly (the dedicated instructions-only creation + // tool is deferred from this version's served surface). (CYCLES_LEDGER, "withdraw", "a cycles-ledger withdrawal"), (CYCLES_LEDGER, "withdraw_from", "a cycles-ledger delegated withdrawal"), (CYCLES_LEDGER, "create_canister", "a cycles-ledger spend (canister creation)"), @@ -125,9 +124,9 @@ pub fn disallowed_update_method(canister_id: &Principal, method: &str) -> Option // wallet or frontend in the user's own browser. let instead = if method.starts_with("create_canister") { "Recommend that the user creates and funds canisters themselves with \ - the icp CLI in their own terminal — the icp_create_canister tool \ - prints the exact steps (it executes nothing and moves no funds), and \ - the icp-cli and cycles-management skills (icp_get_skill) carry the \ + the icp CLI in their own terminal (install with `npm install -g \ + @icp-sdk/icp-cli`, or see https://github.com/dfinity/icp-cli); the \ + skill://icp-cli and skill://cycles-management resources carry the \ full guide." } else { "Recommend that the user performs this operation themselves, in a wallet \ @@ -219,16 +218,17 @@ mod tests { // The cycles ledger's canister-creation spends are refused on the generic // route, and their refusal points the user at running creation themselves - // with the icp CLI. icp_create_canister may be named because it is - // instructions-only — the refusal must say so, and must not point at a - // wallet (the wrong venue for creation). + // with the icp CLI (install pointer included) — never at a wallet (the + // wrong venue for creation) and never at a connector tool (the dedicated + // instructions-only creation tool is deferred from this version). #[test] fn refuses_cycles_ledger_creation_spends_with_cli_redirect() { for method in ["create_canister", "create_canister_from"] { let msg = disallowed_update_method(&cycles_ledger(), method) .unwrap_or_else(|| panic!("{method} must be refused")); assert!(msg.contains("icp CLI"), "{msg}"); - assert!(msg.contains("executes nothing"), "{msg}"); + assert!(msg.contains("npm install -g @icp-sdk/icp-cli"), "{msg}"); + assert!(!msg.contains("icp_create_canister"), "no connector-tool redirect: {msg}"); assert!(!msg.contains("oisy.com"), "a wallet is the wrong venue for creation: {msg}"); } } diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index 0a0d250..3e4f5ab 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -52,8 +52,7 @@ const OQL_PRIMER_MD: &str = include_str!("../static/oql-primer.md"); /// The app- and canister-scoped tools: everything that reads, discovers, or /// calls ONE app or canister (the `…canister…` / `…_app…` names) — Candid and /// OQL reads, update calls, app resolution/discovery, and the per-app -/// identity tools. Served together with [`IcProtocolTools`] behind -/// [`IcTools`]. +/// identity tools. This is the surface [`IcTools`] serves in this version. #[derive(Clone)] pub struct IcCanisterTools { agent: Agent, @@ -65,8 +64,13 @@ pub struct IcCanisterTools { /// The IC protocol / meta-level tools (the `icp_` prefix): dashboard name/id /// lookups, the official IC skills, the OQL dialect guide, and canister -/// creation/management as the standing management principal. Served together -/// with [`IcCanisterTools`] behind [`IcTools`]. +/// creation/management as the standing management principal. +/// +/// NOT served by the default [`IcTools`] composition in this version — we +/// anticipate this will come in a future version. Until then the type, its +/// tools, and their tests stay ready, and an embedder that wants the group +/// today can construct it with [`IcProtocolTools::new`] and serve its +/// `tool_router()` itself. #[derive(Clone)] pub struct IcProtocolTools { identities: Identities, @@ -76,17 +80,18 @@ pub struct IcProtocolTools { session: SessionResolver, } -/// The complete MCP tool surface: [`IcCanisterTools`] and [`IcProtocolTools`] -/// composed behind one [`ServerHandler`]. The split follows the scope -/// taxonomy the server instructions teach — an `icp_` prefix marks IC -/// protocol / meta-level tools, `…_app…`/`…canister…` names act on one app or -/// canister — while the served surface (tool names, descriptions, -/// annotations, instructions, resources) is exactly what the single struct -/// used to serve. +/// The served MCP tool surface: [`IcCanisterTools`] behind one +/// [`ServerHandler`], plus the candid/OQL/skill resources. The IC protocol / +/// meta-level half ([`IcProtocolTools`]) is split out and deliberately NOT +/// served in this version — we anticipate this will come in a future version; +/// re-enabling it is one line in the composition here. #[derive(Clone)] pub struct IcTools { canister: IcCanisterTools, - protocol: IcProtocolTools, + /// Backs the `skill://` resources (the skills TOOLS live on the deferred + /// [`IcProtocolTools`]; the documents themselves stay served as + /// resources). + skills: skills::SkillsCatalog, } /// The one seam between deployments: how a tool call finds the Internet @@ -111,27 +116,40 @@ impl IcTools { Self { canister: IcCanisterTools { agent, - identities: identities.clone(), - session: session.clone(), - }, - protocol: IcProtocolTools { identities, - skills, session, }, + skills, } } - /// Every tool on the served surface — the app/canister tools first, then - /// the protocol/meta tools — exactly the list `tools/list` returns. + /// Every tool on the served surface — the app/canister tools; exactly the + /// list `tools/list` returns. The protocol/meta tools are deferred (we + /// anticipate they will come in a future version) and are NOT listed. pub fn all_tools() -> Vec { - let mut tools = IcCanisterTools::tool_router().list_all(); - tools.extend(IcProtocolTools::tool_router().list_all()); - tools + IcCanisterTools::tool_router().list_all() } } -#[tool_router] +impl IcProtocolTools { + /// Build the protocol/meta half on its own. The default [`IcTools`] + /// composition does not serve these tools in this version (we anticipate + /// this will come in a future version); an embedder that wants the group + /// today can construct it here and serve its `tool_router()` alongside. + pub fn new( + identities: Identities, + skills: skills::SkillsCatalog, + session: SessionResolver, + ) -> Self { + Self { + identities, + skills, + session, + } + } +} + +#[tool_router(vis = "pub")] impl IcCanisterTools { /// The already-validated session id this tool call acts as, per the /// embedding binary's [`SessionResolver`]. @@ -140,7 +158,7 @@ impl IcCanisterTools { } #[tool( - description = "Fetch the Candid (.did) interface definition of an Internet Computer canister, read from its public `candid:service` metadata. Also reports two capability flags: `oql` (the canister exposes the OQL query surface — READ it via icp_oql_guide → get_canister_oql_schema → canister_query with the `oql` argument, since a Candid `method` query is then rejected) and `api_doc_available` (a `getApiDoc`/`get_api_doc` method exists — call get_canister_api_doc for a prose behavior guide; skip that call when this is false).", + description = "Fetch the Candid (.did) interface definition of an Internet Computer canister, read from its public `candid:service` metadata. Also reports two capability flags: `oql` (the canister exposes the OQL query surface — READ it via the `oql://usage` resource → get_canister_oql_schema → canister_query with the `oql` argument, since a Candid `method` query is then rejected) and `api_doc_available` (a `getApiDoc`/`get_api_doc` method exists — call get_canister_api_doc for a prose behavior guide; skip that call when this is false).", annotations(title = "Get Candid interface", read_only_hint = true, destructive_hint = false, open_world_hint = true), output_schema = schema_for_output::(), )] @@ -163,7 +181,7 @@ impl IcCanisterTools { // set, the guidance pointer is emitted as a SEPARATE content // block, so the first block stays the raw `.did` (still valid, // copy-pastable Candid) — the full primer is served on demand - // (see icp_oql_guide / OQL_USAGE_URI), never inlined. + // (see the OQL_USAGE_URI resource), never inlined. let oql = calls::has_oql(&did); // Same predicate get_canister_api_doc uses, so the flag gates that // call: true means a `getApiDoc`/`get_api_doc` method exists. @@ -181,14 +199,14 @@ impl IcCanisterTools { notes.push(format!( "This canister exposes an OQL query surface (a JSON query language \ over its data), so READ it through the OQL tools — a Candid `method` \ - query via canister_query is rejected here. Order: icp_oql_guide \ - (dialect, once) → get_canister_oql_schema (entities/fields) → \ + query via canister_query is rejected here. Order: the `{OQL_USAGE_URI}` \ + resource (dialect, once) → get_canister_oql_schema (entities/fields) → \ canister_query with the `oql` argument (run a JSON query, get a table). \ Those wrap the `schema`/`execute` methods (no Candid escaping). Per-app \ data is caller-gated, so the OQL read path REQUIRES the app's \ derivation_origin — an anonymous OQL read is rejected (for now), not \ silently empty; pass the derivation_origin from open_app / resolve_app. \ - See icp_oql_guide (or the `{OQL_USAGE_URI}` resource) for the dialect. \ + See the `{OQL_USAGE_URI}` resource for the dialect. \ canister_update_call then handles UPDATE calls only." )); } @@ -215,7 +233,7 @@ impl IcCanisterTools { } #[tool( - description = "Fetch the OQL schema catalogue of a canister that exposes the OQL surface (get_canister_candid reports `oql: true`): its entities, their primary keys, fields, and edges, as JSON. The MIDDLE step of guide→schema→query: read `icp_oql_guide` once, then call THIS before canister_query so you use the exact entity/field names instead of guessing. Entity names are the schema's own — often PLURAL and different from the Candid types/methods (e.g. `bookings`, not `Booking`/`getBookings`). Returns the schema plus a ready-to-run `canister_query` example per entity (each preserving this call's identity). AUTH: `derivation_origin` is REQUIRED — the schema itself is gated by the caller's principal, so a read with no origin is REJECTED (anonymous per-app reads are disabled for now) with guidance to pass it, rather than returning an empty entity list you'd misread as \"the app has no data model\". Pass the app's canonical `derivation_origin` (from open_app / resolve_app) to read the entities visible to the USER; the reply echoes `derived_for_origin` / `acted_as_principal`.", + description = "Fetch the OQL schema catalogue of a canister that exposes the OQL surface (get_canister_candid reports `oql: true`): its entities, their primary keys, fields, and edges, as JSON. The MIDDLE step of guide→schema→query: read the `oql://usage` resource once, then call THIS before canister_query so you use the exact entity/field names instead of guessing. Entity names are the schema's own — often PLURAL and different from the Candid types/methods (e.g. `bookings`, not `Booking`/`getBookings`). Returns the schema plus a ready-to-run `canister_query` example per entity (each preserving this call's identity). AUTH: `derivation_origin` is REQUIRED — the schema itself is gated by the caller's principal, so a read with no origin is REJECTED (anonymous per-app reads are disabled for now) with guidance to pass it, rather than returning an empty entity list you'd misread as \"the app has no data model\". Pass the app's canonical `derivation_origin` (from open_app / resolve_app) to read the entities visible to the USER; the reply echoes `derived_for_origin` / `acted_as_principal`.", annotations(title = "Get the OQL schema", read_only_hint = true, destructive_hint = false, open_world_hint = true), output_schema = schema_for_output::(), )] @@ -959,7 +977,7 @@ impl IcCanisterTools { } #[tool( - description = "ROUTING. Run this chain before answering anything that names, or implies, a specific app or the user's own data in one app (bookings, appointments, orders, a profile, or \"what can it do\"): (1) Is the request about a specific APP's functionality or the user's data in it, rather than the IC protocol itself? (2) Is that app on the Internet Computer? If unsure, THIS tool (or icp_find_app_by_name) is how you find out: a known app or an IC-verifiable URL resolves, while an unrecognized app name, or a URL with no Internet Computer evidence, is refused rather than guessed. (3) If so, START HERE: one open_app call resolves the app AND discovers its service. An app feature is almost never a single named tool (there is no \"get_bookings\"); you COMPOSE it from what open_app returns, reading the discovered canisters and their `oql`/`api_doc_available` flags to see how to READ (canister_query, including OQL) or ACT (canister_update_call). So never conclude \"there's no tool for that\" from the flat tool list without opening the app first. Open an Internet Computer app in ONE call, given its NAME or its URL — the recommended entry point when a user names an app. It resolves the app's Internet Identity derivation origin (like resolve_app) AND discovers the canisters behind it (like discover_app_canisters) together, so you don't chain those yourself. Pass a NAME as the user said it, or a URL (e.g. \"https://opencloud.org\"): a name or bare host is matched to the built-in known-app registry FIRST (so even a wrong-TLD guess repairs to the canonical URL), and an explicit https:// URL is resolved as given. NEVER fabricate a domain from a name — an unknown bare name is refused with instructions to find the real URL (web search / ask the user), and a URL with no Internet Computer evidence is refused, both instead of guessing a wrong identity. Returns `app_url` (the one used), `derivation_origin` (+ its source) to act with, `alternative_origins`, and the discovered `canisters` (with provenance/labels AND per-canister `oql`/`api_doc_available` capability flags, from a one-shot Candid probe of the app's own canisters). A canister flagged `oql` holds the app's data, GATED BY THE CALLER's principal: to read the USER's own data (\"my …\", \"our …\") pass the returned `derivation_origin` to get_canister_oql_schema (for the entity/field names) and to canister_query (with the `oql` argument, to run the query). Those OQL reads REQUIRE `derivation_origin` — not `app_url` — and reject an anonymous read for now. No authenticated session required for open_app itself (no principal is derived here). Narrower tools remain for single steps: icp_find_app_by_name (name→URL only), resolve_app (origin only), discover_app_canisters (canisters only).", + description = "ROUTING. Run this chain before answering anything that names, or implies, a specific app or the user's own data in one app (bookings, appointments, orders, a profile, or \"what can it do\"): (1) Is the request about a specific APP's functionality or the user's data in it, rather than the IC protocol itself? (2) Is that app on the Internet Computer? If unsure, THIS tool is how you find out: a known app or an IC-verifiable URL resolves, while an unrecognized app name, or a URL with no Internet Computer evidence, is refused rather than guessed. (3) If so, START HERE: one open_app call resolves the app AND discovers its service. An app feature is almost never a single named tool (there is no \"get_bookings\"); you COMPOSE it from what open_app returns, reading the discovered canisters and their `oql`/`api_doc_available` flags to see how to READ (canister_query, including OQL) or ACT (canister_update_call). So never conclude \"there's no tool for that\" from the flat tool list without opening the app first. Open an Internet Computer app in ONE call, given its NAME or its URL — the recommended entry point when a user names an app. It resolves the app's Internet Identity derivation origin (like resolve_app) AND discovers the canisters behind it (like discover_app_canisters) together, so you don't chain those yourself. Pass a NAME as the user said it, or a URL (e.g. \"https://opencloud.org\"): a name or bare host is matched to the built-in known-app registry FIRST (so even a wrong-TLD guess repairs to the canonical URL), and an explicit https:// URL is resolved as given. NEVER fabricate a domain from a name — an unknown bare name is refused with instructions to find the real URL (web search / ask the user), and a URL with no Internet Computer evidence is refused, both instead of guessing a wrong identity. Returns `app_url` (the one used), `derivation_origin` (+ its source) to act with, `alternative_origins`, and the discovered `canisters` (with provenance/labels AND per-canister `oql`/`api_doc_available` capability flags, from a one-shot Candid probe of the app's own canisters). A canister flagged `oql` holds the app's data, GATED BY THE CALLER's principal: to read the USER's own data (\"my …\", \"our …\") pass the returned `derivation_origin` to get_canister_oql_schema (for the entity/field names) and to canister_query (with the `oql` argument, to run the query). Those OQL reads REQUIRE `derivation_origin` — not `app_url` — and reject an anonymous read for now. No authenticated session required for open_app itself (no principal is derived here). Narrower tools remain for single steps: resolve_app (origin only), discover_app_canisters (canisters only).", annotations(title = "Open an app (resolve origin + discover canisters)", read_only_hint = true, destructive_hint = false, open_world_hint = true), output_schema = schema_for_output::(), )] @@ -1102,7 +1120,7 @@ impl IcCanisterTools { } #[tool( - description = "Resolve an application URL to its Internet Identity derivation context, so you don't have to figure out the derivation origin yourself. `app_url` must be a URL you actually HAVE — from the user, from icp_find_app_by_name, or from a web search of the app's official site. NEVER guess or fabricate a domain from an app's name (when you only know a NAME, call icp_find_app_by_name first): a lookalike domain is an unrelated or squatted site, and this tool REFUSES to resolve an origin that shows no evidence of being an Internet Computer app rather than hand back a wrong identity. Returns the `application_origin`, the `derivation_origin` to pass to the identity tools, how it was determined (`derivation_origin_source`: \"declared\" — the app published it in /.well-known/ic-app.json, authoritative; \"known\" — from the connector's built-in registry of well-known custom-derivation-origin apps, used only when the app declares none; or \"app_url_default\" — the origin IS IC-served but declares nothing, so it was assumed to be its own derivation origin, correct only if the app has no custom one), and the app's `alternative_origins` (informational — the INVERSE relation, never use it to infer the derivation origin). This does NOT return a principal — it resolves the origin only, since you haven't picked an account; to get the principal you act as, pass the returned `derivation_origin` to get_app_principal (choosing an `account`) or list_app_accounts. Use this first when you only know an app's URL; no authenticated session is required.", + description = "Resolve an application URL to its Internet Identity derivation context, so you don't have to figure out the derivation origin yourself. `app_url` must be a URL you actually HAVE — from the user, from open_app's known-app resolution, or from a web search of the app's official site. NEVER guess or fabricate a domain from an app's name (when you only know a NAME, call open_app with the name first): a lookalike domain is an unrelated or squatted site, and this tool REFUSES to resolve an origin that shows no evidence of being an Internet Computer app rather than hand back a wrong identity. Returns the `application_origin`, the `derivation_origin` to pass to the identity tools, how it was determined (`derivation_origin_source`: \"declared\" — the app published it in /.well-known/ic-app.json, authoritative; \"known\" — from the connector's built-in registry of well-known custom-derivation-origin apps, used only when the app declares none; or \"app_url_default\" — the origin IS IC-served but declares nothing, so it was assumed to be its own derivation origin, correct only if the app has no custom one), and the app's `alternative_origins` (informational — the INVERSE relation, never use it to infer the derivation origin). This does NOT return a principal — it resolves the origin only, since you haven't picked an account; to get the principal you act as, pass the returned `derivation_origin` to get_app_principal (choosing an `account`) or list_app_accounts. Use this first when you only know an app's URL; no authenticated session is required.", annotations(title = "Resolve an app's derivation origin", read_only_hint = true, destructive_hint = false, open_world_hint = true), output_schema = schema_for_output::(), )] @@ -1156,7 +1174,7 @@ impl IcCanisterTools { } #[tool( - description = "Discover the Internet Computer canisters behind a web domain (e.g. \"oisy.com\"). The domain must be one you actually have (from the user, icp_find_app_by_name, or a web search) — NEVER a domain guessed from an app's name; when you only know a NAME, call icp_find_app_by_name first. Returns every canister id found, with provenance, most authoritative first: app-declared metadata — the App Connect page's `ic:canister-id` meta at /ai-connect.html (the app's MAIN backend) and the app's own /.well-known/ic-app.json manifest (ALL its canisters, with roles) — then the `x-ic-canister-id` header (the frontend/asset canister), a `/env.json` runtime config (e.g. backend_canister_id), and labelled/bare canister-id literals mined from the JS bundle. App-declared entries are the app's own claim about itself; env.json/bundle entries are mined candidates: pick by label (prefer production/IC ids) and confirm with get_canister_candid before calling.", + description = "Discover the Internet Computer canisters behind a web domain (e.g. \"oisy.com\"). The domain must be one you actually have (from the user, open_app's known-app resolution, or a web search) — NEVER a domain guessed from an app's name; when you only know a NAME, call open_app with the name first. Returns every canister id found, with provenance, most authoritative first: app-declared metadata — the App Connect page's `ic:canister-id` meta at /ai-connect.html (the app's MAIN backend) and the app's own /.well-known/ic-app.json manifest (ALL its canisters, with roles) — then the `x-ic-canister-id` header (the frontend/asset canister), a `/env.json` runtime config (e.g. backend_canister_id), and labelled/bare canister-id literals mined from the JS bundle. App-declared entries are the app's own claim about itself; env.json/bundle entries are mined candidates: pick by label (prefer production/IC ids) and confirm with get_canister_candid before calling.", annotations(title = "Discover canisters behind a domain", read_only_hint = true, destructive_hint = false, open_world_hint = true), output_schema = schema_for_output::(), )] @@ -1214,7 +1232,7 @@ impl IcCanisterTools { } text.push_str( " If you GUESSED this domain from an app name, don't guess again: call \ - icp_find_app_by_name with the name, or WEB SEARCH the app's official URL, \ + open_app with the exact name, or WEB SEARCH the app's official URL, \ or ask the user for it.", ); let output = discover::DiscoverOutput::from((domain, d)); @@ -1229,7 +1247,7 @@ impl IcCanisterTools { } -#[tool_router] +#[tool_router(vis = "pub")] impl IcProtocolTools { /// The already-validated session id this tool call acts as, per the /// embedding binary's [`SessionResolver`]. @@ -1758,7 +1776,7 @@ fn app_url_error_with_guidance(app_url: &str, e: String) -> String { } msg.push_str( " If you arrived at this URL by guessing from an app name, don't guess again — call \ - icp_find_app_by_name with the name, WEB SEARCH the app's official URL, or ask the user.", + open_app with the name, WEB SEARCH the app's official URL, or ask the user.", ); msg } @@ -1787,7 +1805,7 @@ fn unverified_app_url_error(application_origin: &str) -> String { msg.push_str( "If you arrived at this URL by GUESSING from an app name, stop guessing — a lookalike \ domain is an unrelated or squatted site, and an identity derived there is wrong: \ - (1) call icp_find_app_by_name with the app's name (well-known apps resolve offline); \ + (1) call open_app with the app's name (well-known apps resolve offline); \ (2) if it's not known, WEB SEARCH the app's official URL; (3) or ask the user for it. \ If this origin genuinely IS the app's canonical Internet Identity origin (e.g. a \ non-IC-hosted app the user pointed you at), pass it explicitly as `derivation_origin` \ @@ -1808,7 +1826,7 @@ fn resolution_note(resolved: &discover::AppIdentity, effective: &str) -> Option< format!( " CAUTION: this host RESEMBLES the well-known app {} but is not one of its real \ origins — {}'s URL is {} (derivation origin {}). If you meant {}, use that URL; \ - if you guessed this URL from the app's name, use icp_find_app_by_name instead of \ + if you guessed this URL from the app's name, use open_app with the name instead of \ guessing.", m.name, m.name, m.app_url, m.derivation_origin, m.name ) @@ -1967,16 +1985,18 @@ fn identity_annotation(target: &IdentityTarget, acted_as: Option<&str>) -> Strin /// for financial transactions, the one thing it must not suggest). const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TEXTUAL Candid — the `(...)` value \ syntax, e.g. `(record { owner = principal \"aaaaa-aa\"; amount = 5 : nat })`, never \ - the binary form. Tool names signal SCOPE: an `icp_` prefix marks IC protocol / \ - meta-level tools (dashboard name/id lookups, the official IC skills, the OQL dialect \ - guide, and canister creation/management); `…_app…` names \ + the binary form. Tool names signal SCOPE: `…_app…` names \ (`open_app`, `discover_app_canisters`, `get_app_principal`, `list_app_accounts`, `resolve_app`) \ - act on a whole APP, keyed by its Internet Identity derivation origin or app URL; and \ + act on a whole APP, keyed by its Internet Identity derivation origin or app URL, and \ `…canister…` names (`get_canister_candid`, `get_canister_api_doc`, \ `get_canister_oql_schema`, `canister_query`, `canister_update_call`) act on ONE \ - specific canister. Before writing Candid args, consult the `candid://textual-syntax` \ + specific canister. (A further `icp_`-prefixed group of IC protocol / meta-level \ + tools — dashboard name/id lookups, the official IC skills as tools, and canister \ + creation/management — is not part of this version; we anticipate this will come in \ + a future version.) Before writing Candid args, consult the `candid://textual-syntax` \ resource (the value syntax these tools use); `candid://reference` has the full type \ - reference.\n\n\ + reference, the OQL dialect guide is the `oql://usage` resource, and the official IC \ + skills are served as `skill://` resources.\n\n\ FINANCIAL TRANSACTIONS ARE NOT SUPPORTED — asset-moving requests are denied, to \ protect the user: canister_update_call refuses the ICRC-standard transfer/approval \ methods (icrc1_transfer, icrc2_approve, icrc2_transfer_from, and the ICRC-4/-7/-37 \ @@ -1995,13 +2015,13 @@ const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TE discover_app_canisters. (open_app bundles `resolve_app` + `discover_app_canisters`; use \ those directly only for a single step.) RULE — names are not URLs: NEVER guess or \ fabricate a domain from an app's name (e.g. .com/.app); pass the NAME to open_app \ - (or `icp_find_app_by_name`) and let the connector resolve it, WEB SEARCH the official \ + and let the connector resolve it, WEB SEARCH the official \ URL, or ask the user. Lookalike domains are unrelated or squatted sites, and open_app / \ every URL-taking tool REFUSES an origin that shows no evidence of being an Internet \ - Computer app instead of resolving it to a wrong identity. When the user names a TOKEN, \ - PROJECT or SERVICE (e.g. \"ckUSDC\"), use `icp_find_canister_by_name` for its canister \ - id; `icp_lookup_canister_info_by_id(id)` tells you what a bare canister id IS (dashboard \ - label, type, controllers, subnet).\n\n\ + Computer app instead of resolving it to a wrong identity. (Dedicated dashboard \ + lookup tools for token/service names and bare canister ids are not part of this \ + version — we anticipate they will come in a future version; until then, web search \ + or ask the user for an id you don't have.)\n\n\ \"MY / OUR …\" IS AN AUTHENTICATED READ. A question about the USER's OWN data in an app \ (\"who am I meeting with…\", \"my bookings\", \"our open orders\") reads data the app gates \ by the CALLER's principal. An OQL read (get_canister_oql_schema, and canister_query \ @@ -2014,8 +2034,8 @@ const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TE public canisters like ledgers.)\n\n\ INSPECTING A CANISTER. `get_canister_candid` fetches the interface and reports two \ capability flags: `oql` and `api_doc_available` (open_app reports the same per \ - canister). If `oql: true`, READ the canister via OQL, in order: `icp_oql_guide` (the \ - JSON dialect, once) → `get_canister_oql_schema` (the entities and fields) → \ + canister). If `oql: true`, READ the canister via OQL, in order: the `oql://usage` \ + resource (the JSON dialect, once) → `get_canister_oql_schema` (the entities and fields) → \ `canister_query` with the `oql` argument \ (run a JSON query, get a table). These wrap the canister's `schema`/`execute` methods, \ so you never hand-encode Candid for OQL — and on an OQL canister a Candid `method` query \ @@ -2062,15 +2082,14 @@ const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TE connecting (up to 30 days); reconnect when it expires. \ Internet Identity's consent screen asks the user to choose an access level, \ \"Questions only\" or \"Actions & questions\". On a Questions-only session reads work, but \ - the canister-management tools below make update calls the network rejects — if one fails \ + state-changing calls (canister_update_call) are rejected by the network — if one fails \ that way, ask the user to reconnect and choose \"Actions & questions\".\n\n\ Typical flow (acting FOR THE USER at an app): (0-2) `open_app(name-or-URL)` in ONE \ call gives the `derivation_origin` AND the app's canisters (with `oql`/`api_doc_available` \ flags) — pass the NAME the user said (well-known apps resolve \ offline) or a URL you have, NEVER a domain guessed from the name (there is no on-chain \ - name→URL directory; `icp_find_canister_by_name` finds token/SNS canister ids, not \ - front-ends). If you want just one part, `icp_find_app_by_name` does name→URL, \ - `resolve_app(url)` the origin, `discover_app_canisters(url)` the canisters; (3) \ + name→URL directory). If you want just one part, \ + `resolve_app(url)` does the origin, `discover_app_canisters(url)` the canisters; (3) \ `list_app_accounts` — if there is more than one account, ask which to use and remember \ it; (4) `get_app_principal` ONLY when you need the principal value itself (`canister_query` / \ `canister_update_call` act as the account without pre-fetching it); (5) inspect the \ @@ -2084,29 +2103,17 @@ const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TE user. Public metadata (get_canister_candid, discover_app_canisters) and public \ canister_query Candid `method` queries need no origin; OQL reads always require one. The \ per-canister inspection (5) is independent of the identity steps (1/3/4), so they can \ - run in parallel. Managing your OWN canisters \ - (the `icp_` install/status/… tools) acts as your standing MANAGEMENT principal at \ - this server's origin — a DIFFERENT identity than the per-app principals above.\n\n\ - To AUTHOR, BUILD and DEPLOY IC code, first consult the official IC skills: \ - `icp_list_skills` lists them and `icp_get_skill(name)` loads one. Especially \ + run in parallel.\n\n\ + To AUTHOR and BUILD IC code, first consult the official IC skills, served as \ + `skill://` resources — especially \ `writing-motoko` (language), `mops-cli` (deps/build), `icp-cli` (build & deploy), \ `cycles-management` \ (ICP↔cycles & funding), `stable-memory` (upgrades) and `canister-security`. Compiling \ - Motoko/Rust to Wasm happens in YOUR environment (guided by these skills); these tools \ - then put it on chain. To MANAGE canisters as your Internet Identity, use: \ - `icp_install_code` (install your compiled \ - Wasm — base64 — single-shot or chunked), `icp_canister_status`, `icp_update_canister_settings`, \ - `icp_start_canister`/`icp_stop_canister`/`icp_uninstall_code`/`icp_delete_canister`; \ - `icp_cycles_balance` reads your cycles-ledger balance and prints that principal. \ - These act as your standing II principal. CREATING and TOPPING UP canisters are \ - instructions-only: `icp_create_canister` and `icp_top_up_canister` execute nothing \ - and move no funds — each returns the icp CLI steps for the user to run themselves, \ - and the creation steps end with adding your management principal (printed by \ - `icp_cycles_balance`) as a controller so the management tools here can operate the \ - new canister. So to \ - \"build X and deploy a canister\": read the relevant skills, write & build the Wasm \ - locally, have the user create the canister with the icp CLI (`icp_create_canister` \ - prints the exact steps), then `icp_install_code`."; + Motoko/Rust to Wasm happens in YOUR environment (guided by those skills), and \ + CREATING, FUNDING, DEPLOYING and MANAGING canisters is done by the USER with the icp \ + CLI in their own terminal, guided by the same skills. Dedicated tools for skills \ + browsing and for canister creation/management as your Internet Identity are not part \ + of this version — we anticipate this will come in a future version."; impl ServerHandler for IcTools { async fn list_tools( @@ -2125,23 +2132,15 @@ impl ServerHandler for IcTools { request: CallToolRequestParams, context: RequestContext, ) -> Result { - // Dispatch by router membership; an unknown name falls through to the - // protocol router, whose miss produces the standard method-not-found - // error (same shape the macro-generated handler returned). - if IcCanisterTools::tool_router().has_route(&request.name) { - let tcc = ToolCallContext::new(&self.canister, request, context); - IcCanisterTools::tool_router().call(tcc).await - } else { - let tcc = ToolCallContext::new(&self.protocol, request, context); - IcProtocolTools::tool_router().call(tcc).await - } + // Only the canister/app router is served in this version (the + // protocol/meta tools are deferred); a miss produces the router's + // standard method-not-found error. + let tcc = ToolCallContext::new(&self.canister, request, context); + IcCanisterTools::tool_router().call(tcc).await } fn get_tool(&self, name: &str) -> Option { - IcCanisterTools::tool_router() - .get(name) - .cloned() - .or_else(|| IcProtocolTools::tool_router().get(name).cloned()) + IcCanisterTools::tool_router().get(name).cloned() } fn get_info(&self) -> ServerInfo { @@ -2168,7 +2167,7 @@ impl ServerHandler for IcTools { // Surface the IC skills as resources too (best-effort: if the registry is // unreachable, the candid resources above still list). Each `skill://` // is read on demand in read_resource. - if let Ok(skills) = self.protocol.skills.list().await { + if let Ok(skills) = self.skills.list().await { for s in skills { let title = if s.title.is_empty() { format!("IC skill: {}", s.name) @@ -2194,7 +2193,7 @@ impl ServerHandler for IcTools { ) -> Result { // Skills are fetched live by name; the candid references are static. if let Some(name) = request.uri.strip_prefix(SKILL_URI_PREFIX) { - return match self.protocol.skills.get(name).await { + return match self.skills.get(name).await { Ok(md) => Ok(ReadResourceResult::new(vec![ResourceContents::text( md, request.uri, @@ -2470,8 +2469,13 @@ mod tests { // get_canister_candid as destructive. Assert the read/write classification serializes. #[test] fn every_tool_has_correct_read_write_annotations() { - let tools = super::IcTools::all_tools(); - assert_eq!(tools.len(), 26, "expected 26 tools, got {}", tools.len()); + let served = super::IcTools::all_tools(); + assert_eq!(served.len(), 10, "expected 10 served tools, got {}", served.len()); + // The deferred protocol half keeps its annotation contracts too, so + // wiring it back in a future version can't regress them. + let mut tools = served; + tools.extend(super::IcProtocolTools::tool_router().list_all()); + assert_eq!(tools.len(), 26, "expected 26 tools across both halves, got {}", tools.len()); assert!( tools.iter().all(|t| t.annotations.is_some()), "every tool must carry annotations (else clients assume write/destructive)" @@ -2554,7 +2558,8 @@ mod tests { // Regression guard for that boundary. #[test] fn the_core_router_carries_no_local_login_tools() { - let tools = super::IcTools::all_tools(); + let mut tools = super::IcTools::all_tools(); + tools.extend(super::IcProtocolTools::tool_router().list_all()); for name in ["authenticate", "auth_status"] { assert!( tools.iter().all(|t| &*t.name != name), @@ -2563,6 +2568,18 @@ mod tests { } } + // The user-facing contract of THIS version: only the canister/app tools + // are served; the protocol/meta group is deferred, and the instructions + // say a future version brings it. + #[test] + fn the_default_composition_defers_the_protocol_tools() { + let served = super::IcTools::all_tools(); + assert_eq!(served.len(), 10, "{:?}", served.iter().map(|t| &t.name).collect::>()); + assert!(served.iter().all(|t| !t.name.starts_with("icp_"))); + assert_eq!(super::IcProtocolTools::tool_router().list_all().len(), 16); + assert!(super::SERVER_INSTRUCTIONS.contains("future version")); + } + // The tool surface is split by the scope taxonomy the server instructions // teach: every `icp_`-prefixed (protocol / meta-level) tool lives on // IcProtocolTools, everything app-/canister-scoped on IcCanisterTools — @@ -2586,12 +2603,17 @@ mod tests { t.name ); } + // In this version the served surface is the canister half alone: the + // protocol tools are deferred (we anticipate a future version serves + // them again), but stay intact and non-overlapping for the return. let all = super::IcTools::all_tools(); - assert_eq!(all.len(), canister.len() + protocol.len()); - let mut names: Vec<&str> = all.iter().map(|t| &*t.name).collect(); + assert_eq!(all.len(), canister.len()); + let mut names: Vec<&str> = + canister.iter().map(|t| &*t.name).chain(protocol.iter().map(|t| &*t.name)).collect(); names.sort_unstable(); + let total = names.len(); names.dedup(); - assert_eq!(names.len(), all.len(), "tool names must be unique across the split routers"); + assert_eq!(names.len(), total, "tool names must be unique across the split routers"); } // icp_top_up_canister and icp_create_canister are instructions-only, and @@ -2601,7 +2623,7 @@ mod tests { // Guards against a future edit quietly reverting either. #[test] fn top_up_tool_declares_itself_instructions_only() { - let tools = super::IcTools::all_tools(); + let tools = super::IcProtocolTools::tool_router().list_all(); let tool = tools .iter() .find(|t| &*t.name == "icp_top_up_canister") @@ -2614,7 +2636,7 @@ mod tests { #[test] fn create_tool_declares_itself_instructions_only() { - let tools = super::IcTools::all_tools(); + let tools = super::IcProtocolTools::tool_router().list_all(); let tool = tools .iter() .find(|t| &*t.name == "icp_create_canister") @@ -2631,7 +2653,8 @@ mod tests { // whole surface: a new tool added without an output schema fails here. #[test] fn every_tool_declares_an_object_output_schema() { - let tools = super::IcTools::all_tools(); + let mut tools = super::IcTools::all_tools(); + tools.extend(super::IcProtocolTools::tool_router().list_all()); for t in &tools { let schema = t .output_schema @@ -2649,7 +2672,7 @@ mod tests { // Spot-check icp_find_canister_by_name's schema lists the expected properties. #[test] fn find_canister_declares_output_schema() { - let tools = super::IcTools::all_tools(); + let tools = super::IcProtocolTools::tool_router().list_all(); let tool = tools .iter() .find(|t| &*t.name == "icp_find_canister_by_name") @@ -2836,14 +2859,14 @@ mod tests { // The exact wild-observed guess: multidex.com for MULTI/DEX (multidex.ai). let msg = super::unverified_app_url_error("https://multidex.com"); assert!(msg.contains("NO evidence"), "{msg}"); - assert!(msg.contains("icp_find_app_by_name"), "{msg}"); + assert!(msg.contains("open_app"), "{msg}"); assert!(msg.contains("WEB SEARCH"), "{msg}"); assert!(msg.contains("DID YOU MEAN MULTI/DEX"), "{msg}"); assert!(msg.contains("https://multidex.ai"), "{msg}"); assert!(msg.contains("`derivation_origin`"), "escape hatch missing: {msg}"); // A host resembling no known app still gets the redirect, just no repair. let plain = super::unverified_app_url_error("https://example.com"); - assert!(plain.contains("icp_find_app_by_name"), "{plain}"); + assert!(plain.contains("open_app"), "{plain}"); assert!(!plain.contains("DID YOU MEAN"), "{plain}"); } @@ -2859,11 +2882,11 @@ mod tests { assert!(msg.contains("could not resolve"), "{msg}"); assert!(msg.contains("DID YOU MEAN MULTI/DEX"), "{msg}"); assert!(msg.contains("https://multidex.ai"), "{msg}"); - assert!(msg.contains("icp_find_app_by_name"), "{msg}"); + assert!(msg.contains("open_app"), "{msg}"); // Unrelated hosts: guidance without a bogus suggestion. let plain = super::app_url_error_with_guidance("example.com", "timeout".to_string()); assert!(!plain.contains("DID YOU MEAN"), "{plain}"); - assert!(plain.contains("icp_find_app_by_name"), "{plain}"); + assert!(plain.contains("open_app"), "{plain}"); } // The human-readable identity annotation must surface a requested≠derived diff --git a/crates/imcp2-local/src/server.rs b/crates/imcp2-local/src/server.rs index 11946ab..a16874c 100644 --- a/crates/imcp2-local/src/server.rs +++ b/crates/imcp2-local/src/server.rs @@ -383,8 +383,8 @@ mod tests { "authenticate must declare its refresh flag: {schema}" ); // The merged surface keeps the core contract: every tool declares an - // object-rooted outputSchema (the core suite enforces it for the 26; - // this enforces it for the two added here). + // object-rooted outputSchema (the core suite enforces it for the core + // tools; this enforces it for the two added here). for t in &tools { let schema = t .output_schema @@ -410,7 +410,7 @@ mod tests { // A REAL MCP round-trip over an in-process duplex pipe (the same // byte-stream shape stdio serves): initialize; tools/list is the merged - // surface (all 26 core tools + the 2 login tools); tools/call dispatches + // surface (the 10 served core tools + the 2 login tools); tools/call dispatches // login tools to the wrapper and everything else to IcTools; the login // lifecycle (signed out → link → pending) runs through the MCP layer — // all with no network and no browser. @@ -440,8 +440,8 @@ mod tests { let tools = client.list_all_tools().await.expect("tools/list"); assert_eq!( tools.len(), - 28, - "26 core tools + authenticate + auth_status" + 12, + "10 served core tools + authenticate + auth_status" ); for expected in [ "get_canister_candid", diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 1d3adc3..0f66fcd 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -48,7 +48,7 @@ Verified against the live production deployment (2026-07-31): | Claude Code loopback redirects (RFC 8252) | ✅ loopback redirects are exempt from the hosted allow-list | | Discovery documents (RFC 8414 + RFC 9728, path-scoped + root fallback) | ✅ all four live, `WWW-Authenticate` on the 401 points at the resource metadata | | Every tool: `title` + `readOnlyHint`/`destructiveHint` (+ `idempotentHint`, `openWorldHint`) | ✅ on all 26 tools, enforced by a unit test ([`src/tools.rs`](../src/tools.rs)) | -| No catch-all read/write tool; reads and writes are separate tools | ✅ 19 read-only tools (incl. the instructions-only top-up and creation, [#153](https://github.com/dfinity/imcp2/pull/153)/[#154](https://github.com/dfinity/imcp2/pull/154)); writes split per operation | +| No catch-all read/write tool; reads and writes are separate tools | ✅ 9 of the 10 served tools are read-only; the one write is `canister_update_call`. (The protocol/meta group — incl. the instructions-only funding helpers — is deferred to a future version.) | | Tool names ≤ 64 chars | ✅ longest is 30 | | `outputSchema` + structured content on every tool | ✅ enforced by a unit test | | Certificates from a recognized authority | ✅ Let's Encrypt via Caddy | @@ -145,17 +145,14 @@ transactions**, and the portal's compliance step requires acknowledging this. posture is that the server is not a financial tool (this section assumes both are merged):** -- `icp_top_up_canister` is **instructions-only**: it executes nothing and - moves no funds — it returns the CLI steps for the user to run a top-up - themselves, and the server-side execution path (cycles-ledger `withdraw`, - CMC `notify_top_up`) is removed from the binary - ([#153](https://github.com/dfinity/imcp2/pull/153)). -- `icp_create_canister` is likewise **instructions-only**: creating and - funding a canister spends the user's ICP or cycles, so the tool returns the - icp CLI steps — ending with the controller-handover step that lets the - connector's lifecycle tools manage the new canister — and executes nothing; - the ICP-ledger transfer, CMC notify, and cycles-ledger creation paths are - removed from the binary ([#154](https://github.com/dfinity/imcp2/pull/154)). +- **No funding or management tools are served in this version at all.** The + execution paths that once moved funds are removed from the binary + ([#153](https://github.com/dfinity/imcp2/pull/153) / + [#154](https://github.com/dfinity/imcp2/pull/154) made top-up and creation + instructions-only), and the whole protocol/meta tool group — including those + instructions-only helpers — is deferred from the served surface; we + anticipate it will come in a future version. Creating, funding, and managing + canisters is done by the user with the icp CLI in their own terminal. - `canister_update_call` **refuses the standardized ledger methods** that move value or grant spending rights — the ICRC-standard names (ICRC-1/ICRC-2 plus ICRC-4/-7/-37) on every canister, and the ICP and @@ -254,11 +251,10 @@ Paste-and-adapt; portal limits in parentheses. > canister is, fetch its Candid interface, read app data via typed queries > or OQL, and discover the canisters behind any IC app from its name or URL. > With your consent it can also act as your Internet Identity accounts at a - > specific app, and manage canisters you control: check status, create, - > install code, and start/stop. Financial transactions are not supported: - > token-ledger transfer and approval methods are refused to protect you, - > and cycle top-ups and canister creation return CLI instructions for you - > to run yourself instead of executing anything. + > specific app. Financial transactions are not supported: token-ledger + > transfer and approval methods are refused to protect you, and this version + > includes no funding or canister-management tools — we anticipate those + > will come in a future version. > > On the Internet Identity consent screen you explicitly choose the session > duration (10 minutes to 30 days) and the access level: "Questions only" @@ -302,7 +298,8 @@ Paste-and-adapt; portal limits in parentheses. 1. *"What is canister gftcp-myaaa-aaaar-qcaaa-cai? Who controls it and what's its interface?"* 2. *"Open opencloud.org and list my accounts there."* - 3. *"How do I add cycles to canister gftcp-myaaa-aaaar-qcaaa-cai?"* + 3. *"Does the canister behind https://opencloud.org expose an API doc, and + what does its interface look like?"* 4. *"What canisters are behind https://opencloud.org, and which one holds the app's data?"* @@ -315,17 +312,17 @@ Paste-and-adapt; portal limits in parentheses. > Every read-only tool works with any identity, because it reads public > network state. > 2. On the consent screen pick a session duration and an access level: -> "Questions only" exercises the 19 read-only-annotated tools; "Actions & -> questions" additionally allows state-changing calls. +> "Questions only" exercises the 9 read-only-annotated tools; "Actions & +> questions" additionally allows state-changing calls +> (`canister_update_call`). > 3. Try the example prompts above. Questions-only sessions cause management > tools to return an actionable reconnect message rather than an opaque > error — that behavior is intended. Access is revocable at any time at > https://id.ai/manage/settings. -> 4. The canister-management tools act on canisters you control, so a -> brand-new identity has nothing for them to operate on. Ask us at -> mcp@dfinity.org if you would like an identity provisioned with a -> canister to exercise those. Canister creation and top-ups return CLI -> instructions for the user to run themselves — nothing is executed. +> 4. Canister-management tools are not part of this version (we anticipate +> they will come in a future version), so there is nothing to provision: +> creating and managing canisters happens outside the connector, with the +> icp CLI. > 5. Financial ledger operations are refused by design: asking the assistant > to move tokens returns a policy message directing the user to a wallet > they control — that behavior is intended (financial transactions are @@ -344,8 +341,8 @@ public Internet Computer infrastructure (`icp-api.io`, `id.ai`) and forwards user-directed calls to the application canisters the user names — state exactly that in the acknowledgment. The **prompt-injection** acknowledgment needs open disclosure rather than a bare yes: tool descriptions are static and contain no hidden -instructions, but `icp_list_skills`/`icp_get_skill` (and the `skill://` -resources) intentionally return DFINITY-published how-to documents fetched +instructions, but the `skill://` +resources intentionally return DFINITY-published how-to documents fetched live from `skills.internetcomputer.org` at the user's request — describe this in the submission so reviewers see documented, user-requested functionality rather than covertly pulled behavioral instructions. The rest diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index 71a3a5a..2cd7c4c 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -118,11 +118,13 @@ financial-transfers prohibition: server-level instructions rather than the tool description (which stays free of financial language, per maintainer review) ([#154](https://github.com/dfinity/imcp2/pull/154)), and - `icp_top_up_canister` and `icp_create_canister` are instructions-only — - each returns the icp CLI steps for the user to run themselves and executes - nothing ([#153](https://github.com/dfinity/imcp2/pull/153), - [#154](https://github.com/dfinity/imcp2/pull/154)). README, landing page, - and server instructions state that financial transactions are not + no funding or canister-management tools are served in this version at all — + the execution paths that once moved funds are removed from the binary + ([#153](https://github.com/dfinity/imcp2/pull/153), + [#154](https://github.com/dfinity/imcp2/pull/154)), and the whole + protocol/meta tool group (including the instructions-only funding helpers) + is deferred; we anticipate it will come in a future version. README, landing + page, and server instructions state that financial transactions are not supported. ### 4. Test cases (authoring work) @@ -133,8 +135,9 @@ and mobile. Draft set: Positive: 1. "What is canister gftcp-myaaa-aaaar-qcaaa-cai?" → identifies the canister behind opencloud.org, its controllers and interface. -2. "How do I add cycles to canister gftcp-myaaa-aaaar-qcaaa-cai?" → returns step-by-step icp CLI - instructions; nothing is executed. +2. "Does the canister behind https://opencloud.org expose an API doc, and + what does its interface look like?" → interface + capability flags via + get_canister_candid / get_canister_api_doc. 3. "What canisters are behind https://opencloud.org?" → App Connect discovery returns the app's canisters with provenance. 4. "Open opencloud.org and list my accounts there" (signed in) → resolves diff --git a/src/assets/index.html b/src/assets/index.html index 4e487c4..49287d5 100644 --- a/src/assets/index.html +++ b/src/assets/index.html @@ -405,7 +405,7 @@ \ No newline at end of file From 68441a18b428ea578d54f761770d641c09fe36af Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 17:22:49 +0000 Subject: [PATCH 02/14] Propagate the deferral through the docs, crate doc, and tests Per review-bot findings: the crate-level doc no longer claims IcTools composes both routers; the deferral test asserts a deferred name is not routable through the served router, and imcp2-local's live round-trip now calls icp_get_skill and asserts the method-not-found error (hiding calls, not just listings, is the contract); the submission docs' checklist rows distinguish the 10 served tools from the 16 deferred definitions; reviewer prompts and test cases use only served tools (controllers/status/skill/delete cases replaced); the questions-only notes name canister_update_call; the reviewer-provisioning fallback and the lingering instructions-only phrasing now describe the no-funding-tools-served posture. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- crates/imcp2-core/src/lib.rs | 14 ++++++----- crates/imcp2-core/src/tools.rs | 4 ++++ crates/imcp2-local/src/server.rs | 11 +++++++++ docs/anthropic-directory-submission.md | 32 +++++++++++++++----------- docs/openai-directory-submission.md | 19 ++++++++------- 5 files changed, 50 insertions(+), 30 deletions(-) diff --git a/crates/imcp2-core/src/lib.rs b/crates/imcp2-core/src/lib.rs index 870939d..1a74aad 100644 --- a/crates/imcp2-core/src/lib.rs +++ b/crates/imcp2-core/src/lib.rs @@ -1,12 +1,14 @@ //! imcp2-core — the transport/OAuth-agnostic components of the IMCP2 Internet //! Computer MCP server, from which a serving binary is composed: //! -//! * [`tools::IcTools`] — the complete MCP tool surface, composed from -//! [`tools::IcCanisterTools`] and [`tools::IcProtocolTools`] (canister reads and -//! writes in textual Candid, app/canister discovery, OQL, canister -//! management, IC skills), an [`rmcp`] `ServerHandler` ready to serve over -//! whichever transport the binary picks (streamable-HTTP in the hosted -//! `imcp2` server, stdio in a local one). +//! * [`tools::IcTools`] — the served MCP tool surface: [`tools::IcCanisterTools`] +//! (canister reads and writes in textual Candid, app/canister discovery, +//! OQL), an [`rmcp`] `ServerHandler` ready to serve over whichever +//! transport the binary picks (streamable-HTTP in the hosted `imcp2` +//! server, stdio in a local one). The protocol/meta half +//! ([`tools::IcProtocolTools`] — dashboard lookups, the IC skills tools, +//! canister management) is deferred from the default composition; we +//! anticipate it will come in a future version. //! * [`identities`] — the Internet Identity session engine: the per-connection //! session-key grant (registered with II via `mcp_register_v2`) and the //! short-lived per-app account delegations minted from it on demand. diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index 3e4f5ab..7153826 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -2577,6 +2577,10 @@ mod tests { assert_eq!(served.len(), 10, "{:?}", served.iter().map(|t| &t.name).collect::>()); assert!(served.iter().all(|t| !t.name.starts_with("icp_"))); assert_eq!(super::IcProtocolTools::tool_router().list_all().len(), 16); + // tools/call routes through the canister router alone, so a deferred + // name is not just unlisted — it is not routable at all. + assert!(!super::IcCanisterTools::tool_router().has_route("icp_get_skill")); + assert!(!super::IcCanisterTools::tool_router().has_route("icp_install_code")); assert!(super::SERVER_INSTRUCTIONS.contains("future version")); } diff --git a/crates/imcp2-local/src/server.rs b/crates/imcp2-local/src/server.rs index a16874c..6c981d5 100644 --- a/crates/imcp2-local/src/server.rs +++ b/crates/imcp2-local/src/server.rs @@ -509,6 +509,17 @@ mod tests { assert!(is_error, "an invalid canister id is a tool error"); assert!(text.contains("invalid canister id"), "{text}"); + // Deferral: a protocol/meta tool is not just unlisted — calling it + // fails with the router's method-not-found error rather than running. + let mut params = rmcp::model::CallToolRequestParams::new("icp_get_skill"); + params.arguments = serde_json::json!({ "name": "writing-motoko" }) + .as_object() + .cloned(); + assert!( + client.call_tool(params).await.is_err(), + "deferred protocol tools must not be callable" + ); + client.cancel().await.expect("client shutdown"); server.cancel().await.expect("server shutdown"); } diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 0f66fcd..7c14603 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -47,7 +47,7 @@ Verified against the live production deployment (2026-07-31): | Claude's hosted callback `https://claude.ai/api/mcp/auth_callback` accepted | ✅ seeded in the redirect allow-list ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | Claude Code loopback redirects (RFC 8252) | ✅ loopback redirects are exempt from the hosted allow-list | | Discovery documents (RFC 8414 + RFC 9728, path-scoped + root fallback) | ✅ all four live, `WWW-Authenticate` on the 401 points at the resource metadata | -| Every tool: `title` + `readOnlyHint`/`destructiveHint` (+ `idempotentHint`, `openWorldHint`) | ✅ on all 26 tools, enforced by a unit test ([`src/tools.rs`](../src/tools.rs)) | +| Every tool: `title` + `readOnlyHint`/`destructiveHint` (+ `idempotentHint`, `openWorldHint`) | ✅ on all 10 served tools (and on the 16 deferred definitions), enforced by a unit test ([`src/tools.rs`](../src/tools.rs)) | | No catch-all read/write tool; reads and writes are separate tools | ✅ 9 of the 10 served tools are read-only; the one write is `canister_update_call`. (The protocol/meta group — incl. the instructions-only funding helpers — is deferred to a future version.) | | Tool names ≤ 64 chars | ✅ longest is 30 | | `outputSchema` + structured content on every tool | ✅ enforced by a unit test | @@ -168,9 +168,10 @@ both are merged):** **Posture, stated plainly — the black-and-white answer the compliance step needs:** no tool initiates or executes a transfer of the user's funds. -Financial ledger methods are refused, and the funding operations — top-up and -canister creation — are instructions-only. The financial-transactions -acknowledgment is made on that basis, without qualifications. +Financial ledger methods are refused, and no funding or management tools are +served at all — users run those operations themselves with the icp CLI. The +financial-transactions acknowledgment is made on that basis, without +qualifications. **Status: resolved in code.** An email to (2026-07-31) had asked whether cycles funding and the general-purpose @@ -201,10 +202,11 @@ One tension to be ready for: the review criteria say *"Test credentials are required and must be a fully populated account."* Self-serve sign-up is a reasonable answer for an authentication system nobody can pre-provision into, but a reviewer may still ask for a populated account — most plausibly to -exercise the canister-management tools, which need an identity that actually -controls a canister and holds a cycles balance. If that comes back, the -fallback is a dedicated identity with a recovery phrase in the team vault, a -canister it controls, and a small cycles balance. +exercise `canister_update_call` against an app where the identity has data. +If that comes back, the fallback is a dedicated identity with a recovery +phrase in the team vault and an account at a demo app. (The +canister-management tools that would have needed a controlled canister and a +cycles balance are deferred to a future version.) ### 4. Production is behind `main` @@ -295,8 +297,8 @@ Paste-and-adapt; portal limits in parentheses. - **Allowed link URIs:** none needed (no `ui/open-link` usage). - **Example prompts** (≥3 required; all work with a fresh Questions-only session): - 1. *"What is canister gftcp-myaaa-aaaar-qcaaa-cai? Who controls it and - what's its interface?"* + 1. *"What interface does canister gftcp-myaaa-aaaar-qcaaa-cai expose, + and what can it do?"* 2. *"Open opencloud.org and list my accounts there."* 3. *"Does the canister behind https://opencloud.org expose an API doc, and what does its interface look like?"* @@ -315,9 +317,10 @@ Paste-and-adapt; portal limits in parentheses. > "Questions only" exercises the 9 read-only-annotated tools; "Actions & > questions" additionally allows state-changing calls > (`canister_update_call`). -> 3. Try the example prompts above. Questions-only sessions cause management -> tools to return an actionable reconnect message rather than an opaque -> error — that behavior is intended. Access is revocable at any time at +> 3. Try the example prompts above. On a Questions-only session a +> state-changing call (`canister_update_call`) returns an actionable +> reconnect message rather than an opaque error — that behavior is +> intended. Access is revocable at any time at > https://id.ai/manage/settings. > 4. Canister-management tools are not part of this version (we anticipate > they will come in a future version), so there is nothing to provision: @@ -334,7 +337,8 @@ Topics: directory guidelines, first-party API usage, financial transactions, AI media generation, prompt injection, conversation-data collection, public documentation. **Financial transactions** is a clean acknowledgment: no tool initiates or executes a transfer of the user's funds — financial ledger -methods are refused, and top-up and canister creation are instructions-only. +methods are refused, and no funding or management tools are served (users run +those operations with the icp CLI). **First-party API usage** is answered by describing the architecture as it is: DFINITY operates the connector itself; it reaches the network through public Internet Computer infrastructure (`icp-api.io`, `id.ai`) and forwards diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index 2cd7c4c..0d39f4b 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -54,7 +54,7 @@ add details not published in the docs. | Discovery documents (RFC 8414 AS metadata + RFC 9728 protected-resource) | ✅ all live, path-scoped + root fallback | | ChatGPT's callback `https://chatgpt.com/connector/oauth/{callback_id}` accepted | ✅ the redirect allow-list pins `("chatgpt.com", "/connector/oauth/")` as a prefix ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | No machine-to-machine grants (client credentials etc. unsupported by ChatGPT) | ✅ user-consent authorization-code flow only | -| Tools explicitly annotated `readOnlyHint` / `destructiveHint` / `openWorldHint` — "incorrect or missing action labels are a common cause of rejection" | ✅ set on all 26 tools. The unit test enforces annotation presence and the `readOnlyHint`/`destructiveHint` values; `openWorldHint` is declared everywhere but not asserted by the test, so re-check it in the portal's Scan Tools step | +| Tools explicitly annotated `readOnlyHint` / `destructiveHint` / `openWorldHint` — "incorrect or missing action labels are a common cause of rejection" | ✅ set on all 10 served tools (and the 16 deferred definitions). The unit test enforces annotation presence and the `readOnlyHint`/`destructiveHint` values; `openWorldHint` is declared everywhere but not asserted by the test, so re-check it in the portal's Scan Tools step | | Tool names "human-readable, specific, and descriptive"; accurate descriptions; minimum-information requests | ✅ reviewed against the same bar for the Anthropic listing | | Public HTTPS production endpoint, stable and complete ("trial or demo plugins will not be accepted") | ✅ production deployment | | Privacy policy disclosing "categories of personal data collected, purposes of use, categories of recipients, data retention timelines" | ⏳ **pending the production release**: the rewritten policy matches these four required disclosures exactly and is live on staging, but `https://mcp.internetcomputer.org/privacy-policy` serves nothing until the next `release-*` tag ships it (see the checklist) | @@ -133,8 +133,8 @@ At least 5 positive + 3 negative cases with expected outcomes, passing on web and mobile. Draft set: Positive: -1. "What is canister gftcp-myaaa-aaaar-qcaaa-cai?" → identifies the - canister behind opencloud.org, its controllers and interface. +1. "What interface does canister gftcp-myaaa-aaaar-qcaaa-cai expose?" → + the Candid interface plus capability flags, via get_canister_candid. 2. "Does the canister behind https://opencloud.org expose an API doc, and what does its interface look like?" → interface + capability flags via get_canister_candid / get_canister_api_doc. @@ -142,21 +142,20 @@ Positive: discovery returns the app's canisters with provenance. 4. "Open opencloud.org and list my accounts there" (signed in) → resolves the derivation origin, lists II accounts. -5. "Check the status of canister " (signed in, Actions - session, identity controls it) → run state, cycles, module hash. -6. "Get the Motoko skill" → returns the skill document. +5. "Resolve https://opencloud.org to its Internet Identity derivation + origin" → resolve_app returns the origin and how it was determined. Negative: 1. "Open https://no-ic.example.com" (a reserved domain with no Internet Computer presence) → refused by the IC-evidence gate with guidance (web-search or ask the user for the real URL) rather than resolved to a wrong identity. -2. A management call on a "Questions only" session → actionable - reconnect-with-actions message, not an opaque error. +2. A state-changing call (canister_update_call) on a "Questions only" + session → actionable reconnect-with-actions message, not an opaque error. 3. Any authenticated tool with no sign-in → clean 401 → OAuth flow starts (no crash, no hang). -4. "Delete canister " → the network rejects it; the - error is surfaced legibly. +4. "Call an update method on a canister that rejects this caller" → the + canister/network rejects it; the error is surfaced legibly. ### 5. Decisions for the submitter From 3c91e4187adf5356c20073acef8a8ba90f299f61 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 17:56:06 +0000 Subject: [PATCH 03/14] Keep icp_oql_guide served, on the canister side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the author: the OQL dialect guide is part of the canister read flow (guide -> schema -> query), so it moves from IcProtocolTools to IcCanisterTools and stays on the served surface — the one icp_-named tool that does, called out as the explicit exception in the taxonomy and deferral tests. Served surface: 11 tools; deferred: 15. All the guide references that had been rerouted to the oql://usage resource point back at the tool (the resource remains as before), and the counts in the README, landing page, submission docs, and imcp2-local round-trip are updated to match. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- README.md | 11 ++-- crates/imcp2-core/src/calls.rs | 14 ++-- crates/imcp2-core/src/tools.rs | 88 ++++++++++++++------------ crates/imcp2-local/src/server.rs | 6 +- docs/anthropic-directory-submission.md | 6 +- docs/openai-directory-submission.md | 2 +- src/assets/index.html | 2 +- 7 files changed, 69 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 24c67eb..19c61d8 100644 --- a/README.md +++ b/README.md @@ -149,25 +149,26 @@ results). |------|------|---------| | `open_app` | `app` (name **or** URL) | **One-call entry point** when a user names/links an app: resolves the Internet Identity `derivation_origin` *and* discovers the canisters behind it, together. A name or bare host is matched to the known-app registry first (so a wrong-TLD guess repairs to the canonical URL); an explicit `https://` URL is resolved as given. An unknown bare name, or a URL with no IC evidence, is *refused* (never guessed). Also probes the app's own canisters and reports per-canister `oql`/`api_doc_available` capability flags plus a caller-gated data-access note (which canister holds the app data, and the origin to read it as the user). Wraps `resolve_app` + `discover_app_canisters`; no auth | | `discover_app_canisters` | `domain` | Canister ids behind a web domain — app-declared App Connect metadata first (`/ai-connect.html`'s `ic:canister-id` meta, `/.well-known/ic-app.json` manifest), then the frontend via `x-ic-canister-id` and backend candidates via `/env.json` + JS-bundle mining — each with provenance, its IC dashboard label/type where known, and (for the app's own canisters) `oql`/`api_doc_available` capability flags from a one-shot Candid probe | -| `get_canister_candid` | `canister_id` | The canister's `candid:service` interface (`.did` text), plus two capability flags: `oql` (`true` when it exposes an OQL query surface — a `schema` + `execute` pair — with a pointer to the `oql://usage` resource) and `api_doc_available` (`true` when it declares a `getApiDoc`/`get_api_doc` method, gating `get_canister_api_doc`) | +| `get_canister_candid` | `canister_id` | The canister's `candid:service` interface (`.did` text), plus two capability flags: `oql` (`true` when it exposes an OQL query surface — a `schema` + `execute` pair — with a pointer to `icp_oql_guide`) and `api_doc_available` (`true` when it declares a `getApiDoc`/`get_api_doc` method, gating `get_canister_api_doc`) | | `get_canister_api_doc` | `canister_id` | The canister's own prose API guide ("how this app behaves" — units, auth, lifecycle, mutation safety, polling, gotchas), from its `getApiDoc`/`get_api_doc` method. Call **only** when `get_canister_candid`/`open_app` report `api_doc_available`. Returns a **structured** result in every case — `available` + the doc on success, else `available:false` with `expected`/`retry`/`next` so an expected absence is distinct from an unreachable canister | | `canister_query` | `canister_id`, `method?` **or** `oql?`, `args?` (textual Candid), `derivation_origin?`, `account?`, `candid?` | READ a canister — provide EITHER a Candid `query` `method` (with `args`) OR an `oql` query (a JSON object string, run against `execute`). A Candid `method` query may be anonymous or as your account and returns textual Candid; an `oql` query **requires** `derivation_origin` and returns `columns` + `rows` (a table) with `has_more`, validating `start` against the schema on an empty result. On an OQL canister a Candid `method` query is rejected — use `oql`. `candid` is a fallback: the `.did` interface text to encode/decode against when the canister exposes no `candid:service` metadata. Echoes `derived_for_origin` / `requested` / `acted_as_principal` | | `canister_update_call` | `canister_id`, `method`, `args` (textual Candid), `derivation_origin?`, `account?`, `candid?` | Make an UPDATE (state-changing) call; reply as textual Candid; anonymous, or as your account at an app (identified by its canonical II `derivation_origin`, obtained once from `open_app`/`resolve_app`). **Financial transactions are refused**: the ICRC-standard transfer/approval methods (ICRC-1/ICRC-2 and the ICRC-4/-7/-37 equivalents) are disallowed on every canister, and the ICP and cycles ledgers' own value-moving methods (the legacy `transfer`, `withdraw`, the `create_canister` spends) on those ledgers, to protect the user — the refusal directs the user to act themselves — in a wallet they control (e.g. [oisy.com](https://oisy.com)), or, for canister creation, with the [icp CLI](https://github.com/dfinity/icp-cli) in their own terminal. `candid` is the same `.did` fallback as on `canister_query`, used when the interface isn't published on-chain. Echoes `derived_for_origin` / `requested` / `acted_as_principal` | | `get_app_principal` | `derivation_origin`, `account?` | The principal you act as at an app, without a call. Identify the app by its `derivation_origin` (from `open_app`/`resolve_app`). Echoes `derived_for_origin` / `requested` so an origin mismatch is visible | | `list_app_accounts` | `derivation_origin` | The user's Internet Identity accounts at an app — the default account plus any named ones — with name, number, last-used, and the derivation origin they were listed for. Identify the app by its `derivation_origin` (from `open_app`/`resolve_app`) | | `resolve_app` | `app_url` | Resolve an app URL to its Internet Identity derivation context: `application_origin`, the `derivation_origin` to use (declared in `/.well-known/ic-app.json`, else a built-in known-app value, else assumed = app origin — flagged via `derivation_origin_source`: `declared`/`known`/`app_url_default`, with `application_is_ic` echoing the gateway evidence), and the app's `alternative_origins` (informational). An origin with **no IC evidence** that would need the `app_url_default` assumption is **refused** (guessed-domain guard, with a "did you mean" repair when the host resembles a well-known app). Does not return a principal (no account chosen) or require auth — pass the `derivation_origin` to `get_app_principal`/`list_app_accounts` | +| `icp_oql_guide` | — | The OQL query-surface dialect guide (for canisters where `get_canister_candid` reports `oql: true`): the JSON query object, predicate grammar, edges, and paged result shape. The entity/field names come from `get_canister_oql_schema` and queries run through `canister_query` (the `oql` argument) | | `get_canister_oql_schema` | `canister_id`, `derivation_origin`, `account?` | The canister's OQL schema catalogue (entities, primary keys, fields, edges) as JSON — wraps its `schema` method — plus a ready-to-run `canister_query` example per entity. **`derivation_origin` is required**: the schema is caller-gated, so an anonymous read is rejected (for now) with guidance, rather than returning an empty list | > **Deferred in this version.** The `icp_`-prefixed protocol/meta tools — the > dashboard name/id lookups (`icp_find_canister_by_name`, `icp_find_app_by_name`, > `icp_lookup_canister_info_by_id`), the skills tools (`icp_list_skills`, -> `icp_get_skill`), the OQL guide tool (`icp_oql_guide`), and the +> `icp_get_skill`), and the > canister-management group (`icp_cycles_balance`, the instructions-only > `icp_create_canister` / `icp_top_up_canister`, `icp_install_code`, > `icp_canister_status`, `icp_update_canister_settings`, and the lifecycle > tools) — are not served in this version; **we anticipate this will come in a -> future version.** The OQL dialect guide and the official IC skills remain -> available as MCP resources (`oql://usage`, `skill://`), and the types +> future version.** The official IC skills remain +> available as MCP resources (`skill://`), and the types > stay in the library (`IcProtocolTools`) for embedders and for the return. `open_app` (its `app` argument takes a name **or** a URL) is the one-call entry point @@ -365,7 +366,7 @@ individual layer is small. The OQL query path is JSON rather than Candid, so it shares the same 1 MiB size cap but is parsed by the JSON parser, not this structural guard. Rather than inline the whole dialect into every interface read, `get_canister_candid` emits only that flag plus a -one-line pointer; the full guide is served on demand as +one-line pointer; the full guide is served on demand by `icp_oql_guide` and as the `oql://usage` MCP **resource**. Two tools drive the surface: **`get_canister_oql_schema`** returns the entity/field diff --git a/crates/imcp2-core/src/calls.rs b/crates/imcp2-core/src/calls.rs index 712047d..e54ed0b 100644 --- a/crates/imcp2-core/src/calls.rs +++ b/crates/imcp2-core/src/calls.rs @@ -41,8 +41,8 @@ pub struct GetCandidOutput { /// The Candid (`.did`) interface text. pub candid: String, /// True when the interface exposes the standard OQL query surface (both a - /// `schema` and an `execute` method). When set, load the `oql://usage` - /// resource to learn the JSON query dialect, then call + /// `schema` and an `execute` method). When set, load `icp_oql_guide` (or the + /// `oql://usage` resource) to learn the JSON query dialect, then call /// `get_canister_oql_schema` for the entity/field names and `canister_query` /// (its `oql` argument) to run the query. pub oql: bool, @@ -244,8 +244,8 @@ pub struct CanisterQueryArgs { /// An OQL query as a JSON object string — passed straight to the canister's /// `execute` method, so NO Candid escaping is needed (write plain JSON). E.g. /// `{"start":"employee","where":{"icontains":{"field":"lastName","value":"smith"}},"select":["firstName","lastName"],"limit":10}`. - /// Provide EITHER `oql` OR `method` — not both. See the `oql://usage` resource for - /// the dialect and get_canister_oql_schema for the entity/field names. + /// Provide EITHER `oql` OR `method` — not both. See icp_oql_guide for the dialect + /// and get_canister_oql_schema for the entity/field names. /// The OQL path REQUIRES `derivation_origin` (anonymous per-app reads are disabled). #[serde(default)] pub oql: Option, @@ -823,8 +823,8 @@ pub fn oql_query_redirect(did: Option<&str>) -> Option { if did.is_some_and(has_oql) { Some( "this canister exposes an OQL query surface, so its data is READ with an OQL query, \ - NOT a raw Candid `method` query call. Do this instead, in order: (1) the `oql://usage` \ - resource for the JSON dialect (once), (2) `get_canister_oql_schema` for the entity and field \ + NOT a raw Candid `method` query call. Do this instead, in order: (1) `icp_oql_guide` \ + for the JSON dialect (once), (2) `get_canister_oql_schema` for the entity and field \ names, (3) call `canister_query` again with the `oql` argument (a JSON query object) \ instead of `method`. This canister gates data by the caller's principal, so pass the \ app's `derivation_origin` (from open_app / resolve_app) — an anonymous read is rejected. \ @@ -1854,7 +1854,7 @@ mod tests { // Candid `method` query on an OQL canister → redirected, and the message // names the full guide→schema→query path (#5) plus the auth hint. let msg = oql_query_redirect(Some(oql)).expect("query on OQL canister must be redirected"); - assert!(msg.contains("oql://usage"), "message must point to the OQL guide: {msg}"); + assert!(msg.contains("icp_oql_guide"), "message must point to the OQL guide: {msg}"); assert!(msg.contains("get_canister_oql_schema"), "message must point to the OQL schema tool: {msg}"); assert!(msg.contains("canister_query"), "message must point to canister_query's oql path: {msg}"); assert!(msg.contains("`oql`"), "message must name the oql argument: {msg}"); diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index 7153826..47d5e8f 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -51,8 +51,9 @@ const OQL_PRIMER_MD: &str = include_str!("../static/oql-primer.md"); /// The app- and canister-scoped tools: everything that reads, discovers, or /// calls ONE app or canister (the `…canister…` / `…_app…` names) — Candid and -/// OQL reads, update calls, app resolution/discovery, and the per-app -/// identity tools. This is the surface [`IcTools`] serves in this version. +/// OQL reads (with `icp_oql_guide`, the dialect guide those reads use), +/// update calls, app resolution/discovery, and the per-app identity tools. +/// This is the surface [`IcTools`] serves in this version. #[derive(Clone)] pub struct IcCanisterTools { agent: Agent, @@ -62,9 +63,9 @@ pub struct IcCanisterTools { session: SessionResolver, } -/// The IC protocol / meta-level tools (the `icp_` prefix): dashboard name/id -/// lookups, the official IC skills, the OQL dialect guide, and canister -/// creation/management as the standing management principal. +/// The IC protocol / meta-level tools: dashboard name/id lookups, the +/// official IC skills, and canister creation/management as the standing +/// management principal. /// /// NOT served by the default [`IcTools`] composition in this version — we /// anticipate this will come in a future version. Until then the type, its @@ -158,7 +159,7 @@ impl IcCanisterTools { } #[tool( - description = "Fetch the Candid (.did) interface definition of an Internet Computer canister, read from its public `candid:service` metadata. Also reports two capability flags: `oql` (the canister exposes the OQL query surface — READ it via the `oql://usage` resource → get_canister_oql_schema → canister_query with the `oql` argument, since a Candid `method` query is then rejected) and `api_doc_available` (a `getApiDoc`/`get_api_doc` method exists — call get_canister_api_doc for a prose behavior guide; skip that call when this is false).", + description = "Fetch the Candid (.did) interface definition of an Internet Computer canister, read from its public `candid:service` metadata. Also reports two capability flags: `oql` (the canister exposes the OQL query surface — READ it via icp_oql_guide → get_canister_oql_schema → canister_query with the `oql` argument, since a Candid `method` query is then rejected) and `api_doc_available` (a `getApiDoc`/`get_api_doc` method exists — call get_canister_api_doc for a prose behavior guide; skip that call when this is false).", annotations(title = "Get Candid interface", read_only_hint = true, destructive_hint = false, open_world_hint = true), output_schema = schema_for_output::(), )] @@ -199,14 +200,14 @@ impl IcCanisterTools { notes.push(format!( "This canister exposes an OQL query surface (a JSON query language \ over its data), so READ it through the OQL tools — a Candid `method` \ - query via canister_query is rejected here. Order: the `{OQL_USAGE_URI}` \ - resource (dialect, once) → get_canister_oql_schema (entities/fields) → \ + query via canister_query is rejected here. Order: icp_oql_guide \ + (dialect, once) → get_canister_oql_schema (entities/fields) → \ canister_query with the `oql` argument (run a JSON query, get a table). \ Those wrap the `schema`/`execute` methods (no Candid escaping). Per-app \ data is caller-gated, so the OQL read path REQUIRES the app's \ derivation_origin — an anonymous OQL read is rejected (for now), not \ silently empty; pass the derivation_origin from open_app / resolve_app. \ - See the `{OQL_USAGE_URI}` resource for the dialect. \ + See icp_oql_guide (or the `{OQL_USAGE_URI}` resource) for the dialect. \ canister_update_call then handles UPDATE calls only." )); } @@ -233,7 +234,20 @@ impl IcCanisterTools { } #[tool( - description = "Fetch the OQL schema catalogue of a canister that exposes the OQL surface (get_canister_candid reports `oql: true`): its entities, their primary keys, fields, and edges, as JSON. The MIDDLE step of guide→schema→query: read the `oql://usage` resource once, then call THIS before canister_query so you use the exact entity/field names instead of guessing. Entity names are the schema's own — often PLURAL and different from the Candid types/methods (e.g. `bookings`, not `Booking`/`getBookings`). Returns the schema plus a ready-to-run `canister_query` example per entity (each preserving this call's identity). AUTH: `derivation_origin` is REQUIRED — the schema itself is gated by the caller's principal, so a read with no origin is REJECTED (anonymous per-app reads are disabled for now) with guidance to pass it, rather than returning an empty entity list you'd misread as \"the app has no data model\". Pass the app's canonical `derivation_origin` (from open_app / resolve_app) to read the entities visible to the USER; the reply echoes `derived_for_origin` / `acted_as_principal`.", + description = "Load the OQL query-surface guide: the JSON query dialect for canisters that expose OQL (get_canister_candid reports `oql: true`) — entities/fields/edges via `schema`, and the `execute` query object (filters, aggregation, ordering, edge traversal, paging). This is step ONE of the fixed sequence guide→schema→query: read this once, then `get_canister_oql_schema` for the exact entity/field names (they are the schema's own — often PLURAL and unlike the Candid types/methods, e.g. `bookings` not `Booking`/`getBookings`), then `canister_query` with the `oql` argument. Never guess bespoke per-question methods. The schema read and the query REQUIRE the app's `derivation_origin` (from open_app / resolve_app) — anonymous per-app reads are disabled for now and are rejected with guidance. Both wrap the `schema`/`execute` methods, so you write plain JSON — no Candid escaping.", + annotations(title = "Get the OQL query guide", read_only_hint = true, destructive_hint = false, open_world_hint = false), + output_schema = schema_for_output::(), + )] + async fn icp_oql_guide( + &self, + Parameters(_args): Parameters, + ) -> Result { + let output = calls::OqlGuideOutput { content: OQL_PRIMER_MD.to_string() }; + Ok(ok_structured(output.content.clone(), &output)) + } + + #[tool( + description = "Fetch the OQL schema catalogue of a canister that exposes the OQL surface (get_canister_candid reports `oql: true`): its entities, their primary keys, fields, and edges, as JSON. The MIDDLE step of guide→schema→query: read `icp_oql_guide` once, then call THIS before canister_query so you use the exact entity/field names instead of guessing. Entity names are the schema's own — often PLURAL and different from the Candid types/methods (e.g. `bookings`, not `Booking`/`getBookings`). Returns the schema plus a ready-to-run `canister_query` example per entity (each preserving this call's identity). AUTH: `derivation_origin` is REQUIRED — the schema itself is gated by the caller's principal, so a read with no origin is REJECTED (anonymous per-app reads are disabled for now) with guidance to pass it, rather than returning an empty entity list you'd misread as \"the app has no data model\". Pass the app's canonical `derivation_origin` (from open_app / resolve_app) to read the entities visible to the USER; the reply echoes `derived_for_origin` / `acted_as_principal`.", annotations(title = "Get the OQL schema", read_only_hint = true, destructive_hint = false, open_world_hint = true), output_schema = schema_for_output::(), )] @@ -1255,19 +1269,6 @@ impl IcProtocolTools { (self.session)(ctx) } - #[tool( - description = "Load the OQL query-surface guide: the JSON query dialect for canisters that expose OQL (get_canister_candid reports `oql: true`) — entities/fields/edges via `schema`, and the `execute` query object (filters, aggregation, ordering, edge traversal, paging). This is step ONE of the fixed sequence guide→schema→query: read this once, then `get_canister_oql_schema` for the exact entity/field names (they are the schema's own — often PLURAL and unlike the Candid types/methods, e.g. `bookings` not `Booking`/`getBookings`), then `canister_query` with the `oql` argument. Never guess bespoke per-question methods. The schema read and the query REQUIRE the app's `derivation_origin` (from open_app / resolve_app) — anonymous per-app reads are disabled for now and are rejected with guidance. Both wrap the `schema`/`execute` methods, so you write plain JSON — no Candid escaping.", - annotations(title = "Get the OQL query guide", read_only_hint = true, destructive_hint = false, open_world_hint = false), - output_schema = schema_for_output::(), - )] - async fn icp_oql_guide( - &self, - Parameters(_args): Parameters, - ) -> Result { - let output = calls::OqlGuideOutput { content: OQL_PRIMER_MD.to_string() }; - Ok(ok_structured(output.content.clone(), &output)) - } - #[tool( description = "Find Internet Computer canisters by NAME. Searches the IC dashboard's service registries — the ICRC token ledgers (e.g. ckBTC, ckETH, ckUSDC, SNS tokens) by symbol/name, and the SNS project catalog by name — and returns matching canister ids. Use this when the user names a token, project, or service (e.g. \"ckUSDC\") rather than a canister id; then confirm with get_canister_candid, read with canister_query, and write with canister_update_call. (No public name-search exists over arbitrary canisters; this covers the IC's labelled services.)", annotations(title = "Find canisters by name", read_only_hint = true, destructive_hint = false, open_world_hint = true), @@ -1990,12 +1991,13 @@ const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TE act on a whole APP, keyed by its Internet Identity derivation origin or app URL, and \ `…canister…` names (`get_canister_candid`, `get_canister_api_doc`, \ `get_canister_oql_schema`, `canister_query`, `canister_update_call`) act on ONE \ - specific canister. (A further `icp_`-prefixed group of IC protocol / meta-level \ - tools — dashboard name/id lookups, the official IC skills as tools, and canister \ - creation/management — is not part of this version; we anticipate this will come in \ - a future version.) Before writing Candid args, consult the `candid://textual-syntax` \ + specific canister; `icp_oql_guide` serves the OQL dialect those reads use. (A \ + further group of IC protocol / meta-level tools — dashboard name/id lookups, the \ + official IC skills as tools, and canister creation/management — is not part of this \ + version; we anticipate this will come in a future version.) Before writing Candid \ + args, consult the `candid://textual-syntax` \ resource (the value syntax these tools use); `candid://reference` has the full type \ - reference, the OQL dialect guide is the `oql://usage` resource, and the official IC \ + reference, and the official IC \ skills are served as `skill://` resources.\n\n\ FINANCIAL TRANSACTIONS ARE NOT SUPPORTED — asset-moving requests are denied, to \ protect the user: canister_update_call refuses the ICRC-standard transfer/approval \ @@ -2034,8 +2036,8 @@ const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TE public canisters like ledgers.)\n\n\ INSPECTING A CANISTER. `get_canister_candid` fetches the interface and reports two \ capability flags: `oql` and `api_doc_available` (open_app reports the same per \ - canister). If `oql: true`, READ the canister via OQL, in order: the `oql://usage` \ - resource (the JSON dialect, once) → `get_canister_oql_schema` (the entities and fields) → \ + canister). If `oql: true`, READ the canister via OQL, in order: `icp_oql_guide` (the \ + JSON dialect, once) → `get_canister_oql_schema` (the entities and fields) → \ `canister_query` with the `oql` argument \ (run a JSON query, get a table). These wrap the canister's `schema`/`execute` methods, \ so you never hand-encode Candid for OQL — and on an OQL canister a Candid `method` query \ @@ -2470,7 +2472,7 @@ mod tests { #[test] fn every_tool_has_correct_read_write_annotations() { let served = super::IcTools::all_tools(); - assert_eq!(served.len(), 10, "expected 10 served tools, got {}", served.len()); + assert_eq!(served.len(), 11, "expected 11 served tools, got {}", served.len()); // The deferred protocol half keeps its annotation contracts too, so // wiring it back in a future version can't regress them. let mut tools = served; @@ -2574,9 +2576,14 @@ mod tests { #[test] fn the_default_composition_defers_the_protocol_tools() { let served = super::IcTools::all_tools(); - assert_eq!(served.len(), 10, "{:?}", served.iter().map(|t| &t.name).collect::>()); - assert!(served.iter().all(|t| !t.name.starts_with("icp_"))); - assert_eq!(super::IcProtocolTools::tool_router().list_all().len(), 16); + assert_eq!(served.len(), 11, "{:?}", served.iter().map(|t| &t.name).collect::>()); + // icp_oql_guide is the one icp_-named tool that stays served: it is + // part of the canister OQL read flow (guide → schema → query). + assert!(served + .iter() + .all(|t| !t.name.starts_with("icp_") || &*t.name == "icp_oql_guide")); + assert!(served.iter().any(|t| &*t.name == "icp_oql_guide")); + assert_eq!(super::IcProtocolTools::tool_router().list_all().len(), 15); // tools/call routes through the canister router alone, so a deferred // name is not just unlisted — it is not routable at all. assert!(!super::IcCanisterTools::tool_router().has_route("icp_get_skill")); @@ -2584,18 +2591,19 @@ mod tests { assert!(super::SERVER_INSTRUCTIONS.contains("future version")); } - // The tool surface is split by the scope taxonomy the server instructions - // teach: every `icp_`-prefixed (protocol / meta-level) tool lives on - // IcProtocolTools, everything app-/canister-scoped on IcCanisterTools — - // no overlap, nothing dropped, and the composed list is what tools/list - // serves. + // The tool surface is split by scope: the protocol / meta-level tools + // live on IcProtocolTools, everything app-/canister-scoped on + // IcCanisterTools — no overlap, nothing dropped, and the composed list is + // what tools/list serves. icp_oql_guide is the one icp_-named exception + // on the canister side: it serves the OQL dialect the canister read flow + // (guide → schema → query) depends on, so it belongs with those tools. #[test] fn the_split_follows_the_icp_prefix_taxonomy() { let canister = super::IcCanisterTools::tool_router().list_all(); let protocol = super::IcProtocolTools::tool_router().list_all(); for t in &canister { assert!( - !t.name.starts_with("icp_"), + !t.name.starts_with("icp_") || &*t.name == "icp_oql_guide", "{} is icp_-prefixed and belongs on IcProtocolTools", t.name ); diff --git a/crates/imcp2-local/src/server.rs b/crates/imcp2-local/src/server.rs index 6c981d5..86b3b76 100644 --- a/crates/imcp2-local/src/server.rs +++ b/crates/imcp2-local/src/server.rs @@ -410,7 +410,7 @@ mod tests { // A REAL MCP round-trip over an in-process duplex pipe (the same // byte-stream shape stdio serves): initialize; tools/list is the merged - // surface (the 10 served core tools + the 2 login tools); tools/call dispatches + // surface (the 11 served core tools + the 2 login tools); tools/call dispatches // login tools to the wrapper and everything else to IcTools; the login // lifecycle (signed out → link → pending) runs through the MCP layer — // all with no network and no browser. @@ -440,8 +440,8 @@ mod tests { let tools = client.list_all_tools().await.expect("tools/list"); assert_eq!( tools.len(), - 12, - "10 served core tools + authenticate + auth_status" + 13, + "11 served core tools + authenticate + auth_status" ); for expected in [ "get_canister_candid", diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 7c14603..26406fa 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -47,8 +47,8 @@ Verified against the live production deployment (2026-07-31): | Claude's hosted callback `https://claude.ai/api/mcp/auth_callback` accepted | ✅ seeded in the redirect allow-list ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | Claude Code loopback redirects (RFC 8252) | ✅ loopback redirects are exempt from the hosted allow-list | | Discovery documents (RFC 8414 + RFC 9728, path-scoped + root fallback) | ✅ all four live, `WWW-Authenticate` on the 401 points at the resource metadata | -| Every tool: `title` + `readOnlyHint`/`destructiveHint` (+ `idempotentHint`, `openWorldHint`) | ✅ on all 10 served tools (and on the 16 deferred definitions), enforced by a unit test ([`src/tools.rs`](../src/tools.rs)) | -| No catch-all read/write tool; reads and writes are separate tools | ✅ 9 of the 10 served tools are read-only; the one write is `canister_update_call`. (The protocol/meta group — incl. the instructions-only funding helpers — is deferred to a future version.) | +| Every tool: `title` + `readOnlyHint`/`destructiveHint` (+ `idempotentHint`, `openWorldHint`) | ✅ on all 11 served tools (and on the 15 deferred definitions), enforced by a unit test ([`src/tools.rs`](../src/tools.rs)) | +| No catch-all read/write tool; reads and writes are separate tools | ✅ 10 of the 11 served tools are read-only; the one write is `canister_update_call`. (The protocol/meta group — incl. the instructions-only funding helpers — is deferred to a future version.) | | Tool names ≤ 64 chars | ✅ longest is 30 | | `outputSchema` + structured content on every tool | ✅ enforced by a unit test | | Certificates from a recognized authority | ✅ Let's Encrypt via Caddy | @@ -314,7 +314,7 @@ Paste-and-adapt; portal limits in parentheses. > Every read-only tool works with any identity, because it reads public > network state. > 2. On the consent screen pick a session duration and an access level: -> "Questions only" exercises the 9 read-only-annotated tools; "Actions & +> "Questions only" exercises the 10 read-only-annotated tools; "Actions & > questions" additionally allows state-changing calls > (`canister_update_call`). > 3. Try the example prompts above. On a Questions-only session a diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index 0d39f4b..1daea95 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -54,7 +54,7 @@ add details not published in the docs. | Discovery documents (RFC 8414 AS metadata + RFC 9728 protected-resource) | ✅ all live, path-scoped + root fallback | | ChatGPT's callback `https://chatgpt.com/connector/oauth/{callback_id}` accepted | ✅ the redirect allow-list pins `("chatgpt.com", "/connector/oauth/")` as a prefix ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | No machine-to-machine grants (client credentials etc. unsupported by ChatGPT) | ✅ user-consent authorization-code flow only | -| Tools explicitly annotated `readOnlyHint` / `destructiveHint` / `openWorldHint` — "incorrect or missing action labels are a common cause of rejection" | ✅ set on all 10 served tools (and the 16 deferred definitions). The unit test enforces annotation presence and the `readOnlyHint`/`destructiveHint` values; `openWorldHint` is declared everywhere but not asserted by the test, so re-check it in the portal's Scan Tools step | +| Tools explicitly annotated `readOnlyHint` / `destructiveHint` / `openWorldHint` — "incorrect or missing action labels are a common cause of rejection" | ✅ set on all 11 served tools (and the 15 deferred definitions). The unit test enforces annotation presence and the `readOnlyHint`/`destructiveHint` values; `openWorldHint` is declared everywhere but not asserted by the test, so re-check it in the portal's Scan Tools step | | Tool names "human-readable, specific, and descriptive"; accurate descriptions; minimum-information requests | ✅ reviewed against the same bar for the Anthropic listing | | Public HTTPS production endpoint, stable and complete ("trial or demo plugins will not be accepted") | ✅ production deployment | | Privacy policy disclosing "categories of personal data collected, purposes of use, categories of recipients, data retention timelines" | ⏳ **pending the production release**: the rewritten policy matches these four required disclosures exactly and is live on staging, but `https://mcp.internetcomputer.org/privacy-policy` serves nothing until the next `release-*` tag ships it (see the checklist) | diff --git a/src/assets/index.html b/src/assets/index.html index 49287d5..7fa74cb 100644 --- a/src/assets/index.html +++ b/src/assets/index.html @@ -405,7 +405,7 @@ \ No newline at end of file From f52deb2f01bb27ddedd0d7e82e0cfb3e4a5fd565 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 18:05:18 +0000 Subject: [PATCH 04/14] Point remaining served-surface guidance away from deferred tools Three strings still reachable from the served surface named deferred tools (review findings on #158): - resolve_app's app_url input-schema doc said the URL may come from icp_find_app_by_name; it now points at open_app / web search. - The unknown-skill error behind skill:// resource reads said to call icp_list_skills; it now says to list the skill:// resources. - get_app_principal's Questions-only note listed the management verbs and icp_canister_status; it now names canister_update_call, matching the server instructions. READ_ONLY_MSG in identities.rs keeps its management wording: it is only reachable through the deferred management tools (require_write call sites), where that wording stays accurate. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- crates/imcp2-core/src/identities.rs | 8 ++++---- crates/imcp2-core/src/skills.rs | 2 +- crates/imcp2-core/src/tools.rs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/imcp2-core/src/identities.rs b/crates/imcp2-core/src/identities.rs index 891110d..57caafa 100644 --- a/crates/imcp2-core/src/identities.rs +++ b/crates/imcp2-core/src/identities.rs @@ -450,10 +450,10 @@ pub struct AccountsOutput { #[derive(Debug, Deserialize, schemars::JsonSchema)] pub struct ResolveAppArgs { /// The application's URL, e.g. "https://oisy.com". Must be a URL you actually - /// have — given by the user, returned by `icp_find_app_by_name`, or found by a - /// web search of the app's official site. NEVER a domain guessed from an app's - /// name (guessed lookalike domains are unrelated or squatted sites and are - /// refused). + /// have — given by the user, shown by `open_app` for a well-known app name, or + /// found by a web search of the app's official site. NEVER a domain guessed + /// from an app's name (guessed lookalike domains are unrelated or squatted + /// sites and are refused). pub app_url: String, } diff --git a/crates/imcp2-core/src/skills.rs b/crates/imcp2-core/src/skills.rs index 27358e1..b6941d6 100644 --- a/crates/imcp2-core/src/skills.rs +++ b/crates/imcp2-core/src/skills.rs @@ -209,7 +209,7 @@ impl SkillsCatalog { let entry = skills.iter().find(|s| s.name.eq_ignore_ascii_case(name)); if entry.is_none() { return Err(format!( - "no skill named `{name}` — call icp_list_skills to see the available skills" + "no skill named `{name}` — list the `skill://` resources to see the available skills" )); } // Use the manifest's markdown URL only when it stays on the configured diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index 47d5e8f..35bced0 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -932,15 +932,15 @@ impl IcCanisterTools { Err(e) => return Ok(err(format!("could not derive principal for {}: {e}", target.origin))), }; // Surface a query-only session (H2) so the LLM won't attempt (and have the - // IC reject at ingress) canister-management updates. + // IC reject at ingress) state-changing update calls. let read_only = self.identities.is_read_only(&session_id).await == Some(true); let mut text = format!("{principal}\n\n[{}]", identity_annotation(&target, None)); if read_only { text.push_str( "\n\n(This Internet Identity session was authorized for \"Questions only\": reads work, \ - but canister management — create/install/start/stop/delete, and icp_canister_status — \ - needs update access. Ask the user to reconnect and choose \"Actions & questions\" on \ - Internet Identity's consent screen.)", + but state-changing calls (canister_update_call) are rejected by the network. Ask the \ + user to reconnect and choose \"Actions & questions\" on Internet Identity's consent \ + screen.)", ); } let output = identities::PrincipalOutput { From 27cd70a3669bfb7cd0abc8219ab8c135896d6d3b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 18:08:22 +0000 Subject: [PATCH 05/14] State what re-enabling the deferred router actually takes The IcTools doc claimed re-enabling IcProtocolTools "is one line in the composition", but IcTools no longer retains a protocol instance: wiring it back touches the struct, constructor, all_tools, call_tool, and get_tool. Say so (and note the direct-serving alternative) instead of overpromising. Doc-only; flagged by review on #158. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- crates/imcp2-core/src/tools.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index 35bced0..a4e4f53 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -85,7 +85,9 @@ pub struct IcProtocolTools { /// [`ServerHandler`], plus the candid/OQL/skill resources. The IC protocol / /// meta-level half ([`IcProtocolTools`]) is split out and deliberately NOT /// served in this version — we anticipate this will come in a future version; -/// re-enabling it is one line in the composition here. +/// re-enabling it means composing its router back into `all_tools`, +/// `call_tool`, and `get_tool` here (or serving [`IcProtocolTools`] directly, +/// which stays fully usable as library code). #[derive(Clone)] pub struct IcTools { canister: IcCanisterTools, From 23048782e168c8118b31c7dc552537a0e61ad4cf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 18:14:16 +0000 Subject: [PATCH 06/14] Pin the deferred-call error to the router's actual shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The imcp2-local deferral regression only asserted is_err(), so a disconnect or an unrelated error would have passed it (review finding on #158). It now expects the exact error the served router emits on a miss — code -32602 (invalid params) with "tool not found" — which is rmcp's shape for ANY unknown tool name, so deferred tools stay indistinguishable from nonexistent ones. The comments (and the IcTools dispatch note) called this "method-not-found"; corrected, since the router never emits -32601 for a miss. Also stop claiming IcProtocolTools can be served "directly": it has no ServerHandler impl, so an embedder dispatches its router from their own handler. The IcTools doc now says that. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- crates/imcp2-core/src/tools.rs | 8 +++++--- crates/imcp2-local/src/server.rs | 20 +++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index a4e4f53..ed006df 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -86,8 +86,9 @@ pub struct IcProtocolTools { /// meta-level half ([`IcProtocolTools`]) is split out and deliberately NOT /// served in this version — we anticipate this will come in a future version; /// re-enabling it means composing its router back into `all_tools`, -/// `call_tool`, and `get_tool` here (or serving [`IcProtocolTools`] directly, -/// which stays fully usable as library code). +/// `call_tool`, and `get_tool` here (or dispatching [`IcProtocolTools`]'s +/// router from an embedder's own [`ServerHandler`] — the type has no handler +/// of its own but stays fully usable as library code). #[derive(Clone)] pub struct IcTools { canister: IcCanisterTools, @@ -2138,7 +2139,8 @@ impl ServerHandler for IcTools { ) -> Result { // Only the canister/app router is served in this version (the // protocol/meta tools are deferred); a miss produces the router's - // standard method-not-found error. + // standard "tool not found" (invalid-params) error — the same shape + // any unknown tool name gets. let tcc = ToolCallContext::new(&self.canister, request, context); IcCanisterTools::tool_router().call(tcc).await } diff --git a/crates/imcp2-local/src/server.rs b/crates/imcp2-local/src/server.rs index 86b3b76..2cb8b76 100644 --- a/crates/imcp2-local/src/server.rs +++ b/crates/imcp2-local/src/server.rs @@ -510,15 +510,25 @@ mod tests { assert!(text.contains("invalid canister id"), "{text}"); // Deferral: a protocol/meta tool is not just unlisted — calling it - // fails with the router's method-not-found error rather than running. + // fails with the router's standard "tool not found" error (the + // invalid-params shape any unknown tool name gets), rather than + // running — and rather than dying some other way: a disconnect or a + // different error must not pass this regression. let mut params = rmcp::model::CallToolRequestParams::new("icp_get_skill"); params.arguments = serde_json::json!({ "name": "writing-motoko" }) .as_object() .cloned(); - assert!( - client.call_tool(params).await.is_err(), - "deferred protocol tools must not be callable" - ); + let err = client + .call_tool(params) + .await + .expect_err("deferred protocol tools must not be callable"); + match err { + rmcp::ServiceError::McpError(e) => { + assert_eq!(e.code, rmcp::model::ErrorCode::INVALID_PARAMS, "{e:?}"); + assert!(e.message.contains("tool not found"), "{e:?}"); + } + other => panic!("expected the router's tool-not-found error, got {other:?}"), + } client.cancel().await.expect("client shutdown"); server.cancel().await.expect("server shutdown"); From 0954cd6dbf43a1caac158fc891a62ac98db63cdc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 18:24:34 +0000 Subject: [PATCH 07/14] Propagate the deferral through the remaining prose surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round on #158 found deferred-group references still written as served behavior in surfaces the earlier sweeps missed: - README's typical-flow tail: managing your own canisters is now described as deferred (future version), with the icp CLI as the current path. - Privacy policy (served asset + draft): the own-origin stable identity exists only for the canister-management tools, which are not part of this version — both copies now say so instead of describing the derivation as live; the identifier-linkage section no longer claims a stable identity is held. - scoping-local-deployment.md: the session-free local tool list drops the skills/lookup tools (deferred; skill documents remain skill:// resources) and gains icp_oql_guide; the security model separates what the served surface wields (per-app update calls) from what the deferred management tools would add if an embedder serves them. - OpenAI submission fallback provisioning matches the Anthropic doc: demo-app account, no controlled canister / cycles balance (the tools that needed them are deferred). - Anthropic checklist row: provision a demo-app account, not funding. - Taxonomy test header no longer claims the composed list is what tools/list serves (the canister half alone is, in this version). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- README.md | 7 ++++--- crates/imcp2-core/src/tools.rs | 5 +++-- docs/anthropic-directory-submission.md | 2 +- docs/icp-mcp-privacy-policy-draft.md | 17 ++++++++++------- docs/openai-directory-submission.md | 4 +++- docs/scoping-local-deployment.md | 12 ++++++++---- src/assets/privacy-policy.html | 17 ++++++++++------- 7 files changed, 39 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 19c61d8..5c7c7fd 100644 --- a/README.md +++ b/README.md @@ -219,9 +219,10 @@ Genuinely public reads via a `canister_query` Candid `method` query or the public-metadata tools (`get_canister_candid`, `discover_app_canisters`) skip steps 3/4 and need no origin; OQL reads always require one. The per-canister inspection (5) is independent of the identity steps (3/4), so they can run in parallel. Managing your -**own** canisters (the `icp_*` create/install/status/… tools) acts as your standing -**management principal** at this server's origin — a *different* identity than the -per-app principals above. +**own** canisters is not part of this version — the `icp_*` create/install/status/… tools +are deferred (we anticipate they will come in a future version, acting as a standing +**management principal** at this server's origin, a *different* identity than the +per-app principals above); today, create and manage canisters with the icp CLI. ### App-declared canister metadata (App Connect) diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index ed006df..bdabca7 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -2597,8 +2597,9 @@ mod tests { // The tool surface is split by scope: the protocol / meta-level tools // live on IcProtocolTools, everything app-/canister-scoped on - // IcCanisterTools — no overlap, nothing dropped, and the composed list is - // what tools/list serves. icp_oql_guide is the one icp_-named exception + // IcCanisterTools — no overlap, nothing dropped: every definition lives + // on exactly one router, while tools/list serves the canister half alone + // in this version. icp_oql_guide is the one icp_-named exception // on the canister side: it serves the OQL dialect the canister read flow // (guide → schema → query) depends on, so it belongs with those tools. #[test] diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 26406fa..b5e4964 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -357,7 +357,7 @@ conversation beyond tool arguments and generates no media. - [ ] Dedicated ICP MCP privacy policy live at `https://mcp.internetcomputer.org/privacy-policy` (page + landing-page link merged; needs the production release) and entered in the portal (blocker 1) - [ ] Reply received from mcp-review@anthropic.com settling the financial-transactions and first-party-API acknowledgments (asked 2026-07-31; blocker 2) -- [x] Reviewer access settled: self-serve Internet Identity, instructions in the test-credentials field (blocker 3) — provision a funded identity only if a reviewer asks +- [x] Reviewer access settled: self-serve Internet Identity, instructions in the test-credentials field (blocker 3) — if a reviewer asks for a populated account, provision a demo-app account (no funding: the tools that needed it are deferred) - [ ] `release-*` tag cut; `/version` on production shows the intended commit (blocker 4) - [x] Square PNG icon exported — `docs/assets/icp-logo-{1024,512}.png` (blocker 5) - [ ] Every tool exercised once by the submitter (portal asks you to confirm this; MCP Inspector or a custom connector in Claude both count) diff --git a/docs/icp-mcp-privacy-policy-draft.md b/docs/icp-mcp-privacy-policy-draft.md index 922cc38..87b3d4e 100644 --- a/docs/icp-mcp-privacy-policy-draft.md +++ b/docs/icp-mcp-privacy-policy-draft.md @@ -138,9 +138,10 @@ keeps, for the remainder of the session: the application's domain, the account number used, the per-application key it generated, and Internet Identity's signed authorization for that application (itself valid for at most one hour). Keeping this avoids re-deriving an authorization on every -call to the same application. The Service's own origin is treated as one -such application when you use the canister-management tools, and the -identity derived for it is stable across your connections. All of this is +call to the same application. (The canister-management tools, which would +treat the Service's own origin as one such application with an identity that +is stable across your connections, are not part of this version — we +anticipate they will come in a future version.) All of this is held in memory only, bounded in size, and discarded when the session ends or the Service restarts. Legal basis: performance of the service you requested. @@ -289,10 +290,12 @@ Internet Identity gives each application a different identity for you, so applications cannot recognise you across applications. The Service is a participant in that design and we want to be plain about what it can see. While a session is live it holds, in memory, the per-application identities -it has derived for you, including a stable identity for you at its own -origin that the canister-management tools act as; software holding that -stable identity could in principle associate separate sessions with the same -user. Three things limit that in practice: none of these identifiers is +it has derived for you. (The canister-management tools, which would also +derive a stable identity for you at the Service's own origin — one that +software holding it could in principle use to associate separate sessions +with the same user — are not part of this version; we anticipate they will +come in a future version.) Three things limit what the Service could link +in practice: none of these identifiers is written to logs, which carry only a per-connection identifier that is new for every connection (section 5); the applications you visit are not written to logs either; and nothing the Service discloses to an application lets diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index 1daea95..00b622e 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -93,7 +93,9 @@ and self-serve. Position to state in the Testing tab: reviewers create their own Internet Identity in under a minute (instructions on the landing page); every read-only tool works with any identity because it reads public network state. Have the fallback ready (a provisioned identity with a recovery -phrase, a controlled canister, and a cycles balance) if review pushes back — +phrase in the team vault and an account at a demo app — the +canister-management tools that would have needed a controlled canister and a +cycles balance are deferred to a future version) if review pushes back — and expect a higher chance of push-back than at Anthropic given the login-and-password wording. diff --git a/docs/scoping-local-deployment.md b/docs/scoping-local-deployment.md index 2c35576..fcdb135 100644 --- a/docs/scoping-local-deployment.md +++ b/docs/scoping-local-deployment.md @@ -466,8 +466,10 @@ middleware; a stdio pipe has no per-request middleware at all), so the middlewar with their transports and core only receives their outcome. Tools that are already session-free work unchanged locally: `get_canister_candid`, -`get_canister_api_doc`, `open_app`, `resolve_app`, `discover_app_canisters`, the skills/lookup -tools, and the anonymous path of `canister_query`. +`get_canister_api_doc`, `open_app`, `resolve_app`, `discover_app_canisters`, `icp_oql_guide`, +and the anonymous path of `canister_query`. (The skills and lookup tools are deferred from +the served surface — we anticipate they will come in a future version; the skill documents +stay served as `skill://` resources.) ### 8. Security model @@ -478,8 +480,10 @@ the transient login listener (component 5), and reaching it confers nothing: the page and the `#4091` well-known are static, and `/redeem` only advances the connect this process started (`state` must match) with a chain targeting the in-process `X` — it can neither invoke tools nor read the session. But the client then wields the user's -**real production II accounts** on mainnet (canister create/install/start/stop/delete, any -update call / cycles spend, per-app delegations for every origin). This must be stated plainly: +**real production II accounts** on mainnet (any update call under a per-app delegation, for +every origin — and the deferred management tools, if an embedder serves them, add canister +create/install/start/stop/delete and cycles spend as the management principal). This must be +stated plainly: **treat the binary and its client config like a wallet.** There is no revocable token — only the II grant (reconnect/expiry). diff --git a/src/assets/privacy-policy.html b/src/assets/privacy-policy.html index e79a835..3773155 100644 --- a/src/assets/privacy-policy.html +++ b/src/assets/privacy-policy.html @@ -272,9 +272,10 @@

1. Data Categories Processed, Purposes, and Legal Bases

domain, the account number used, the per-application key it generated, and Internet Identity's signed authorization for that application (itself valid for at most one hour). Keeping this avoids re-deriving an authorization on -every call to the same application. The Service's own origin is treated as -one such application when you use the canister-management tools, and the -identity derived for it is stable across your connections. All of this is +every call to the same application. (The canister-management tools, which +would treat the Service's own origin as one such application with an +identity that is stable across your connections, are not part of this +version — we anticipate they will come in a future version.) All of this is held in memory only, bounded in size, and discarded when the session ends or the Service restarts. Legal basis: performance of the service you requested.

@@ -449,10 +450,12 @@

6. Identifiers, and What They Could Link

applications cannot recognise you across applications. The Service is a participant in that design and we want to be plain about what it can see. While a session is live it holds, in memory, the per-application identities -it has derived for you, including a stable identity for you at its own origin -that the canister-management tools act as; software holding that stable -identity could in principle associate separate sessions with the same user. -Three things limit that in practice: none of these identifiers is written to +it has derived for you. (The canister-management tools, which would also +derive a stable identity for you at the Service's own origin — one that +software holding it could in principle use to associate separate sessions +with the same user — are not part of this version; we anticipate they will +come in a future version.) Three things limit what the Service could link +in practice: none of these identifiers is written to logs, which carry only a per-connection identifier that is new for every connection (section 5); the applications you visit are not written to logs either; and nothing the Service discloses to an application lets that From 787dd23f6d3e088fa5b43d252d546bb3f72debcd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 18:31:54 +0000 Subject: [PATCH 08/14] Describe the Questions-only rejection path the served write actually has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit canister_update_call carries no read-only preflight: on a Questions-only session it dispatches the update and reports the network's ingress rejection as a failed call (the require_write preflight belongs to the deferred management tools). The submission docs' reviewer steps claimed the tool itself returns a reconnect message; they now describe the real path — the call fails and the server instructions prime the assistant to explain the access level and recommend reconnecting. Also: the management principal is per-user and stable across connections (identities.rs management_identity), not per-connection — README now says so, matching the privacy-policy wording. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- README.md | 2 +- docs/anthropic-directory-submission.md | 8 +++++--- docs/openai-directory-submission.md | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5c7c7fd..e5d0908 100644 --- a/README.md +++ b/README.md @@ -398,7 +398,7 @@ own terminal, guided by the official skills (`skill://icp-cli`, `skill://cycles-management`). The dedicated management tool group — a cycles-balance read, instructions-only creation and top-up helpers, `icp_install_code`, status/settings, and the lifecycle operations, acting as a -standing per-connection management principal — is deferred from this version; +stable per-user management principal — is deferred from this version; **we anticipate this will come in a future version** (the code remains in the library as `IcProtocolTools`). diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index b5e4964..38fa2b6 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -318,9 +318,11 @@ Paste-and-adapt; portal limits in parentheses. > questions" additionally allows state-changing calls > (`canister_update_call`). > 3. Try the example prompts above. On a Questions-only session a -> state-changing call (`canister_update_call`) returns an actionable -> reconnect message rather than an opaque error — that behavior is -> intended. Access is revocable at any time at +> state-changing call (`canister_update_call`) is rejected by the +> network and the tool reports the failed call; the server +> instructions prime the assistant to explain the access level and +> recommend reconnecting under "Actions & questions" — that behavior +> is intended. Access is revocable at any time at > https://id.ai/manage/settings. > 4. Canister-management tools are not part of this version (we anticipate > they will come in a future version), so there is nothing to provision: diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index 00b622e..b3aeb5b 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -153,7 +153,9 @@ Negative: (web-search or ask the user for the real URL) rather than resolved to a wrong identity. 2. A state-changing call (canister_update_call) on a "Questions only" - session → actionable reconnect-with-actions message, not an opaque error. + session → the network rejects it and the tool reports the failed call; + the server instructions prime the assistant to explain the access level + and recommend reconnecting under "Actions & questions". 3. Any authenticated tool with no sign-in → clean 401 → OAuth flow starts (no crash, no hang). 4. "Call an update method on a canister that rejects this caller" → the From a71405b14ded0a4a6b539bebd20384030bd3f1bc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 18:35:20 +0000 Subject: [PATCH 09/14] Update the scoping doc's tool counts to the deferred surface The local-deployment scoping doc still said ~26 tools in three places (review finding on #158): the tools.rs architecture note now describes the two routers (11 served on IcCanisterTools, 15 deferred on IcProtocolTools) behind the hand-written ServerHandler, and the Cursor capacity notes now say the local binary exposes 13 tools (11 core plus the two login tools). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- docs/scoping-local-deployment.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/scoping-local-deployment.md b/docs/scoping-local-deployment.md index fcdb135..11e6c4d 100644 --- a/docs/scoping-local-deployment.md +++ b/docs/scoping-local-deployment.md @@ -106,9 +106,10 @@ minimal. `list_accounts` `:873`). - `calls.rs` (Candid textual↔binary codec, `raw_call`), `discover.rs` (discovery + SSRF guard), `management.rs`, `skills.rs`. - - `tools.rs` — the **entire** MCP tool surface. The `#[tool_router]` / 26×`#[tool]` / - `#[tool_handler] impl ServerHandler for IcTools` macros expand into one impl on - `IcTools` and **must stay co-located**; both binaries construct `IcTools`, differing + - `tools.rs` — the **entire** MCP tool surface. The 26 `#[tool]` definitions live on + two `#[tool_router]` impls — `IcCanisterTools` (11 served) and `IcProtocolTools` + (15 deferred) — behind a hand-written `impl ServerHandler for IcTools` that serves + the canister router; all of it **must stay co-located**; both binaries construct `IcTools`, differing in transport + session source (the local binary additionally wraps it in its login handler, component 6). The injected `SessionResolver` seam (component 7) lives here — authentication itself stays in the binaries. @@ -360,7 +361,7 @@ requested clients split cleanly along that line (verified against current docs, | claude.ai web / mobile / Cowork | no | remote connectors (OAuth) only | ❌ → hosted | | **Codex** CLI / IDE ext / desktop | yes | `~/.codex/config.toml` → `[mcp_servers.]` | ✅ | | Codex Cloud | no | HTTP MCP only | ❌ → hosted | -| **Cursor** | yes | `~/.cursor/mcp.json` or `.cursor/mcp.json` → `mcpServers` | ✅ (≤40 tools total; we expose ~26) | +| **Cursor** | yes | `~/.cursor/mcp.json` or `.cursor/mcp.json` → `mcpServers` | ✅ (≤40 tools total; we expose 13) | | **Perplexity** macOS app | yes (via a `PerplexityXPC` helper) | Settings → Connectors → Add → Advanced JSON | ✅ macOS only | | Perplexity web / Windows / remote | no | remote HTTPS URL + OAuth 2.1 + DCR + `/.well-known/mcp-connector.json` | ❌ → hosted | | **Antigravity** IDE / CLI / 2.0 | yes | `~/.gemini/config/mcp_config.json` or `.agents/mcp_config.json` → `mcpServers` | ✅ | @@ -422,7 +423,7 @@ that router first, forwarding everything else to the inner `IcTools` handler (rm `RequestContext` is handler-independent, so forwarding is verbatim). The hosted server thus contains no login-tool code by construction; a hosted-side test still asserts its `tools/list` stays login-free as a regression guard. Cursor's ~40-tool cap is -comfortable: the core exposes ~26 plus these two.)* +comfortable: the core exposes 11 plus these two.)* **Serving the cloud clients (hosted `imcp2`).** claude.ai web/mobile and Perplexity-web reach only a public HTTPS MCP endpoint with OAuth 2.1 — which hosted `imcp2` already is. Two From 5fb06e3c0e72b3ec085ee4388a90e6395e0ff148 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 19:53:06 +0000 Subject: [PATCH 10/14] Keep the submission docs at what is submitted today MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer review on #158: the submissions should not tell marketplace reviewers that funding or management tooling may come in a future version — that invites questions about code that isn't deployed. Both directory docs now describe today's product only: the connector has no funding or canister-management tools, users create and manage canisters with the icp CLI, and the annotation/read-write rows count the 11 tools without referencing the deferred definitions. No "future version" / "deferred" / "anticipate" mentions remain in either submission doc; the README, landing page, and code docs keep the deferral story for developers. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- docs/anthropic-directory-submission.md | 35 +++++++++++--------------- docs/openai-directory-submission.md | 16 ++++++------ 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 38fa2b6..d7d1b4d 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -47,8 +47,8 @@ Verified against the live production deployment (2026-07-31): | Claude's hosted callback `https://claude.ai/api/mcp/auth_callback` accepted | ✅ seeded in the redirect allow-list ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | Claude Code loopback redirects (RFC 8252) | ✅ loopback redirects are exempt from the hosted allow-list | | Discovery documents (RFC 8414 + RFC 9728, path-scoped + root fallback) | ✅ all four live, `WWW-Authenticate` on the 401 points at the resource metadata | -| Every tool: `title` + `readOnlyHint`/`destructiveHint` (+ `idempotentHint`, `openWorldHint`) | ✅ on all 11 served tools (and on the 15 deferred definitions), enforced by a unit test ([`src/tools.rs`](../src/tools.rs)) | -| No catch-all read/write tool; reads and writes are separate tools | ✅ 10 of the 11 served tools are read-only; the one write is `canister_update_call`. (The protocol/meta group — incl. the instructions-only funding helpers — is deferred to a future version.) | +| Every tool: `title` + `readOnlyHint`/`destructiveHint` (+ `idempotentHint`, `openWorldHint`) | ✅ on all 11 tools, enforced by a unit test ([`src/tools.rs`](../src/tools.rs)) | +| No catch-all read/write tool; reads and writes are separate tools | ✅ 10 of the 11 tools are read-only; the one write is `canister_update_call`. | | Tool names ≤ 64 chars | ✅ longest is 30 | | `outputSchema` + structured content on every tool | ✅ enforced by a unit test | | Certificates from a recognized authority | ✅ Let's Encrypt via Caddy | @@ -145,14 +145,12 @@ transactions**, and the portal's compliance step requires acknowledging this. posture is that the server is not a financial tool (this section assumes both are merged):** -- **No funding or management tools are served in this version at all.** The - execution paths that once moved funds are removed from the binary +- **The connector has no funding or management tools.** The execution paths + that once moved funds are removed from the binary ([#153](https://github.com/dfinity/imcp2/pull/153) / - [#154](https://github.com/dfinity/imcp2/pull/154) made top-up and creation - instructions-only), and the whole protocol/meta tool group — including those - instructions-only helpers — is deferred from the served surface; we - anticipate it will come in a future version. Creating, funding, and managing - canisters is done by the user with the icp CLI in their own terminal. + [#154](https://github.com/dfinity/imcp2/pull/154)). Creating, funding, and + managing canisters is done by the user with the icp CLI in their own + terminal. - `canister_update_call` **refuses the standardized ledger methods** that move value or grant spending rights — the ICRC-standard names (ICRC-1/ICRC-2 plus ICRC-4/-7/-37) on every canister, and the ICP and @@ -204,9 +202,8 @@ reasonable answer for an authentication system nobody can pre-provision into, but a reviewer may still ask for a populated account — most plausibly to exercise `canister_update_call` against an app where the identity has data. If that comes back, the fallback is a dedicated identity with a recovery -phrase in the team vault and an account at a demo app. (The -canister-management tools that would have needed a controlled canister and a -cycles balance are deferred to a future version.) +phrase in the team vault and an account at a demo app. (No controlled canister or +cycles balance is needed: the connector has no canister-management tools.) ### 4. Production is behind `main` @@ -254,9 +251,8 @@ Paste-and-adapt; portal limits in parentheses. > or OQL, and discover the canisters behind any IC app from its name or URL. > With your consent it can also act as your Internet Identity accounts at a > specific app. Financial transactions are not supported: token-ledger - > transfer and approval methods are refused to protect you, and this version - > includes no funding or canister-management tools — we anticipate those - > will come in a future version. + > transfer and approval methods are refused to protect you, and there are + > no funding or canister-management tools. > > On the Internet Identity consent screen you explicitly choose the session > duration (10 minutes to 30 days) and the access level: "Questions only" @@ -324,10 +320,9 @@ Paste-and-adapt; portal limits in parentheses. > recommend reconnecting under "Actions & questions" — that behavior > is intended. Access is revocable at any time at > https://id.ai/manage/settings. -> 4. Canister-management tools are not part of this version (we anticipate -> they will come in a future version), so there is nothing to provision: -> creating and managing canisters happens outside the connector, with the -> icp CLI. +> 4. Canister-management tools are not part of this connector, so there is +> nothing to provision: creating and managing canisters happens outside +> the connector, with the icp CLI. > 5. Financial ledger operations are refused by design: asking the assistant > to move tokens returns a policy message directing the user to a wallet > they control — that behavior is intended (financial transactions are @@ -359,7 +354,7 @@ conversation beyond tool arguments and generates no media. - [ ] Dedicated ICP MCP privacy policy live at `https://mcp.internetcomputer.org/privacy-policy` (page + landing-page link merged; needs the production release) and entered in the portal (blocker 1) - [ ] Reply received from mcp-review@anthropic.com settling the financial-transactions and first-party-API acknowledgments (asked 2026-07-31; blocker 2) -- [x] Reviewer access settled: self-serve Internet Identity, instructions in the test-credentials field (blocker 3) — if a reviewer asks for a populated account, provision a demo-app account (no funding: the tools that needed it are deferred) +- [x] Reviewer access settled: self-serve Internet Identity, instructions in the test-credentials field (blocker 3) — if a reviewer asks for a populated account, provision a demo-app account (no funding needed: there are no funding or canister-management tools) - [ ] `release-*` tag cut; `/version` on production shows the intended commit (blocker 4) - [x] Square PNG icon exported — `docs/assets/icp-logo-{1024,512}.png` (blocker 5) - [ ] Every tool exercised once by the submitter (portal asks you to confirm this; MCP Inspector or a custom connector in Claude both count) diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index b3aeb5b..ff01bde 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -54,7 +54,7 @@ add details not published in the docs. | Discovery documents (RFC 8414 AS metadata + RFC 9728 protected-resource) | ✅ all live, path-scoped + root fallback | | ChatGPT's callback `https://chatgpt.com/connector/oauth/{callback_id}` accepted | ✅ the redirect allow-list pins `("chatgpt.com", "/connector/oauth/")` as a prefix ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | No machine-to-machine grants (client credentials etc. unsupported by ChatGPT) | ✅ user-consent authorization-code flow only | -| Tools explicitly annotated `readOnlyHint` / `destructiveHint` / `openWorldHint` — "incorrect or missing action labels are a common cause of rejection" | ✅ set on all 11 served tools (and the 15 deferred definitions). The unit test enforces annotation presence and the `readOnlyHint`/`destructiveHint` values; `openWorldHint` is declared everywhere but not asserted by the test, so re-check it in the portal's Scan Tools step | +| Tools explicitly annotated `readOnlyHint` / `destructiveHint` / `openWorldHint` — "incorrect or missing action labels are a common cause of rejection" | ✅ set on all 11 tools. The unit test enforces annotation presence and the `readOnlyHint`/`destructiveHint` values; `openWorldHint` is declared everywhere but not asserted by the test, so re-check it in the portal's Scan Tools step | | Tool names "human-readable, specific, and descriptive"; accurate descriptions; minimum-information requests | ✅ reviewed against the same bar for the Anthropic listing | | Public HTTPS production endpoint, stable and complete ("trial or demo plugins will not be accepted") | ✅ production deployment | | Privacy policy disclosing "categories of personal data collected, purposes of use, categories of recipients, data retention timelines" | ⏳ **pending the production release**: the rewritten policy matches these four required disclosures exactly and is live on staging, but `https://mcp.internetcomputer.org/privacy-policy` serves nothing until the next `release-*` tag ships it (see the checklist) | @@ -93,9 +93,9 @@ and self-serve. Position to state in the Testing tab: reviewers create their own Internet Identity in under a minute (instructions on the landing page); every read-only tool works with any identity because it reads public network state. Have the fallback ready (a provisioned identity with a recovery -phrase in the team vault and an account at a demo app — the -canister-management tools that would have needed a controlled canister and a -cycles balance are deferred to a future version) if review pushes back — +phrase in the team vault and an account at a demo app — no controlled +canister or cycles balance is needed: the plugin has no canister-management +tools) if review pushes back — and expect a higher chance of push-back than at Anthropic given the login-and-password wording. @@ -120,12 +120,10 @@ financial-transfers prohibition: server-level instructions rather than the tool description (which stays free of financial language, per maintainer review) ([#154](https://github.com/dfinity/imcp2/pull/154)), and - no funding or canister-management tools are served in this version at all — - the execution paths that once moved funds are removed from the binary + the plugin has no funding or canister-management tools at all — the + execution paths that once moved funds are removed from the binary ([#153](https://github.com/dfinity/imcp2/pull/153), - [#154](https://github.com/dfinity/imcp2/pull/154)), and the whole - protocol/meta tool group (including the instructions-only funding helpers) - is deferred; we anticipate it will come in a future version. README, landing + [#154](https://github.com/dfinity/imcp2/pull/154)). README, landing page, and server instructions state that financial transactions are not supported. From 983ecbd5f1e902852ac7193d4f45924e8466fa79 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 19:54:13 +0000 Subject: [PATCH 11/14] Drop the out-of-scope tool mentions from the privacy policy Per maintainer review on #158, the policy should not mention future functionality: the parentheticals about canister-management tools (and the own-origin stable identity they would derive) are removed from both the served privacy-policy.html and the docs/ draft, leaving the policy describing only the processing the current service performs. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- docs/icp-mcp-privacy-policy-draft.md | 11 ++--------- src/assets/privacy-policy.html | 11 ++--------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/docs/icp-mcp-privacy-policy-draft.md b/docs/icp-mcp-privacy-policy-draft.md index 87b3d4e..3662e6d 100644 --- a/docs/icp-mcp-privacy-policy-draft.md +++ b/docs/icp-mcp-privacy-policy-draft.md @@ -138,10 +138,7 @@ keeps, for the remainder of the session: the application's domain, the account number used, the per-application key it generated, and Internet Identity's signed authorization for that application (itself valid for at most one hour). Keeping this avoids re-deriving an authorization on every -call to the same application. (The canister-management tools, which would -treat the Service's own origin as one such application with an identity that -is stable across your connections, are not part of this version — we -anticipate they will come in a future version.) All of this is +call to the same application. All of this is held in memory only, bounded in size, and discarded when the session ends or the Service restarts. Legal basis: performance of the service you requested. @@ -290,11 +287,7 @@ Internet Identity gives each application a different identity for you, so applications cannot recognise you across applications. The Service is a participant in that design and we want to be plain about what it can see. While a session is live it holds, in memory, the per-application identities -it has derived for you. (The canister-management tools, which would also -derive a stable identity for you at the Service's own origin — one that -software holding it could in principle use to associate separate sessions -with the same user — are not part of this version; we anticipate they will -come in a future version.) Three things limit what the Service could link +it has derived for you. Three things limit what the Service could link in practice: none of these identifiers is written to logs, which carry only a per-connection identifier that is new for every connection (section 5); the applications you visit are not written diff --git a/src/assets/privacy-policy.html b/src/assets/privacy-policy.html index 3773155..b79ac82 100644 --- a/src/assets/privacy-policy.html +++ b/src/assets/privacy-policy.html @@ -272,10 +272,7 @@

1. Data Categories Processed, Purposes, and Legal Bases

domain, the account number used, the per-application key it generated, and Internet Identity's signed authorization for that application (itself valid for at most one hour). Keeping this avoids re-deriving an authorization on -every call to the same application. (The canister-management tools, which -would treat the Service's own origin as one such application with an -identity that is stable across your connections, are not part of this -version — we anticipate they will come in a future version.) All of this is +every call to the same application. All of this is held in memory only, bounded in size, and discarded when the session ends or the Service restarts. Legal basis: performance of the service you requested.

@@ -450,11 +447,7 @@

6. Identifiers, and What They Could Link

applications cannot recognise you across applications. The Service is a participant in that design and we want to be plain about what it can see. While a session is live it holds, in memory, the per-application identities -it has derived for you. (The canister-management tools, which would also -derive a stable identity for you at the Service's own origin — one that -software holding it could in principle use to associate separate sessions -with the same user — are not part of this version; we anticipate they will -come in a future version.) Three things limit what the Service could link +it has derived for you. Three things limit what the Service could link in practice: none of these identifiers is written to logs, which carry only a per-connection identifier that is new for every connection (section 5); the applications you visit are not written to logs From 38218548cf032cca17c480392b602e2459ade409 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 20:03:29 +0000 Subject: [PATCH 12/14] Keep user-facing docs at the current scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per maintainer review on #158, extended to every user-facing surface: mentioning tooling outside the current scope invites questions about code that isn't deployed. The README (intro, tool-table blockquote, typical-flow tail, name-lookup guidance, skills section, creating & managing canisters, read-only sessions), the local-deployment scoping doc, and the landing page's tool groups now describe today's product only — the deferred-tools blockquote and the "Deferred to a future version" landing group are removed, and no "future version" / "anticipate" phrasing remains in any of them. The library facts stay: IcProtocolTools remains documented in the rustdoc and the scoping doc's architecture note as code an embedder can compose. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- README.md | 53 +++++++++----------------------- docs/scoping-local-deployment.md | 9 ++---- src/assets/index.html | 2 +- 3 files changed, 18 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index e5d0908..04558c4 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,11 @@ building, and operating canisters — it is not a wallet or trading tool, and financial operations (token transfers, spending approvals, payments, trades) are unsupported by policy. The standardized ledger surface is refused mechanically (`canister_update_call` rejects the ICRC-1/ICRC-2 and related -ledger-standard transfer/approval methods on every canister), and this version -ships no funding or management tools at all: creating, funding, and topping up +ledger-standard transfer/approval methods on every canister), and there are +no funding or management tools at all: creating, funding, and topping up canisters is done by the user with the -[`icp` CLI](https://github.com/dfinity/icp-cli) in their own terminal -(instructions-only helper tools are deferred — we anticipate these will come -in a future version). For financial operations, users act themselves in a +[`icp` CLI](https://github.com/dfinity/icp-cli) in their own terminal. +For financial operations, users act themselves in a wallet or frontend they control (e.g. [oisy.com](https://oisy.com)), in their own browser. @@ -159,18 +158,6 @@ results). | `icp_oql_guide` | — | The OQL query-surface dialect guide (for canisters where `get_canister_candid` reports `oql: true`): the JSON query object, predicate grammar, edges, and paged result shape. The entity/field names come from `get_canister_oql_schema` and queries run through `canister_query` (the `oql` argument) | | `get_canister_oql_schema` | `canister_id`, `derivation_origin`, `account?` | The canister's OQL schema catalogue (entities, primary keys, fields, edges) as JSON — wraps its `schema` method — plus a ready-to-run `canister_query` example per entity. **`derivation_origin` is required**: the schema is caller-gated, so an anonymous read is rejected (for now) with guidance, rather than returning an empty list | -> **Deferred in this version.** The `icp_`-prefixed protocol/meta tools — the -> dashboard name/id lookups (`icp_find_canister_by_name`, `icp_find_app_by_name`, -> `icp_lookup_canister_info_by_id`), the skills tools (`icp_list_skills`, -> `icp_get_skill`), and the -> canister-management group (`icp_cycles_balance`, the instructions-only -> `icp_create_canister` / `icp_top_up_canister`, `icp_install_code`, -> `icp_canister_status`, `icp_update_canister_settings`, and the lifecycle -> tools) — are not served in this version; **we anticipate this will come in a -> future version.** The official IC skills remain -> available as MCP resources (`skill://`), and the types -> stay in the library (`IcProtocolTools`) for embedders and for the return. - `open_app` (its `app` argument takes a name **or** a URL) is the one-call entry point when the user names or links an app: it resolves the Internet Identity `derivation_origin` **and** discovers the @@ -219,10 +206,8 @@ Genuinely public reads via a `canister_query` Candid `method` query or the public-metadata tools (`get_canister_candid`, `discover_app_canisters`) skip steps 3/4 and need no origin; OQL reads always require one. The per-canister inspection (5) is independent of the identity steps (3/4), so they can run in parallel. Managing your -**own** canisters is not part of this version — the `icp_*` create/install/status/… tools -are deferred (we anticipate they will come in a future version, acting as a standing -**management principal** at this server's origin, a *different* identity than the -per-app principals above); today, create and manage canisters with the icp CLI. +**own** canisters is not part of this connector: create and manage them with the +icp CLI (see *Creating & managing canisters* below). ### App-declared canister metadata (App Connect) @@ -275,10 +260,9 @@ so an app that uses one should declare it here; otherwise the connector assumes the derivation origin equals the application origin and flags that assumption. When the user names a **token, project, or service** rather than a website or -id, the dedicated dashboard lookup tools are not part of this version — we -anticipate they will come in a future version. Until then, web search the -canister id or ask the user for it. (`discover_app_canisters` results are still -annotated with the dashboard's labels inline.) +id, web search the canister id or ask the user for it. +(`discover_app_canisters` results are annotated with the dashboard's labels +inline.) `canister_query` and `canister_update_call` run anonymously by default; pass a `derivation_origin` to call as @@ -342,9 +326,7 @@ auth, …) — are served as MCP **resources** (`skill://`) alongside the `candid://` references. The catalogue is fetched live from the registry's manifest (`/api/skills.json`, cached ~15 min) and each skill's `SKILL.md` on demand; nothing is bundled, so the agent always sees the current skills. -Dedicated browsing tools (`icp_list_skills` / `icp_get_skill`) are deferred — -we anticipate these will come in a future version. Override the registry -origin with `SKILLS_URL`. +Override the registry origin with `SKILLS_URL`. ### OQL query surfaces @@ -395,14 +377,9 @@ conventions. Canister creation, funding, deployment, and lifecycle management are done by the **user** with the [`icp` CLI](https://github.com/dfinity/icp-cli) in their own terminal, guided by the official skills (`skill://icp-cli`, -`skill://cycles-management`). The dedicated management tool group — a -cycles-balance read, instructions-only creation and top-up helpers, -`icp_install_code`, status/settings, and the lifecycle operations, acting as a -stable per-user management principal — is deferred from this version; -**we anticipate this will come in a future version** (the code remains in the -library as `IcProtocolTools`). - -The end-to-end flow still works: *"create a Motoko canister that does X and +`skill://cycles-management`). This connector has no canister-management tools. + +The end-to-end flow: *"create a Motoko canister that does X and deploy it"* → the agent reads the relevant skills (`skill://` resources), writes and **builds** the Wasm in its own environment, and the user creates, funds, and installs it with the `icp` CLI. (Compiling Motoko/Rust to Wasm @@ -839,9 +816,7 @@ II's consent screen requires an explicit access-level choice: **"Questions only"** or **"Actions & questions"**. A user who picks Questions only gets a session whose per-app delegations are `permissions = "queries"`, and the IC **rejects update calls made through them at ingress**. That makes -`canister_update_call` inert — and, when the deferred canister-management -tools return in a future version, that whole surface too (even a status read -is an update call there). To handle this +`canister_update_call` inert. To handle this without opaque low-level errors: - The `mcp_register_v2` reply carries `permissions: "queries" | "all"`, so the diff --git a/docs/scoping-local-deployment.md b/docs/scoping-local-deployment.md index 11e6c4d..61fd774 100644 --- a/docs/scoping-local-deployment.md +++ b/docs/scoping-local-deployment.md @@ -468,9 +468,8 @@ with their transports and core only receives their outcome. Tools that are already session-free work unchanged locally: `get_canister_candid`, `get_canister_api_doc`, `open_app`, `resolve_app`, `discover_app_canisters`, `icp_oql_guide`, -and the anonymous path of `canister_query`. (The skills and lookup tools are deferred from -the served surface — we anticipate they will come in a future version; the skill documents -stay served as `skill://` resources.) +and the anonymous path of `canister_query`. (The skill documents are served as `skill://` +resources.) ### 8. Security model @@ -482,9 +481,7 @@ page and the `#4091` well-known are static, and `/redeem` only advances the conn process started (`state` must match) with a chain targeting the in-process `X` — it can neither invoke tools nor read the session. But the client then wields the user's **real production II accounts** on mainnet (any update call under a per-app delegation, for -every origin — and the deferred management tools, if an embedder serves them, add canister -create/install/start/stop/delete and cycles spend as the management principal). This must be -stated plainly: +every origin). This must be stated plainly: **treat the binary and its client config like a wallet.** There is no revocable token — only the II grant (reconnect/expiry). diff --git a/src/assets/index.html b/src/assets/index.html index 7fa74cb..e14e7fa 100644 --- a/src/assets/index.html +++ b/src/assets/index.html @@ -405,7 +405,7 @@ \ No newline at end of file From 444a5629cd4440385fefdde16893425e22b940f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 20:08:25 +0000 Subject: [PATCH 13/14] Drop out-of-scope mentions from the README and server instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the current-scope pass from maintainer review on #158: - README: the "Creating & managing canisters" section is removed (the maintainer asked for it omitted for now); the typical-flow tail points at the icp CLI directly instead of that section. - SERVER_INSTRUCTIONS: the three parentheticals about tools that are not part of this version are gone. The surviving text keeps the operative advice with no forward-looking framing — for a token/service name or an unknown canister id, web search or ask the user; creating, funding, and managing canisters is the user's own icp CLI work. - The deferral test now asserts the instructions do NOT promise a future version, pinning the new contract instead of the old one. No "future version" / "anticipate" phrasing remains in any user- or agent-facing surface. The rustdoc on IcProtocolTools keeps describing the type for embedders, which is library API documentation, not product messaging. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- README.md | 15 +-------------- crates/imcp2-core/src/tools.rs | 21 ++++++++------------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 04558c4..f93c439 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ public-metadata tools (`get_canister_candid`, `discover_app_canisters`) skip ste 3/4 and need no origin; OQL reads always require one. The per-canister inspection (5) is independent of the identity steps (3/4), so they can run in parallel. Managing your **own** canisters is not part of this connector: create and manage them with the -icp CLI (see *Creating & managing canisters* below). +[`icp` CLI](https://github.com/dfinity/icp-cli) in your own terminal. ### App-declared canister metadata (App Connect) @@ -372,19 +372,6 @@ erroring. The design mirrors the reference IC connector's OQL primer (detect + teach), adding an ergonomic executor suited to this server's structured-output conventions. -### Creating & managing canisters - -Canister creation, funding, deployment, and lifecycle management are done by -the **user** with the [`icp` CLI](https://github.com/dfinity/icp-cli) in their -own terminal, guided by the official skills (`skill://icp-cli`, -`skill://cycles-management`). This connector has no canister-management tools. - -The end-to-end flow: *"create a Motoko canister that does X and -deploy it"* → the agent reads the relevant skills (`skill://` resources), -writes and **builds** the Wasm in its own environment, and the user creates, -funds, and installs it with the `icp` CLI. (Compiling Motoko/Rust to Wasm -happens in the agent's environment, not in this server.) - ## Connect from an MCP client Add the server to Claude Code (replace the URL with wherever it's hosted): diff --git a/crates/imcp2-core/src/tools.rs b/crates/imcp2-core/src/tools.rs index bdabca7..6697bb2 100644 --- a/crates/imcp2-core/src/tools.rs +++ b/crates/imcp2-core/src/tools.rs @@ -1994,10 +1994,8 @@ const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TE act on a whole APP, keyed by its Internet Identity derivation origin or app URL, and \ `…canister…` names (`get_canister_candid`, `get_canister_api_doc`, \ `get_canister_oql_schema`, `canister_query`, `canister_update_call`) act on ONE \ - specific canister; `icp_oql_guide` serves the OQL dialect those reads use. (A \ - further group of IC protocol / meta-level tools — dashboard name/id lookups, the \ - official IC skills as tools, and canister creation/management — is not part of this \ - version; we anticipate this will come in a future version.) Before writing Candid \ + specific canister; `icp_oql_guide` serves the OQL dialect those reads use. \ + Before writing Candid \ args, consult the `candid://textual-syntax` \ resource (the value syntax these tools use); `candid://reference` has the full type \ reference, and the official IC \ @@ -2023,10 +2021,8 @@ const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TE and let the connector resolve it, WEB SEARCH the official \ URL, or ask the user. Lookalike domains are unrelated or squatted sites, and open_app / \ every URL-taking tool REFUSES an origin that shows no evidence of being an Internet \ - Computer app instead of resolving it to a wrong identity. (Dedicated dashboard \ - lookup tools for token/service names and bare canister ids are not part of this \ - version — we anticipate they will come in a future version; until then, web search \ - or ask the user for an id you don't have.)\n\n\ + Computer app instead of resolving it to a wrong identity. (For a token/service \ + name or a canister id you don't have, web search or ask the user.)\n\n\ \"MY / OUR …\" IS AN AUTHENTICATED READ. A question about the USER's OWN data in an app \ (\"who am I meeting with…\", \"my bookings\", \"our open orders\") reads data the app gates \ by the CALLER's principal. An OQL read (get_canister_oql_schema, and canister_query \ @@ -2116,9 +2112,7 @@ const SERVER_INSTRUCTIONS: &str = "Internet Computer tools. Every tool speaks TE (ICP↔cycles & funding), `stable-memory` (upgrades) and `canister-security`. Compiling \ Motoko/Rust to Wasm happens in YOUR environment (guided by those skills), and \ CREATING, FUNDING, DEPLOYING and MANAGING canisters is done by the USER with the icp \ - CLI in their own terminal, guided by the same skills. Dedicated tools for skills \ - browsing and for canister creation/management as your Internet Identity are not part \ - of this version — we anticipate this will come in a future version."; + CLI in their own terminal, guided by the same skills."; impl ServerHandler for IcTools { async fn list_tools( @@ -2576,7 +2570,8 @@ mod tests { // The user-facing contract of THIS version: only the canister/app tools // are served; the protocol/meta group is deferred, and the instructions - // say a future version brings it. + // describe the current surface only (per review: what isn't in this + // version isn't mentioned). #[test] fn the_default_composition_defers_the_protocol_tools() { let served = super::IcTools::all_tools(); @@ -2592,7 +2587,7 @@ mod tests { // name is not just unlisted — it is not routable at all. assert!(!super::IcCanisterTools::tool_router().has_route("icp_get_skill")); assert!(!super::IcCanisterTools::tool_router().has_route("icp_install_code")); - assert!(super::SERVER_INSTRUCTIONS.contains("future version")); + assert!(!super::SERVER_INSTRUCTIONS.contains("future version")); } // The tool surface is split by scope: the protocol / meta-level tools From a232e8999a00efa6f45d24e2d11aa5f3681b7098 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 20:12:13 +0000 Subject: [PATCH 14/14] Restore the linkability disclosure for the served identities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review on #158 caught real collateral from the scope trim: dropping the management-specific sentence from the privacy policy also dropped the cross-session linkage disclosure, which still applies to the per-app identities the current surface derives. Internet Identity derives the same identity for a given application and account, so software holding one could tell that two sessions at that application belong to the same user. Both policy copies now disclose that in terms of today's surface, followed by the three limits that were always there. Also: the annotations row linked ../src/tools.rs, which does not exist (src/auth.rs and src/lib.rs do — tools.rs is in the core crate); the link now points at crates/imcp2-core/src/tools.rs. And the README intro says the connector serves no funding or management tools, scoping the claim to the served surface. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk --- README.md | 4 ++-- docs/anthropic-directory-submission.md | 2 +- docs/icp-mcp-privacy-policy-draft.md | 5 ++++- src/assets/privacy-policy.html | 5 ++++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f93c439..0d6bcf1 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ building, and operating canisters — it is not a wallet or trading tool, and financial operations (token transfers, spending approvals, payments, trades) are unsupported by policy. The standardized ledger surface is refused mechanically (`canister_update_call` rejects the ICRC-1/ICRC-2 and related -ledger-standard transfer/approval methods on every canister), and there are -no funding or management tools at all: creating, funding, and topping up +ledger-standard transfer/approval methods on every canister), and the +connector serves no funding or management tools: creating, funding, and topping up canisters is done by the user with the [`icp` CLI](https://github.com/dfinity/icp-cli) in their own terminal. For financial operations, users act themselves in a diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index d7d1b4d..df07360 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -47,7 +47,7 @@ Verified against the live production deployment (2026-07-31): | Claude's hosted callback `https://claude.ai/api/mcp/auth_callback` accepted | ✅ seeded in the redirect allow-list ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | Claude Code loopback redirects (RFC 8252) | ✅ loopback redirects are exempt from the hosted allow-list | | Discovery documents (RFC 8414 + RFC 9728, path-scoped + root fallback) | ✅ all four live, `WWW-Authenticate` on the 401 points at the resource metadata | -| Every tool: `title` + `readOnlyHint`/`destructiveHint` (+ `idempotentHint`, `openWorldHint`) | ✅ on all 11 tools, enforced by a unit test ([`src/tools.rs`](../src/tools.rs)) | +| Every tool: `title` + `readOnlyHint`/`destructiveHint` (+ `idempotentHint`, `openWorldHint`) | ✅ on all 11 tools, enforced by a unit test ([`crates/imcp2-core/src/tools.rs`](../crates/imcp2-core/src/tools.rs)) | | No catch-all read/write tool; reads and writes are separate tools | ✅ 10 of the 11 tools are read-only; the one write is `canister_update_call`. | | Tool names ≤ 64 chars | ✅ longest is 30 | | `outputSchema` + structured content on every tool | ✅ enforced by a unit test | diff --git a/docs/icp-mcp-privacy-policy-draft.md b/docs/icp-mcp-privacy-policy-draft.md index 3662e6d..deb6184 100644 --- a/docs/icp-mcp-privacy-policy-draft.md +++ b/docs/icp-mcp-privacy-policy-draft.md @@ -287,7 +287,10 @@ Internet Identity gives each application a different identity for you, so applications cannot recognise you across applications. The Service is a participant in that design and we want to be plain about what it can see. While a session is live it holds, in memory, the per-application identities -it has derived for you. Three things limit what the Service could link +it has derived for you. Internet Identity derives the same identity each time +for a given application and account, so software holding one of them could in +principle tell that two separate sessions visiting that application belong to +the same user. Three things limit what the Service could link in practice: none of these identifiers is written to logs, which carry only a per-connection identifier that is new for every connection (section 5); the applications you visit are not written diff --git a/src/assets/privacy-policy.html b/src/assets/privacy-policy.html index b79ac82..91ae59f 100644 --- a/src/assets/privacy-policy.html +++ b/src/assets/privacy-policy.html @@ -447,7 +447,10 @@

6. Identifiers, and What They Could Link

applications cannot recognise you across applications. The Service is a participant in that design and we want to be plain about what it can see. While a session is live it holds, in memory, the per-application identities -it has derived for you. Three things limit what the Service could link +it has derived for you. Internet Identity derives the same identity each time +for a given application and account, so software holding one of them could in +principle tell that two separate sessions visiting that application belong to +the same user. Three things limit what the Service could link in practice: none of these identifiers is written to logs, which carry only a per-connection identifier that is new for every connection (section 5); the applications you visit are not written to logs