chore: move examples out of various places into a top-level dir (#10)

This commit is contained in:
Kalvin C 2025-03-03 11:50:34 -08:00 • committed by GitHub
commit 6609aa6aa8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 163 additions and 1 deletions

84
examples/README.md Normal file
View file

@ -0,0 +1,84 @@
# Model Context Protocol Examples
This directory contains examples demonstrating how to use the Model Context Protocol (MCP) Rust SDK.
## Structure
- `clients/`: Examples of MCP clients
- `servers/`: Examples of MCP servers
- `macros/`: Examples of MCP macros
## Running Client Examples
The client examples demonstrate different ways to connect to MCP servers.
### Available Examples
You can run the examples in two ways:
#### Option 1: From the examples/clients directory
```bash
cd examples/clients
cargo run --example clients
cargo run --example sse
cargo run --example stdio
cargo run --example stdio_integration
```
#### Option 2: From the root directory
```bash
cargo run -p mcp-client-examples --example clients
cargo run -p mcp-client-examples --example sse
cargo run -p mcp-client-examples --example stdio
cargo run -p mcp-client-examples --example stdio_integration
```
## Running Server Examples
The server examples demonstrate how to implement MCP servers.
### Available Examples
You can run the server examples in two ways:
#### Option 1: From the examples/servers directory
```bash
cd examples/servers
cargo run --example counter-server
```
#### Option 2: From the root directory
```bash
cargo run -p mcp-server-examples --example counter-server
```
## Running Macros Examples
The macros examples demonstrate how to use the MCP macros to create tools.
### Available Examples
You can run the macros examples in two ways:
#### Option 1: From the examples/macros directory
```bash
cd examples/macros
cargo run --example calculator
```
#### Option 2: From the root directory
```bash
cargo run -p mcp-macros-examples --example calculator
```
## Notes
- Some examples may require additional setup or running both client and server components.
- The server examples use standard I/O for communication, so they can be connected to client examples using stdio transport.
- For SSE examples, you may need to run a separate SSE server or use a compatible MCP server implementation.

View file

@ -0,0 +1,33 @@
[package]
name = "mcp-client-examples"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
mcp-client = { path = "../../crates/mcp-client" }
mcp-core = { path = "../../crates/mcp-core" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
rand = "0.8"
futures = "0.3"
anyhow = "1.0"
[[example]]
name = "clients"
path = "src/clients.rs"
[[example]]
name = "sse"
path = "src/sse.rs"
[[example]]
name = "stdio"
path = "src/stdio.rs"
[[example]]
name = "stdio_integration"
path = "src/stdio_integration.rs"

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -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(())
}

View file

@ -0,0 +1,97 @@
// This example shows how to use the mcp-client crate to interact with a server that has a simple counter tool.
// The server is started by running `cargo run -p mcp-server` in the root of the mcp-server crate.
use anyhow::Result;
use mcp_client::client::{
ClientCapabilities, ClientInfo, Error as ClientError, McpClient, McpClientTrait,
};
use mcp_client::transport::{StdioTransport, Transport};
use mcp_client::McpService;
use std::collections::HashMap;
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();
// Create the transport
let transport = StdioTransport::new(
"cargo",
vec!["run", "-p", "mcp-server"]
.into_iter()
.map(|s| s.to_string())
.collect(),
HashMap::new(),
);
// Start the transport to get a handle
let transport_handle = transport.start().await.unwrap();
// Create the service with timeout middleware
let service = McpService::with_timeout(transport_handle, Duration::from_secs(10));
// Create client
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 'increment' tool 3 times
for _ in 0..3 {
let increment_result = client.call_tool("increment", serde_json::json!({})).await?;
println!("Tool result for 'increment': {increment_result:?}\n");
}
// Call tool 'get_value'
let get_value_result = client.call_tool("get_value", serde_json::json!({})).await?;
println!("Tool result for 'get_value': {get_value_result:?}\n");
// Call tool 'decrement' once
let decrement_result = client.call_tool("decrement", serde_json::json!({})).await?;
println!("Tool result for 'decrement': {decrement_result:?}\n");
// Call tool 'get_value'
let get_value_result = client.call_tool("get_value", serde_json::json!({})).await?;
println!("Tool result for 'get_value': {get_value_result:?}\n");
// List resources
let resources = client.list_resources(None).await?;
println!("Resources: {resources:?}\n");
// Read resource
let resource = client.read_resource("memo://insights").await?;
println!("Resource: {resource:?}\n");
let prompts = client.list_prompts(None).await?;
println!("Prompts: {prompts:?}\n");
let prompt = client
.get_prompt(
"example_prompt",
serde_json::json!({"message": "hello there!"}),
)
.await?;
println!("Prompt: {prompt:?}\n");
Ok(())
}

