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:
parent
31493cfbcc
commit
0be6067326
40 changed files with 8676 additions and 4466 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
52
examples/clients/src/collection.rs
Normal file
52
examples/clients/src/collection.rs
Normal 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(())
|
||||
}
|
||||
100
examples/clients/src/everything_stdio.rs
Normal file
100
examples/clients/src/everything_stdio.rs
Normal 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(())
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
49
examples/clients/src/std_io.rs
Normal file
49
examples/clients/src/std_io.rs
Normal 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(())
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
))),
|
||||
}
|
||||
}
|
||||
26
examples/rig-integration/Cargo.toml
Normal file
26
examples/rig-integration/Cargo.toml
Normal 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"
|
||||
10
examples/rig-integration/config.toml
Normal file
10
examples/rig-integration/config.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
deepseek_key = ""
|
||||
cohere_key = ""
|
||||
|
||||
[mcp]
|
||||
|
||||
[[mcp.server]]
|
||||
name = "git"
|
||||
protocol = "stdio"
|
||||
command = "uvx"
|
||||
args = ["mcp-server-git"]
|
||||
114
examples/rig-integration/src/chat.rs
Normal file
114
examples/rig-integration/src/chat.rs
Normal 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(())
|
||||
}
|
||||
20
examples/rig-integration/src/config.rs
Normal file
20
examples/rig-integration/src/config.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
83
examples/rig-integration/src/config/mcp.rs
Normal file
83
examples/rig-integration/src/config/mcp.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
64
examples/rig-integration/src/main.rs
Normal file
64
examples/rig-integration/src/main.rs
Normal 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(())
|
||||
}
|
||||
113
examples/rig-integration/src/mcp_adaptor.rs
Normal file
113
examples/rig-integration/src/mcp_adaptor.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
Loading…
Reference in a new issue