Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nearai/ironclaw/llms.txt

Use this file to discover all available pages before exploring further.

IronClaw supports the Model Context Protocol (MCP), allowing you to connect to external tool servers that provide additional capabilities through a standardized JSON-RPC interface. Location: src/tools/mcp/

What is MCP?

MCP (Model Context Protocol) is a standardized protocol for connecting AI agents to external tool servers. It provides:
  • JSON-RPC communication: Standard request/response protocol
  • Tool discovery: Dynamic tool listing via tools/list
  • Tool execution: Invoke tools via tools/call
  • Authentication: Support for OAuth and custom auth
  • Ecosystem: Pre-built servers for GitHub, Notion, Postgres, etc.
Protocol version: 2024-11-05 Location: src/tools/mcp/protocol.rs:6

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                          MCP Server Integration                              │
│                                                                              │
│   IronClaw Agent ──▶ McpClient ──▶ HTTP/JSON-RPC ──▶ MCP Server            │
│                          │                              (external process)   │
│                          │                                                   │
│                          ▼                                                   │
│                   Tool Wrapper ──▶ ToolRegistry                             │
│                   (implements Tool trait)                                    │
└─────────────────────────────────────────────────────────────────────────────┘

MCP Protocol Types

Tool Definition

Location: src/tools/mcp/protocol.rs:8-28
pub struct McpTool {
    pub name: String,
    pub description: String,
    pub input_schema: serde_json::Value,
    pub annotations: Option<McpToolAnnotations>,
}

pub struct McpToolAnnotations {
    pub destructive_hint: bool,
    pub side_effects_hint: bool,
    pub read_only_hint: bool,
    pub execution_time_hint: Option<ExecutionTimeHint>,
}

pub enum ExecutionTimeHint {
    Fast,    // < 1 second
    Medium,  // 1-10 seconds
    Slow,    // > 10 seconds
}
Example tool from MCP server:
{
  "name": "github-copilot_list_issues",
  "description": "List issues for a repository",
  "inputSchema": {
    "type": "object",
    "properties": {
      "owner": { "type": "string", "description": "Repository owner" },
      "repo": { "type": "string", "description": "Repository name" },
      "state": { "type": "string", "enum": ["open", "closed", "all"] }
    },
    "required": ["owner", "repo"]
  },
  "annotations": {
    "destructive_hint": false,
    "read_only_hint": true
  }
}

Requests

Location: src/tools/mcp/protocol.rs:78-148
pub struct McpRequest {
    pub jsonrpc: String,  // "2.0"
    pub id: u64,
    pub method: String,
    pub params: Option<serde_json::Value>,
}

impl McpRequest {
    // Initialize connection
    pub fn initialize(id: u64) -> Self;
    
    // List available tools
    pub fn list_tools(id: u64) -> Self;
    
    // Call a tool
    pub fn call_tool(id: u64, name: &str, arguments: serde_json::Value) -> Self;
}

Responses

Location: src/tools/mcp/protocol.rs:150-176
pub struct McpResponse {
    pub jsonrpc: String,
    pub id: u64,
    pub result: Option<serde_json::Value>,
    pub error: Option<McpError>,
}

pub struct McpError {
    pub code: i32,
    pub message: String,
    pub data: Option<serde_json::Value>,
}

Content Blocks

Location: src/tools/mcp/protocol.rs:268-292
pub enum ContentBlock {
    Text { text: String },
    Image { data: String, mime_type: String },
    Resource { uri: String, mime_type: Option<String>, text: Option<String> },
}

MCP Client

Location: src/tools/mcp/client.rs
pub struct McpClient {
    base_url: String,
    http_client: reqwest::Client,
    request_id: AtomicU64,
    config: Option<McpServerConfig>,
    session_manager: Option<Arc<McpSessionManager>>,
    secrets: Option<Arc<dyn SecretsStore>>,
    user_id: Option<String>,
}

impl McpClient {
    // Create client for unauthenticated server
    pub fn new(base_url: impl Into<String>) -> Self;
    
    // Create client for authenticated server
    pub fn new_authenticated(
        config: McpServerConfig,
        session_manager: Arc<McpSessionManager>,
        secrets: Arc<dyn SecretsStore>,
        user_id: impl Into<String>,
    ) -> Self;
    
    // Initialize connection
    pub async fn initialize(&self) -> Result<InitializeResult, McpError>;
    
    // List available tools
    pub async fn list_tools(&self) -> Result<Vec<McpTool>, McpError>;
    
    // Call a tool
    pub async fn call_tool(
        &self,
        name: &str,
        arguments: serde_json::Value,
    ) -> Result<CallToolResult, McpError>;
    
