Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[workspace]
members = ["crates/*"]
resolver = "2"

[workspace.dependencies]
mcp-core = { path = "./crates/mcp-core" }
mcp-macros = { path = "./crates/mcp-macros" }

[workspace.package]
edition = "2021"
version = "1.0.7"
authors = ["Block <ai-oss-tools@block.xyz>"]
license = "MIT"
repository = "https://github.com/modelcontextprotocol/rust-sdk/"
description = "Rust SDK for the Model Context Protocol"
24 changes: 24 additions & 0 deletions crates/mcp-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "mcp-client"
version.workspace = true
edition.workspace = true

[dependencies]
mcp-core = { workspace = true }
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "rustls-tls"] }
eventsource-client = "0.12.0"
futures = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
async-trait = "0.1.83"
url = "2.5.4"
thiserror = "1.0"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tower = { version = "0.4", features = ["timeout", "util"] }
tower-service = "0.3"
rand = "0.8"

[dev-dependencies]
11 changes: 11 additions & 0 deletions crates/mcp-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Testing stdio transport

```bash
cargo run -p mcp-client --example stdio
```

## Testing SSE transport

1. Start the MCP server in one terminal: `fastmcp run -t sse echo.py`
2. Run the client example in new terminal: `cargo run -p mcp-client --example sse`

131 changes: 131 additions & 0 deletions crates/mcp-client/examples/clients.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use mcp_client::{
client::{ClientCapabilities, ClientInfo, McpClient, McpClientTrait},
transport::{SseTransport, StdioTransport, Transport},
McpService,
};
use rand::Rng;
use rand::SeedableRng;
use std::time::Duration;
use std::{collections::HashMap, sync::Arc};
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize logging
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env().add_directive("mcp_client=debug".parse().unwrap()),
)
.init();

let transport1 = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()], HashMap::new());
let handle1 = transport1.start().await?;
let service1 = McpService::with_timeout(handle1, Duration::from_secs(30));
let client1 = McpClient::new(service1);

let transport2 = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()], HashMap::new());
let handle2 = transport2.start().await?;
let service2 = McpService::with_timeout(handle2, Duration::from_secs(30));
let client2 = McpClient::new(service2);

let transport3 = SseTransport::new("http://localhost:8000/sse", HashMap::new());
let handle3 = transport3.start().await?;
let service3 = McpService::with_timeout(handle3, Duration::from_secs(10));
let client3 = McpClient::new(service3);

// Initialize both clients
let mut clients: Vec<Box<dyn McpClientTrait>> =
vec![Box::new(client1), Box::new(client2), Box::new(client3)];

// Initialize all clients
for (i, client) in clients.iter_mut().enumerate() {
let info = ClientInfo {
name: format!("example-client-{}", i + 1),
version: "1.0.0".to_string(),
};
let capabilities = ClientCapabilities::default();

println!("\nInitializing client {}", i + 1);
let init_result = client.initialize(info, capabilities).await?;
println!("Client {} initialized: {:?}", i + 1, init_result);
}

// List tools for all clients
for (i, client) in clients.iter_mut().enumerate() {
let tools = client.list_tools(None).await?;
println!("\nClient {} tools: {:?}", i + 1, tools);
}

println!("\n\n----------------------------------\n\n");

// Wrap clients in Arc before spawning tasks
let clients = Arc::new(clients);
let mut handles = vec![];

for i in 0..20 {
let clients = Arc::clone(&clients);
let handle = tokio::spawn(async move {
// let mut rng = rand::thread_rng();
let mut rng = rand::rngs::StdRng::from_entropy();
tokio::time::sleep(Duration::from_millis(rng.gen_range(5..50))).await;

// Randomly select an operation
match rng.gen_range(0..4) {
0 => {
println!("\n{i}: Listing tools for client 1 (stdio)");
match clients[0].list_tools(None).await {
Ok(tools) => {
println!(" {i}: -> Got tools, first one: {:?}", tools.tools.first())
}
Err(e) => println!(" {i}: -> Error: {}", e),
}
}
1 => {
println!("\n{i}: Calling tool for client 2 (stdio)");
match clients[1]
.call_tool("git_status", serde_json::json!({ "repo_path": "." }))
.await
{
Ok(result) => println!(
" {i}: -> Tool execution result, is_error: {:?}",
result.is_error
),
Err(e) => println!(" {i}: -> Error: {}", e),
}
}
2 => {
println!("\n{i}: Listing tools for client 3 (sse)");
match clients[2].list_tools(None).await {
Ok(tools) => {
println!(" {i}: -> Got tools, first one: {:?}", tools.tools.first())
}
Err(e) => println!(" {i}: -> Error: {}", e),
}
}
3 => {
println!("\n{i}: Calling tool for client 3 (sse)");
match clients[2]
.call_tool(
"echo_tool",
serde_json::json!({ "message": "Client with SSE transport - calling a tool" }),
)
.await
{
Ok(result) => println!(" {i}: -> Tool execution result, is_error: {:?}", result.is_error),
Err(e) => println!(" {i}: -> Error: {}", e),
}
}
_ => unreachable!(),
}
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
});
handles.push(handle);
}

