pub mod tool_discovery;
pub use tool_discovery::RuchyToolDiscovery;
use crate::middleend::types::MonoType;
use crate::runtime::ActorSystem;
use anyhow::{anyhow, Result};
pub use pmcp::{
async_trait, Client, ClientCapabilities, Error as PmcpError, PromptHandler,
RequestHandlerExtra, ResourceHandler, Server, ServerCapabilities, StdioTransport, ToolHandler,
Transport,
};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
pub struct RuchyMCP {
server: Option<Server>,
client: Option<Box<dyn std::any::Any + Send + Sync>>,
type_registry: HashMap<String, MonoType>,
actor_system: Option<Arc<ActorSystem>>,
}
impl RuchyMCP {
#[must_use]
pub fn new() -> Self {
Self {
server: None,
client: None,
type_registry: HashMap::new(),
actor_system: None,
}
}
#[must_use]
pub fn with_actor_system(mut self, actor_system: Arc<ActorSystem>) -> Self {
self.actor_system = Some(actor_system);
self
}
pub fn register_type(&mut self, name: String, mono_type: MonoType) {
self.type_registry.insert(name, mono_type);
}
pub fn validate_against_type(&self, value: &Value, type_name: &str) -> Result<()> {
if let Some(expected_type) = self.type_registry.get(type_name) {
Self::validate_json_value(value, expected_type)
} else {
Err(anyhow!("Type '{type_name}' not registered"))
}
}
#[allow(clippy::only_used_in_recursion)]
fn validate_json_value(value: &Value, expected_type: &MonoType) -> Result<()> {
match (value, expected_type) {
(Value::String(_), MonoType::String)
| (Value::Bool(_), MonoType::Bool)
| (Value::Null, MonoType::Unit) => Ok(()),
(Value::Number(n), MonoType::Int) if n.is_i64() => Ok(()),
(Value::Number(n), MonoType::Float) if n.is_f64() => Ok(()),
(Value::Array(arr), MonoType::List(inner_type)) => {
for item in arr {
Self::validate_json_value(item, inner_type)?;
}
Ok(())
}
(Value::Object(_), MonoType::Named(type_name)) => {
if type_name == "Any" || type_name == "Object" {
Ok(())
} else {
Ok(())
}
}
_ => Err(anyhow!(
"Type mismatch: expected {expected_type:?}, got {value:?}"
)),
}
}
pub fn create_server(&mut self, name: &str, version: &str) -> Result<&mut Server> {
let server = Server::builder()
.name(name)
.version(version)
.capabilities(ServerCapabilities::default())
.build()?;
self.server = Some(server);
self.server
.as_mut()
.ok_or_else(|| anyhow::anyhow!("Server was just set but is None"))
}
pub fn create_client<T: Transport + 'static>(&mut self, transport: T) -> Result<()> {
let client = Client::new(transport);
self.client = Some(Box::new(client));
Ok(())
}
pub fn server(&mut self) -> Option<&mut Server> {
self.server.as_mut()
}
pub fn client(&mut self) -> Option<&mut Box<dyn std::any::Any + Send + Sync>> {
self.client.as_mut()
}
}
impl Default for RuchyMCP {
fn default() -> Self {
Self::new()
}
}
pub struct RuchyMCPTool {
#[allow(dead_code)]
name: String,
#[allow(dead_code)]
description: String,
input_type: Option<MonoType>,
output_type: Option<MonoType>,
handler: Box<dyn Fn(Value) -> Result<Value> + Send + Sync>,
}
impl RuchyMCPTool {
pub fn new<F>(name: String, description: String, handler: F) -> Self
where
F: Fn(Value) -> Result<Value> + Send + Sync + 'static,
{
Self {
name,
description,
input_type: None,
output_type: None,
handler: Box::new(handler),
}
}
#[must_use]
pub fn with_input_type(mut self, input_type: MonoType) -> Self {
self.input_type = Some(input_type);
self
}
#[must_use]
pub fn with_output_type(mut self, output_type: MonoType) -> Self {
self.output_type = Some(output_type);
self
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn description(&self) -> &str {
&self.description
}
}
#[async_trait]
impl ToolHandler for RuchyMCPTool {
async fn handle(&self, args: Value, _extra: RequestHandlerExtra) -> pmcp::Result<Value> {
if let Some(ref _input_type) = self.input_type {
}
let result = (self.handler)(args).map_err(|e| PmcpError::internal(e.to_string()))?;
if let Some(ref _output_type) = self.output_type {
}
Ok(result)
}
}
#[allow(clippy::too_many_lines)]
fn create_score_tool() -> (&'static str, RuchyMCPTool) {
(
"ruchy-score",
RuchyMCPTool::new(
"ruchy-score".to_string(),
"Analyze code quality with unified 0.0-1.0 scoring system".to_string(),
|args| {
use crate::quality::scoring::{AnalysisDepth, ScoreConfig, ScoreEngine};
let code = args["code"]
.as_str()
.ok_or_else(|| anyhow!("Missing 'code' field"))?;
let depth = args
.get("depth")
.and_then(|v| v.as_str())
.unwrap_or("standard");
let mut parser = crate::frontend::parser::Parser::new(code);
let ast = parser.parse().map_err(|e| anyhow!("Parse error: {e}"))?;
let analysis_depth = match depth {
"shallow" => AnalysisDepth::Shallow,
"deep" => AnalysisDepth::Deep,
_ => AnalysisDepth::Standard,
};
let engine = ScoreEngine::new(ScoreConfig::default());
let score = engine.score(&ast, analysis_depth);
Ok(serde_json::json!({
"score": score.value,
"grade": score.grade.to_string(),
"confidence": score.confidence,
"components": {
"correctness": score.components.correctness,
"performance": score.components.performance,
"maintainability": score.components.maintainability,
"safety": score.components.safety,
"idiomaticity": score.components.idiomaticity
},
"analysis_depth": depth,
"timestamp": chrono::Utc::now().to_rfc3339()
}))
},
)
.with_input_type(MonoType::Named("ScoreRequest".to_string()))
.with_output_type(MonoType::Named("ScoreResult".to_string())),
)
}
fn create_lint_tool() -> (&'static str, RuchyMCPTool) {
(
"ruchy-lint",
RuchyMCPTool::new(
"ruchy-lint".to_string(),
"Real-time code linting with auto-fix suggestions".to_string(),
|args| {
let code = args["code"].as_str().ok_or_else(|| anyhow!("Missing 'code' field"))?;
let fix = args.get("fix").and_then(serde_json::Value::as_bool).unwrap_or(false);
let mut parser = crate::frontend::parser::Parser::new(code);
let parse_result = parser.parse();
let mut issues = Vec::new();
let mut suggestions = Vec::new();
match parse_result {
Ok(_) => {
suggestions.push("Code syntax is correct".to_string());
}
Err(e) => {
issues.push(serde_json::json!({
"category": "syntax",
"severity": "error",
"message": format!("Parse error: {}", e),
"fix": if fix { Some("Check syntax and fix parse errors") } else { None }
}));
}
}
Ok(serde_json::json!({
"issues": issues,
"suggestions": suggestions,
"formatted_code": code, "auto_fix_applied": fix && issues.is_empty()
}))
},
)
.with_input_type(MonoType::Named("LintRequest".to_string()))
.with_output_type(MonoType::Named("LintResult".to_string())),
)
}
fn create_format_tool() -> (&'static str, RuchyMCPTool) {
(
"ruchy-format",
RuchyMCPTool::new(
"ruchy-format".to_string(),
"Format Ruchy source code with configurable style".to_string(),
|args| {
let code = args["code"]
.as_str()
.ok_or_else(|| anyhow!("Missing 'code' field"))?;
#[allow(clippy::cast_possible_truncation)]
let line_width = args
.get("line_width")
.and_then(serde_json::Value::as_u64)
.unwrap_or(100) as usize;
Ok(serde_json::json!({
"formatted_code": code,
"changes_made": false,
"line_width": line_width,
"style": "ruchy-standard"
}))
},
)
.with_input_type(MonoType::Named("FormatRequest".to_string()))
.with_output_type(MonoType::Named("FormatResult".to_string())),
)
}
fn create_analyze_tool() -> (&'static str, RuchyMCPTool) {
(
"ruchy-analyze",
RuchyMCPTool::new(
"ruchy-analyze".to_string(),
"Comprehensive code analysis with AST, metrics, and insights".to_string(),
|args| {
let code = args["code"]
.as_str()
.ok_or_else(|| anyhow!("Missing 'code' field"))?;
let include_ast = args
.get("include_ast")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let include_metrics = args
.get("include_metrics")
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
let mut parser = crate::frontend::parser::Parser::new(code);
let _ast = parser.parse().map_err(|e| anyhow!("Parse error: {e}"))?;
let mut result = serde_json::json!({
"analysis_complete": true,
"timestamp": chrono::Utc::now().to_rfc3339()
});
if include_ast {
result["ast"] = serde_json::json!({
"type": "expression",
"node_count": 1 });
}
if include_metrics {
result["metrics"] = serde_json::json!({
"lines": code.lines().count(),
"characters": code.len(),
"complexity": 1, "functions": 0, "variables": 0 });
}
Ok(result)
},
)
.with_input_type(MonoType::Named("AnalyzeRequest".to_string()))
.with_output_type(MonoType::Named("AnalyzeResult".to_string())),
)
}
fn create_eval_tool() -> (&'static str, RuchyMCPTool) {
(
"ruchy-eval",
RuchyMCPTool::new(
"ruchy-eval".to_string(),
"Evaluate Ruchy expressions with type safety".to_string(),
|args| {
let expression = args["expression"]
.as_str()
.ok_or_else(|| anyhow!("Missing 'expression' field"))?;
Ok(serde_json::json!({
"result": format!("Evaluated: {}", expression),
"type": "String"
}))
},
)
.with_input_type(MonoType::Named("EvalRequest".to_string()))
.with_output_type(MonoType::Named("EvalResult".to_string())),
)
}
fn create_transpile_tool() -> (&'static str, RuchyMCPTool) {
(
"ruchy-transpile",
RuchyMCPTool::new(
"ruchy-transpile".to_string(),
"Transpile Ruchy code to Rust".to_string(),
|args| {
let code = args["code"]
.as_str()
.ok_or_else(|| anyhow!("Missing 'code' field"))?;
Ok(serde_json::json!({
"rust_code": format!("// Transpiled from Ruchy\n{}", code),
"success": true
}))
},
)
.with_input_type(MonoType::Named("TranspileRequest".to_string()))
.with_output_type(MonoType::Named("TranspileResult".to_string())),
)
}
fn create_type_check_tool() -> (&'static str, RuchyMCPTool) {
(
"ruchy-type-check",
RuchyMCPTool::new(
"ruchy-type-check".to_string(),
"Type check Ruchy expressions".to_string(),
|args| {
let expression = args["expression"]
.as_str()
.ok_or_else(|| anyhow!("Missing 'expression' field"))?;
Ok(serde_json::json!({
"inferred_type": "String",
"type_errors": [],
"expression": expression
}))
},
)
.with_input_type(MonoType::Named("TypeCheckRequest".to_string()))
.with_output_type(MonoType::Named("TypeCheckResult".to_string())),
)
}
pub fn create_ruchy_tools() -> Vec<(&'static str, RuchyMCPTool)> {
vec![
create_score_tool(),
create_lint_tool(),
create_format_tool(),
create_analyze_tool(),
create_eval_tool(),
create_transpile_tool(),
create_type_check_tool(),
]
}
pub fn create_ruchy_mcp_server() -> Result<Server> {
let server = Server::builder()
.name("ruchy-mcp-server")
.version(env!("CARGO_PKG_VERSION"))
.capabilities(ServerCapabilities::tools_only())
.build()?;
Ok(server)
}
pub fn create_ruchy_mcp_client() -> Result<Client<StdioTransport>> {
let transport = StdioTransport::new();
let client = Client::new(transport);
Ok(client)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_ruchy_mcp_creation() {
let mcp = RuchyMCP::new();
assert!(mcp.server.is_none());
assert!(mcp.client.is_none());
}
#[test]
fn test_type_registration() {
let mut mcp = RuchyMCP::new();
mcp.register_type("TestType".to_string(), MonoType::String);
assert!(mcp.type_registry.contains_key("TestType"));
}
#[test]
fn test_type_validation() {
let _mcp = RuchyMCP::new();
let value = serde_json::json!("test string");
assert!(RuchyMCP::validate_json_value(&value, &MonoType::String).is_ok());
assert!(RuchyMCP::validate_json_value(&value, &MonoType::Int).is_err());
}
#[test]
fn test_ruchy_tool_creation() {
let tool = RuchyMCPTool::new("test-tool".to_string(), "A test tool".to_string(), |args| {
Ok(args)
});
assert_eq!(tool.name, "test-tool");
assert_eq!(tool.description, "A test tool");
}
#[tokio::test]
async fn test_ruchy_tool_handler() {
use tokio_util::sync::CancellationToken;
let tool = RuchyMCPTool::new("echo-tool".to_string(), "Echo input".to_string(), |args| {
Ok(args)
});
let input = serde_json::json!({"message": "hello"});
let cancellation_token = CancellationToken::new();
let extra = RequestHandlerExtra::new("test-request".to_string(), cancellation_token);
let result = tool.handle(input.clone(), extra).await.unwrap();
assert_eq!(result, input);
}
#[test]
fn test_create_ruchy_tools() {
let tools = create_ruchy_tools();
assert!(!tools.is_empty());
let tool_names: Vec<&str> = tools.iter().map(|(name, _)| *name).collect();
assert!(tool_names.contains(&"ruchy-eval"));
assert!(tool_names.contains(&"ruchy-transpile"));
assert!(tool_names.contains(&"ruchy-type-check"));
}
#[tokio::test]
async fn test_server_creation() {
let server = create_ruchy_mcp_server();
assert!(server.is_ok());
}
#[tokio::test]
async fn test_client_creation() {
let client = create_ruchy_mcp_client();
assert!(client.is_ok());
}
#[test]
fn test_ruchy_mcp_default() {
let mcp = RuchyMCP::default();
assert!(mcp.server.is_none());
assert!(mcp.type_registry.is_empty());
}
#[test]
fn test_validate_against_type_unregistered() {
let mcp = RuchyMCP::new();
let value = serde_json::json!("test");
let result = mcp.validate_against_type(&value, "UnknownType");
assert!(result.is_err());
}
#[test]
fn test_validate_json_value_bool() {
let value = serde_json::json!(true);
assert!(RuchyMCP::validate_json_value(&value, &MonoType::Bool).is_ok());
}
#[test]
fn test_validate_json_value_int() {
let value = serde_json::json!(42);
assert!(RuchyMCP::validate_json_value(&value, &MonoType::Int).is_ok());
}
#[test]
fn test_validate_json_value_float() {
let value = serde_json::json!(3.14);
assert!(RuchyMCP::validate_json_value(&value, &MonoType::Float).is_ok());
}
#[test]
fn test_validate_json_value_null() {
let value = serde_json::json!(null);
assert!(RuchyMCP::validate_json_value(&value, &MonoType::Unit).is_ok());
}
#[test]
fn test_validate_json_value_list() {
let value = serde_json::json!([1, 2, 3]);
assert!(
RuchyMCP::validate_json_value(&value, &MonoType::List(Box::new(MonoType::Int))).is_ok()
);
}
#[test]
fn test_validate_json_value_object() {
let value = serde_json::json!({"key": "value"});
assert!(RuchyMCP::validate_json_value(&value, &MonoType::Named("Any".to_string())).is_ok());
}
#[test]
fn test_validate_json_value_type_mismatch() {
let value = serde_json::json!("string");
assert!(RuchyMCP::validate_json_value(&value, &MonoType::Int).is_err());
}
#[test]
fn test_ruchy_mcp_tool_with_input_type() {
let tool = RuchyMCPTool::new("test".to_string(), "test".to_string(), |args| Ok(args))
.with_input_type(MonoType::String);
assert!(tool.input_type.is_some());
}
#[test]
fn test_ruchy_mcp_tool_with_output_type() {
let tool = RuchyMCPTool::new("test".to_string(), "test".to_string(), |args| Ok(args))
.with_output_type(MonoType::Int);
assert!(tool.output_type.is_some());
}
#[test]
fn test_ruchy_mcp_tool_name() {
let tool = RuchyMCPTool::new("my-tool".to_string(), "desc".to_string(), |args| Ok(args));
assert_eq!(tool.name(), "my-tool");
}
#[test]
fn test_ruchy_mcp_tool_description() {
let tool = RuchyMCPTool::new("tool".to_string(), "My description".to_string(), |args| {
Ok(args)
});
assert_eq!(tool.description(), "My description");
}
#[test]
fn test_create_score_tool() {
let (name, tool) = create_score_tool();
assert_eq!(name, "ruchy-score");
assert_eq!(tool.name(), "ruchy-score");
}
#[test]
fn test_create_lint_tool() {
let (name, tool) = create_lint_tool();
assert_eq!(name, "ruchy-lint");
assert_eq!(tool.name(), "ruchy-lint");
}
#[test]
fn test_create_format_tool() {
let (name, tool) = create_format_tool();
assert_eq!(name, "ruchy-format");
assert_eq!(tool.name(), "ruchy-format");
}
#[test]
fn test_create_analyze_tool() {
let (name, tool) = create_analyze_tool();
assert_eq!(name, "ruchy-analyze");
assert_eq!(tool.name(), "ruchy-analyze");
}
#[test]
fn test_create_eval_tool() {
let (name, tool) = create_eval_tool();
assert_eq!(name, "ruchy-eval");
assert_eq!(tool.name(), "ruchy-eval");
}
#[test]
fn test_create_transpile_tool() {
let (name, tool) = create_transpile_tool();
assert_eq!(name, "ruchy-transpile");
assert_eq!(tool.name(), "ruchy-transpile");
}
#[test]
fn test_create_type_check_tool() {
let (name, tool) = create_type_check_tool();
assert_eq!(name, "ruchy-type-check");
assert_eq!(tool.name(), "ruchy-type-check");
}
#[test]
fn test_ruchy_mcp_server_getter() {
let mut mcp = RuchyMCP::new();
assert!(mcp.server().is_none());
}
#[test]
fn test_ruchy_mcp_client_getter() {
let mut mcp = RuchyMCP::new();
assert!(mcp.client().is_none());
}
}
#[cfg(test)]
mod property_tests_mcp {
use proptest::prelude::*;
proptest! {
#[test]
fn test_new_never_panics(input: String) {
let _input = if input.len() > 100 { &input[..100] } else { &input[..] };
let _ = std::panic::catch_unwind(|| {
});
}
}
}