feat(phase10): complete MCP-only architecture migration
Phase 10 "Cleanup & Production Polish" is now complete. All LLM interactions now go through the Model Context Protocol (MCP), removing direct provider dependencies from CLI/TUI. ## Major Changes ### MCP Architecture - All providers (local and cloud Ollama) now use RemoteMcpClient - Removed owlen-ollama dependency from owlen-tui - MCP LLM server accepts OLLAMA_URL environment variable for cloud providers - Proper notification handling for streaming responses - Fixed response deserialization (McpToolResponse unwrapping) ### Code Cleanup - Removed direct OllamaProvider instantiation from TUI - Updated collect_models_from_all_providers() to use MCP for all providers - Updated switch_provider() to use MCP with environment configuration - Removed unused general config variable ### Documentation - Added comprehensive MCP Architecture section to docs/architecture.md - Documented MCP communication flow and cloud provider support - Updated crate breakdown to reflect MCP servers ### Security & Performance - Path traversal protection verified for all resource operations - Process isolation via separate MCP server processes - Tool permissions controlled via consent manager - Clean release build of entire workspace verified ## Benefits of MCP Architecture 1. **Separation of Concerns**: TUI/CLI never directly instantiates providers 2. **Process Isolation**: LLM interactions run in separate processes 3. **Extensibility**: New providers can be added as MCP servers 4. **Multi-Transport**: Supports STDIO, HTTP, and WebSocket 5. **Tool Integration**: MCP servers expose tools to LLMs This completes Phase 10 and establishes a clean, production-ready architecture for future development. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
use super::protocol::methods;
|
||||
use super::protocol::{RequestId, RpcErrorResponse, RpcRequest, RpcResponse, PROTOCOL_VERSION};
|
||||
use super::protocol::{
|
||||
RequestId, RpcErrorResponse, RpcNotification, RpcRequest, RpcResponse, PROTOCOL_VERSION,
|
||||
};
|
||||
use super::{McpClient, McpToolCall, McpToolDescriptor, McpToolResponse};
|
||||
use crate::consent::{ConsentManager, ConsentScope};
|
||||
use crate::tools::{Tool, WebScrapeTool, WebSearchTool};
|
||||
@@ -148,11 +150,12 @@ impl RemoteMcpClient {
|
||||
.join("../..")
|
||||
.canonicalize()
|
||||
.map_err(Error::Io)?;
|
||||
// Prefer the generic file‑server binary over the LLM server, as the tests
|
||||
// exercise the resource tools (read/write/delete).
|
||||
// Prefer the LLM server binary as it provides both LLM and resource tools.
|
||||
// The generic file-server is kept as a fallback for testing.
|
||||
let candidates = [
|
||||
"target/debug/owlen-mcp-server",
|
||||
"target/debug/owlen-mcp-llm-server",
|
||||
"target/release/owlen-mcp-llm-server",
|
||||
"target/debug/owlen-mcp-server",
|
||||
];
|
||||
let binary_path = candidates
|
||||
.iter()
|
||||
@@ -160,8 +163,8 @@ impl RemoteMcpClient {
|
||||
.find(|p| p.exists())
|
||||
.ok_or_else(|| {
|
||||
Error::NotImplemented(format!(
|
||||
"owlen-mcp server binary not found; checked {} and {}",
|
||||
candidates[0], candidates[1]
|
||||
"owlen-mcp server binary not found; checked {}, {}, and {}",
|
||||
candidates[0], candidates[1], candidates[2]
|
||||
))
|
||||
})?;
|
||||
let config = crate::config::McpServerConfig {
|
||||
@@ -263,29 +266,48 @@ impl RemoteMcpClient {
|
||||
}
|
||||
|
||||
// STDIO path (default).
|
||||
let mut line = String::new();
|
||||
{
|
||||
let mut stdout = self
|
||||
.stdout
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::Network("STDIO stdout not available".into()))?
|
||||
.lock()
|
||||
.await;
|
||||
stdout.read_line(&mut line).await?;
|
||||
}
|
||||
// Try to parse successful response first
|
||||
if let Ok(resp) = serde_json::from_str::<RpcResponse>(&line) {
|
||||
if resp.id == id {
|
||||
return Ok(resp.result);
|
||||
// Loop to skip notifications and find the response with matching ID.
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
{
|
||||
let mut stdout = self
|
||||
.stdout
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::Network("STDIO stdout not available".into()))?
|
||||
.lock()
|
||||
.await;
|
||||
stdout.read_line(&mut line).await?;
|
||||
}
|
||||
|
||||
// Try to parse as notification first (has no id field)
|
||||
if let Ok(_notif) = serde_json::from_str::<RpcNotification>(&line) {
|
||||
// Skip notifications and continue reading
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to parse successful response
|
||||
if let Ok(resp) = serde_json::from_str::<RpcResponse>(&line) {
|
||||
if resp.id == id {
|
||||
return Ok(resp.result);
|
||||
}
|
||||
// If ID doesn't match, continue (though this shouldn't happen)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback to error response
|
||||
if let Ok(err_resp) = serde_json::from_str::<RpcErrorResponse>(&line) {
|
||||
return Err(Error::Network(format!(
|
||||
"MCP server error {}: {}",
|
||||
err_resp.error.code, err_resp.error.message
|
||||
)));
|
||||
}
|
||||
|
||||
// If we can't parse as any known type, return error
|
||||
return Err(Error::Network(format!(
|
||||
"Unable to parse server response: {}",
|
||||
line.trim()
|
||||
)));
|
||||
}
|
||||
// Fallback to error response
|
||||
let err_resp: RpcErrorResponse =
|
||||
serde_json::from_str(&line).map_err(Error::Serialization)?;
|
||||
Err(Error::Network(format!(
|
||||
"MCP server error {}: {}",
|
||||
err_resp.error.code, err_resp.error.message
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,14 +458,9 @@ impl McpClient for RemoteMcpClient {
|
||||
// specific tool name and its arguments. Wrap the incoming call accordingly.
|
||||
let payload = serde_json::to_value(&call)?;
|
||||
let result = self.send_rpc(methods::TOOLS_CALL, payload).await?;
|
||||
// The server returns the tool's output directly; construct a matching response.
|
||||
Ok(McpToolResponse {
|
||||
name: call.name,
|
||||
success: true,
|
||||
output: result,
|
||||
metadata: std::collections::HashMap::new(),
|
||||
duration_ms: 0,
|
||||
})
|
||||
// The server returns an McpToolResponse; deserialize it.
|
||||
let response: McpToolResponse = serde_json::from_value(result)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user