Skip to content

Commit fe1108c

Browse files
authored
Merge pull request #1 from modelcontextprotocol/alexhancock/rust-sdk-init
Bring in mcp-* crates from goose as a starting point
2 parents 91e8ba6 + 79225a0 commit fe1108c

File tree

31 files changed

+3930
-0
lines changed

31 files changed

+3930
-0
lines changed

Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[workspace]
2+
members = ["crates/*"]
3+
resolver = "2"
4+
5+
[workspace.dependencies]
6+
mcp-core = { path = "./crates/mcp-core" }
7+
mcp-macros = { path = "./crates/mcp-macros" }
8+
9+
[workspace.package]
10+
edition = "2021"
11+
version = "1.0.7"
12+
authors = ["Block <[email protected]>"]
13+
license = "MIT"
14+
repository = "https://github.com/modelcontextprotocol/rust-sdk/"
15+
description = "Rust SDK for the Model Context Protocol"

crates/mcp-client/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "mcp-client"
3+
version.workspace = true
4+
edition.workspace = true
5+
6+
[dependencies]
7+
mcp-core = { workspace = true }
8+
tokio = { version = "1", features = ["full"] }
9+
reqwest = { version = "0.11", default-features = false, features = ["json", "stream", "rustls-tls"] }
10+
eventsource-client = "0.12.0"
11+
futures = "0.3"
12+
serde = { version = "1.0", features = ["derive"] }
13+
serde_json = "1.0"
14+
async-trait = "0.1.83"
15+
url = "2.5.4"
16+
thiserror = "1.0"
17+
anyhow = "1.0"
18+
tracing = "0.1"
19+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
20+
tower = { version = "0.4", features = ["timeout", "util"] }
21+
tower-service = "0.3"
22+
rand = "0.8"
23+
24+
[dev-dependencies]

