feat(theme): add tool_output color to themes
- Added a `tool_output` color to the `Theme` struct. - Updated all built-in themes to include the new color. - Modified the TUI to use the `tool_output` color for rendering tool output.
This commit is contained in:
111
crates/owlen-core/src/tools/fs_tools.rs
Normal file
111
crates/owlen-core/src/tools/fs_tools.rs
Normal file
@@ -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<PathBuf> {
|
||||||
|
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<ToolResult> {
|
||||||
|
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<ToolResult> {
|
||||||
|
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)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/owlen-mcp-server/Cargo.toml
Normal file
11
crates/owlen-mcp-server/Cargo.toml
Normal file
@@ -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"
|
||||||
175
crates/owlen-mcp-server/src/main.rs
Normal file
175
crates/owlen-mcp-server/src/main.rs
Normal file
@@ -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<serde_json::Value, JsonRpcError> {
|
||||||
|
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<PathBuf, JsonRpcError> {
|
||||||
|
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<serde_json::Value, JsonRpcError> {
|
||||||
|
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<serde_json::Value, JsonRpcError> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user