Rustango docs
← Cookbook

Chapter 17 — MCP server: expose tools to AI agents

The mcp feature turns a rustango app into a Model Context Protocol server: external AI agents authenticate as tenant-scoped identities, are granted skills (bundles of tools + a prompt + resources), and call your framework-exposed tools over JSON-RPC 2.0 / Streamable HTTP.

# Cargo.toml
rustango = { version = "0.43", features = ["mcp"] }
# mcp pulls tenancy + sse + serializer + openapi automatically.

1. Register a tool

A tool is a typed-input async handler registered at compile time. The input type provides the MCP inputSchema (via OpenApiSchema) and is used to validate arguments before the handler runs.

use rustango::mcp::{McpContext, McpError};
use rustango::openapi::{OpenApiSchema, Schema};

#[derive(serde::Deserialize)]
struct AddInput { a: i64, b: i64 }

impl OpenApiSchema for AddInput {
    fn openapi_schema() -> Schema {
        Schema::object()
            .property("a", Schema::integer())
            .property("b", Schema::integer())
            .required(["a", "b"])
    }
}

rustango::register_mcp_tool!(
    "math.add", "Add two integers", AddInput,
    |ctx: McpContext, input: AddInput| async move {
        // `ctx.pool` is the caller's tenant pool; `ctx.agent` the principal.
        let _ = &ctx.pool;
        Ok::<_, McpError>(serde_json::json!({ "sum": input.a + input.b }))
    },
);

A static, always-available resource:

rustango::register_mcp_resource!(
    "rustango://about", "About", "text/plain",
    || "This server is powered by rustango.".to_string(),
);

2. Mount the router (multi-tenant)

let api = axum::Router::new()
    .nest("/mcp", rustango::mcp::secure_tenant_router());  // agent-guarded
// or from settings: rustango::mcp::secure_tenant_router_from_settings(&settings.mcp)

secure_tenant_router adds POST /mcp/token (credential exchange) and guards the JSON-RPC POST /mcp behind a tenant-pinned agent JWT, signed with RUSTANGO_SESSION_SECRET.

MCP is an optional feature — its tables aren't in the universal bootstrap

The agent/skill models (rustango_agents, rustango_agent_skills, …_skill_tools, …_agent_grants, …_skill_resources) are gated behind the mcp feature, so a tenancy project that doesn't enable mcp never compiles them and never carries them in its bootstrap migration. Like the framework's other optional tables (content_types, audit_log, permissions), they're created via the lazy ensure-table path the first time the MCP layer touches them (manage create-agent / create-skill / grant-skill), not by the core tenant bootstrap. App make_migrations never emits create/drop for rustango_* tables — the framework reserves that prefix for tables it manages itself.

3. Provision agents + skills (CLI)

manage create-agent  acme ci-bot              # prints a one-time secret
manage create-skill  acme math --name "Math" --tools math.add \
                       --instructions "You can do arithmetic."
manage grant-skill   acme ci-bot math         # ci-bot can now call math.add
manage list-agents   acme
manage list-skills   acme
manage rotate-agent-secret acme ci-bot        # new secret; old one dies
manage revoke-skill  acme ci-bot math

4. The agent's flow

POST /mcp/token  {"name":"ci-bot","secret":"<prefix.secret>"}
   → { "access_token": "<jwt>", "token_type": "Bearer", "expires_in": 900 }

POST /mcp   Authorization: Bearer <jwt>
   initialize → capabilities
   tools/list → only the tools the agent's skills grant
   tools/call {"name":"math.add","arguments":{"a":2,"b":3}} → {"sum":5}
   prompts/get {"name":"math"} → the skill's instructions
   resources/read {"uri":"rustango://about"} → the resource body

Authorization is fail-closed: a tool/prompt/resource outside the agent's granted skills is never listed and never executed, and a token minted for tenant A is refused on tenant B. Every tools/call is audited. Configure the mount prefix, token TTL, SSE, CORS, and rate limit under [mcp] in your settings file.