webpuppet_mcp/
server.rs

1//! MCP server implementation.
2
3use std::io::{BufRead, BufReader, Write};
4use std::sync::Arc;
5
6use tokio::sync::RwLock;
7
8use webpuppet::PermissionGuard;
9
10use crate::error::{codes, Result};
11use crate::protocol::{
12    ClientCapabilities, InitializeParams, InitializeResult, JsonRpcId, JsonRpcRequest,
13    JsonRpcResponse, ListToolsResult, McpMessage, ServerCapabilities, ServerInfo, ToolCallParams,
14    ToolsCapability,
15};
16use crate::tools::ToolRegistry;
17
18/// MCP protocol version.
19pub const PROTOCOL_VERSION: &str = "2024-11-05";
20
21/// Server name.
22pub const SERVER_NAME: &str = "webpuppet-mcp";
23
24/// Server version.
25pub const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
26
27/// MCP server state.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ServerState {
30    /// Waiting for initialization.
31    Uninitialized,
32    /// Server is initialized and ready.
33    Ready,
34    /// Server is shutting down.
35    ShuttingDown,
36}
37
38/// MCP server for webpuppet.
39pub struct McpServer {
40    state: Arc<RwLock<ServerState>>,
41    tools: Arc<ToolRegistry>,
42    #[allow(dead_code)]
43    client_capabilities: Arc<RwLock<Option<ClientCapabilities>>>,
44}
45
46impl McpServer {
47    /// Create a new MCP server with secure permissions.
48    pub fn new() -> Self {
49        Self::with_permissions(PermissionGuard::secure())
50    }
51
52    /// Create a new MCP server with custom permissions.
53    pub fn with_permissions(permissions: PermissionGuard) -> Self {
54        Self {
55            state: Arc::new(RwLock::new(ServerState::Uninitialized)),
56            tools: Arc::new(ToolRegistry::new(permissions)),
57            client_capabilities: Arc::new(RwLock::new(None)),
58        }
59    }
60
61    /// Create a new MCP server with visible browser (non-headless).
62    pub fn with_visible_browser(permissions: PermissionGuard) -> Self {
63        Self {
64            state: Arc::new(RwLock::new(ServerState::Uninitialized)),
65            tools: Arc::new(ToolRegistry::with_visible_browser(permissions)),
66            client_capabilities: Arc::new(RwLock::new(None)),
67        }
68    }
69
70    /// Run the server on stdio.
71    pub async fn run_stdio(&self) -> Result<()> {
72        let stdin = std::io::stdin();
73        let mut stdout = std::io::stdout();
74        let reader = BufReader::new(stdin.lock());
75
76        tracing::info!("MCP server starting on stdio");
77
78        for line in reader.lines() {
79            let line = line?;
80
81            if line.is_empty() {
82                continue;
83            }
84
85            tracing::debug!("Received: {}", line);
86
87            let response = self.handle_message(&line).await;
88
89            if let Some(response) = response {
90                let json = serde_json::to_string(&response)?;
91                tracing::debug!("Sending: {}", json);
92                writeln!(stdout, "{}", json)?;
93                stdout.flush()?;
94            }
95
96            // Check if we should exit
97            if *self.state.read().await == ServerState::ShuttingDown {
98                break;
99            }
100        }
101
102        tracing::info!("MCP server shutting down");
103        Ok(())
104    }
105
106    /// Handle an incoming message.
107    pub async fn handle_message(&self, json: &str) -> Option<JsonRpcResponse> {
108        match McpMessage::parse(json) {
109            Ok(McpMessage::Request(request)) => Some(self.handle_request(request).await),
110            Ok(McpMessage::Notification(notification)) => {
111                self.handle_notification(notification).await;
112                None
113            }
114            Ok(McpMessage::Response(_)) => {
115                // We don't expect responses in this direction
116                None
117            }
118            Err(e) => Some(JsonRpcResponse::error(
119                None,
120                codes::PARSE_ERROR,
121                e.to_string(),
122            )),
123        }
124    }
125
126    /// Handle a JSON-RPC request.
127    async fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse {
128        let id = request.id.clone();
129
130        match request.method.as_str() {
131            "initialize" => self.handle_initialize(id, request.params).await,
132            "tools/list" => self.handle_tools_list(id).await,
133            "tools/call" => self.handle_tools_call(id, request.params).await,
134            "ping" => JsonRpcResponse::success(id, serde_json::json!({})),
135            "shutdown" => {
136                *self.state.write().await = ServerState::ShuttingDown;
137                JsonRpcResponse::success(id, serde_json::json!({}))
138            }
139            _ => JsonRpcResponse::error(
140                id,
141                codes::METHOD_NOT_FOUND,
142                format!("method not found: {}", request.method),
143            ),
144        }
145    }
146
147    /// Handle a notification (no response expected).
148    async fn handle_notification(&self, notification: JsonRpcRequest) {
149        match notification.method.as_str() {
150            "notifications/initialized" => {
151                tracing::info!("Client initialized");
152            }
153            "notifications/cancelled" => {
154                tracing::debug!("Request cancelled by client");
155            }
156            "exit" => {
157                *self.state.write().await = ServerState::ShuttingDown;
158            }
159            _ => {
160                tracing::debug!("Unknown notification: {}", notification.method);
161            }
162        }
163    }
164
165    /// Handle initialize request.
166    async fn handle_initialize(
167        &self,
168        id: Option<JsonRpcId>,
169        params: Option<serde_json::Value>,
170    ) -> JsonRpcResponse {
171        // Parse params
172        let _params: InitializeParams = match params {
173            Some(p) => match serde_json::from_value(p) {
174                Ok(params) => params,
175                Err(e) => {
176                    return JsonRpcResponse::error(
177                        id,
178                        codes::INVALID_PARAMS,
179                        format!("invalid initialize params: {}", e),
180                    );
181                }
182            },
183            None => {
184                return JsonRpcResponse::error(
185                    id,
186                    codes::INVALID_PARAMS,
187                    "initialize params required",
188                );
189            }
190        };
191
192        // Update state
193        *self.state.write().await = ServerState::Ready;
194
195        // Return capabilities
196        let result = InitializeResult {
197            protocol_version: PROTOCOL_VERSION.into(),
198            capabilities: ServerCapabilities {
199                tools: Some(ToolsCapability {
200                    list_changed: false,
201                }),
202                resources: None,
203                prompts: None,
204                logging: None,
205            },
206            server_info: ServerInfo {
207                name: SERVER_NAME.into(),
208                version: SERVER_VERSION.into(),
209            },
210        };
211
212        JsonRpcResponse::success(id, result)
213    }
214
215    /// Handle tools/list request.
216    async fn handle_tools_list(&self, id: Option<JsonRpcId>) -> JsonRpcResponse {
217        let state = *self.state.read().await;
218        if state != ServerState::Ready {
219            return JsonRpcResponse::error(id, codes::INTERNAL_ERROR, "server not initialized");
220        }
221
222        let tools = self.tools.list_tools();
223        let result = ListToolsResult { tools };
224
225        JsonRpcResponse::success(id, result)
226    }
227
228    /// Handle tools/call request.
229    async fn handle_tools_call(
230        &self,
231        id: Option<JsonRpcId>,
232        params: Option<serde_json::Value>,
233    ) -> JsonRpcResponse {
234        let state = *self.state.read().await;
235        if state != ServerState::Ready {
236            return JsonRpcResponse::error(id, codes::INTERNAL_ERROR, "server not initialized");
237        }
238
239        // Parse params
240        let params: ToolCallParams = match params {
241            Some(p) => match serde_json::from_value(p) {
242                Ok(params) => params,
243                Err(e) => {
244                    return JsonRpcResponse::error(
245                        id,
246                        codes::INVALID_PARAMS,
247                        format!("invalid tool call params: {}", e),
248                    );
249                }
250            },
251            None => {
252                return JsonRpcResponse::error(
253                    id,
254                    codes::INVALID_PARAMS,
255                    "tool call params required",
256                );
257            }
258        };
259
260        // Execute tool
261        match self.tools.execute(&params.name, params.arguments).await {
262            Ok(result) => JsonRpcResponse::success(id, result),
263            Err(e) => {
264                tracing::error!("Tool {} failed: {}", params.name, e);
265                JsonRpcResponse::error(id, e.code(), e.to_string())
266            }
267        }
268    }
269}
270
271impl Default for McpServer {
272    fn default() -> Self {
273        Self::new()
274    }
275}