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 atMcpServer::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_routeradds the plain-root fallback documents for the origin’s default instance, andauth_callbacks_routerthe 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
Metricshandle and the two request middlewares. Uses theprometheuscrate at the versiondfinity/icpins, 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 areimcp2-core’s — one defining crate, so type identity is preserved). The ICAgenttype the components are built around, re-exported so callers construct the injected agent from the exactic-agentversion 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 areimcp2-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 inMcpConfigwhen the instance’s routers are built. Each instance gets its ownIdentities+AuthStore, so sessions/tokens never cross instances; II trust in the user’s settings is by ORIGIN, which all instances share. - McpConfig
- Everything an
McpServeris built from. - McpServer
- One MCP server instance: the shared state behind
Self::mcp_routerandSelf::well_known_router. Cheap to clone (everything inside is shared). - Session
Gauges - The components this server is composed from, re-exported so embedders keep
importing everything from
imcp2(the types areimcp2-core’s — one defining crate, so type identity is preserved). The two/versionsession gauges, returned together byMcpServer::session_gaugesso a scrape locks and iterates the session map once and reports a consistentactive <= livepair. - Shared
Clients - The dynamic-client-registration store, shared by every instance’s
AuthStore. Client registration is II-agnostic (it only pins redirect URIs to aclient_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 areimcp2-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 wantAgent::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-callbacksand 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’sMcpServers 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.