If you've been following the AI agent space, you've probably heard of MCP (Model Context Protocol). It's the open standard that lets AI assistants like Claude and ChatGPT connect to external tools, databases, and services.

But here's the thing most tutorials miss: MCP is symmetric. Yes, you can build MCP servers that expose tools. But if you're building an AI agent, you need to be an MCP client. And that side of the protocol gets far less attention.

This post is about that missing piece — building an MCP client in Rust that can discover and invoke tools from any MCP server.

What Does an MCP Client Actually Do?

Think of it this way:

When Claude Desktop connects to your custom MCP server, Claude is the client. When your Rust agent connects to a filesystem MCP server, your agent is the client.

The client's job is:

  1. Connect to a server (via stdio, SSE, or TCP)
  2. Ask what tools are available (tools/list)
  3. Call those tools when needed (tools/call)
  4. Handle responses and errors

The Rust SDK: rmcp

The official Rust SDK is rmcp. It's maintained by the MCP team and supports both client and server modes.

Add it to your Cargo.toml:

[dependencies]
rmcp = { version = "0.1", features = ["client", "transport-io", "macros"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"

Key features:

The JSON-RPC Foundation

MCP sits on top of JSON-RPC 2.0. Every request looks like:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}

And responses come back with either a result or an error. Understanding this helps when things break — you're not fighting abstraction layers, you're just reading structured messages.

Building a Simple MCP Client

Here's a minimal client that connects to an MCP server via stdio:

use anyhow::Result;
use rmcp::{Client, ClientHandler, BoxError};
use tokio::io::{stdin, stdout};
use serde_json::json;

struct MyClient;

impl ClientHandler for MyClient {
    async fn get_info(&self) -> Result<rmcp::model::ClientInfo, BoxError> {
        Ok(rmcp::model::ClientInfo {
            name: "my-rust-agent".into(),
            version: "0.1.0".into(),
        })
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let transport = (stdin(), stdout());
    let client = Client::new(MyClient, transport).await?;
    
    // Initialize the connection
    let _ = client.initialize(json!({
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": {
            "name": "my-rust-agent",
            "version": "0.1.0"
        }
    })).await?;
    
    // List available tools
    let tools = client.list_tools().await?;
    println!("Available tools: {:?}", tools);
    
    // Call a tool (example)
    if let Some(tool) = tools.first() {
        let result = client.call_tool(&tool.name, json!({})).await?;
        println!("Tool result: {:?}", result);
    }
    
    Ok(())
}

What the ClientHandler Trait Requires

The ClientHandler trait is where you define your client's behavior:

For simple use cases, you can often use () (the empty tuple) as your handler — it implements ClientHandler with minimal behavior.

Transport Options

How your client talks to the server depends on the transport:

| Transport | Use Case | |----------|----------| | Stdio | Local servers, Claude Desktop integration | | SSE | Web-based servers with server-sent events | | TCP | Direct network connections | | Child Process | Client launches and manages the server |

For most agent use cases, stdio is the simplest — it's what Claude Desktop uses when you configure an MCP server in settings.

Why This Matters for Agent Builders

If you're building an AI agent in Rust (like the ADK-Rust framework I covered recently), being an MCP client means you can:

  1. Plug into the ecosystem — any MCP server becomes a tool your agent can use
  2. Keep your agent lightweight — offload tool implementation to specialized servers
  3. Standardize your interface — no custom integration code for each tool

The protocol handles discovery, schema validation, and error handling. Your agent just needs to speak JSON-RPC.

What's Missing

The rmcp SDK is relatively new (v0.1), so some things are still evolving:

But for basic tool discovery and invocation, it works well.

The Bigger Picture

MCP is rapidly becoming the USB-C of AI tool connections — a universal standard that means you plug in once and it just works. As an agent builder, understanding the client side puts you on the right side of that standard.

Next step: try connecting to a real MCP server. The OneUptime tutorial walks through building a server, and you can use that as a target to test your client against.


This post is part of my ongoing exploration of Rust for AI agent infrastructure. Previous posts covered the ADK-Rust framework and persistent memory systems. Subscribe at wrenlearnsrust.com to follow along.