    // Create Tool wrappers for registry
    pub async fn create_tools(&self) -> Result<Vec<Arc<dyn Tool>>, McpError>;
}

Configuration

Location: src/tools/mcp/config.rs

Server Config

pub struct McpServerConfig {
    pub name: String,
    pub url: String,
    pub auth: Option<OAuthConfig>,
    pub metadata: HashMap<String, String>,
}

pub struct OAuthConfig {
    pub authorization_url: String,
    pub token_url: String,
    pub client_id: String,
    pub client_secret: Option<String>,
    pub scopes: Vec<String>,
    pub redirect_uri: Option<String>,
}

Config File

mcp_servers.json:
{
  "servers": {
    "github": {
      "url": "https://mcp.github.com",
      "auth": {
        "type": "oauth",
        "authorization_url": "https://github.com/login/oauth/authorize",
        "token_url": "https://github.com/login/oauth/access_token",
        "client_id": "${GITHUB_OAUTH_CLIENT_ID}",
        "client_secret": "${GITHUB_OAUTH_CLIENT_SECRET}",
        "scopes": ["repo", "read:org"]
      }
    },
    "notion": {
      "url": "https://mcp.notion.so",
      "auth": {
        "type": "oauth",
        "authorization_url": "https://api.notion.com/v1/oauth/authorize",
        "token_url": "https://api.notion.com/v1/oauth/token",
        "client_id": "${NOTION_OAUTH_CLIENT_ID}",
        "scopes": []
      }
    },
    "postgres": {
      "url": "http://localhost:3000",
      "metadata": {
        "description": "Local PostgreSQL MCP server"
      }
    }
  }
}

Authentication

OAuth Flow

Location: src/tools/mcp/auth.rs
// Check if user has valid access token
pub async fn is_authenticated(
    config: &McpServerConfig,
    session_manager: &McpSessionManager,
    user_id: &str,
) -> bool;

// Refresh expired access token
pub async fn refresh_access_token(
    config: &McpServerConfig,
    session_manager: &McpSessionManager,
    secrets: &dyn SecretsStore,
    user_id: &str,
) -> Result<String, AuthError>;
OAuth flow steps:
  1. Check if access token exists and is valid
  2. If expired, use refresh token to get new access token
  3. If no refresh token, initiate new OAuth flow
  4. Store new tokens securely

Session Management

Location: src/tools/mcp/session.rs
pub struct McpSessionManager {
    sessions: RwLock<HashMap<String, McpSession>>,
}

pub struct McpSession {
    pub user_id: String,
    pub server_name: String,
    pub access_token: String,
    pub refresh_token: Option<String>,
    pub expires_at: Option<SystemTime>,
}

impl McpSessionManager {
    pub fn new() -> Self;
    
    pub async fn get_session(
        &self,
        user_id: &str,
        server_name: &str,
    ) -> Option<McpSession>;
    
    pub async fn store_session(&self, session: McpSession);
    
    pub async fn remove_session(&self, user_id: &str, server_name: &str);
}

Usage Examples

Connect to Unauthenticated Server

use ironclaw::tools::mcp::McpClient;

// Connect to local server
let client = McpClient::new("http://localhost:3000");

// Initialize connection
let init = client.initialize().await?;
println!("Connected to: {:?}", init.server_info);

// List tools
let tools = client.list_tools().await?;
for tool in tools {
    println!("- {}: {}", tool.name, tool.description);
}

// Call a tool
let result = client.call_tool(
    "postgres_query",
    serde_json::json!({
        "query": "SELECT * FROM users LIMIT 10"
    }),
).await?;

println!("Result: {:?}", result);

Connect to OAuth Server

use ironclaw::tools::mcp::{McpClient, McpServerConfig, OAuthConfig, McpSessionManager};
use std::sync::Arc;

// Load config
let config = McpServerConfig {
    name: "github".to_string(),
    url: "https://mcp.github.com".to_string(),
    auth: Some(OAuthConfig {
        authorization_url: "https://github.com/login/oauth/authorize".to_string(),
        token_url: "https://github.com/login/oauth/access_token".to_string(),
        client_id: std::env::var("GITHUB_OAUTH_CLIENT_ID")?,
        client_secret: Some(std::env::var("GITHUB_OAUTH_CLIENT_SECRET")?),
        scopes: vec!["repo".to_string()],
        redirect_uri: None,
    }),
    metadata: HashMap::new(),
};

let session_manager = Arc::new(McpSessionManager::new());
let secrets = Arc::new(my_secrets_store);

