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`)
});
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
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 IC
Agenttype the server is 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
- 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
McpConfigwhen 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 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
- 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-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).