1use serde_json::json;
13use tracing::{debug, error};
14
15use crate::{
16 error::ServerError,
17 handler::{
18 forward_rpc_to_portal, sandbox_get_metrics_impl, sandbox_start_impl, sandbox_stop_impl,
19 },
20 payload::{
21 JsonRpcError, JsonRpcRequest, JsonRpcResponse, JsonRpcResponseOrNotification,
22 ProcessedNotification, SandboxMetricsGetParams, SandboxStartParams, SandboxStopParams,
23 JSONRPC_VERSION,
24 },
25 state::AppState,
26 ServerResult,
27};
28
29const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
35
36const SERVER_NAME: &str = "microsandbox-server";
38const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
39
40pub async fn handle_mcp_initialize(
46 _state: AppState,
47 request: JsonRpcRequest,
48) -> ServerResult<JsonRpcResponse> {
49 debug!("Handling MCP initialize request");
50
51 let result = json!({
52 "protocolVersion": MCP_PROTOCOL_VERSION,
53 "capabilities": {
54 "tools": {
55 "listChanged": false
56 },
57 "prompts": {
58 "listChanged": false
59 }
60 },
61 "serverInfo": {
62 "name": SERVER_NAME,
63 "version": SERVER_VERSION
64 }
65 });
66
67 Ok(JsonRpcResponse::success(result, request.id))
68}
69
70pub async fn handle_mcp_list_tools(
72 _state: AppState,
73 request: JsonRpcRequest,
74) -> ServerResult<JsonRpcResponse> {
75 debug!("Handling MCP list tools request");
76
77 let tools = json!({
78 "tools": [
79 {
80 "name": "sandbox_start",
81 "description": "Start a new sandbox with specified configuration. This creates an isolated environment for code execution. IMPORTANT: Always stop the sandbox when done to prevent it from running indefinitely and consuming resources. SUPPORTED IMAGES: Only 'microsandbox/python' (for Python code) and 'microsandbox/node' (for Node.js code) are currently supported.",
82 "inputSchema": {
83 "type": "object",
84 "properties": {
85 "sandbox": {
86 "type": "string",
87 "description": "Name of the sandbox to start"
88 },
89 "namespace": {
90 "type": "string",
91 "description": "Namespace for the sandbox"
92 },
93 "config": {
94 "type": "object",
95 "description": "Sandbox configuration",
96 "properties": {
97 "image": {
98 "type": "string",
99 "description": "Docker image to use. Only 'microsandbox/python' and 'microsandbox/node' are supported.",
100 "enum": ["microsandbox/python", "microsandbox/node"]
101 },
102 "memory": {
103 "type": "integer",
104 "description": "Memory limit in MiB"
105 },
106 "cpus": {
107 "type": "integer",
108 "description": "Number of CPUs"
109 },
110 "volumes": {
111 "type": "array",
112 "items": {"type": "string"},
113 "description": "Volume mounts"
114 },
115 "ports": {
116 "type": "array",
117 "items": {"type": "string"},
118 "description": "Port mappings"
119 },
120 "envs": {
121 "type": "array",
122 "items": {"type": "string"},
123 "description": "Environment variables"
124 }
125 }
126 }
127 },
128 "required": ["sandbox", "namespace"]
129 }
130 },
131 {
132 "name": "sandbox_stop",
133 "description": "Stop a running sandbox and clean up its resources. CRITICAL: Always call this when you're finished with a sandbox to prevent resource leaks and indefinite running. Failing to stop sandboxes will cause them to consume system resources unnecessarily.",
134 "inputSchema": {
135 "type": "object",
136 "properties": {
137 "sandbox": {
138 "type": "string",
139 "description": "Name of the sandbox to stop"
140 },
141 "namespace": {
142 "type": "string",
143 "description": "Namespace of the sandbox"
144 }
145 },
146 "required": ["sandbox", "namespace"]
147 }
148 },
149 {
150 "name": "sandbox_run_code",
151 "description": "Execute code in a running sandbox. PREREQUISITES: The target sandbox must be started first using sandbox_start - this will fail if the sandbox is not running. TIMING: Code execution is synchronous and may take time depending on complexity. Long-running code will block until completion or timeout.",
152 "inputSchema": {
153 "type": "object",
154 "properties": {
155 "sandbox": {
156 "type": "string",
157 "description": "Name of the sandbox (must be already started)"
158 },
159 "namespace": {
160 "type": "string",
161 "description": "Namespace of the sandbox"
162 },
163 "code": {
164 "type": "string",
165 "description": "Code to execute"
166 },
167 "language": {
168 "type": "string",
169 "description": "Programming language (e.g., 'python', 'nodejs')"
170 }
171 },
172 "required": ["sandbox", "namespace", "code", "language"]
173 }
174 },
175 {
176 "name": "sandbox_run_command",
177 "description": "Execute a command in a running sandbox. PREREQUISITES: The target sandbox must be started first using sandbox_start - this will fail if the sandbox is not running. TIMING: Command execution is synchronous and may take time depending on the command complexity. Long-running commands will block until completion or timeout.",
178 "inputSchema": {
179 "type": "object",
180 "properties": {
181 "sandbox": {
182 "type": "string",
183 "description": "Name of the sandbox (must be already started)"
184 },
185 "namespace": {
186 "type": "string",
187 "description": "Namespace of the sandbox"
188 },
189 "command": {
190 "type": "string",
191 "description": "Command to execute"
192 },
193 "args": {
194 "type": "array",
195 "items": {"type": "string"},
196 "description": "Command arguments"
197 }
198 },
199 "required": ["sandbox", "namespace", "command"]
200 }
201 },
202 {
203 "name": "sandbox_get_metrics",
204 "description": "Get metrics and status for sandboxes including CPU usage, memory consumption, and running state. This tool can check the status of any sandbox regardless of whether it's running or not",
205 "inputSchema": {
206 "type": "object",
207 "properties": {
208 "sandbox": {
209 "type": "string",
210 "description": "Optional specific sandbox name to get metrics for"
211 },
212 "namespace": {
213 "type": "string",
214 "description": "Namespace to query (use '*' for all namespaces)"
215 }
216 },
217 "required": ["namespace"]
218 }
219 }
220 ]
221 });
222
223 Ok(JsonRpcResponse::success(tools, request.id))
224}
225
226pub async fn handle_mcp_list_prompts(
228 _state: AppState,
229 request: JsonRpcRequest,
230) -> ServerResult<JsonRpcResponse> {
231 debug!("Handling MCP list prompts request");
232
233 let prompts = json!({
234 "prompts": [
235 {
236 "name": "create_python_sandbox",
237 "description": "Create a Python development sandbox",
238 "arguments": [
239 {
240 "name": "sandbox_name",
241 "description": "Name for the new sandbox",
242 "required": true
243 },
244 {
245 "name": "namespace",
246 "description": "Namespace for the sandbox",
247 "required": true
248 }
249 ]
250 },
251 {
252 "name": "create_node_sandbox",
253 "description": "Create a Node.js development sandbox",
254 "arguments": [
255 {
256 "name": "sandbox_name",
257 "description": "Name for the new sandbox",
258 "required": true
259 },
260 {
261 "name": "namespace",
262 "description": "Namespace for the sandbox",
263 "required": true
264 }
265 ]
266 }
267 ]
268 });
269
270 Ok(JsonRpcResponse::success(prompts, request.id))
271}
272
273pub async fn handle_mcp_get_prompt(
275 _state: AppState,
276 request: JsonRpcRequest,
277) -> ServerResult<JsonRpcResponse> {
278 debug!("Handling MCP get prompt request");
279
280 let params = request.params.as_object().ok_or_else(|| {
281 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
282 "Request parameters must be an object".to_string(),
283 ))
284 })?;
285
286 let prompt_name = params.get("name").and_then(|v| v.as_str()).ok_or_else(|| {
287 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
288 "Missing required 'name' parameter".to_string(),
289 ))
290 })?;
291
292 let arguments = params.get("arguments").and_then(|v| v.as_object());
293
294 let result = match prompt_name {
295 "create_python_sandbox" => {
296 let sandbox_name = arguments
297 .and_then(|args| args.get("sandbox_name"))
298 .and_then(|v| v.as_str())
299 .unwrap_or("python-sandbox");
300 let namespace = arguments
301 .and_then(|args| args.get("namespace"))
302 .and_then(|v| v.as_str())
303 .unwrap_or("default");
304
305 json!({
306 "description": "Create a Python development sandbox",
307 "messages": [
308 {
309 "role": "user",
310 "content": {
311 "type": "text",
312 "text": format!(
313 "Create a Python sandbox named '{}' in namespace '{}' using the sandbox_start tool with the following configuration:\n\n\
314 - Image: microsandbox/python\n\
315 - Memory: 512 MiB\n\
316 - CPUs: 1\n\
317 - Working directory: /workspace\n\n\
318 This will set up a Python development environment ready for code execution.",
319 sandbox_name, namespace
320 )
321 }
322 }
323 ]
324 })
325 }
326 "create_node_sandbox" => {
327 let sandbox_name = arguments
328 .and_then(|args| args.get("sandbox_name"))
329 .and_then(|v| v.as_str())
330 .unwrap_or("node-sandbox");
331 let namespace = arguments
332 .and_then(|args| args.get("namespace"))
333 .and_then(|v| v.as_str())
334 .unwrap_or("default");
335
336 json!({
337 "description": "Create a Node.js development sandbox",
338 "messages": [
339 {
340 "role": "user",
341 "content": {
342 "type": "text",
343 "text": format!(
344 "Create a Node.js sandbox named '{}' in namespace '{}' using the sandbox_start tool with the following configuration:\n\n\
345 - Image: microsandbox/node\n\
346 - Memory: 512 MiB\n\
347 - CPUs: 1\n\
348 - Working directory: /workspace\n\n\
349 This will set up a Node.js development environment ready for JavaScript execution.",
350 sandbox_name, namespace
351 )
352 }
353 }
354 ]
355 })
356 }
357 _ => {
358 return Err(ServerError::NotFound(format!(
359 "Prompt '{}' not found",
360 prompt_name
361 )));
362 }
363 };
364
365 Ok(JsonRpcResponse::success(result, request.id))
366}
367
368pub async fn handle_mcp_call_tool(
370 state: AppState,
371 request: JsonRpcRequest,
372) -> ServerResult<JsonRpcResponse> {
373 debug!("Handling MCP call tool request");
374
375 let params = request.params.as_object().ok_or_else(|| {
376 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
377 "Request parameters must be an object".to_string(),
378 ))
379 })?;
380
381 let tool_name = params.get("name").and_then(|v| v.as_str()).ok_or_else(|| {
382 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
383 "Missing required 'name' parameter".to_string(),
384 ))
385 })?;
386
387 let arguments = params.get("arguments").ok_or_else(|| {
388 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
389 "Missing required 'arguments' parameter".to_string(),
390 ))
391 })?;
392
393 let internal_method = match tool_name {
395 "sandbox_start" => "sandbox.start",
396 "sandbox_stop" => "sandbox.stop",
397 "sandbox_run_code" => "sandbox.repl.run",
398 "sandbox_run_command" => "sandbox.command.run",
399 "sandbox_get_metrics" => "sandbox.metrics.get",
400 _ => {
401 return Err(ServerError::NotFound(format!(
402 "Tool '{}' not found",
403 tool_name
404 )));
405 }
406 };
407
408 let internal_request = JsonRpcRequest {
410 jsonrpc: JSONRPC_VERSION.to_string(),
411 method: internal_method.to_string(),
412 params: arguments.clone(),
413 id: request.id.clone(),
414 };
415
416 let internal_response = if matches!(internal_method, "sandbox.repl.run" | "sandbox.command.run")
418 {
419 match forward_rpc_to_portal(state, internal_request).await {
421 Ok((_, json_response)) => json_response.0,
422 Err(e) => {
423 error!("Failed to forward request to portal: {}", e);
424 return Ok(JsonRpcResponse::error(
425 JsonRpcError {
426 code: -32603,
427 message: format!("Internal error: {}", e),
428 data: None,
429 },
430 request.id,
431 ));
432 }
433 }
434 } else {
435 match internal_method {
437 "sandbox.start" => {
438 let params: SandboxStartParams = serde_json::from_value(arguments.clone())
439 .map_err(|e| {
440 return JsonRpcResponse::error(
441 JsonRpcError {
442 code: -32602,
443 message: format!("Invalid parameters: {}", e),
444 data: None,
445 },
446 request.id.clone(),
447 );
448 })
449 .unwrap();
450
451 match sandbox_start_impl(state, params).await {
452 Ok(result) => JsonRpcResponse::success(json!(result), request.id.clone()),
453 Err(e) => JsonRpcResponse::error(
454 JsonRpcError {
455 code: -32603,
456 message: format!("Sandbox start failed: {}", e),
457 data: None,
458 },
459 request.id.clone(),
460 ),
461 }
462 }
463 "sandbox.stop" => {
464 let params: SandboxStopParams = serde_json::from_value(arguments.clone())
465 .map_err(|e| {
466 return JsonRpcResponse::error(
467 JsonRpcError {
468 code: -32602,
469 message: format!("Invalid parameters: {}", e),
470 data: None,
471 },
472 request.id.clone(),
473 );
474 })
475 .unwrap();
476
477 match sandbox_stop_impl(state, params).await {
478 Ok(result) => JsonRpcResponse::success(json!(result), request.id.clone()),
479 Err(e) => JsonRpcResponse::error(
480 JsonRpcError {
481 code: -32603,
482 message: format!("Sandbox stop failed: {}", e),
483 data: None,
484 },
485 request.id.clone(),
486 ),
487 }
488 }
489 "sandbox.metrics.get" => {
490 let params: SandboxMetricsGetParams = serde_json::from_value(arguments.clone())
491 .map_err(|e| {
492 return JsonRpcResponse::error(
493 JsonRpcError {
494 code: -32602,
495 message: format!("Invalid parameters: {}", e),
496 data: None,
497 },
498 request.id.clone(),
499 );
500 })
501 .unwrap();
502
503 match sandbox_get_metrics_impl(state, params).await {
504 Ok(result) => JsonRpcResponse::success(json!(result), request.id.clone()),
505 Err(e) => JsonRpcResponse::error(
506 JsonRpcError {
507 code: -32603,
508 message: format!("Get metrics failed: {}", e),
509 data: None,
510 },
511 request.id.clone(),
512 ),
513 }
514 }
515 _ => JsonRpcResponse::error(
516 JsonRpcError {
517 code: -32601,
518 message: format!("Method not found: {}", internal_method),
519 data: None,
520 },
521 request.id.clone(),
522 ),
523 }
524 };
525
526 let mcp_result = if let Some(result) = internal_response.result {
528 json!({
529 "content": [
530 {
531 "type": "text",
532 "text": serde_json::to_string_pretty(&result).unwrap_or_else(|_| result.to_string())
533 }
534 ]
535 })
536 } else if let Some(error) = internal_response.error {
537 json!({
538 "content": [
539 {
540 "type": "text",
541 "text": format!("Error: {}", error.message)
542 }
543 ],
544 "isError": true
545 })
546 } else {
547 json!({
548 "content": [
549 {
550 "type": "text",
551 "text": "No result returned"
552 }
553 ]
554 })
555 };
556
557 Ok(JsonRpcResponse::success(mcp_result, request.id))
558}
559
560pub async fn handle_mcp_notifications_initialized(
562 _state: AppState,
563 _request: JsonRpcRequest,
564) -> ServerResult<ProcessedNotification> {
565 debug!("Handling MCP notifications/initialized");
566
567 Ok(ProcessedNotification::processed())
570}
571
572pub async fn handle_mcp_method(
574 state: AppState,
575 request: JsonRpcRequest,
576) -> ServerResult<JsonRpcResponseOrNotification> {
577 match request.method.as_str() {
578 "initialize" => {
579 let response = handle_mcp_initialize(state, request).await?;
580 Ok(JsonRpcResponseOrNotification::response(response))
581 }
582 "tools/list" => {
583 let response = handle_mcp_list_tools(state, request).await?;
584 Ok(JsonRpcResponseOrNotification::response(response))
585 }
586 "tools/call" => {
587 let response = handle_mcp_call_tool(state, request).await?;
588 Ok(JsonRpcResponseOrNotification::response(response))
589 }
590 "prompts/list" => {
591 let response = handle_mcp_list_prompts(state, request).await?;
592 Ok(JsonRpcResponseOrNotification::response(response))
593 }
594 "prompts/get" => {
595 let response = handle_mcp_get_prompt(state, request).await?;
596 Ok(JsonRpcResponseOrNotification::response(response))
597 }
598 "notifications/initialized" => {
599 let notification = handle_mcp_notifications_initialized(state, request).await?;
600 Ok(JsonRpcResponseOrNotification::notification(notification))
601 }
602 _ => Err(ServerError::NotFound(format!(
603 "MCP method '{}' not found",
604 request.method
605 ))),
606 }
607}