// Create authenticated client
let client = McpClient::new_authenticated(
    config,
    session_manager,
    secrets,
    "user_123",
);

// Use as normal
let tools = client.list_tools().await?;

Register MCP Tools

use ironclaw::tools::ToolRegistry;

// Create client and initialize
let client = McpClient::new("http://localhost:3000");
client.initialize().await?;

// Create tool wrappers
let tools = client.create_tools().await?;

// Register with tool registry
let registry = Arc::new(ToolRegistry::new());
for tool in tools {
    registry.register(tool).await;
}

println!("Registered {} MCP tools", registry.count());

Tool Annotations

MCP tools can provide hints about their behavior:
impl McpTool {
    pub fn requires_approval(&self) -> bool {
        self.annotations
            .as_ref()
            .map(|a| a.destructive_hint)
            .unwrap_or(false)
    }
}
Location: src/tools/mcp/protocol.rs:68-76 Annotation types:
  • destructive_hint: Tool performs destructive operations (requires approval)
  • side_effects_hint: Tool has side effects beyond return value
  • read_only_hint: Tool only reads data
  • execution_time_hint: Expected execution time (fast/medium/slow)

MCP vs WASM Tools

Both are first-class in the extension system (ironclaw tool install handles both), but they have different strengths. WASM Tools (IronClaw native):
  • ✅ Sandboxed: fuel metering, memory limits, no access except allowlist
  • ✅ Credentials injected by host, tool code never sees actual token
  • ✅ Output scanned for secret leakage before returning to LLM
  • ✅ Auth (OAuth/manual) declared in capabilities, agent handles flow
  • ✅ Single binary, no process management, works offline
  • ❌ Must build yourself in Rust, no ecosystem, synchronous only
MCP Servers (Model Context Protocol):
  • ✅ Growing ecosystem of pre-built servers (GitHub, Notion, Postgres, etc.)
  • ✅ Any language (TypeScript/Python most common)
  • ✅ Can do websockets, streaming, background polling
  • ❌ External process with full system access (no sandbox)
  • ❌ Manages own credentials, IronClaw can’t prevent leaks
Decision guide:
ScenarioUse
Good MCP server already existsMCP
Handles sensitive credentials (email send, banking)WASM
Quick prototype or one-off integrationMCP
Core capability you’ll maintain long-termWASM
Needs background connections (websockets, polling)MCP
Multiple tools share one OAuth token (e.g., Google suite)WASM
Source: src/tools/README.md:105-136

MCP Server Examples

Popular MCP servers:
  • @modelcontextprotocol/server-github - GitHub API access
  • @modelcontextprotocol/server-postgres - PostgreSQL queries
  • @modelcontextprotocol/server-filesystem - File operations
  • @modelcontextprotocol/server-slack - Slack integration
  • notion-mcp-server - Notion workspace access
  • mcp-server-sqlite - SQLite database access
Find more at: https://github.com/modelcontextprotocol

Running MCP Servers

Node.js Server

# Install MCP server
npm install -g @modelcontextprotocol/server-postgres

# Run server
PORT=3000 DATABASE_URL=postgresql://... npx mcp-server-postgres

Python Server

# Install MCP server
pip install mcp-server-sqlite

# Run server
mcp-server-sqlite --port 3000 --db-path ./data.db

Docker Server

docker run -p 3000:3000 \
  -e DATABASE_URL=postgresql://... \
  modelcontextprotocol/server-postgres

Protocol Details

Initialize Handshake

// Request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "roots": { "listChanged": false },
      "sampling": {}
    },
    "clientInfo": {
      "name": "ironclaw",
      "version": "0.1.0"
    }
  }
}

// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": { "listChanged": false }
    },
    "serverInfo": {
      "name": "postgres-mcp-server",
      "version": "1.0.0"
    }
  }
}

List Tools

// Request
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}

// Response
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "postgres_query",
        "description": "Execute a SQL query",
        "inputSchema": {
          "type": "object",
          "properties": {
            "query": { "type": "string", "description": "SQL query to execute" }
          },
          "required": ["query"]
        },
        "annotations": {
          "destructive_hint": false,
          "read_only_hint": true
        }
      }
    ]
  }
}

Call Tool

// Request
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "postgres_query",
    "arguments": {
      "query": "SELECT * FROM users LIMIT 5"
    }
  }
}

// Response
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "id | name | email\n1 | Alice | alice@example.com\n2 | Bob | bob@example.com"
      }
    ],
    "is_error": false
  }
}

Next Steps

Building Tools

Create custom tools using the builder

WASM Tools

Build sandboxed WASM tools

Build docs developers (and LLMs) love