Merge branch 'main' into weather-server-go
This commit is contained in:
commit
066cbd78bf
5 changed files with 2911 additions and 6 deletions
8
mcp-client-python/uv.lock
generated
8
mcp-client-python/uv.lock
generated
|
|
@ -442,7 +442,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.21.0"
|
||||
version = "1.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -456,11 +456,13 @@ dependencies = [
|
|||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/54/dd2330ef4611c27ae59124820863c34e1d3edb1133c58e6375e2d938c9c5/mcp-1.21.0.tar.gz", hash = "sha256:bab0a38e8f8c48080d787233343f8d301b0e1e95846ae7dead251b2421d99855", size = 452697, upload-time = "2025-11-06T23:19:58.432Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/1a/9c8a5362e3448d585081d6c7aa95898a64e0ac59d3e26169ae6c3ca5feaf/mcp-1.23.0.tar.gz", hash = "sha256:84e0c29316d0a8cf0affd196fd000487ac512aa3f771b63b2ea864e22961772b", size = 596506, upload-time = "2025-12-02T13:40:02.558Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/47/850b6edc96c03bd44b00de9a0ca3c1cc71e0ba1cd5822955bc9e4eb3fad3/mcp-1.21.0-py3-none-any.whl", hash = "sha256:598619e53eb0b7a6513db38c426b28a4bdf57496fed04332100d2c56acade98b", size = 173672, upload-time = "2025-11-06T23:19:56.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/b2/28739ce409f98159c0121eab56e69ad71546c4f34ac8b42e58c03f57dccc/mcp-1.23.0-py3-none-any.whl", hash = "sha256:5a645cf111ed329f4619f2629a3f15d9aabd7adc2ea09d600d31467b51ecb64f", size = 231427, upload-time = "2025-12-02T13:40:00.738Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
2667
mcp-client-rust/Cargo.lock
generated
Normal file
2667
mcp-client-rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
mcp-client-rust/Cargo.toml
Normal file
15
mcp-client-rust/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "mcp-client-rust"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
genai = "0.4.2"
|
||||
rmcp = { version = "0.8.0", features = ["server", "client", "transport-io", "transport-child-process"] }
|
||||
tokio = { version = "1.47.1", features = ['full']}
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = {version = "0.3", features = ["env-filter"]}
|
||||
serde_json = "1.0.128"
|
||||
dotenvy = "0.15.7"
|
||||
reqwest = "0.12.23"
|
||||
219
mcp-client-rust/src/main.rs
Normal file
219
mcp-client-rust/src/main.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use genai::Client;
|
||||
use genai::chat::{
|
||||
ChatMessage, ChatRequest, ChatResponse, ContentPart, Tool as GenaiTool, ToolResponse,
|
||||
};
|
||||
use rmcp::model::{CallToolRequestParam, Tool as McpTool};
|
||||
use rmcp::service::{RoleClient, RunningService, ServiceExt};
|
||||
use rmcp::transport::TokioChildProcess;
|
||||
use serde_json::Value;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
const MODEL_ANTHROPIC: &str = "claude-sonnet-4-20250514";
|
||||
|
||||
struct MCPClient {
|
||||
anthropic: Client,
|
||||
session: Option<RunningService<RoleClient, ()>>,
|
||||
tools: Vec<GenaiTool>,
|
||||
}
|
||||
|
||||
impl MCPClient {
|
||||
fn new() -> Result<Self> {
|
||||
Ok(MCPClient {
|
||||
anthropic: Client::default(),
|
||||
session: None,
|
||||
tools: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn connect_to_server(&mut self, server_args: &[String]) -> Result<()> {
|
||||
if self.session.is_some() {
|
||||
bail!("Client is already connected to a server");
|
||||
}
|
||||
|
||||
let mut command = Command::new(&server_args[0]);
|
||||
command.args(&server_args[1..]);
|
||||
|
||||
let process = TokioChildProcess::new(command)
|
||||
.with_context(|| format!("Failed to spawn server process for {:?}", server_args))?;
|
||||
|
||||
let session = ().serve(process).await?;
|
||||
|
||||
let rmcp_tools = session
|
||||
.list_all_tools()
|
||||
.await
|
||||
.context("Unable to list tools from server")?;
|
||||
|
||||
let tool_names: Vec<String> = rmcp_tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.to_string())
|
||||
.collect();
|
||||
|
||||
println!("Connected to server with tools: {tool_names:?}");
|
||||
|
||||
self.tools = convert_tools(&rmcp_tools);
|
||||
self.session = Some(session);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_query(&mut self, query: &str) -> Result<String> {
|
||||
let session = self
|
||||
.session
|
||||
.as_ref()
|
||||
.context("Client is not connected to any server")?;
|
||||
|
||||
let mut messages = vec![ChatMessage::user(query)];
|
||||
let mut final_text = Vec::new();
|
||||
|
||||
// Initial Claude API call with tools
|
||||
let mut chat_req = ChatRequest::new(messages.clone()).with_tools(self.tools.clone());
|
||||
let mut chat_rsp = self.request_model(&chat_req).await?;
|
||||
|
||||
// Process response content - collect text and handle tool calls
|
||||
for text in chat_rsp.texts() {
|
||||
final_text.push(text.to_string());
|
||||
}
|
||||
|
||||
let tool_calls = chat_rsp.tool_calls();
|
||||
if !tool_calls.is_empty() {
|
||||
// Append assistant's response to message history
|
||||
messages.push(ChatMessage::assistant(chat_rsp.content.clone()));
|
||||
|
||||
// Execute each tool call and collect responses
|
||||
let mut tool_results = Vec::new();
|
||||
for tool_call in tool_calls {
|
||||
// Add information about the tool call to final text
|
||||
let tool_args_str = serde_json::to_string(&tool_call.fn_arguments)
|
||||
.unwrap_or_else(|_| "{}".to_string());
|
||||
|
||||
final_text.push(format!(
|
||||
"[Calling tool {} with args {}]",
|
||||
tool_call.fn_name, tool_args_str
|
||||
));
|
||||
|
||||
// Query the MCP server
|
||||
let tool_result = session
|
||||
.call_tool(CallToolRequestParam {
|
||||
name: tool_call.fn_name.clone().into(),
|
||||
arguments: tool_call.fn_arguments.as_object().cloned(),
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("Tool call {} failed", tool_call.fn_name))?;
|
||||
|
||||
let payload = serde_json::to_string(&tool_result)
|
||||
.context("Failed to serialize tool result")?;
|
||||
|
||||
tool_results.push(ContentPart::ToolResponse(ToolResponse::new(
|
||||
tool_call.call_id.clone(),
|
||||
payload,
|
||||
)));
|
||||
}
|
||||
|
||||
// Append tool responses to message history
|
||||
messages.push(ChatMessage::user(tool_results));
|
||||
|
||||
// Build the next request and query model
|
||||
chat_req = ChatRequest::new(messages.clone());
|
||||
chat_rsp = self.request_model(&chat_req).await?;
|
||||
|
||||
// Collect text from response
|
||||
for text in chat_rsp.texts() {
|
||||
final_text.push(text.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(final_text.join("\n"))
|
||||
}
|
||||
|
||||
async fn request_model(&self, chat_req: &ChatRequest) -> Result<ChatResponse> {
|
||||
let response = self
|
||||
.anthropic
|
||||
.exec_chat(MODEL_ANTHROPIC, chat_req.clone(), None)
|
||||
.await
|
||||
.context("Anthropic chat request failed")?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn chat_loop(&mut self) -> Result<()> {
|
||||
println!("\nMCP Client Started!");
|
||||
println!("Type your queries or 'quit' to exit.");
|
||||
|
||||
let mut stdin = BufReader::new(io::stdin());
|
||||
let mut input = String::new();
|
||||
|
||||
loop {
|
||||
print!("\nQuery: ");
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
|
||||
input.clear();
|
||||
if stdin.read_line(&mut input).await? == 0 {
|
||||
break; // EOF
|
||||
}
|
||||
|
||||
let query = input.trim();
|
||||
if query.eq_ignore_ascii_case("quit") {
|
||||
break;
|
||||
}
|
||||
if query.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match self.process_query(query).await {
|
||||
Ok(response) => println!("\n{}", response),
|
||||
Err(err) => println!("\nError: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cleanup(&mut self) -> Result<()> {
|
||||
if let Some(session) = self.session.take() {
|
||||
let _ = session.cancel().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
dotenvy::dotenv().context("Failed to load env file")?;
|
||||
|
||||
let mut args = std::env::args();
|
||||
let _ = args.next();
|
||||
let server_args: Vec<String> = args.collect();
|
||||
|
||||
if server_args.is_empty() {
|
||||
eprintln!("Usage: cargo run -- <server_script_or_binary> [args...]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let mut client = MCPClient::new()?;
|
||||
|
||||
let result = async {
|
||||
client.connect_to_server(&server_args).await?;
|
||||
client.chat_loop().await
|
||||
}
|
||||
.await;
|
||||
|
||||
let cleanup_result = client.cleanup().await;
|
||||
|
||||
result?;
|
||||
cleanup_result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_tools(tools: &[McpTool]) -> Vec<GenaiTool> {
|
||||
tools
|
||||
.iter()
|
||||
.map(|tool| GenaiTool {
|
||||
name: tool.name.to_string(),
|
||||
description: tool.description.as_deref().map(str::to_string),
|
||||
schema: Some(Value::Object(tool.input_schema.as_ref().clone())),
|
||||
config: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
8
weather-server-python/uv.lock
generated
8
weather-server-python/uv.lock
generated
|
|
@ -320,7 +320,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.21.0"
|
||||
version = "1.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -334,11 +334,13 @@ dependencies = [
|
|||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/54/dd2330ef4611c27ae59124820863c34e1d3edb1133c58e6375e2d938c9c5/mcp-1.21.0.tar.gz", hash = "sha256:bab0a38e8f8c48080d787233343f8d301b0e1e95846ae7dead251b2421d99855", size = 452697, upload-time = "2025-11-06T23:19:58.432Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/1a/9c8a5362e3448d585081d6c7aa95898a64e0ac59d3e26169ae6c3ca5feaf/mcp-1.23.0.tar.gz", hash = "sha256:84e0c29316d0a8cf0affd196fd000487ac512aa3f771b63b2ea864e22961772b", size = 596506, upload-time = "2025-12-02T13:40:02.558Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/47/850b6edc96c03bd44b00de9a0ca3c1cc71e0ba1cd5822955bc9e4eb3fad3/mcp-1.21.0-py3-none-any.whl", hash = "sha256:598619e53eb0b7a6513db38c426b28a4bdf57496fed04332100d2c56acade98b", size = 173672, upload-time = "2025-11-06T23:19:56.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/b2/28739ce409f98159c0121eab56e69ad71546c4f34ac8b42e58c03f57dccc/mcp-1.23.0-py3-none-any.whl", hash = "sha256:5a645cf111ed329f4619f2629a3f15d9aabd7adc2ea09d600d31467b51ecb64f", size = 231427, upload-time = "2025-12-02T13:40:00.738Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
|
|||
Loading…
Reference in a new issue