MCP integration
The Model Context Protocol (MCP) lets agents discover and use tools, resources, and prompts from external servers. This chapter covers agentkit-mcp: how MCP fits into the capability and tool layers, and how auth and lifecycle are managed.
What MCP solves
Without MCP, every external integration is a custom tool. Connecting to GitHub means writing a GitHub tool. Connecting to a database means writing a database tool. Each one has bespoke connection logic, auth handling, and discovery.
MCP standardizes this: external servers expose capabilities through a uniform protocol, and the agent discovers them at runtime instead of compile time.
Without MCP: With MCP:
Agent Agent
├── GitHubTool (custom) ├── MCP client
├── DatabaseTool (custom) │ ├── github-server (discovered)
├── SlackTool (custom) │ ├── database-server (discovered)
└── JiraTool (custom) │ └── slack-server (discovered)
│
Each tool: custom code, Each server: standard protocol,
custom auth, custom schema standard auth, standard schema
MCP in the capability model
MCP servers expose three capability types, which map directly to agentkit’s capability layer:
| MCP concept | agentkit abstraction | How it’s used |
|---|---|---|
| MCP tools | Invocable → adapted to Tool | Model calls them during turns |
| MCP resources | ResourceProvider | Host reads them for context loading |
| MCP prompts | PromptProvider | Host renders them for transcript injection |
An MCP server implements CapabilityProvider, exposing all three through one registration point.
Server configuration
#![allow(unused)]
fn main() {
pub struct McpServerConfig {
pub id: McpServerId,
pub transport: McpTransportBinding,
pub auth: McpAuthConfig,
pub metadata: MetadataMap,
}
}
Built-in transports: stdio (local child process), Streamable HTTP (modern remote MCP), and legacy SSE (deprecated HTTP+SSE compatibility). Custom transports implement the McpTransportFactory trait.
Discovery
After connecting, the server’s capabilities are captured in a snapshot:
#![allow(unused)]
fn main() {
pub struct McpDiscoverySnapshot {
pub server_id: McpServerId,
pub tools: Vec<McpToolDescriptor>,
pub resources: Vec<McpResourceDescriptor>,
pub prompts: Vec<McpPromptDescriptor>,
pub metadata: MetadataMap,
}
}
Snapshots are cacheable and refreshable. Hosts choose which capabilities to expose — discovery doesn’t automatically register everything.
Tool adaptation
McpToolAdapter wraps an MCP tool as a Tool implementation:
- Exposes a
ToolSpecderived from the MCP tool descriptor - Translates
ToolRequestinto MCP invocation - Translates MCP responses into normalized
ToolResult - Surfaces auth interruptions as
ToolInterruption::AuthRequired
Namespacing
MCP tools are namespaced by default: mcp.<server_id>.<tool_name>. This prevents collisions with native tools. Hosts can override names if they want a cleaner surface.
Auth as interruption
MCP auth follows the same interrupt pattern as tool approvals:
- Tool invocation triggers an auth requirement
- The tool adapter returns
ToolInterruption::AuthRequired - The loop surfaces it as
LoopStep::Interrupt(AuthRequest) - The host performs the auth flow (OAuth, API key entry, etc.)
- The host resolves the interrupt and the operation resumes
Auth is never hidden retry logic. The host always knows when auth is happening and controls the flow.
For non-tool MCP operations (connecting, reading resources), auth follows the same pattern but through the MCP manager API rather than the loop interrupt system.
Resources and prompts
Resources and prompts have dedicated APIs, separate from the tool path:
#![allow(unused)]
fn main() {
pub trait McpResourceStore {
async fn list_resources(&self, server: &McpServerId) -> ...;
async fn read_resource(&self, server: &McpServerId, resource: &McpResourceId) -> ...;
}
pub trait McpPromptStore {
async fn list_prompts(&self, server: &McpServerId) -> ...;
async fn get_prompt(&self, server: &McpServerId, prompt: &McpPromptId, args: Value) -> ...;
}
}
MCP resources integrate with agentkit-context for injecting project-specific data into the transcript. MCP prompts integrate with context loading for template-based prompt assembly.
Transports
Transport details stay inside agentkit-mcp:
#![allow(unused)]
fn main() {
pub trait McpTransport: Send {
async fn send(&mut self, message: McpFrame) -> Result<(), McpError>;
async fn recv(&mut self) -> Result<Option<McpFrame>, McpError>;
async fn close(&mut self) -> Result<(), McpError>;
}
}
Built-in transports:
| Transport | Connection | Use case |
|---|---|---|
| stdio | Spawn child process, pipe stdin/stdout | Local tool servers |
| Streamable HTTP | HTTP POST with JSON or SSE responses | Modern remote tool servers |
| SSE | HTTP connection with server-sent events | Legacy remote tool servers |
The rest of agentkit doesn’t know whether a server is reached via stdio, TCP, or WebSocket. The transport is configured in McpServerConfig and the MCP manager handles the connection lifecycle.
stdio transport
The most common pattern for local MCP servers. The agent spawns the server as a child process and communicates over stdin/stdout:
Agent process ──── stdin ────▶ MCP server process
◀── stdout ────
This is how tools like GitHub’s MCP server, filesystem tools, and database connectors typically run. The server starts on demand and exits when the agent disconnects.
Streamable HTTP transport
For modern remote MCP servers that run as HTTP services. The agent sends JSON-RPC over HTTP POST, receives either JSON or SSE responses, and tracks negotiated session/protocol headers:
Agent ──── HTTP POST ────▶ Remote MCP server
◀── JSON or SSE ───
If an SSE response stream is interrupted before the matching response arrives, the client can resume with Last-Event-ID.
Legacy SSE transport
For older MCP servers that still use the deprecated HTTP+SSE transport, agentkit-mcp also keeps the original SSE endpoint flow.
The full picture
┌──────────────────────────────────────────────────────────┐
│ Agent loop │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Native tools │ │ MCP tools │ │
│ │ (ToolRegistry) │ │ (McpToolAdapter) │ │
│ │ fs.read_file │ │ mcp.github.search │ │
│ │ shell.exec │ │ mcp.db.query │ │
│ └──────────┬───────────┘ └──────────┬───────────┘ │
│ │ │ │
│ └──── unified tool list ───┘ │
│ │ │
│ presented to model │
│ │
│ MCP resources ──▶ ContextLoader ──▶ transcript │
│ MCP prompts ──▶ ContextLoader ──▶ transcript │
└──────────────────────────────────────────────────────────┘
Native tools and MCP tools appear as a single list to the model. The model doesn’t know (or need to know) which tools come from MCP and which are native. The mcp.<server_id>. prefix distinguishes them in the tool name for human readers and policy evaluation, but the model just sees a tool spec with a name and schema.
Example:
openrouter-mcp-tooldemonstrates MCP tool discovery and invocation.openrouter-agent-clishows MCP integrated into a full agent with context, tools, and compaction.Crate:
agentkit-mcp— depends onagentkit-capabilities,agentkit-tools-core, andagentkit-core.