Move whole rmcp crate to offcial rust sdk (#44)

* get rid of async-trait

* move rmcp to official-rust-sdk

* replace master with main

* update readme, change license

* fix typo

* Update README.md

Co-authored-by: Jefry Dewangga <jefrydco@gmail.com>

* fix typos

---------

Co-authored-by: = <=>
Co-authored-by: Jefry Dewangga <jefrydco@gmail.com>
This commit is contained in:
4t145 2025-03-28 05:33:41 +08:00 • committed by GitHub
commit 0be6067326
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 8676 additions and 4466 deletions

View file

@ -1,86 +1,31 @@
# Model Context Protocol Examples
# Client Examples
This directory contains examples demonstrating how to use the Model Context Protocol (MCP) Rust SDK.
- [Client SSE](clients/src/sse.rs), using reqwest and eventsource-client.
- [Client stdio](clients/src/std_io.rs), using tokio to spawn child process.
- [Everything](clients/src/everything_stdio.rs), test with `@modelcontextprotocol/server-everything`
- [Collection](clients/src/collection.rs), How to transpose service into dynamic object, so they will have a same type.
## Structure
# Server Examples
- `clients/`: Examples of MCP clients
- `servers/`: Examples of MCP servers
- `macros/`: Examples of MCP macros
- [Server SSE](servers/src/axum.rs), using axum as web server.
- [Server stdio](servers/src/std_io.rs), using tokio async io.
## Running Client Examples
The client examples demonstrate different ways to connect to MCP servers.
# Transport Examples
Before running the examples, ensure you have `uv` installed. You can find the installation instructions [here](https://github.com/astral-sh/uv).
- [Tcp](transport/src/tcp.rs)
- [Transport on http upgrade](transport/src/http_upgrade.rs)
- [Unix Socket](transport/src/unix_socket.rs)
- [Websocket](transport/src/websocket.rs)
### Available Examples
You can run the examples in two ways:
# Intergration
- [Rig](examples/rig-integration) A stream chatbot with rig
#### Option 1: From the examples/clients directory
# WASI
- [WASI-P2 runtime](examples/wasi) How it works with wasip2
```bash
cd examples/clients
cargo run --example clients
cargo run --example sse
cargo run --example stdio
cargo run --example stdio_integration
## Use Mcp Inspector
```sh
npx @modelcontextprotocol/inspector
```
#### 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

@ -1,12 +1,16 @@
[package]
name = "mcp-client-examples"
version = "0.1.0"
edition = "2021"
version = "0.1.5"
edition = "2024"
publish = false
[dependencies]
mcp-client = { path = "../../crates/mcp-client" }
mcp-core = { path = "../../crates/mcp-core" }
rmcp = { path = "../../crates/rmcp", features = [
"client",
"transport-sse",
"transport-child-process",
"tower"
] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@ -16,18 +20,21 @@ rand = "0.8"
futures = "0.3"
anyhow = "1.0"
[[example]]
name = "clients"
path = "src/clients.rs"
tower = "0.5"
[[example]]
name = "sse"
path = "src/sse.rs"
[[example]]
name = "stdio"
path = "src/stdio.rs"
name = "std_io"
path = "src/std_io.rs"
[[example]]
name = "stdio_integration"
path = "src/stdio_integration.rs"
name = "everything_stdio"
path = "src/everything_stdio.rs"
[[example]]
name = "collection"
path = "src/collection.rs"

View file

@ -1,131 +0,0 @@
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,52 @@
use std::collections::HashMap;
use anyhow::Result;
use rmcp::service::ServiceExt;
use rmcp::{model::CallToolRequestParam, transport::TokioChildProcess};
use tokio::process::Command;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let mut client_list = HashMap::new();
for idx in 0..10 {
let service = ()
.into_dyn()
.serve(TokioChildProcess::new(
Command::new("uvx").arg("mcp-server-git"),
)?)
.await?;
client_list.insert(idx, service);
}
for (_, service) in client_list.iter() {
// Initialize
let _server_info = service.peer_info();
// List tools
let _tools = service.list_tools(Default::default()).await?;
// Call tool 'git_status' with arguments = {"repo_path": "."}
let _tool_result = service
.call_tool(CallToolRequestParam {
name: "git_status".into(),
arguments: serde_json::json!({ "repo_path": "." }).as_object().cloned(),
})
.await?;
}
for (_, service) in client_list {
service.cancel().await?;
}
Ok(())
}

View file

@ -0,0 +1,100 @@
use anyhow::Result;
use rmcp::{
ServiceExt,
model::{CallToolRequestParam, GetPromptRequestParam, ReadResourceRequestParam},
object,
transport::TokioChildProcess,
};
use tokio::process::Command;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
// Start server
let service = ()
.serve(TokioChildProcess::new(
Command::new("npx")
.arg("-y")
.arg("@modelcontextprotocol/server-everything"),
)?)
.await?;
// Initialize
let server_info = service.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
// List tools
let tools = service.list_all_tools().await?;
tracing::info!("Available tools: {tools:#?}");
// Call tool echo
let tool_result = service
.call_tool(CallToolRequestParam {
name: "echo".into(),
arguments: Some(object!({ "message": "hi from rmcp" })),
})
.await?;
tracing::info!("Tool result for echo: {tool_result:#?}");
// Call tool longRunningOperation
let tool_result = service
.call_tool(CallToolRequestParam {
name: "longRunningOperation".into(),
arguments: Some(object!({ "duration": 3, "steps": 1 })),
})
.await?;
tracing::info!("Tool result for longRunningOperation: {tool_result:#?}");
// List resources
let resources = service.list_all_resources().await?;
tracing::info!("Available resources: {resources:#?}");
// Read resource
let resource = service
.read_resource(ReadResourceRequestParam {
uri: "test://static/resource/3".into(),
})
.await?;
tracing::info!("Resource: {resource:#?}");
// List prompts
let prompts = service.list_all_prompts().await?;
tracing::info!("Available prompts: {prompts:#?}");
// Get simple prompt
let prompt = service
.get_prompt(GetPromptRequestParam {
name: "simple_prompt".into(),
arguments: None,
})
.await?;
tracing::info!("Prompt - simple: {prompt:#?}");
// Get complex prompt (returns text & image)
let prompt = service
.get_prompt(GetPromptRequestParam {
name: "complex_prompt".into(),
arguments: Some(object!({ "temperature": "0.5", "style": "formal" })),
})
.await?;
tracing::info!("Prompt - complex: {prompt:#?}");
// List resource templates
let resource_templates = service.list_all_resource_templates().await?;
tracing::info!("Available resource templates: {resource_templates:#?}");
service.cancel().await?;
Ok(())
}

View file

@ -1,70 +1,48 @@
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;
use rmcp::model::{ClientCapabilities, ClientInfo, Implementation};
use rmcp::{ServiceExt, model::CallToolRequestParam, transport::SseTransport};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
#[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()),
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.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");
let transport = SseTransport::start("http://localhost:8000/sse").await?;
let client_info = ClientInfo {
protocol_version: Default::default(),
capabilities: ClientCapabilities::default(),
client_info: Implementation {
name: "test sse client".to_string(),
version: "0.0.1".to_string(),
},
};
let client = client_info.serve(transport).await.inspect_err(|e| {
tracing::error!("client error: {:?}", e);
})?;
// 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;
let server_info = client.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
// List tools
let tools = client.list_tools(None).await?;
println!("Available tools: {tools:?}\n");
let tools = client.list_tools(Default::default()).await?;
tracing::info!("Available tools: {tools:#?}");
// Call tool
let tool_result = client
.call_tool(
"echo_tool",
serde_json::json!({ "message": "Client with SSE transport - calling a tool" }),
)
.call_tool(CallToolRequestParam {
name: "increment".into(),
arguments: serde_json::json!({}).as_object().cloned(),
})
.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");
tracing::info!("Tool result: {tool_result:#?}");
client.cancel().await?;
Ok(())
}

View file

@ -0,0 +1,49 @@
use anyhow::Result;
use rmcp::{model::CallToolRequestParam, service::ServiceExt, transport::TokioChildProcess};
use tokio::process::Command;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let service = ()
.serve(TokioChildProcess::new(
Command::new("uvx").arg("mcp-server-git"),
)?)
.await?;
// or
// serve_client(
// (),
// TokioChildProcess::new(Command::new("uvx").arg("mcp-server-git"))?,
// )
// .await?;
// Initialize
let server_info = service.peer_info();
tracing::info!("Connected to server: {server_info:#?}");
// List tools
let tools = service.list_tools(Default::default()).await?;
tracing::info!("Available tools: {tools:#?}");
// Call tool 'git_status' with arguments = {"repo_path": "."}
let tool_result = service
.call_tool(CallToolRequestParam {
name: "git_status".into(),
arguments: serde_json::json!({ "repo_path": "." }).as_object().cloned(),
})
.await?;
tracing::info!("Tool result: {tool_result:#?}");
service.cancel().await?;
Ok(())
}

View file

@ -1,61 +0,0 @@
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

@ -1,103 +0,0 @@
// 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-examples",
"--example",
"counter-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

@ -1,18 +0,0 @@
[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

@ -1,53 +0,0 @@
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,26 @@
[package]
name = "rig-integration"
edition.workspace = true
version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description.workspace = true
keywords.workspace = true
homepage.workspace = true
categories.workspace = true
readme.workspace = true
[dependencies]
rig-core = "0.10.0"
tokio = { version = "1", features = ["full"] }
rmcp = { path = "../../crates/rmcp", features = [
"client",
"transport-child-process",
"transport-sse",
], no-default-features = true }
anyhow = "1.0"
serde_json = "1"
serde = { version = "1", features = ["derive"] }
toml = "0.8"
futures = "0.3"

View file

@ -0,0 +1,10 @@
deepseek_key = ""
cohere_key = ""
[mcp]
[[mcp.server]]
name = "git"
protocol = "stdio"
command = "uvx"
args = ["mcp-server-git"]

View file

@ -0,0 +1,114 @@
use futures::StreamExt;
use rig::{
agent::Agent,
message::Message,
providers::deepseek::DeepSeekCompletionModel,
streaming::{StreamingChat, StreamingChoice},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
pub async fn cli_chatbot(chatbot: Agent<DeepSeekCompletionModel>) -> anyhow::Result<()> {
let mut chat_log = vec![];
let mut output = BufWriter::new(tokio::io::stdout());
let mut input = BufReader::new(tokio::io::stdin());
output.write_all(b"Enter :q to quit\n").await?;
loop {
output.write_all(b"\x1b[32muser>\x1b[0m ").await?;
// Flush stdout to ensure the prompt appears before input
output.flush().await?;
let mut input_buf = String::new();
input.read_line(&mut input_buf).await?;
// Remove the newline character from the input
let input = input_buf.trim();
// Check for a command to exit
if input == ":q" {
break;
}
match chatbot.stream_chat(input, chat_log.clone()).await {
Ok(mut response) => {
chat_log.push(Message::user(input));
stream_output_agent_start(&mut output).await?;
let mut message_buf = String::new();
while let Some(message) = response.next().await {
match message {
Ok(StreamingChoice::Message(text)) => {
message_buf.push_str(&text);
output_agent(text, &mut output).await?;
}
Ok(StreamingChoice::ToolCall(name, _, param)) => {
chat_log.push(Message::assistant(format!(
"Calling tool: {name} with args: {param}"
)));
let result = chatbot.tools.call(&name, param.to_string()).await;
match result {
Ok(tool_call_result) => {
stream_output_agent_finished(&mut output).await?;
stream_output_toolcall(&tool_call_result, &mut output).await?;
stream_output_agent_start(&mut output).await?;
chat_log.push(Message::user(tool_call_result));
}
Err(e) => {
output_error(e, &mut output).await?;
}
}
}
Err(error) => {
output_error(error, &mut output).await?;
}
}
}
chat_log.push(Message::assistant(message_buf));
stream_output_agent_finished(&mut output).await?;
}
Err(error) => {
output_error(error, &mut output).await?;
}
}
}
Ok(())
}
pub async fn output_error(
e: impl std::fmt::Display,
output: &mut BufWriter<tokio::io::Stdout>,
) -> std::io::Result<()> {
output.write_all(b"\x1b[31mERROR: \x1b[0m").await?;
output.write_all(e.to_string().as_bytes()).await?;
output.write_all(b"\n").await?;
Ok(())
}
pub async fn output_agent(
content: impl std::fmt::Display,
output: &mut BufWriter<tokio::io::Stdout>,
) -> std::io::Result<()> {
output.write_all(content.to_string().as_bytes()).await?;
output.flush().await?;
Ok(())
}
pub async fn stream_output_toolcall(
content: impl std::fmt::Display,
output: &mut BufWriter<tokio::io::Stdout>,
) -> std::io::Result<()> {
output.write_all(b"\x1b[33mtool>\x1b[0m ").await?;
output.write_all(content.to_string().as_bytes()).await?;
output.write_all(b"\n").await?;
Ok(())
}
pub async fn stream_output_agent_start(
output: &mut BufWriter<tokio::io::Stdout>,
) -> std::io::Result<()> {
output.write_all(b"\x1b[34magent>\x1b[0m ").await?;
Ok(())
}
pub async fn stream_output_agent_finished(
output: &mut BufWriter<tokio::io::Stdout>,
) -> std::io::Result<()> {
output.write_all(b"\n").await?;
Ok(())
}

View file

@ -0,0 +1,20 @@
use std::path::Path;
use serde::{Deserialize, Serialize};
pub mod mcp;
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub mcp: mcp::McpConfig,
pub deepseek_key: Option<String>,
pub cohere_key: Option<String>,
}
impl Config {
pub async fn retrive(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let content = tokio::fs::read_to_string(path).await?;
let config: Self = toml::from_str(&content)?;
Ok(config)
}
}

View file

@ -0,0 +1,83 @@
use std::{collections::HashMap, process::Stdio};
use rmcp::{RoleClient, ServiceExt, service::RunningService};
use serde::{Deserialize, Serialize};
use crate::mcp_adaptor::McpManager;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct McpServerConfig {
name: String,
#[serde(flatten)]
transport: McpServerTransportConfig,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "protocol", rename_all = "lowercase")]
pub enum McpServerTransportConfig {
Sse {
url: String,
},
Stdio {
command: String,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
envs: HashMap<String, String>,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub struct McpConfig {
server: Vec<McpServerConfig>,
}
impl McpConfig {
pub async fn create_manager(&self) -> anyhow::Result<McpManager> {
let mut clients = HashMap::new();
let mut task_set = tokio::task::JoinSet::<anyhow::Result<_>>::new();
for server in &self.server {
let server = server.clone();
task_set.spawn(async move {
let client = server.transport.start().await?;
anyhow::Result::Ok((server.name.clone(), client))
});
}
let start_up_result = task_set.join_all().await;
for result in start_up_result {
match result {
Ok((name, client)) => {
clients.insert(name, client);
}
Err(e) => {
eprintln!("Failed to start server: {:?}", e);
}
}
}
Ok(McpManager { clients })
}
}
impl McpServerTransportConfig {
pub async fn start(&self) -> anyhow::Result<RunningService<RoleClient, ()>> {
let client = match self {
McpServerTransportConfig::Sse { url } => {
let transport = rmcp::transport::SseTransport::start(url).await?;
().serve(transport).await?
}
McpServerTransportConfig::Stdio {
command,
args,
envs,
} => {
let transport = rmcp::transport::TokioChildProcess::new(
tokio::process::Command::new(command)
.args(args)
.envs(envs)
.stderr(Stdio::null()),
)?;
().serve(transport).await?
}
};
Ok(client)
}
}

View file

@ -0,0 +1,64 @@
use rig::{
embeddings::EmbeddingsBuilder,
providers::{
cohere,
deepseek::{self, DEEPSEEK_CHAT},
},
vector_store::in_memory_store::InMemoryVectorStore,
};
pub mod chat;
pub mod config;
pub mod mcp_adaptor;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = config::Config::retrive("config.toml").await?;
let openai_client = {
if let Some(key) = config.deepseek_key {
deepseek::Client::new(&key)
} else {
deepseek::Client::from_env()
}
};
let cohere_client = {
if let Some(key) = config.cohere_key {
cohere::Client::new(&key)
} else {
cohere::Client::from_env()
}
};
let mcp_manager = config.mcp.create_manager().await?;
eprintln!(
"MCP Manager created, {} servers started",
mcp_manager.clients.len()
);
let tool_set = mcp_manager.get_tool_set().await?;
let embedding_model =
cohere_client.embedding_model(cohere::EMBED_MULTILINGUAL_V3, "search_document");
let embeddings = EmbeddingsBuilder::new(embedding_model.clone())
.documents(tool_set.schemas()?)?
.build()
.await?;
let store = InMemoryVectorStore::from_documents_with_id_f(embeddings, |f| {
eprintln!("store tool {}", f.name);
f.name.clone()
});
let index = store.index(embedding_model);
let dpsk = openai_client
.agent(DEEPSEEK_CHAT)
.context(
r#"You are an assistant here to help the user to do some works.
When you need to use tools, you should select which tool is most appropriate to meet the user's requirement.
Follow these instructions closely.
1. Consider the user's request carefully and identify the core elements of the request.
2. Select which tool among those made available to you is appropriate given the context.
3. This is very important: never perform the operation yourself and never give me the direct result.
Always respond with the name of the tool that should be used and the appropriate inputs
in the following format:
Tool: <tool name>
Inputs: <list of inputs>"#,
)
.dynamic_tools(4, index, tool_set)
.build();
chat::cli_chatbot(dpsk).await?;
Ok(())
}

View file

@ -0,0 +1,113 @@
use std::collections::HashMap;
use rig::tool::{ToolDyn as RigTool, ToolEmbeddingDyn, ToolSet};
use rmcp::{
RoleClient,
model::{CallToolRequestParam, CallToolResult, Tool as McpTool},
service::{RunningService, ServerSink},
};
pub struct McpToolAdaptor {
tool: McpTool,
server: ServerSink,
}
impl RigTool for McpToolAdaptor {
fn name(&self) -> String {
self.tool.name.to_string()
}
fn definition(
&self,
_prompt: String,
) -> std::pin::Pin<Box<dyn Future<Output = rig::completion::ToolDefinition> + Send + Sync + '_>>
{
Box::pin(std::future::ready(rig::completion::ToolDefinition {
name: self.name(),
description: self.tool.description.to_string(),
parameters: self.tool.schema_as_json_value(),
}))
}
fn call(
&self,
args: String,
) -> std::pin::Pin<
Box<dyn Future<Output = Result<String, rig::tool::ToolError>> + Send + Sync + '_>,
> {
let server = self.server.clone();
Box::pin(async move {
let call_mcp_tool_result = server
.call_tool(CallToolRequestParam {
name: self.tool.name.clone(),
arguments: serde_json::from_str(&args)
.map_err(rig::tool::ToolError::JsonError)?,
})
.await
.map_err(|e| rig::tool::ToolError::ToolCallError(Box::new(e)))?;
Ok(convert_mcp_call_tool_result_to_string(call_mcp_tool_result))
})
}
}
impl ToolEmbeddingDyn for McpToolAdaptor {
fn embedding_docs(&self) -> Vec<String> {
vec![
self.tool.description.clone().to_string(),
format!("Tool name: {}", self.tool.name),
format!("Tool capability: {}", self.tool.description),
]
}
fn context(&self) -> serde_json::Result<serde_json::Value> {
Ok(serde_json::json!({
"tool_name": self.tool.name,
}))
}
}
pub struct McpManager {
pub clients: HashMap<String, RunningService<RoleClient, ()>>,
}
impl McpManager {
pub async fn get_tool_set(&self) -> anyhow::Result<ToolSet> {
let mut tool_set = ToolSet::default();
let mut task = tokio::task::JoinSet::<anyhow::Result<_>>::new();
for client in self.clients.values() {
let server = client.peer().clone();
task.spawn(get_tool_set(server));
}
let results = task.join_all().await;
for result in results {
match result {
Err(e) => {
eprintln!("Failed to get tool set: {:?}", e);
}
Ok(tools) => {
tool_set.add_tools(tools);
}
}
}
Ok(tool_set)
}
}
pub fn convert_mcp_call_tool_result_to_string(result: CallToolResult) -> String {
serde_json::to_string(&result).unwrap()
}
pub async fn get_tool_set(server: ServerSink) -> anyhow::Result<ToolSet> {
let tools = server.list_all_tools().await?;
let mut tool_set = ToolSet::default();
for tool in tools {
eprintln!("get tool: {}", tool.name);
let adaptor = McpToolAdaptor {
tool: tool.clone(),
server: server.clone(),
};
tool_set.add_tool(adaptor);
}
Ok(tool_set)
}

View file

@ -1,48 +1,36 @@
[package]
name = "mcp-server-examples"
version = "0.1.0"
edition = "2021"
version = "0.1.5"
edition = "2024"
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 = ["io-util"] }
rmcp= { path = "../../crates/rmcp", features = ["server", "transport-sse-server", "transport-io"] }
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "io-std", "signal"] }
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"
tracing-subscriber = { version = "0.3", features = [
"env-filter",
"std",
"fmt",
] }
futures = "0.3"
rand = { version = "0.9" }
axum = { version = "0.8", features = ["macros"] }
schemars = { version = "0.8", optional = true }
# [dev-dependencies.'cfg(target_arch="linux")'.dependencies]
[dev-dependencies]
tokio-util = { version = "0.7", features = ["io", "codec"]}
rand = { version = "0.8" }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
axum = { version = "0.8", features = ["macros"] }
tokio = { version = "1", features = ["full"] }
actix-web = "4"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
tokio = { version = "1", features = ["io-util", "rt", "time", "macros"] }
wasi = { version = "0.11.0+wasi-snapshot-preview1" }
tokio-stream = { version = "0.1" }
# tokio-util = { version = "0.7", features = ["io", "codec"] }
tokio-util = { version = "0.7", features = ["codec"] }
[[example]]
name = "counter-server"
path = "src/counter_server.rs"
name = "std_io"
path = "src/std_io.rs"
[[example]]
name = "axum"
path = "src/axum.rs"
[[example]]
name = "wasi_std_io"
path = "src/wasi_std_io.rs"
[[example]]
name = "actix_web"
path = "src/actix_web.rs"
path = "src/axum.rs"

View file

@ -1,168 +0,0 @@
use actix_web::web::{Bytes, Data, Payload, Query};
use actix_web::{get, post, App, Error, HttpResponse, HttpServer, Result};
use futures::{StreamExt, TryStreamExt};
use mcp_server::{ByteTransport, Server};
use std::collections::HashMap;
use tokio_util::codec::FramedRead;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use actix_web::middleware::Logger;
use mcp_server::router::RouterService;
use std::sync::Arc;
use tokio::{
io::{self, AsyncWriteExt},
sync::Mutex,
};
mod common;
use common::counter;
type C2SWriter = Arc<Mutex<io::WriteHalf<io::SimplexStream>>>;
type SessionId = Arc<str>;
const BIND_ADDRESS: &str = "127.0.0.1:8000";
#[derive(Clone, Default)]
pub struct AppState {
txs: Arc<tokio::sync::RwLock<HashMap<SessionId, C2SWriter>>>,
}
impl AppState {
pub fn new() -> Self {
Self {
txs: Default::default(),
}
}
}
fn session_id() -> SessionId {
let id = format!("{:016x}", rand::random::<u128>());
Arc::from(id)
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostEventQuery {
pub session_id: String,
}
#[post("/sse")]
async fn post_event_handler(
app_state: Data<AppState>,
query: Query<PostEventQuery>,
mut payload: Payload,
) -> Result<HttpResponse, actix_web::Error> {
const BODY_BYTES_LIMIT: usize = 1 << 22;
let session_id = &query.session_id;
let write_stream = {
let rg = app_state.txs.read().await;
match rg.get(session_id.as_str()) {
Some(stream) => stream.clone(),
None => return Ok(HttpResponse::NotFound().finish()),
}
};
let mut write_stream = write_stream.lock().await;
let mut size = 0;
// Process the request body in chunks
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
size += chunk.len();
if size > BODY_BYTES_LIMIT {
return Ok(HttpResponse::PayloadTooLarge().finish());
}
if (write_stream.write_all(&chunk).await).is_err() {
return Ok(HttpResponse::InternalServerError().finish());
}
}
if (write_stream.write_u8(b'\n').await).is_err() {
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Accepted().finish())
}
#[get("/sse")]
async fn sse_handler(app_state: Data<AppState>) -> Result<HttpResponse, Error> {
// it's 4KB
const BUFFER_SIZE: usize = 1 << 12;
let session = session_id();
tracing::info!(%session, "sse connection");
let (c2s_read, c2s_write) = tokio::io::simplex(BUFFER_SIZE);
let (s2c_read, s2c_write) = tokio::io::simplex(BUFFER_SIZE);
app_state
.txs
.write()
.await
.insert(session.clone(), Arc::new(Mutex::new(c2s_write)));
{
let session = session.clone();
let app_state = app_state.clone();
tokio::spawn(async move {
let router = RouterService(counter::CounterRouter::new());
let server = Server::new(router);
let bytes_transport = ByteTransport::new(c2s_read, s2c_write);
let _result = server
.run(bytes_transport)
.await
.inspect_err(|e| tracing::error!(?e, "server run error"));
tracing::info!(%session, "connection closed, removing session");
app_state.txs.write().await.remove(&session);
});
}
// Create SSE stream with correct types
let stream = futures::stream::once(futures::future::ready(Ok::<_, io::Error>(Bytes::from(
format!("event: endpoint\ndata: ?sessionId={}\n\n", session),
))))
.chain(
FramedRead::new(s2c_read, common::jsonrpc_frame_codec::JsonRpcFrameCodec)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
.map_ok(move |bytes| {
let message = match std::str::from_utf8(&bytes) {
Ok(message) => format!("event: message\ndata: {}\n\n", message),
Err(_) => "event: error\ndata: Invalid UTF-8 data\n\n".to_string(),
};
Bytes::from(message)
}),
);
// Return SSE response
Ok(HttpResponse::Ok()
.append_header(("Content-Type", "text/event-stream"))
.append_header(("Cache-Control", "no-cache"))
.append_header(("Connection", "keep-alive"))
.streaming(stream))
}
#[actix_web::main]
async fn main() -> io::Result<()> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| format!("info,{}=debug", env!("CARGO_CRATE_NAME")).into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
tracing::debug!("starting server at {}", BIND_ADDRESS);
let app_state = Data::new(AppState::new());
HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.app_data(app_state.clone())
.service(sse_handler)
.service(post_event_handler)
})
.bind(BIND_ADDRESS)?
.run()
.await
}

Some files were not shown because too many files have changed in this diff Show more