// Wait for all tasks to complete
for handle in handles {
handle.await.unwrap().unwrap();
}

Ok(())
}
70 changes: 70 additions & 0 deletions crates/mcp-client/examples/sse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use anyhow::Result;
use mcp_client::client::{ClientCapabilities, ClientInfo, McpClient, McpClientTrait};
use mcp_client::transport::{SseTransport, Transport};
use mcp_client::McpService;
use std::collections::HashMap;
use std::time::Duration;
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env()
.add_directive("mcp_client=debug".parse().unwrap())
.add_directive("eventsource_client=info".parse().unwrap()),
)
.init();

// Create the base transport
let transport = SseTransport::new("http://localhost:8000/sse", HashMap::new());

// Start transport
let handle = transport.start().await?;

// Create the service with timeout middleware
let service = McpService::with_timeout(handle, Duration::from_secs(3));

// Create client
let mut client = McpClient::new(service);
println!("Client created\n");

// Initialize
let server_info = client
.initialize(
ClientInfo {
name: "test-client".into(),
version: "1.0.0".into(),
},
ClientCapabilities::default(),
)
.await?;
println!("Connected to server: {server_info:?}\n");

// Sleep for 100ms to allow the server to start - surprisingly this is required!
tokio::time::sleep(Duration::from_millis(500)).await;

// List tools
let tools = client.list_tools(None).await?;
println!("Available tools: {tools:?}\n");

// Call tool
let tool_result = client
.call_tool(
"echo_tool",
serde_json::json!({ "message": "Client with SSE transport - calling a tool" }),
)
.await?;
println!("Tool result: {tool_result:?}\n");

// List resources
let resources = client.list_resources(None).await?;
println!("Resources: {resources:?}\n");

// Read resource
let resource = client.read_resource("echo://fixedresource").await?;
println!("Resource: {resource:?}\n");

Ok(())
}
61 changes: 61 additions & 0 deletions crates/mcp-client/examples/stdio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use std::collections::HashMap;

use anyhow::Result;
use mcp_client::{
ClientCapabilities, ClientInfo, Error as ClientError, McpClient, McpClientTrait, McpService,
StdioTransport, Transport,
};
use std::time::Duration;
use tracing_subscriber::EnvFilter;

#[tokio::main]
async fn main() -> Result<(), ClientError> {
// Initialize logging
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env()
.add_directive("mcp_client=debug".parse().unwrap())
.add_directive("eventsource_client=debug".parse().unwrap()),
)
.init();

// 1) Create the transport
let transport = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()], HashMap::new());

// 2) Start the transport to get a handle
let transport_handle = transport.start().await?;

// 3) Create the service with timeout middleware
let service = McpService::with_timeout(transport_handle, Duration::from_secs(10));

// 4) Create the client with the middleware-wrapped service
let mut client = McpClient::new(service);

// Initialize
let server_info = client
.initialize(
ClientInfo {
name: "test-client".into(),
version: "1.0.0".into(),
},
ClientCapabilities::default(),
)
.await?;
println!("Connected to server: {server_info:?}\n");

// List tools
let tools = client.list_tools(None).await?;
println!("Available tools: {tools:?}\n");

// Call tool 'git_status' with arguments = {"repo_path": "."}
let tool_result = client
.call_tool("git_status", serde_json::json!({ "repo_path": "." }))
.await?;
println!("Tool result: {tool_result:?}\n");

// List resources
let resources = client.list_resources(None).await?;
println!("Available resources: {resources:?}\n");

Ok(())
}
Loading