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`)
    });
    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) and SKILLS_URL (the IC skills registry).

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 IC Agent type the server is 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
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 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
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).