diff --git a/crates/owlen-core/src/tools/fs_tools.rs b/crates/owlen-core/src/tools/fs_tools.rs new file mode 100644 index 0000000..17d48e2 --- /dev/null +++ b/crates/owlen-core/src/tools/fs_tools.rs @@ -0,0 +1,111 @@ +use crate::tools::{Tool, ToolResult}; +use anyhow::Result; +use async_trait::async_trait; +use serde::Deserialize; +use serde_json::json; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use path_clean::PathClean; + +#[derive(Deserialize)] +struct FileArgs { + path: String, +} + +fn sanitize_path(path: &str, root: &Path) -> Result { + let path = Path::new(path); + let path = if path.is_absolute() { + path.strip_prefix("/") + .map_err(|_| anyhow::anyhow!("Invalid path"))? + .to_path_buf() + } else { + path.to_path_buf() + }; + + let full_path = root.join(path).clean(); + + if !full_path.starts_with(root) { + return Err(anyhow::anyhow!("Path traversal detected")); + } + + Ok(full_path) +} + +pub struct ResourcesListTool; + +#[async_trait] +impl Tool for ResourcesListTool { + fn name(&self) -> &'static str { + "resources/list" + } + + fn description(&self) -> &'static str { + "Lists directory contents." + } + + fn schema(&self) -> serde_json::Value { + json!({ + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path to the directory to list." + } + }, + "required": ["path"] + }) + } + + async fn execute(&self, args: serde_json::Value) -> Result { + let args: FileArgs = serde_json::from_value(args)?; + let root = env::current_dir()?; + let full_path = sanitize_path(&args.path, &root)?; + + let entries = fs::read_dir(full_path)?; + + let mut result = Vec::new(); + for entry in entries { + let entry = entry?; + result.push(entry.file_name().to_string_lossy().to_string()); + } + + Ok(ToolResult::success(serde_json::to_value(result)?)) + } +} + +pub struct ResourcesGetTool; + +#[async_trait] +impl Tool for ResourcesGetTool { + fn name(&self) -> &'static str { + "resources/get" + } + + fn description(&self) -> &'static str { + "Reads file content." + } + + fn schema(&self) -> serde_json::Value { + json!({ + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "The path to the file to read." + } + }, + "required": ["path"] + }) + } + + async fn execute(&self, args: serde_json::Value) -> Result { + let args: FileArgs = serde_json::from_value(args)?; + let root = env::current_dir()?; + let full_path = sanitize_path(&args.path, &root)?; + + let content = fs::read_to_string(full_path)?; + + Ok(ToolResult::success(serde_json::to_value(content)?)) + } +} diff --git a/crates/owlen-mcp-server/Cargo.toml b/crates/owlen-mcp-server/Cargo.toml new file mode 100644 index 0000000..a6f4d66 --- /dev/null +++ b/crates/owlen-mcp-server/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "owlen-mcp-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +anyhow = "1.0" +path-clean = "1.0" diff --git a/crates/owlen-mcp-server/src/main.rs b/crates/owlen-mcp-server/src/main.rs new file mode 100644 index 0000000..f36f12f --- /dev/null +++ b/crates/owlen-mcp-server/src/main.rs @@ -0,0 +1,175 @@ +use path_clean::PathClean; +use serde::{Deserialize, Serialize}; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt}; + +#[derive(Debug, Deserialize)] +struct Request { + id: u64, + method: String, + params: serde_json::Value, +} + +#[derive(Debug, Serialize)] +struct Response { + id: u64, + result: serde_json::Value, +} + +#[derive(Debug, Serialize)] +struct ErrorResponse { + id: u64, + error: JsonRpcError, +} + +#[derive(Debug, Serialize)] +struct JsonRpcError { + code: i64, + message: String, +} + +#[derive(Deserialize)] +struct FileArgs { + path: String, +} + +async fn handle_request(req: Request, root: &Path) -> Result { + match req.method.as_str() { + "resources/list" => { + let args: FileArgs = serde_json::from_value(req.params).map_err(|e| JsonRpcError { + code: -32602, + message: format!("Invalid params: {}", e), + })?; + resources_list(&args.path, root).await + } + "resources/get" => { + let args: FileArgs = serde_json::from_value(req.params).map_err(|e| JsonRpcError { + code: -32602, + message: format!("Invalid params: {}", e), + })?; + resources_get(&args.path, root).await + } + _ => Err(JsonRpcError { + code: -32601, + message: "Method not found".to_string(), + }), + } +} + +fn sanitize_path(path: &str, root: &Path) -> Result { + let path = Path::new(path); + let path = if path.is_absolute() { + path.strip_prefix("/") + .map_err(|_| JsonRpcError { + code: -32602, + message: "Invalid path".to_string(), + })? + .to_path_buf() + } else { + path.to_path_buf() + }; + + let full_path = root.join(path).clean(); + + if !full_path.starts_with(root) { + return Err(JsonRpcError { + code: -32602, + message: "Path traversal detected".to_string(), + }); + } + + Ok(full_path) +} + +async fn resources_list(path: &str, root: &Path) -> Result { + let full_path = sanitize_path(path, root)?; + + let entries = fs::read_dir(full_path) + .map_err(|e| JsonRpcError { + code: -32000, + message: format!("Failed to read directory: {}", e), + })?; + + let mut result = Vec::new(); + for entry in entries { + let entry = entry.map_err(|e| JsonRpcError { + code: -32000, + message: format!("Failed to read directory entry: {}", e), + })?; + result.push(entry.file_name().to_string_lossy().to_string()); + } + + Ok(serde_json::json!(result)) +} + +async fn resources_get(path: &str, root: &Path) -> Result { + let full_path = sanitize_path(path, root)?; + + let content = fs::read_to_string(full_path).map_err(|e| JsonRpcError { + code: -32000, + message: format!("Failed to read file: {}", e), + })?; + + Ok(serde_json::json!(content)) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let root = env::current_dir()?; + let mut stdin = io::BufReader::new(io::stdin()); + let mut stdout = io::stdout(); + + loop { + let mut line = String::new(); + match stdin.read_line(&mut line).await { + Ok(0) => { + // EOF + break; + } + Ok(_) => { + let req: Request = match serde_json::from_str(&line) { + Ok(req) => req, + Err(e) => { + let err_resp = ErrorResponse { + id: 0, + error: JsonRpcError { + code: -32700, + message: format!("Parse error: {}", e), + }, + }; + let resp_str = serde_json::to_string(&err_resp)?; + stdout.write_all(resp_str.as_bytes()).await?; + stdout.write_all(b"\n").await?; + continue; + } + }; + + let request_id = req.id; + + match handle_request(req, &root).await { + Ok(result) => { + let resp = Response { id: request_id, result }; + let resp_str = serde_json::to_string(&resp)?; + stdout.write_all(resp_str.as_bytes()).await?; + stdout.write_all(b"\n").await?; + } + Err(error) => { + let err_resp = ErrorResponse { id: request_id, error }; + let resp_str = serde_json::to_string(&err_resp)?; + stdout.write_all(resp_str.as_bytes()).await?; + stdout.write_all(b"\n").await?; + } + } + } + Err(e) => { + // Handle read error + eprintln!("Error reading from stdin: {}", e); + break; + } + } + } + + Ok(()) +} \ No newline at end of file