refactor(workspace)!: move MCP crates under crates/mcp/ and update paths

This commit is contained in:
2025-10-17 00:31:35 +02:00
parent 3271697f6b
commit d4030dc598
14 changed files with 10 additions and 10 deletions

View File

@@ -0,0 +1,417 @@
//! Code execution tools using Docker sandboxing
use crate::sandbox::Sandbox;
use async_trait::async_trait;
use owlen_core::Result;
use owlen_core::tools::{Tool, ToolResult};
use serde_json::{Value, json};
use std::path::PathBuf;
/// Tool for compiling projects (Rust, Node.js, Python)
pub struct CompileProjectTool {
sandbox: Sandbox,
}
impl Default for CompileProjectTool {
fn default() -> Self {
Self::new()
}
}
impl CompileProjectTool {
pub fn new() -> Self {
Self {
sandbox: Sandbox::default(),
}
}
}
#[async_trait]
impl Tool for CompileProjectTool {
fn name(&self) -> &'static str {
"compile_project"
}
fn description(&self) -> &'static str {
"Compile a project (Rust, Node.js, Python). Detects project type automatically."
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"project_path": {
"type": "string",
"description": "Path to the project root"
},
"project_type": {
"type": "string",
"enum": ["rust", "node", "python"],
"description": "Project type (auto-detected if not specified)"
}
},
"required": ["project_path"]
})
}
async fn execute(&self, args: Value) -> Result<ToolResult> {
let project_path = args
.get("project_path")
.and_then(|v| v.as_str())
.ok_or_else(|| owlen_core::Error::InvalidInput("Missing project_path".into()))?;
let path = PathBuf::from(project_path);
if !path.exists() {
return Ok(ToolResult::error("Project path does not exist"));
}
// Detect project type
let project_type = if let Some(pt) = args.get("project_type").and_then(|v| v.as_str()) {
pt.to_string()
} else if path.join("Cargo.toml").exists() {
"rust".to_string()
} else if path.join("package.json").exists() {
"node".to_string()
} else if path.join("setup.py").exists() || path.join("pyproject.toml").exists() {
"python".to_string()
} else {
return Ok(ToolResult::error("Could not detect project type"));
};
// Execute compilation
let result = match project_type.as_str() {
"rust" => self.sandbox.execute_rust(&path, &["cargo", "build"]).await,
"node" => {
self.sandbox
.execute_node(&path, &["npm", "run", "build"])
.await
}
"python" => {
// Python typically doesn't need compilation, but we can check syntax
self.sandbox
.execute_python(&path, &["python", "-m", "compileall", "."])
.await
}
_ => return Ok(ToolResult::error("Unsupported project type")),
};
match result {
Ok(exec_result) => {
if exec_result.timed_out {
Ok(ToolResult::error("Compilation timed out"))
} else if exec_result.exit_code == 0 {
Ok(ToolResult::success(json!({
"success": true,
"stdout": exec_result.stdout,
"stderr": exec_result.stderr,
"project_type": project_type
})))
} else {
Ok(ToolResult::success(json!({
"success": false,
"exit_code": exec_result.exit_code,
"stdout": exec_result.stdout,
"stderr": exec_result.stderr,
"project_type": project_type
})))
}
}
Err(e) => Ok(ToolResult::error(&format!("Compilation failed: {}", e))),
}
}
}
/// Tool for running test suites
pub struct RunTestsTool {
sandbox: Sandbox,
}
impl Default for RunTestsTool {
fn default() -> Self {
Self::new()
}
}
impl RunTestsTool {
pub fn new() -> Self {
Self {
sandbox: Sandbox::default(),
}
}
}
#[async_trait]
impl Tool for RunTestsTool {
fn name(&self) -> &'static str {
"run_tests"
}
fn description(&self) -> &'static str {
"Run tests for a project (Rust, Node.js, Python)"
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"project_path": {
"type": "string",
"description": "Path to the project root"
},
"test_filter": {
"type": "string",
"description": "Optional test filter/pattern"
}
},
"required": ["project_path"]
})
}
async fn execute(&self, args: Value) -> Result<ToolResult> {
let project_path = args
.get("project_path")
.and_then(|v| v.as_str())
.ok_or_else(|| owlen_core::Error::InvalidInput("Missing project_path".into()))?;
let path = PathBuf::from(project_path);
if !path.exists() {
return Ok(ToolResult::error("Project path does not exist"));
}
let test_filter = args.get("test_filter").and_then(|v| v.as_str());
// Detect project type and run tests
let result = if path.join("Cargo.toml").exists() {
let cmd = if let Some(filter) = test_filter {
vec!["cargo", "test", filter]
} else {
vec!["cargo", "test"]
};
self.sandbox.execute_rust(&path, &cmd).await
} else if path.join("package.json").exists() {
self.sandbox.execute_node(&path, &["npm", "test"]).await
} else if path.join("pytest.ini").exists()
|| path.join("setup.py").exists()
|| path.join("pyproject.toml").exists()
{
let cmd = if let Some(filter) = test_filter {
vec!["pytest", "-k", filter]
} else {
vec!["pytest"]
};
self.sandbox.execute_python(&path, &cmd).await
} else {
return Ok(ToolResult::error("Could not detect test framework"));
};
match result {
Ok(exec_result) => Ok(ToolResult::success(json!({
"success": exec_result.exit_code == 0 && !exec_result.timed_out,
"exit_code": exec_result.exit_code,
"stdout": exec_result.stdout,
"stderr": exec_result.stderr,
"timed_out": exec_result.timed_out
}))),
Err(e) => Ok(ToolResult::error(&format!("Tests failed to run: {}", e))),
}
}
}
/// Tool for formatting code
pub struct FormatCodeTool {
sandbox: Sandbox,
}
impl Default for FormatCodeTool {
fn default() -> Self {
Self::new()
}
}
impl FormatCodeTool {
pub fn new() -> Self {
Self {
sandbox: Sandbox::default(),
}
}
}
#[async_trait]
impl Tool for FormatCodeTool {
fn name(&self) -> &'static str {
"format_code"
}
fn description(&self) -> &'static str {
"Format code using project-appropriate formatter (rustfmt, prettier, black)"
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"project_path": {
"type": "string",
"description": "Path to the project root"
},
"check_only": {
"type": "boolean",
"description": "Only check formatting without modifying files",
"default": false
}
},
"required": ["project_path"]
})
}
async fn execute(&self, args: Value) -> Result<ToolResult> {
let project_path = args
.get("project_path")
.and_then(|v| v.as_str())
.ok_or_else(|| owlen_core::Error::InvalidInput("Missing project_path".into()))?;
let path = PathBuf::from(project_path);
if !path.exists() {
return Ok(ToolResult::error("Project path does not exist"));
}
let check_only = args
.get("check_only")
.and_then(|v| v.as_bool())
.unwrap_or(false);
// Detect project type and run formatter
let result = if path.join("Cargo.toml").exists() {
let cmd = if check_only {
vec!["cargo", "fmt", "--", "--check"]
} else {
vec!["cargo", "fmt"]
};
self.sandbox.execute_rust(&path, &cmd).await
} else if path.join("package.json").exists() {
let cmd = if check_only {
vec!["npx", "prettier", "--check", "."]
} else {
vec!["npx", "prettier", "--write", "."]
};
self.sandbox.execute_node(&path, &cmd).await
} else if path.join("setup.py").exists() || path.join("pyproject.toml").exists() {
let cmd = if check_only {
vec!["black", "--check", "."]
} else {
vec!["black", "."]
};
self.sandbox.execute_python(&path, &cmd).await
} else {
return Ok(ToolResult::error("Could not detect project type"));
};
match result {
Ok(exec_result) => Ok(ToolResult::success(json!({
"success": exec_result.exit_code == 0,
"formatted": !check_only && exec_result.exit_code == 0,
"stdout": exec_result.stdout,
"stderr": exec_result.stderr
}))),
Err(e) => Ok(ToolResult::error(&format!("Formatting failed: {}", e))),
}
}
}
/// Tool for linting code
pub struct LintCodeTool {
sandbox: Sandbox,
}
impl Default for LintCodeTool {
fn default() -> Self {
Self::new()
}
}
impl LintCodeTool {
pub fn new() -> Self {
Self {
sandbox: Sandbox::default(),
}
}
}
#[async_trait]
impl Tool for LintCodeTool {
fn name(&self) -> &'static str {
"lint_code"
}
fn description(&self) -> &'static str {
"Lint code using project-appropriate linter (clippy, eslint, pylint)"
}
fn schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"project_path": {
"type": "string",
"description": "Path to the project root"
},
"fix": {
"type": "boolean",
"description": "Automatically fix issues if possible",
"default": false
}
},
"required": ["project_path"]
})
}
async fn execute(&self, args: Value) -> Result<ToolResult> {
let project_path = args
.get("project_path")
.and_then(|v| v.as_str())
.ok_or_else(|| owlen_core::Error::InvalidInput("Missing project_path".into()))?;
let path = PathBuf::from(project_path);
if !path.exists() {
return Ok(ToolResult::error("Project path does not exist"));
}
let fix = args.get("fix").and_then(|v| v.as_bool()).unwrap_or(false);
// Detect project type and run linter
let result = if path.join("Cargo.toml").exists() {
let cmd = if fix {
vec!["cargo", "clippy", "--fix", "--allow-dirty"]
} else {
vec!["cargo", "clippy"]
};
self.sandbox.execute_rust(&path, &cmd).await
} else if path.join("package.json").exists() {
let cmd = if fix {
vec!["npx", "eslint", ".", "--fix"]
} else {
vec!["npx", "eslint", "."]
};
self.sandbox.execute_node(&path, &cmd).await
} else if path.join("setup.py").exists() || path.join("pyproject.toml").exists() {
// pylint doesn't have auto-fix
self.sandbox.execute_python(&path, &["pylint", "."]).await
} else {
return Ok(ToolResult::error("Could not detect project type"));
};
match result {
Ok(exec_result) => {
let issues_found = exec_result.exit_code != 0;
Ok(ToolResult::success(json!({
"success": true,
"issues_found": issues_found,
"exit_code": exec_result.exit_code,
"stdout": exec_result.stdout,
"stderr": exec_result.stderr
})))
}
Err(e) => Ok(ToolResult::error(&format!("Linting failed: {}", e))),
}
}
}