Skip to main content

Crate imcp2

Crate imcp2 

Source
Expand description

IMCP2 — Internet Computer MCP server as an embeddable axum library.

The server exposes MCP tools over streamable HTTP that talk to the Internet Computer via ic-agent (discover the canisters behind an app, read/write canisters as the user’s Internet Identity account, OQL reads, canister management, …), gated by an OAuth 2.1 authorization server whose login mechanism is Internet Identity’s /mcp connect handshake.

One McpServer serves one Internet Identity instance (IiInstance) and packages everything as two axum::Routers:

  • McpServer::mcp_router — the MCP endpoint plus the OAuth authorization server under /oauth, nested at McpServer::mcp_path.
  • McpServer::well_known_router — the OAuth discovery documents. Well-known URIs are origin-scoped (RFC 8615), so this router is merged at the application root; its handlers are parametric on the path the mcp router is nested at (the RFC 8414 / RFC 9728 path-inserted locations). McpServer::root_well_known_router adds the plain-root fallback documents for the origin’s default instance, and auth_callbacks_router the origin-global II auth-callback allow-list covering every instance.

The IC Agent is inherited from the embedding application, not built here: a host (an API boundary node, a gateway, or the bundled binary) passes in its own agent, so anonymous canister calls go through the host’s boundary-node client and the whole process links a single ic-agent.

use imcp2::{auth_callbacks_router, Agent, IiInstance, McpConfig, McpServer, SharedClients, IC_URL};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Or hand in the host's own agent instead of building a default one.
    let agent = Agent::builder().with_url(IC_URL).build()?;
    // Where imcp2 keeps operational state (the client-registration store).
    let state_dir = std::path::PathBuf::from("/var/lib/imcp2");
    let server = McpServer::new(McpConfig {
        agent,
        instance: IiInstance::beta().map_err(anyhow::Error::msg)?,
        public_url: "https://mcp.example.com".into(),
        mcp_path: "/mcp".into(),
        clients: SharedClients::load(&state_dir),
        state_dir,
        require_resource: true, // strict RFC 8707 (reject a missing `resource`)
        cimd_enabled: true, // Client ID Metadata Documents (URL client_ids); false turns them off
    });
    server.spawn_session_reaper();
    let app = axum::Router::new()
        // nest_service (not nest): it also forwards the bare
        // trailing-slash form (`/mcp/`) into the router.
        .nest_service(server.mcp_path(), server.mcp_router())
        .merge(server.well_known_router())
        // Exactly one instance per origin also answers the root probes
        // and serves the origin-global II auth-callback allow-list.
        .merge(server.root_well_known_router())
        .merge(auth_callbacks_router(&[&server]));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await?;
    axum::serve(listener, app).await?;
    Ok(())
}

Several instances can share one origin (e.g. production II at /mcp, beta II at /mcp-beta): give each its own McpServer (sessions and tokens never cross instances), share ONE SharedClients between them — loaded once from the same McpConfig::state_dir — so dynamic client registrations (II-agnostic) persist to a single snapshot, and pass every instance to auth_callbacks_router so the one allow-list document declares all callbacks.

The operational-files location is McpConfig::state_dir (the embedder supplies it; the imcp2 binary reads $IMCP2_STATE_DIR). Remaining knobs are environment variables read where they are used: II_URL / II_CANISTER_ID and II_URL_PROD / II_CANISTER_ID_PROD (the Internet Identity instances).

Re-exports§

pub use ic_agent;

Modules§

metrics
Prometheus instrumentation, usable by embedders as well as by the bundled binary — it exports the Metrics handle and the two request middlewares. Uses the prometheus crate at the version dfinity/ic pins, so these series land in the same clusters without a second dialect.

Structs§

Agent
The components this server is composed from, re-exported so embedders keep importing everything from imcp2 (the types are imcp2-core’s — one defining crate, so type identity is preserved). The IC Agent type the components are built around, re-exported so callers construct the injected agent from the exact ic-agent version this crate links. A low level Agent to make calls to a Replica endpoint.
IiInstance
The components this server is composed from, re-exported so embedders keep importing everything from imcp2 (the types are imcp2-core’s — one defining crate, so type identity is preserved). One Internet Identity instance this server can connect users against — purely which II (origin + canister), nothing about where the instance is mounted on this server: the mount path is deployment composition, chosen in McpConfig when the instance’s routers are built. Each instance gets its own Identities + AuthStore, so sessions/tokens never cross instances; II trust in the user’s settings is by ORIGIN, which all instances share.
McpConfig
Everything an McpServer is built from.
McpServer
One MCP server instance: the shared state behind Self::mcp_router and Self::well_known_router. Cheap to clone (everything inside is shared).
SessionGauges
The components this server is composed from, re-exported so embedders keep importing everything from imcp2 (the types are imcp2-core’s — one defining crate, so type identity is preserved). The two /version session gauges, returned together by McpServer::session_gauges so a scrape locks and iterates the session map once and reports a consistent active <= live pair.
SharedClients
The dynamic-client-registration store, shared by every instance’s AuthStore. Client registration is II-agnostic (it only pins redirect URIs to a client_id), so a client registered against either instance’s AS is known to both — and, since both stores share one map, the persisted snapshot never loses the other instance’s entries.

Constants§

IC_URL
The components this server is composed from, re-exported so embedders keep importing everything from imcp2 (the types are imcp2-core’s — one defining crate, so type identity is preserved). A sensible default IC API boundary node (the public mainnet endpoint) for callers that just want Agent::builder().with_url(IC_URL).build(). A host with its own boundary-node routing supplies an agent built against that instead.

Functions§

auth_callbacks_router
The origin-global II auth-callback allow-list (II #4091): before contacting the connect callback named in the (attacker-craftable) link fragment, II fetches this origin’s /.well-known/ii-auth-callbacks and requires the callback to be EXACTLY one of the declared entries — fail-closed, so serving it is mandatory once #4091 ships. The path carries no instance prefix, so ONE document must declare every instance’s callback: pass all of an origin’s McpServers and merge the router at the application root. CORS-open (II’s frontend fetches it cross-origin).
ii_app_metadata_router
The origin-global II app metadata document. Any origin can name itself and link its own privacy policy and terms of service on Internet Identity’s screens — permissionlessly, with no involvement from the II team — by serving them here; II reads the document when the user connects this server and shows the name beside the origin it has verified. The metadata is only as trustworthy as the origin serving it, so it identifies this deployment, it does not authenticate it.