View file

@ -0,0 +1,18 @@
[package]
name = "mcp-macros-examples"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
mcp-core = { path = "../../crates/mcp-core" }
mcp-macros = { path = "../../crates/mcp-macros" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
async-trait = "0.1"
schemars = "0.8"
[[example]]
name = "calculator"
path = "src/calculator.rs"

View file

@ -0,0 +1,53 @@
use mcp_core::handler::{ToolError, ToolHandler};
use mcp_macros::tool;
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
// Create an instance of our tool
let calculator = Calculator;
// Print tool information
println!("Tool name: {}", calculator.name());
println!("Tool description: {}", calculator.description());
println!("Tool schema: {}", calculator.schema());
// Test the tool with some sample input
let input = serde_json::json!({
"x": 5,
"y": 3,
"operation": "multiply"
});
let result = calculator.call(input).await?;
println!("Result: {}", result);
Ok(())
}
#[tool(
name = "calculator",
description = "Perform basic arithmetic operations",
params(
x = "First number in the calculation",
y = "Second number in the calculation",
operation = "The operation to perform (add, subtract, multiply, divide)"
)
)]
async fn calculator(x: i32, y: i32, operation: String) -> Result<i32, ToolError> {
match operation.as_str() {
"add" => Ok(x + y),
"subtract" => Ok(x - y),
"multiply" => Ok(x * y),
"divide" => {
if y == 0 {
Err(ToolError::ExecutionError("Division by zero".into()))
} else {
Ok(x / y)
}
}
_ => Err(ToolError::InvalidParameters(format!(
"Unknown operation: {}",
operation
))),
}
}

View file

