Completes Phase 4 (Agentic Loop with ReAct), Phase 7 (Code Execution), and Phase 8 (Prompt Server) as specified in the implementation plan. **Phase 4: Agentic Loop with ReAct Pattern (agent.rs - 398 lines)** - Complete AgentExecutor with reasoning loop - LlmResponse enum: ToolCall, FinalAnswer, Reasoning - ReAct parser supporting THOUGHT/ACTION/ACTION_INPUT/FINAL_ANSWER - Tool discovery and execution integration - AgentResult with iteration tracking and message history - Integration with owlen-agent CLI binary and TUI **Phase 7: Code Execution with Docker Sandboxing** *Sandbox Module (sandbox.rs - 255 lines):* - Docker-based execution using bollard - Resource limits: 512MB memory, 50% CPU - Network isolation (no network access) - Timeout handling (30s default) - Container auto-cleanup - Support for Rust, Node.js, Python environments *Tool Suite (tools.rs - 410 lines):* - CompileProjectTool: Build projects with auto-detection - RunTestsTool: Execute test suites with optional filters - FormatCodeTool: Run formatters (rustfmt/prettier/black) - LintCodeTool: Run linters (clippy/eslint/pylint) - All tools support check-only and auto-fix modes *MCP Server (lib.rs - 183 lines):* - Full JSON-RPC protocol implementation - Tool registry with dynamic dispatch - Initialize/tools/list/tools/call support **Phase 8: Prompt Server with YAML & Handlebars** *Prompt Server (lib.rs - 405 lines):* - YAML-based template storage in ~/.config/owlen/prompts/ - Handlebars 6.0 template engine integration - PromptTemplate with metadata (name, version, mode, description) - Four MCP tools: - get_prompt: Retrieve template by name - render_prompt: Render with Handlebars variables - list_prompts: List all available templates - reload_prompts: Hot-reload from disk *Default Templates:* - chat_mode_system.yaml: ReAct prompt for chat mode - code_mode_system.yaml: ReAct prompt with code tools **Configuration & Integration:** - Added Agent module to owlen-core - Updated owlen-agent binary to use new AgentExecutor API - Updated TUI to integrate with agent result structure - Added error handling for Agent variant **Dependencies Added:** - bollard 0.17 (Docker API) - handlebars 6.0 (templating) - serde_yaml 0.9 (YAML parsing) - tempfile 3.0 (temporary directories) - uuid 1.0 with v4 feature **Tests:** - mode_tool_filter.rs: Tool filtering by mode - prompt_server.rs: Prompt management tests - Sandbox tests (Docker-dependent, marked #[ignore]) All code compiles successfully and follows project conventions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
187 lines
6.7 KiB
Rust
187 lines
6.7 KiB
Rust
//! MCP server exposing code execution tools with Docker sandboxing.
|
|
//!
|
|
//! This server provides:
|
|
//! - compile_project: Build projects (Rust, Node.js, Python)
|
|
//! - run_tests: Execute test suites
|
|
//! - format_code: Run code formatters
|
|
//! - lint_code: Run linters
|
|
|
|
pub mod sandbox;
|
|
pub mod tools;
|
|
|
|
use owlen_core::mcp::protocol::{
|
|
methods, ErrorCode, InitializeParams, InitializeResult, RequestId, RpcError, RpcErrorResponse,
|
|
RpcRequest, RpcResponse, ServerCapabilities, ServerInfo, PROTOCOL_VERSION,
|
|
};
|
|
use owlen_core::tools::{Tool, ToolResult};
|
|
use serde_json::{json, Value};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt};
|
|
|
|
use tools::{CompileProjectTool, FormatCodeTool, LintCodeTool, RunTestsTool};
|
|
|
|
/// Tool registry for the code server
|
|
#[allow(dead_code)]
|
|
struct ToolRegistry {
|
|
tools: HashMap<String, Box<dyn Tool + Send + Sync>>,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
impl ToolRegistry {
|
|
fn new() -> Self {
|
|
let mut tools: HashMap<String, Box<dyn Tool + Send + Sync>> = HashMap::new();
|
|
tools.insert(
|
|
"compile_project".to_string(),
|
|
Box::new(CompileProjectTool::new()),
|
|
);
|
|
tools.insert("run_tests".to_string(), Box::new(RunTestsTool::new()));
|
|
tools.insert("format_code".to_string(), Box::new(FormatCodeTool::new()));
|
|
tools.insert("lint_code".to_string(), Box::new(LintCodeTool::new()));
|
|
Self { tools }
|
|
}
|
|
|
|
fn list_tools(&self) -> Vec<owlen_core::mcp::McpToolDescriptor> {
|
|
self.tools
|
|
.values()
|
|
.map(|tool| owlen_core::mcp::McpToolDescriptor {
|
|
name: tool.name().to_string(),
|
|
description: tool.description().to_string(),
|
|
input_schema: tool.schema(),
|
|
requires_network: tool.requires_network(),
|
|
requires_filesystem: tool.requires_filesystem(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
async fn execute(&self, name: &str, args: Value) -> Result<ToolResult, String> {
|
|
self.tools
|
|
.get(name)
|
|
.ok_or_else(|| format!("Tool not found: {}", name))?
|
|
.execute(args)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
let mut stdin = io::BufReader::new(io::stdin());
|
|
let mut stdout = io::stdout();
|
|
|
|
let registry = Arc::new(ToolRegistry::new());
|
|
|
|
loop {
|
|
let mut line = String::new();
|
|
match stdin.read_line(&mut line).await {
|
|
Ok(0) => break, // EOF
|
|
Ok(_) => {
|
|
let req: RpcRequest = match serde_json::from_str(&line) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
let err = RpcErrorResponse::new(
|
|
RequestId::Number(0),
|
|
RpcError::parse_error(format!("Parse error: {}", e)),
|
|
);
|
|
let s = serde_json::to_string(&err)?;
|
|
stdout.write_all(s.as_bytes()).await?;
|
|
stdout.write_all(b"\n").await?;
|
|
stdout.flush().await?;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let resp = handle_request(req.clone(), registry.clone()).await;
|
|
match resp {
|
|
Ok(r) => {
|
|
let s = serde_json::to_string(&r)?;
|
|
stdout.write_all(s.as_bytes()).await?;
|
|
stdout.write_all(b"\n").await?;
|
|
stdout.flush().await?;
|
|
}
|
|
Err(e) => {
|
|
let err = RpcErrorResponse::new(req.id.clone(), e);
|
|
let s = serde_json::to_string(&err)?;
|
|
stdout.write_all(s.as_bytes()).await?;
|
|
stdout.write_all(b"\n").await?;
|
|
stdout.flush().await?;
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Error reading stdin: {}", e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
async fn handle_request(
|
|
req: RpcRequest,
|
|
registry: Arc<ToolRegistry>,
|
|
) -> Result<RpcResponse, RpcError> {
|
|
match req.method.as_str() {
|
|
methods::INITIALIZE => {
|
|
let params: InitializeParams =
|
|
serde_json::from_value(req.params.unwrap_or_else(|| json!({})))
|
|
.map_err(|e| RpcError::invalid_params(format!("Invalid init params: {}", e)))?;
|
|
if !params.protocol_version.eq(PROTOCOL_VERSION) {
|
|
return Err(RpcError::new(
|
|
ErrorCode::INVALID_REQUEST,
|
|
format!(
|
|
"Incompatible protocol version. Client: {}, Server: {}",
|
|
params.protocol_version, PROTOCOL_VERSION
|
|
),
|
|
));
|
|
}
|
|
let result = InitializeResult {
|
|
protocol_version: PROTOCOL_VERSION.to_string(),
|
|
server_info: ServerInfo {
|
|
name: "owlen-mcp-code-server".to_string(),
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
},
|
|
capabilities: ServerCapabilities {
|
|
supports_tools: Some(true),
|
|
supports_resources: Some(false),
|
|
supports_streaming: Some(false),
|
|
},
|
|
};
|
|
Ok(RpcResponse::new(
|
|
req.id,
|
|
serde_json::to_value(result).unwrap(),
|
|
))
|
|
}
|
|
methods::TOOLS_LIST => {
|
|
let tools = registry.list_tools();
|
|
Ok(RpcResponse::new(req.id, json!(tools)))
|
|
}
|
|
methods::TOOLS_CALL => {
|
|
let call = serde_json::from_value::<owlen_core::mcp::McpToolCall>(
|
|
req.params.unwrap_or_else(|| json!({})),
|
|
)
|
|
.map_err(|e| RpcError::invalid_params(format!("Invalid tool call: {}", e)))?;
|
|
|
|
let result: ToolResult = registry
|
|
.execute(&call.name, call.arguments)
|
|
.await
|
|
.map_err(|e| RpcError::internal_error(format!("Tool execution failed: {}", e)))?;
|
|
|
|
let resp = owlen_core::mcp::McpToolResponse {
|
|
name: call.name,
|
|
success: result.success,
|
|
output: result.output,
|
|
metadata: result.metadata,
|
|
duration_ms: result.duration.as_millis() as u128,
|
|
};
|
|
Ok(RpcResponse::new(
|
|
req.id,
|
|
serde_json::to_value(resp).unwrap(),
|
|
))
|
|
}
|
|
_ => Err(RpcError::method_not_found(&req.method)),
|
|
}
|
|
}
|