Skip to content

Test the MCP↔II connect handshake against a live Internet Identity canister (PocketIC) - #76

Merged
aterga merged 6 commits into
mainfrom
claude/mcp-rust-library-crate-l3zzd1
Jul 24, 2026
Merged

aterga merged 6 commits into
mainfrom
claude/mcp-rust-library-crate-l3zzd1

Conversation

@aterga

@aterga aterga commented Jul 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #26. Adds a real Internet Identity ↔ MCP connect-handshake test: a genuine Internet Identity canister (the release-2026-07-17 wasm) runs in PocketIC and issues a real canister-signed registration delegation, which the real McpServer redeems through its own ic-agent. Nothing about II is mocked — every canister interaction and every signature is genuine. The test plays only the browser/frontend role (the human's clicks).

It also lands the supporting production change that makes this possible: verify the registration chain against the injected agent's root key rather than a hardcoded mainnet key, so a PocketIC-hosted II's chain verifies (and, in production, the agent's key is mainnet — behavior-preserving).

Related issues

Follow-up to #26.

Changes

  • src/e2e_handshake.rs (new) — in-crate test, #[cfg(all(test, feature = "e2e"))]. Starts PocketIC, installs the real II wasm with the captcha disabled (init arg), then:
    • plays the frontend: register an anchor → mcp_set_config (trust the server) → prepare_mcp_registration_delegation → get_mcp_registration_delegation (the real canister-signed P_reg → X hop);
    • drives the real server end to end: /oauth/register → /oauth/authorize (mints X) → /oauth/connect/redeem (the server signs mcp_register_v2 on the live II, verifying the canister signature against PocketIC's fetched root key) → /oauth/token → asserts the minted bearer token authenticates (session_for_token) and its TTL tracks the II grant.
    • II candid types are hand-rolled from the release-tag .did (no git dependency).
  • src/identities.rs — registration_identity now uses DelegatedIdentity::new_with_root_key(&self.agent.read_root_key()) instead of DelegatedIdentity::new (hardcoded mainnet). Ties chain-trust to the same agent that makes the mcp_register_v2 call; the reply is already certificate-verified against that key, so this is consistent, not a weakening.
  • Cargo.toml — pocket-ic as an optional dependency behind a new e2e feature; the default build compiles and downloads neither.
  • Removed the interim cfg(test) registration seam and its mocked connect→token test — redeem_registration_delegation now has a single real path.

Testing

  • cargo build --locked --all-targets — default build stays lean (no pocket-ic).
  • cargo test --locked --all-targets — 136 lib + 7 router tests pass.
  • cargo test --features e2e --no-run — the PocketIC test compiles.
  • Executing the e2e test needs two artifacts cargo does not fetch — II_WASM (the release internet_identity_backend.wasm.gz) and POCKET_IC_BIN (the pocket-ic v15 server binary) — and could not be run in this environment (release-asset downloads are blocked here). Run it where those are provisioned: II_WASM=… POCKET_IC_BIN=… cargo test --features e2e. The test skips cleanly when they're unset, so default CI stays green; a dedicated CI job that downloads them (and later bumps the wasm) is the follow-up.
  • cargo fmt/clippy: the repo is not default-rustfmt-clean by design and CI gates on build+test only; the new code matches the surrounding style and adds no clippy warnings.

Checklist

  • I have read the Contributing guidelines.
  • Docs/comments updated — the new module documents the flow and exactly how to run it.
  • No secrets, credentials, or internal-only information are included.

🤖 Generated with Claude Code

https://claude.ai/code/session_017h4prMHkLo6UnwnuoPgmUj

The router tests exercised the handshake scaffolding (input validation,
endpoint wiring, the pinned callback page, the 401 challenge) but never the
success path: a redeemed grant becoming a working access token. Close that
gap with an end-to-end test of connect_redeem → token → session_for_token.

The one step that cannot run hermetically is the registration redemption:
DelegatedIdentity::new verifies the delivered chain against the IC mainnet
root key, and mcp_register_v2 is a live update call to Internet Identity. Add
a cfg(test)-only seam (Identities::with_register_override) that stands in for
exactly that step — so a fabricated-but-structurally-valid chain drives the
whole flow while everything around redemption (parse_registration_delegation,
the cookie/initiator and single-flight guards, PKCE-bound code minting, the
S256 token exchange, TTL-from-grant, token authentication, single-use codes)
runs unchanged. The seam adds nothing to non-test builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h4prMHkLo6UnwnuoPgmUj
@aterga
aterga marked this pull request as ready for review July 23, 2026 15:49
Copilot AI review requested due to automatic review settings July 23, 2026 15:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a test-only seam to stub the non-hermetic Internet Identity registration redemption step, enabling a new fully hermetic end-to-end unit test that exercises the MCP↔II connect handshake success path from connect/redeem through OAuth token exchange and token-authenticated session resolution.

Changes:

  • Introduces a #[cfg(test)] Identities::with_register_override hook to override redeem_registration_delegation’s live/crypto-dependent registration step while preserving real session bookkeeping (grant expiry + permissions).
  • Adds test_store_registering plus a new test that drives connect_redeem → token → session_for_token, including PKCE S256 enforcement, TTL bounded by grant expiry, and single-use authorization codes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/identities.rs Adds a cfg(test)-only registration override seam for hermetic tests, without affecting non-test builds.
src/auth.rs Adds a hermetic success-path test (redeem → token → session auth) and a helper store wired to the new seam.

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

registration_identity() built the redeeming DelegatedIdentity with
DelegatedIdentity::new, which hardcodes the IC mainnet BLS root key. Switch
to new_with_root_key(&self.agent.read_root_key()) so the delivered
P_reg -> Y -> X chain is verified against the SAME network the agent then
makes the mcp_register_v2 call to, instead of an independently pinned
mainnet key.

Behavior-preserving in production (the injected agent is mainnet and never
fetches another key, so read_root_key() == the mainnet key), and more
consistent: a host pointed at a non-mainnet IC — e.g. a local replica after
fetch_root_key — now verifies and signs against one trust anchor. This is
also what lets a real Internet Identity canister on PocketIC issue a chain
the server can verify, which the forthcoming end-to-end test relies on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h4prMHkLo6UnwnuoPgmUj
Copilot AI review requested due to automatic review settings July 23, 2026 16:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

src/auth.rs:2825

  • The new hermetic success-path test seeds AuthzPending directly but never mints the connect’s registration key (X). In production, redemption requires that X was minted for the connect, and the delegation chain’s final hop must target it. Consider minting X via store.identities.registration_pubkey_b64(state).await and passing its DER bytes into well_formed_reg_chain_json(...) to keep the test exercising those invariants.
        // A PKCE pair, seeded as an in-flight connect whose initiator cookie the
        // redeem must present (bypassing the browser redirect that would set it).
        let verifier = "connect-redeem-verifier-0123456789";
        let challenge = super::pkce_s256(verifier);
        let cookie = "bind-connect";
        let state = "sess-connect";
        store.authz.write().await.insert(

src/auth.rs:2853

  • After minting x_der, pass it into well_formed_reg_chain_json(&x_der) so the redeem request’s chain targets the connect’s registration key (X).
            headers,
            axum::Json(super::RedeemBody {
                state: state.into(),
                delegation: well_formed_reg_chain_json(),
            }),

Comment thread src/identities.rs Resolved
Comment thread src/auth.rs Resolved
Replaces the cfg(test) registration seam and its mocked connect->token test
with a real end-to-end test: a genuine Internet Identity canister (release
wasm) runs in PocketIC and issues a real canister-signed registration
delegation, which the real server redeems through its own ic-agent. Nothing
about II is mocked.

The test (src/e2e_handshake.rs) plays only the browser/frontend role —
register an anchor, mcp_set_config to trust the server, then the
prepare_mcp_registration_delegation + get_mcp_registration_delegation
ceremony — and feeds the resulting real delegation to /oauth/connect/redeem.
The server verifies the canister signature against PocketIC's fetched root
key (via the new_with_root_key change) and signs mcp_register_v2 on the live
II, then /oauth/token issues a bearer token that authenticates. So the whole
redeem->code->token->auth path runs against a real II, not a stand-in.

Gating keeps the default build lean and offline:
- pocket-ic is an optional dependency behind a new `e2e` feature, so plain
  `cargo build`/`cargo test` neither compile nor download it.
- The test is in-crate under #[cfg(all(test, feature = "e2e"))] (an integration
  test in tests/ cannot use an optional dependency), and skips at runtime
  unless II_WASM and POCKET_IC_BIN are set:
    II_WASM=... POCKET_IC_BIN=... cargo test --features e2e

The II candid types are hand-rolled from internet_identity.did at tag
release-2026-07-17 so the crate takes no git dependency. Removing the seam
leaves redeem_registration_delegation with a single real path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h4prMHkLo6UnwnuoPgmUj
Copilot AI review requested due to automatic review settings July 24, 2026 11:44
@aterga aterga changed the title Cover the MCP↔II connect handshake success path with a hermetic test Test the MCP↔II connect handshake against a live Internet Identity canister (PocketIC) Jul 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

Cargo.toml:61

  • This feature comment also references tests/e2e_pocket_ic.rs, but the gated PocketIC test is src/e2e_handshake.rs. Updating the path here will keep the run instructions discoverable and accurate.
# Compiles + enables the real II<>MCP handshake test against a live Internet
# Identity canister in PocketIC (needs II_WASM + POCKET_IC_BIN at runtime; see
# tests/e2e_pocket_ic.rs). Off by default: the standard `cargo test` neither
# builds `pocket-ic` nor touches the network.

Comment thread Cargo.toml Resolved
The e2e test is an in-crate module (src/e2e_handshake.rs), not
tests/e2e_pocket_ic.rs — correct the two manifest references (Copilot review).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h4prMHkLo6UnwnuoPgmUj
Copilot AI review requested due to automatic review settings July 24, 2026 12:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/e2e_handshake.rs Resolved
The PocketIC test set the process-global OAUTH_CLIENTS_FILE to a fixed temp
filename, which under parallel test threads could leak into other tests
calling SharedClients::load() and contend on the shared file. Use a
per-process/timestamped unique filename and a Drop guard that restores (or
clears) the env var even on panic-unwind, and removes the temp file (Copilot
review).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h4prMHkLo6UnwnuoPgmUj
Copilot AI review requested due to automatic review settings July 24, 2026 14:22
@aterga
aterga requested a review from a team as a code owner July 24, 2026 14:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Comment thread src/e2e_handshake.rs Resolved
…variant

Doc-only: the delegation record's permissions field is opt text, distinct from
the named Permissions variant (which is only the prepare_ argument / register
reply). Spell it out at the type so the distinction (the II #40 outage class)
isn't misread. No behavior change (Copilot review).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017h4prMHkLo6UnwnuoPgmUj
Copilot AI review requested due to automatic review settings July 24, 2026 14:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

src/e2e_handshake.rs:277

  • These doc comments claim the OAUTH_CLIENTS_FILE override “can't leak into other test threads running in parallel”, but the env var is process-global and will be visible to any concurrently running test while this one is executing (it’s only restored on drop). This can make --features e2e test runs flaky when other tests also call SharedClients::load() (e.g. tests/routers.rs).
/// Restores (or clears) the process-global `OAUTH_CLIENTS_FILE` on drop — even on
/// a panic-unwind — so this test's override can't leak into other test threads
/// running in parallel, and removes the temp file it pointed at.

src/e2e_handshake.rs:310

  • Same issue as above: this comment says the env-var override “can't leak into other test threads”, but it can during the test run. Consider updating the wording and/or ensuring the recommended invocation runs this test in isolation (e.g. filter by test name and/or use --test-threads=1).
    // Isolate the OAuth client store: a UNIQUE temp file (so concurrent runs
    // never collide on a fixed name) plus a guard that restores/clears the
    // process-global `OAUTH_CLIENTS_FILE` on drop — even on a panic — so the
    // override can't leak into other test threads.

@aterga
aterga merged commit f63118e into main Jul 24, 2026
2 checks passed
aterga pushed a commit that referenced this pull request Aug 20, 2026
Re-verify every claim and file:line reference against the rebased tree
and absorb what main already did or changed:

- The five vestigial deps (ed25519-dalek, p256, ic-signature-verification,
  ic-representation-independent-hash, schemars 0.8) were already removed
  on main (#100) — reframe that stripping work as done.
- Account for the new hosted-only surfaces the local split must gate:
  prometheus + src/metrics.rs (/metrics, #124) and McpConfig's
  state_dir/require_resource (#144/#127); note the local binary needs no
  state directory at all. pocket-ic (e2e-only, #76) never enters any
  closure.
- Rewrite §10: production II now serves at /mcp by default (#92), CI
  probes it (#113), and the PocketIC e2e harness verifies the full
  connect contract — the remaining unknowns are the local binary's
  http://127.0.0.1 mixed-content and CORS behavior, and the harness is
  the plan for testing the local flow.
- Prod-instance env overrides are II_URL_PROD/II_CANISTER_ID_PROD (#92);
  registration chains are also verified client-side against the agent's
  root key (new_with_root_key); registration_pubkey_b64 is now fallible;
  AuthStore's dropped fields now include mcp_path and require_resource,
  and the dropped OAuth side grew RFC 8707/9207 and the bounded DCR store.
- Note the crates.io publishing dimension (v0.2.0, tag-guarded trusted
  publishing, docs/ excluded from the .crate) and re-stamp every stale
  file:line reference (auth.rs shifted ~500-650 lines; the 13 call-site
  list and all evidence-index anchors updated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK
aterga added a commit that referenced this pull request Aug 25, 2026
…148)

* Add scoping doc for a local-deployment MCP instance

Analyze the current beta/prod composition and scope a secondary server
instance that bridges the MCP tools to a local dfx replica instead of
mainnet. Documents the mainnet assumptions that would break locally
(root-key fetch, hard-coded IC_URL, the discovery SSRF/https guard,
target_origin canonicalization, and the CMC/ledger management path), a
proposed local run-profile design, the security guardrails, a phased
work breakdown, and open decisions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Rescope: minimal stdio local binary, no OAuth, prod II

Rewrite the scoping doc for the intended model: a separate, minimal
run-it-yourself binary that speaks MCP over stdio and talks to mainnet
IC + production Internet Identity — not a local dfx replica. It drops
the entire OAuth 2.1 authorization-server layer (the local client is
co-located, so the stdio process boundary replaces bearer-token auth)
while keeping Internet Identity: login runs as a built-in browser
handshake and per-app account delegations work as today.

Adds a verified dependency-stripping analysis (four unreferenced crates
plus a vestigial schemars drop; rmcp swaps to the stdio transport and
drops auth), the auth.rs OAuth-drop/II-connect-keep partition, the
tool/session singleton seam, a 3-crate workspace layout, the security
model, and the production-II verification risks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Keep the crate named imcp2 in the layout

The shared library crate stays imcp2 (its existing embeddable identity)
and the hosted binary stays named imcp2, so the Dockerfile, systemd
unit, and deploy scripts that build/run an imcp2 binary are unchanged.
The hosted server + OAuth layer move behind a default-on `hosted`
feature (optional axum/tower-http); the new minimal stdio binary is a
separate imcp2-local crate depending on imcp2 with default-features
off, so it never compiles the OAuth/HTTP deps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Add "Working with AI tool clients" section

Cover how imcp2-local integrates with Claude (Desktop/Code + the
cloud surfaces), Codex, Cursor, Perplexity, and Antigravity, verified
against each client's current MCP docs.

Key findings: a stdio server is reachable only by clients that spawn a
local subprocess (Claude Desktop/Code, Codex CLI/IDE/desktop, Cursor,
Antigravity, and the Perplexity macOS app via its PerplexityXPC helper);
cloud/remote-only surfaces (claude.ai web/mobile, Perplexity web/Windows,
Codex Cloud) can't reach localhost and need the hosted imcp2 server — the
reason the OAuth layer stays in imcp2. Host-driven OAuth never applies to
stdio, and stderr is invisible in chat on every client, so the II login
URL is surfaced in-band via an authenticate tool (lazy, non-blocking)
plus a best-effort browser auto-open. Adds a capability matrix, per-client
registration snippets, the login invariants, and the Perplexity remote
OAuth/discovery caveats; updates the login section and open decisions to
match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Update the scoping doc for the 63-commit rebase onto v0.2.0

Re-verify every claim and file:line reference against the rebased tree
and absorb what main already did or changed:

- The five vestigial deps (ed25519-dalek, p256, ic-signature-verification,
  ic-representation-independent-hash, schemars 0.8) were already removed
  on main (#100) — reframe that stripping work as done.
- Account for the new hosted-only surfaces the local split must gate:
  prometheus + src/metrics.rs (/metrics, #124) and McpConfig's
  state_dir/require_resource (#144/#127); note the local binary needs no
  state directory at all. pocket-ic (e2e-only, #76) never enters any
  closure.
- Rewrite §10: production II now serves at /mcp by default (#92), CI
  probes it (#113), and the PocketIC e2e harness verifies the full
  connect contract — the remaining unknowns are the local binary's
  http://127.0.0.1 mixed-content and CORS behavior, and the harness is
  the plan for testing the local flow.
- Prod-instance env overrides are II_URL_PROD/II_CANISTER_ID_PROD (#92);
  registration chains are also verified client-side against the agent's
  root key (new_with_root_key); registration_pubkey_b64 is now fallible;
  AuthStore's dropped fields now include mcp_path and require_resource,
  and the dropped OAuth side grew RFC 8707/9207 and the bounded DCR store.
- Note the crates.io publishing dimension (v0.2.0, tag-guarded trusted
  publishing, docs/ excluded from the .crate) and re-stamp every stale
  file:line reference (auth.rs shifted ~500-650 lines; the 13 call-site
  list and all evidence-index anchors updated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Restructure the design doc to the standard feature-doc shape

Reorganize into Summary -> Problem -> Non-goals -> Approach -> Design
components -> Implementation Stages (with the verification plan and
open decisions under the stages, and the code-evidence index as an
appendix). Content is unchanged apart from the connective text: the
same components, client matrix, security model, and file:line evidence,
now in the house design-doc structure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Add a concise high-level companion to the local-deployment design

docs/local-deployment.md is the short, stakeholder-facing overview
(same Summary -> Problem -> Non-goals -> Approach -> Design components
-> Implementation Stages shape, one altitude up: no file:line evidence
or dependency minutiae); the full doc keeps the detail and both docs
cross-link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Refer to ICP CLI-spawned local replicas, not dfx

Local replicas are spawned by the ICP CLI now; drop the dfx naming from
both design docs' non-goals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Make the local-replica mode a test configuration, not a non-goal

Talking to an ICP CLI-spawned local replica is a nice-to-have rather
than something we rule out: a test build enables it with an explicit
IC-endpoint override plus a loopback-guarded fetch_root_key, and the
integration tests run imcp2-local in exactly that configuration
(against an ICP CLI-spawned replica or PocketIC carrying a deployed II
canister). Chains are already verified against the injected agent's
root key, so the flow works on a test network. The shipped binary still
targets mainnet + production II only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* State the no-listening-socket claim precisely

"The server has no listening socket" overclaimed: it is true of the MCP
tool surface (stdio), but the login handshake necessarily binds a
transient loopback listener — II navigates the browser to the callback
and fetches the #4091 well-known from the callback's origin, both of
which require a real HTTP origin (a custom URI scheme has no origin and
II offers no device-grant alternative; this is the RFC 8252 loopback
redirect). Scope the trust-boundary claim to the tool surface, note that
reaching the login listener confers nothing (static pages; /redeem is
state-correlated and only accepts a chain targeting the in-process X),
and add the rationale to the login component.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Recommend an imcp2-core crate over the hosted feature flag

Make the shared components a new imcp2-core library that both binaries
build on: imcp2 (unchanged name, lib + hosted binary, OAuth AS, routers,
metrics, e2e harness) and imcp2-local depend on it, and imcp2 re-exports
core's public items so existing embedders keep compiling.

The default-on `hosted` cargo feature is demoted to a rejected
alternative: features unify additively, so any crate enabling
imcp2/hosted would switch the OAuth/axum closure on for the whole build
graph, leaving the minimal-closure claim invocation-dependent; it also
needs cfg gates scattered through the code, where the crate boundary
states the component split structurally — with the core split the
session seam needs no #[cfg] at all (AuthedSession lives in core and the
Bearer arm reads http::request::Parts). Cost accepted: imcp2-core
becomes a second published crate (family-internal, 0.x, published before
imcp2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Make the session-handling component self-contained in the overview

The overview's "session seam" bullet leaned on the full doc's jargon
(seam, bearer-token lookup, singleton). State the motivation instead:
every tool call acts as the user's II session, the hosted server
resolves it per request from the OAuth bearer token, the single-user
local server reads the one in-memory session from login, and the tool
implementations are identical in both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Make the design docs self-contained

State the final design plainly instead of narrating how it evolved:
drop the rejected-alternative paragraphs and every contrast with
superseded drafts ("not a cargo feature", "originally proposed",
"since this design's first draft", "the original concern is resolved"),
recast the dependency profile per crate as final state, and remove the
settled layout item from the open decisions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Drop the local-replica bullet from Non-goals

The local-replica test configuration is covered where it belongs (the
II wiring component and the verification plan), and the mainnet +
production II targeting is already stated in the Approach, so the
non-goal bullet added nothing but a hedge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Resolve the login-flow choices like the ICP CLI; add end-user setup UX

The login listener is axum and the browser opens via open::that,
matching the ICP CLI's web-identity flow (icp identity link web: an
axum Router on 127.0.0.1:0 with graceful shutdown; open = "5"), so the
two tools behave the same way. Session persistence stays in-memory.
With all three choices settled, the open-decisions sections are gone.

Add component 9, end-user setup (UX): per-client steps with no JSON or
config editing — the double-click .mcpb bundle for Claude Desktop, an
Add-to-Cursor install link, one pasted command for Claude Code and
Codex, connector UIs for Perplexity (macOS) and Antigravity, an
`imcp2-local setup` command that detects installed clients and writes
their configs, and the signing/notarization packaging that makes
double-click installs work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Add the upgrade story to the end-user setup UX

Client registrations point at a stable install path, so an upgrade is
an in-place binary swap picked up on the next server start — nothing to
re-register and no state to migrate (sessions are in-memory).
Distribution and updates follow the ICP CLI: cargo-dist installers with
the axoupdater updater shipped alongside, and channel detection that
surfaces the right upgrade command (axoupdater receipt / Homebrew /
npm). The Claude Desktop bundle upgrades on its own channel by
installing the new .mcpb version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Add signing and provenance to the end-user setup component

Two layers for two verifiers. OS execution gates: macOS Developer ID +
notarization (Gatekeeper is the verifier for browser-downloaded files —
including binaries spawned as MCP subprocesses — and accepts only
Apple-issued signatures; a bare signed+notarized binary passes, no
installer artifact needed) and Windows Authenticode via Azure Trusted
Signing or an EV cert. Supply-chain provenance: keyless GitHub artifact
attestations on every artifact, verifiable with gh attestation verify.
The .mcpb's substance is the signed binary inside (the format specifies
no bundle-level signing), with directory listing as the trusted
distribution path. Notes which channels bypass the gates (installer
script, Homebrew, npm), where the steps hook into the cargo-dist
pipeline, and what DFINITY must acquire with lead time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Promote signing and provenance to its own design component

Give the signing content top-level visibility as component 10 instead
of a block inside the end-user setup component, and mirror it in the
overview's component list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Commit to PocketIC as the integration-test dependency

Integration tests run imcp2-local's local-replica test configuration
against PocketIC carrying a deployed II canister (the pocket-ic dev
dependency, reusing the e2e harness's real-II setup). The endpoint
override still lets a developer point a test build at any other local
replica, but the test plan names one vehicle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* Reconcile the dependency and AuthStore claims with the axum decision

The login-listener decision (axum, matching the ICP CLI) left three
stale claims behind: imcp2-local does depend on axum for its three
transient login routes, so say so in the crate-layout bullet, the net
dependency list, and the overview, scoping the absence claim to
tower-http/prometheus/OAuth and the hosted MCP transport. Also state
the AuthStore reality under the crate split: it stays intact in imcp2
(the retained OAuth routes need all of its state), the extracted
primitives carry none of it, and the local binary replaces it with a
minimal pending-connect state rather than a slimmed copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* docs: address second review round on the local-deployment design

- Component 6: rmcp's #[tool_router] is type-level per impl block, so
  construction mode alone cannot hide tools. Specify that the local-only
  authenticate/auth_status tools live in their own router that only the
  local binary composes, with a hosted-side tools/list exclusion test.
- Component 7: the Singleton session source holds a shared, replaceable
  slot rather than a fixed String — the II contract requires a fresh
  session key after Unauthorized, so re-login must be a same-process
  step with no client restart.
- Sessions wording (both docs): replace the misleading "re-login per
  run" with the actual lifetime — sign in again after a restart, on
  grant expiry, or on revocation; note the one-hour grant requested
  today and the user's consent-time choice of effective TTL.
- Verification: PocketIC exercises the protocol, not browser policy, so
  add a real-browser compatibility gate (automated Chromium/Firefox
  login run plus a manual Safari/enterprise-policy pass) to Stage 2's
  login exit criterion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* docs: address third review round on the local-deployment design

- Component 6: the login tools cannot be an impl block in imcp2-core —
  they drive the loopback listener and browser opener, which live in
  imcp2-local, and core cannot call back into its dependent crate. They
  now live on a small wrapper handler in imcp2-local that merges
  tools/list and dispatches tools/call to its own router before
  forwarding to the inner IcTools handler (RequestContext<RoleServer>
  is handler-independent). Hosted stays login-free by construction,
  with the tools/list test kept as a regression guard.
- Component 1: the crate split is not visibility-neutral — workspace
  packages share no crate-private access, so the composition API
  (IcTools/IcTools::new with SessionSource, the Identities
  registration/redeem entry points, the iiconnect builders) graduates
  from pub(crate) to core's deliberate public API, re-exported by
  imcp2; dropped the "kept verbatim" phrasing.
- Component 4/5: dropping the redeem's redirect tail breaks the
  callback page's only success arm (it treats a JSON redirect as
  success, error otherwise). Specify the second success arm: the local
  redeem answers {"done": true} and the shared page renders "Signed in
  — you can close this tab" in place; hosted keeps answering redirect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

* docs: address fourth review round on the local-deployment design

- Dependency isolation: distinguish direct dependencies and linked code
  from unavoidable transitives. prometheus and the OAuth modules are
  structurally absent from imcp2-local's graph (it never depends on
  imcp2), but tower-http still appears deep in the graph as an
  rmcp/reqwest internal — the guarantee is that the hosted CORS layer
  is not a dependency, not that the crate name vanishes. Scope the
  Stage 1 exit criterion the same way.
- Versioning: an "internal" 0.x label on imcp2-core cannot exempt items
  imcp2 re-exports — those are imcp2 public API and breaking them is a
  breaking change for imcp2 embedders under imcp2's own semver. State
  that the label relaxes stability only for core-only surface, with the
  re-export set curated to the pre-split imcp2 API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AqNkpMQiQHzYxC2djTfvBK

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants