use crate::integrations::mcp::registry::StdioServerSpec;
use crate::integrations::mcp::types::{McpTool, ToolProvider, VerificationStatus};
use crate::integrations::schemapin::key_store::LocalKeyStore;
use crate::integrations::schemapin::native_client::{NativeSchemaPinClient, SchemaPinClient};
use crate::integrations::schemapin::types::{PinnedKey, VerifyArgs};
use rmcp::{
model::CallToolRequestParams,
transport::{ConfigureCommandExt, TokioChildProcess},
ServiceExt,
};
use sha2::Digest;
use std::io::Write;
pub struct RmcpStdioClient;
impl RmcpStdioClient {
fn transport(spec: &StdioServerSpec) -> Result<TokioChildProcess, String> {
let args = spec.args.clone();
let env = spec.env.clone();
TokioChildProcess::new(
tokio::process::Command::new(&spec.command).configure(|cmd| {
cmd.args(&args);
for (k, v) in &env {
cmd.env(k, v);
}
}),
)
.map_err(|e| format!("failed to spawn MCP server '{}': {}", spec.command, e))
}
pub async fn list_tools(spec: &StdioServerSpec) -> Result<Vec<rmcp::model::Tool>, String> {
let client = ().serve(Self::transport(spec)?).await.map_err(|e| e.to_string())?;
let tools = client.list_all_tools().await.map_err(|e| e.to_string());
let _ = client.cancel().await;
tools
}
pub async fn call_tool(
spec: &StdioServerSpec,
tool: &str,
args: serde_json::Map<String, serde_json::Value>,
) -> Result<serde_json::Value, String> {
let client = ().serve(Self::transport(spec)?).await.map_err(|e| e.to_string())?;
let params = CallToolRequestParams::new(tool.to_string()).with_arguments(args);
let out = client.call_tool(params).await.map_err(|e| e.to_string());
let _ = client.cancel().await;
let result = out?;
if result.is_error.unwrap_or(false) {
return Err(format!(
"tool '{}' reported error: {}",
tool,
serde_json::to_string(&result.content).unwrap_or_default()
));
}
serde_json::to_value(&result.content).map_err(|e| e.to_string())
}
pub async fn verified_invoke(
spec: &StdioServerSpec,
tool: &str,
args: serde_json::Map<String, serde_json::Value>,
enforce: bool,
) -> Result<serde_json::Value, String> {
if enforce {
let tools = Self::list_tools(spec).await?;
let rmcp_tool = tools
.iter()
.find(|t| t.name.as_ref() == tool)
.ok_or_else(|| format!("tool '{}' not found on server", tool))?;
let mcp_tool = McpTool {
name: rmcp_tool.name.to_string(),
description: rmcp_tool
.description
.clone()
.map(|c| c.to_string())
.unwrap_or_default(),
schema: serde_json::to_value(&*rmcp_tool.input_schema)
.map_err(|e| e.to_string())?,
provider: ToolProvider {
identifier: spec.command.clone(),
name: spec.command.clone(),
public_key_url: spec.public_key_url.clone().unwrap_or_default(),
version: None,
},
verification_status: VerificationStatus::Pending,
metadata: None,
sensitive_params: Vec::new(),
};
let key_store = LocalKeyStore::new().map_err(|e| e.to_string())?;
let client = NativeSchemaPinClient::new();
let verified = verify_via_schemapin(&client, &key_store, &mcp_tool).await?;
if !verified {
return Err(format!(
"tool '{}' is not SchemaPin-verified and enforcement is on (fail-closed). \
Sign the tool schema or run with verification disabled for local dev.",
tool
));
}
}
Self::call_tool(spec, tool, args).await
}
}
async fn verify_via_schemapin(
client: &NativeSchemaPinClient,
key_store: &LocalKeyStore,
tool: &McpTool,
) -> Result<bool, String> {
let has_signature = tool
.schema
.get("signature")
.and_then(|s| s.as_str())
.is_some();
if !has_signature || tool.provider.public_key_url.is_empty() {
return Ok(false);
}
let fetch_client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.https_only(true)
.build()
.map_err(|e| e.to_string())?;
let response = fetch_client
.get(&tool.provider.public_key_url)
.send()
.await
.map_err(|e| format!("failed to fetch provider public key: {e}"))?;
if !response.status().is_success() {
return Err(format!(
"provider public key fetch returned HTTP {}",
response.status()
));
}
let key_data = response
.text()
.await
.map_err(|e| format!("failed to read provider public key response: {e}"))?;
if key_data.trim().is_empty() {
return Err("provider public key response was empty".to_string());
}
let mut hasher = sha2::Sha256::new();
hasher.update(key_data.as_bytes());
let fingerprint = hex::encode(hasher.finalize());
key_store
.pin_key(PinnedKey::new(
tool.provider.identifier.clone(),
key_data,
"ES256".to_string(),
fingerprint,
))
.map_err(|e| e.to_string())?;
let mut temp_file = tempfile::NamedTempFile::new().map_err(|e| e.to_string())?;
let schema_json = serde_json::to_string_pretty(&tool.schema).map_err(|e| e.to_string())?;
temp_file
.write_all(schema_json.as_bytes())
.map_err(|e| e.to_string())?;
let schema_path = temp_file.path().to_string_lossy().to_string();
let verify_args = VerifyArgs::new(schema_path, tool.provider.public_key_url.clone());
let result = client
.verify_schema(verify_args)
.await
.map_err(|e| e.to_string())?;
Ok(result.success)
}