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:
2025-10-06 21:59:08 +02:00
parent a909455f97
commit c9c3d17db0
3 changed files with 297 additions and 0 deletions

View 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)?))
}
}

View 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"

View 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(())
}