-
Notifications
You must be signed in to change notification settings - Fork 2
Mcp #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mcp #18
Changes from all commits
2de9749
7202a28
40b284d
0324baf
8775ac2
3b968e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| use rmcp::{ | ||
| ServiceExt, | ||
| model::{CallToolRequestParams, Content, RawContent, ResourceContents, Tool}, | ||
| service::{RoleClient, RunningService}, | ||
| transport::{TokioChildProcess, streamable_http_client::StreamableHttpClientTransport}, | ||
| }; | ||
|
|
||
| use crate::{ | ||
| config::McpServerConfig, | ||
| error::{Error, Result}, | ||
| }; | ||
|
|
||
| pub struct McpClient { | ||
| service: RunningService<RoleClient, ()>, | ||
| pub server_name: String, | ||
| } | ||
|
|
||
| impl McpClient { | ||
| pub async fn connect(name: &str, config: &McpServerConfig) -> Result<Self> { | ||
| if let Some(ref url) = config.url { | ||
| let transport = StreamableHttpClientTransport::from_uri(url.as_str()); | ||
| let service = () | ||
| .serve(transport) | ||
| .await | ||
| .map_err(|e| Error::Other(format!("MCP HTTP connect to '{}' failed: {}", name, e)))?; | ||
| Ok(Self { service, server_name: name.to_string() }) | ||
| } else if let Some(ref command) = config.command { | ||
| let mut cmd = tokio::process::Command::new(command); | ||
| cmd.args(&config.args); | ||
| for (k, v) in &config.env { | ||
| cmd.env(k, v); | ||
| } | ||
| let transport = TokioChildProcess::new(cmd) | ||
| .map_err(|e| Error::Other(format!("MCP spawn '{}' failed: {}", name, e)))?; | ||
| let service = () | ||
| .serve(transport) | ||
| .await | ||
| .map_err(|e| Error::Other(format!("MCP stdio connect to '{}' failed: {}", name, e)))?; | ||
| Ok(Self { service, server_name: name.to_string() }) | ||
| } else { | ||
| Err(Error::ConfigError(format!( | ||
| "MCP server '{}' must have either 'command' (stdio) or 'url' (HTTP)", | ||
| name | ||
| ))) | ||
| } | ||
| } | ||
|
|
||
| pub async fn list_tools(&self) -> Result<Vec<Tool>> { | ||
| self.service | ||
| .list_all_tools() | ||
| .await | ||
| .map_err(|e| Error::Other(format!("MCP list_tools failed for '{}': {}", self.server_name, e))) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| pub async fn call_tool(&self, name: &str, args_json: &str) -> Result<String> { | ||
| let params = build_call_params(name, args_json)?; | ||
|
|
||
| let result = self | ||
| .service | ||
| .peer() | ||
| .call_tool(params) | ||
| .await | ||
| .map_err(|e| Error::ToolExecutionError(format!("MCP tool '{}' on '{}' failed: {}", name, self.server_name, e)))?; | ||
|
|
||
| if result.is_error.unwrap_or(false) { | ||
| return Err(Error::ToolExecutionError(extract_text_content(&result.content))); | ||
| } | ||
|
|
||
| Ok(extract_text_content(&result.content)) | ||
| } | ||
| } | ||
|
|
||
| fn build_call_params(name: &str, args_json: &str) -> Result<CallToolRequestParams> { | ||
| let trimmed = args_json.trim(); | ||
| if trimmed.is_empty() || trimmed == "{}" { | ||
| return Ok(CallToolRequestParams::new(name.to_string())); | ||
| } | ||
| let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(trimmed)?; | ||
| Ok(CallToolRequestParams::new(name.to_string()).with_arguments(map)) | ||
| } | ||
|
|
||
| fn extract_text_content(content: &[Content]) -> String { | ||
| content | ||
| .iter() | ||
| .map(|item| match &**item { | ||
| RawContent::Text(t) => t.text.clone(), | ||
| RawContent::Image(i) => format!("[image: {}]", i.mime_type), | ||
| RawContent::Audio(a) => format!("[audio: {}]", a.mime_type), | ||
| RawContent::Resource(r) => match &r.resource { | ||
| ResourceContents::TextResourceContents { text, .. } => text.clone(), | ||
| ResourceContents::BlobResourceContents { uri, mime_type, .. } => { | ||
| format!("[blob: {} ({})]", uri, mime_type.as_deref().unwrap_or("unknown")) | ||
| } | ||
| }, | ||
| RawContent::ResourceLink(l) => format!("[resource: {}]", l.uri), | ||
| }) | ||
| .collect::<Vec<_>>() | ||
| .join("\n") | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mod client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mod tool_handler; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use std::collections::BTreeMap; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use std::sync::Arc; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use client::McpClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use tool_handler::McpToolHandler; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use crate::{config::McpServerConfig, error::Result, tools::ToolHandler}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pub(crate) async fn load_mcp_tools(configs: &BTreeMap<String, McpServerConfig>) -> Vec<Box<dyn ToolHandler>> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mut handlers: Vec<Box<dyn ToolHandler>> = Vec::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (name, config) in configs { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| match connect_server(name, config).await { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(server_handlers) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tracing::info!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server = %name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| count = server_handlers.len(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "MCP server connected" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handlers.extend(server_handlers); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Err(e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tracing::warn!(server = %name, error = %e, "MCP server failed to connect"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "mod.rs" -path "*/mcp/*" -type fRepository: weirdstuff-dev/openheim Length of output: 84 🏁 Script executed: find . -name "*.rs" | head -20Repository: weirdstuff-dev/openheim Length of output: 485 🏁 Script executed: fd -e rs mcp | head -20Repository: weirdstuff-dev/openheim Length of output: 49 🏁 Script executed: rg "connect_server" -A 5 -B 5Repository: weirdstuff-dev/openheim Length of output: 1234 🏁 Script executed: rg "McpClient" -A 10 -B 2Repository: weirdstuff-dev/openheim Length of output: 4000 🏁 Script executed: wc -l src/mcp/mod.rsRepository: weirdstuff-dev/openheim Length of output: 85 🏁 Script executed: cat -n src/mcp/mod.rsRepository: weirdstuff-dev/openheim Length of output: 1954 🏁 Script executed: wc -l src/mcp/client.rsRepository: weirdstuff-dev/openheim Length of output: 88 🏁 Script executed: cat -n src/mcp/client.rsRepository: weirdstuff-dev/openheim Length of output: 4104 🏁 Script executed: rg "timeout|Timeout" src/mcp/Repository: weirdstuff-dev/openheim Length of output: 49 Add a connection timeout to prevent MCP servers from blocking startup indefinitely.
Proposed fix: wrap each `connect_server` call with `tokio::time::timeout`+use std::time::Duration;
+
+const MCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) async fn load_mcp_tools(configs: &BTreeMap<String, McpServerConfig>) -> Vec<Box<dyn ToolHandler>> {
let mut handlers: Vec<Box<dyn ToolHandler>> = Vec::new();
for (name, config) in configs {
- match connect_server(name, config).await {
- Ok(server_handlers) => {
+ match tokio::time::timeout(MCP_CONNECT_TIMEOUT, connect_server(name, config)).await {
+ Ok(Ok(server_handlers)) => {
tracing::info!(
server = %name,
count = server_handlers.len(),
"MCP server connected"
);
handlers.extend(server_handlers);
}
- Err(e) => {
- tracing::warn!(server = %name, error = %e, "MCP server failed to connect");
+ Ok(Err(e)) => {
+ tracing::warn!(server = %name, error = %e, "MCP server failed to connect");
+ }
+ Err(_) => {
+ tracing::warn!(server = %name, timeout_secs = MCP_CONNECT_TIMEOUT.as_secs(), "MCP server connection timed out");
}
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handlers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async fn connect_server(name: &str, config: &McpServerConfig) -> Result<Vec<Box<dyn ToolHandler>>> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let client = Arc::new(McpClient::connect(name, config).await?); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let tools = client.list_tools().await?; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Sanitise the prefix: hyphens and spaces become underscores so the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // combined name is a valid identifier for tool-call APIs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let prefix: String = name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .chars() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .collect(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let handlers = tools | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .iter() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(|tool| -> Box<dyn ToolHandler> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Box::new(McpToolHandler::new(Arc::clone(&client), tool, &prefix)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .collect(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Ok(handlers) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| use std::sync::Arc; | ||
|
|
||
| use async_trait::async_trait; | ||
| use rmcp::model::Tool as McpTool; | ||
|
|
||
| use crate::{ | ||
| core::models::{FunctionDefinition, Tool}, | ||
| error::Result, | ||
| tools::ToolHandler, | ||
| }; | ||
|
|
||
| use super::client::McpClient; | ||
|
|
||
| pub struct McpToolHandler { | ||
| client: Arc<McpClient>, | ||
| /// Original tool name as reported by the MCP server. | ||
| tool_name: String, | ||
| /// Name exposed to the LLM: `{server_prefix}__{tool_name}`. | ||
| prefixed_name: String, | ||
| description: String, | ||
| schema: serde_json::Value, | ||
| } | ||
|
|
||
| impl McpToolHandler { | ||
| pub fn new(client: Arc<McpClient>, tool: &McpTool, server_prefix: &str) -> Self { | ||
| let tool_name = tool.name.to_string(); | ||
| let prefixed_name = format!("{}__{}", server_prefix, tool_name); | ||
| let description = tool.description.as_deref().unwrap_or("").to_string(); | ||
| let schema = serde_json::to_value(&tool.input_schema) | ||
| .unwrap_or_else(|_| serde_json::json!({"type": "object", "properties": {}})); | ||
|
|
||
| Self { client, tool_name, prefixed_name, description, schema } | ||
| } | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl ToolHandler for McpToolHandler { | ||
| fn definition(&self) -> Tool { | ||
| Tool { | ||
| tool_type: "function".to_string(), | ||
| function: FunctionDefinition { | ||
| name: self.prefixed_name.clone(), | ||
| description: self.description.clone(), | ||
| parameters: self.schema.clone(), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| async fn execute(&self, args: &str) -> Result<String> { | ||
| self.client.call_tool(&self.tool_name, args).await | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.