@ -0,0 +1,22 @@
[package]
name = "mcp-server-examples"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
mcp-server = { path = "../../crates/mcp-server" }
mcp-core = { path = "../../crates/mcp-core" }
mcp-macros = { path = "../../crates/mcp-macros" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
futures = "0.3"
[[example]]
name = "counter-server"
path = "src/counter_server.rs"

View file

@ -0,0 +1,217 @@
use anyhow::Result;
use mcp_core::content::Content;
use mcp_core::handler::{PromptError, ResourceError};
use mcp_core::prompt::{Prompt, PromptArgument};
use mcp_core::{handler::ToolError, protocol::ServerCapabilities, resource::Resource, tool::Tool};
use mcp_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
use serde_json::Value;
use std::{future::Future, pin::Pin, sync::Arc};
use tokio::{
io::{stdin, stdout},
sync::Mutex,
};
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::{self, EnvFilter};
// A simple counter service that demonstrates the Router trait
#[derive(Clone)]
struct CounterRouter {
counter: Arc<Mutex<i32>>,
}
impl CounterRouter {
fn new() -> Self {
Self {
counter: Arc::new(Mutex::new(0)),
}
}
async fn increment(&self) -> Result<i32, ToolError> {
let mut counter = self.counter.lock().await;
*counter += 1;
Ok(*counter)
}
async fn decrement(&self) -> Result<i32, ToolError> {
let mut counter = self.counter.lock().await;
*counter -= 1;
Ok(*counter)
}
async fn get_value(&self) -> Result<i32, ToolError> {
let counter = self.counter.lock().await;
Ok(*counter)
}
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
Resource::new(uri, Some("text/plain".to_string()), Some(name.to_string())).unwrap()
}
}
impl Router for CounterRouter {
fn name(&self) -> String {
"counter".to_string()
}
fn instructions(&self) -> String {
"This server provides a counter tool that can increment and decrement values. The counter starts at 0 and can be modified using the 'increment' and 'decrement' tools. Use 'get_value' to check the current count.".to_string()
}
fn capabilities(&self) -> ServerCapabilities {
CapabilitiesBuilder::new()
.with_tools(false)
.with_resources(false, false)
.with_prompts(false)
.build()
}
fn list_tools(&self) -> Vec<Tool> {
vec![
Tool::new(
"increment".to_string(),
"Increment the counter by 1".to_string(),
serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
),
Tool::new(
"decrement".to_string(),
"Decrement the counter by 1".to_string(),
serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
),
Tool::new(
"get_value".to_string(),
"Get the current counter value".to_string(),
serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
),
]
}
fn call_tool(
&self,
tool_name: &str,
_arguments: Value,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
let this = self.clone();
let tool_name = tool_name.to_string();
Box::pin(async move {
match tool_name.as_str() {
"increment" => {
let value = this.increment().await?;
Ok(vec![Content::text(value.to_string())])
}
"decrement" => {
let value = this.decrement().await?;
Ok(vec![Content::text(value.to_string())])
}
"get_value" => {
let value = this.get_value().await?;
Ok(vec![Content::text(value.to_string())])
}
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
}
})
}
fn list_resources(&self) -> Vec<Resource> {
vec![
self._create_resource_text("str:////Users/to/some/path/", "cwd"),
self._create_resource_text("memo://insights", "memo-name"),
]
}
fn read_resource(
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>> {
let uri = uri.to_string();
Box::pin(async move {
match uri.as_str() {
"str:////Users/to/some/path/" => {
let cwd = "/Users/to/some/path/";
Ok(cwd.to_string())
}
"memo://insights" => {
let memo =
"Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...";
Ok(memo.to_string())
}
_ => Err(ResourceError::NotFound(format!(
"Resource {} not found",
uri
))),
}
})
}
fn list_prompts(&self) -> Vec<Prompt> {
vec![Prompt::new(
"example_prompt",
Some("This is an example prompt that takes one required agrument, message"),
Some(vec![PromptArgument {
name: "message".to_string(),
description: Some("A message to put in the prompt".to_string()),
required: Some(true),
}]),
)]
}
fn get_prompt(
&self,
prompt_name: &str,
) -> Pin<Box<dyn Future<Output = Result<String, PromptError>> + Send + 'static>> {
let prompt_name = prompt_name.to_string();
Box::pin(async move {
match prompt_name.as_str() {
"example_prompt" => {
let prompt = "This is an example prompt with your message here: '{message}'";
Ok(prompt.to_string())
}
_ => Err(PromptError::NotFound(format!(
"Prompt {} not found",
prompt_name
))),
}
})
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Set up file appender for logging
let file_appender = RollingFileAppender::new(Rotation::DAILY, "logs", "mcp-server.log");
// Initialize the tracing subscriber with file and stdout logging
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into()))
.with_writer(file_appender)
.with_target(false)
.with_thread_ids(true)
.with_file(true)
.with_line_number(true)
.init();
tracing::info!("Starting MCP server");
// Create an instance of our counter router
let router = RouterService(CounterRouter::new());
// Create and run the server
let server = Server::new(router);
let transport = ByteTransport::new(stdin(), stdout());
tracing::info!("Server initialized and ready to handle requests");
Ok(server.run(transport).await?)
}