crates/mcp-client/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Testing stdio transport
2+
3+
```bash
4+
cargo run -p mcp-client --example stdio
5+
```
6+
7+
## Testing SSE transport
8+
9+
1. Start the MCP server in one terminal: `fastmcp run -t sse echo.py`
10+
2. Run the client example in new terminal: `cargo run -p mcp-client --example sse`
11+
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use mcp_client::{
2+
client::{ClientCapabilities, ClientInfo, McpClient, McpClientTrait},
3+
transport::{SseTransport, StdioTransport, Transport},
4+
McpService,
5+
};
6+
use rand::Rng;
7+
use rand::SeedableRng;
8+
use std::time::Duration;
9+
use std::{collections::HashMap, sync::Arc};
10+
use tracing_subscriber::EnvFilter;
11+
12+
#[tokio::main]
13+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
14+
// Initialize logging
15+
tracing_subscriber::fmt()
16+
.with_env_filter(
17+
EnvFilter::from_default_env().add_directive("mcp_client=debug".parse().unwrap()),
18+
)
19+
.init();
20+
21+
let transport1 = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()], HashMap::new());
22+
let handle1 = transport1.start().await?;
23+
let service1 = McpService::with_timeout(handle1, Duration::from_secs(30));
24+
let client1 = McpClient::new(service1);
25+
26+
let transport2 = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()], HashMap::new());
27+
let handle2 = transport2.start().await?;
28+
let service2 = McpService::with_timeout(handle2, Duration::from_secs(30));
29+
let client2 = McpClient::new(service2);
30+
31+
let transport3 = SseTransport::new("http://localhost:8000/sse", HashMap::new());
32+
let handle3 = transport3.start().await?;
33+
let service3 = McpService::with_timeout(handle3, Duration::from_secs(10));
34+
let client3 = McpClient::new(service3);
35+
36+
// Initialize both clients
37+
let mut clients: Vec<Box<dyn McpClientTrait>> =
38+
vec![Box::new(client1), Box::new(client2), Box::new(client3)];
39+
40+
// Initialize all clients
41+
for (i, client) in clients.iter_mut().enumerate() {
42+
let info = ClientInfo {
43+
name: format!("example-client-{}", i + 1),
44+
version: "1.0.0".to_string(),
45+
};
46+
let capabilities = ClientCapabilities::default();
47+
48+
println!("\nInitializing client {}", i + 1);
49+
let init_result = client.initialize(info, capabilities).await?;
50+
println!("Client {} initialized: {:?}", i + 1, init_result);
51+
}
52+
53+
// List tools for all clients
54+
for (i, client) in clients.iter_mut().enumerate() {
55+
let tools = client.list_tools(None).await?;
56+
println!("\nClient {} tools: {:?}", i + 1, tools);
57+
}
58+
59+
println!("\n\n----------------------------------\n\n");
60+
61+
// Wrap clients in Arc before spawning tasks
62+
let clients = Arc::new(clients);
63+
let mut handles = vec![];
64+
65+
for i in 0..20 {
66+
let clients = Arc::clone(&clients);
67+
let handle = tokio::spawn(async move {
68+
// let mut rng = rand::thread_rng();
69+
let mut rng = rand::rngs::StdRng::from_entropy();
70+
tokio::time::sleep(Duration::from_millis(rng.gen_range(5..50))).await;
71+
72+
// Randomly select an operation
73+
match rng.gen_range(0..4) {
74+
0 => {
75+
println!("\n{i}: Listing tools for client 1 (stdio)");
76+
match clients[0].list_tools(None).await {
77+
Ok(tools) => {
78+
println!(" {i}: -> Got tools, first one: {:?}", tools.tools.first())
79+
}
80+
Err(e) => println!(" {i}: -> Error: {}", e),
81+
}
82+
}
83+
1 => {
84+
println!("\n{i}: Calling tool for client 2 (stdio)");
85+
match clients[1]
86+
.call_tool("git_status", serde_json::json!({ "repo_path": "." }))
87+
.await
88+
{
89+
Ok(result) => println!(
90+
" {i}: -> Tool execution result, is_error: {:?}",
91+
result.is_error
92+
),
93+
Err(e) => println!(" {i}: -> Error: {}", e),
94+
}
95+
}
96+
2 => {
97+
println!("\n{i}: Listing tools for client 3 (sse)");
98+
match clients[2].list_tools(None).await {
99+
Ok(tools) => {
100+
println!(" {i}: -> Got tools, first one: {:?}", tools.tools.first())
101+
}
102+
Err(e) => println!(" {i}: -> Error: {}", e),
103+
}
104+
}
105+
3 => {
106+
println!("\n{i}: Calling tool for client 3 (sse)");
107+
match clients[2]
108+
.call_tool(
109+
"echo_tool",
110+
serde_json::json!({ "message": "Client with SSE transport - calling a tool" }),
111+
)
112+
.await
113+
{
114+
Ok(result) => println!(" {i}: -> Tool execution result, is_error: {:?}", result.is_error),
115+
Err(e) => println!(" {i}: -> Error: {}", e),
116+
}
117+
}
118+
_ => unreachable!(),
119+
}
120+
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
121+
});
122+
handles.push(handle);
123+
}
124+
125+
// Wait for all tasks to complete
126+
for handle in handles {
127+
handle.await.unwrap().unwrap();
128+
}
129+
130+
Ok(())
131+
}

crates/mcp-client/examples/sse.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use anyhow::Result;
2+
use mcp_client::client::{ClientCapabilities, ClientInfo, McpClient, McpClientTrait};
3+
use mcp_client::transport::{SseTransport, Transport};
4+
use mcp_client::McpService;
5+
use std::collections::HashMap;
6+
use std::time::Duration;
7+
use tracing_subscriber::EnvFilter;
8+
9+
#[tokio::main]
10+
async fn main() -> Result<()> {
11+
// Initialize logging
12+
tracing_subscriber::fmt()
13+
.with_env_filter(
14+
EnvFilter::from_default_env()
15+
.add_directive("mcp_client=debug".parse().unwrap())
16+
.add_directive("eventsource_client=info".parse().unwrap()),
17+
)
18+
.init();
19+
20+
// Create the base transport
21+
let transport = SseTransport::new("http://localhost:8000/sse", HashMap::new());
22+
23+
// Start transport
24+
let handle = transport.start().await?;
25+
26+
// Create the service with timeout middleware
27+
let service = McpService::with_timeout(handle, Duration::from_secs(3));
28+
29+
// Create client
30+
let mut client = McpClient::new(service);
31+
println!("Client created\n");
32+
33+
// Initialize
34+
let server_info = client
35+
.initialize(
36+
ClientInfo {
37+
name: "test-client".into(),
38+
version: "1.0.0".into(),
39+
},
40+
ClientCapabilities::default(),
41+
)
42+
.await?;
43+
println!("Connected to server: {server_info:?}\n");
44+
45+
// Sleep for 100ms to allow the server to start - surprisingly this is required!
46+
tokio::time::sleep(Duration::from_millis(500)).await;
47+
48+
// List tools
49+
let tools = client.list_tools(None).await?;
50+
println!("Available tools: {tools:?}\n");
51+
52+
// Call tool
53+
let tool_result = client
54+
.call_tool(
55+
"echo_tool",
56+
serde_json::json!({ "message": "Client with SSE transport - calling a tool" }),
57+
)
58+
.await?;
59+
println!("Tool result: {tool_result:?}\n");
60+
61+
// List resources
62+
let resources = client.list_resources(None).await?;
63+
println!("Resources: {resources:?}\n");
64+
65+
// Read resource
66+
let resource = client.read_resource("echo://fixedresource").await?;
67+
println!("Resource: {resource:?}\n");
68+
69+
Ok(())
70+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use std::collections::HashMap;
2+
3+
use anyhow::Result;
4+
use mcp_client::{
5+
ClientCapabilities, ClientInfo, Error as ClientError, McpClient, McpClientTrait, McpService,
6+
StdioTransport, Transport,
7+
};
8+
use std::time::Duration;
9+
use tracing_subscriber::EnvFilter;
10+
11+
#[tokio::main]
12+
async fn main() -> Result<(), ClientError> {
13+
// Initialize logging
14+
tracing_subscriber::fmt()
15+
.with_env_filter(
16+
EnvFilter::from_default_env()
17+
.add_directive("mcp_client=debug".parse().unwrap())
18+
.add_directive("eventsource_client=debug".parse().unwrap()),
19+
)
20+
.init();
21+
22+
// 1) Create the transport
23+
let transport = StdioTransport::new("uvx", vec!["mcp-server-git".to_string()], HashMap::new());
24+
25+
// 2) Start the transport to get a handle
26+
let transport_handle = transport.start().await?;
27+
28+
// 3) Create the service with timeout middleware
29+
let service = McpService::with_timeout(transport_handle, Duration::from_secs(10));
30+
31+
// 4) Create the client with the middleware-wrapped service
32+
let mut client = McpClient::new(service);
33+
34+
// Initialize
35+
let server_info = client
36+
.initialize(
37+
ClientInfo {
38+
name: "test-client".into(),
39+
version: "1.0.0".into(),
40+
},
41+
ClientCapabilities::default(),
42+
)
43+
.await?;
44+
println!("Connected to server: {server_info:?}\n");
45+
46+
// List tools
47+
let tools = client.list_tools(None).await?;
48+
println!("Available tools: {tools:?}\n");
49+
50+
// Call tool 'git_status' with arguments = {"repo_path": "."}
51+
let tool_result = client
52+
.call_tool("git_status", serde_json::json!({ "repo_path": "." }))
53+
.await?;
54+
println!("Tool result: {tool_result:?}\n");
55+
56+
// List resources
57+
let resources = client.list_resources(None).await?;
58+
println!("Available resources: {resources:?}\n");
59+
60+
Ok(())
61+
}

0 commit comments

Comments
 (0)