1pub mod actions;
15pub mod jsonrpc;
16
17use anyhow::Result;
18use axum::{
19 extract::{rejection::JsonRejection, DefaultBodyLimit, Json, State as AxumState},
20 http::StatusCode,
21 response::{IntoResponse, Response},
22 routing::post,
23 Router,
24};
25use jsonrpc::{ErrorCode, JsonRpcError, JsonRpcMessage, JsonRpcResponse};
26use serde_json::Value;
27use std::net::SocketAddr;
28use std::sync::Arc;
29use tokio::sync::mpsc;
30use tracing::{debug, error, info, trace, warn};
31
32use crate::console_error;
33#[cfg(not(target_arch = "wasm32"))]
34use crate::llm::action_helper::call_llm;
35#[cfg(not(target_arch = "wasm32"))]
36use crate::llm::ollama_client::OllamaClient;
37#[cfg(not(target_arch = "wasm32"))]
38use crate::logging::emit::Log;
39#[cfg(not(target_arch = "wasm32"))]
40use crate::protocol::Event;
41#[cfg(not(target_arch = "wasm32"))]
42use crate::server::connection::ConnectionId;
43#[cfg(not(target_arch = "wasm32"))]
44use crate::server::McpProtocol;
45#[cfg(not(target_arch = "wasm32"))]
46use crate::state::app_state::AppState;
47#[cfg(not(target_arch = "wasm32"))]
48use crate::state::server::{ConnectionStatus, ProtocolConnectionInfo, ServerId};
49#[cfg(not(target_arch = "wasm32"))]
50use actions::{
51 MCP_INITIALIZE_EVENT, MCP_PROMPTS_GET_EVENT, MCP_PROMPTS_LIST_EVENT, MCP_RESOURCES_LIST_EVENT,
52 MCP_RESOURCES_READ_EVENT, MCP_TOOLS_CALL_EVENT, MCP_TOOLS_LIST_EVENT,
53};
54#[cfg(not(target_arch = "wasm32"))]
55use jsonrpc::RequestId;
56
57#[derive(Clone)]
59pub struct McpServerState {
60 pub llm_client: OllamaClient,
62 pub app_state: Arc<AppState>,
64 pub status_tx: mpsc::UnboundedSender<String>,
66 pub server_id: ServerId,
68 pub protocol: Arc<McpProtocol>,
70 pub local_addr: SocketAddr,
72}
73
74pub const MAX_REQUEST_BODY_BYTES: usize = 2 * 1024 * 1024;
84
85#[cfg(not(target_arch = "wasm32"))]
88const BODY_TOO_LARGE_MESSAGE: &str = "request body too large";
89
90#[cfg(not(target_arch = "wasm32"))]
96const MAX_TRACE_BYTES: usize = 4096;
97
98#[cfg(not(target_arch = "wasm32"))]
100const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["2024-11-05", "2025-03-26", "2025-06-18"];
101
102#[cfg(not(target_arch = "wasm32"))]
107const MCP_SERVER_BUSY_CODE: i32 = -32000;
108
109#[cfg(not(target_arch = "wasm32"))]
111const DEFAULT_PROTOCOL_VERSION: &str = "2024-11-05";
112
113#[cfg(not(target_arch = "wasm32"))]
120const FIRST_REQUEST_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
121
122#[cfg(not(target_arch = "wasm32"))]
134const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
135
136#[cfg(not(target_arch = "wasm32"))]
138const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
139
140#[cfg(not(target_arch = "wasm32"))]
149const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
150 Content-Type: application/json\r\nContent-Length: 82\r\nRetry-After: 5\r\n\
151 Connection: close\r\n\r\n\
152 {\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32000,\"message\":\"server at capacity\"}}";
153
154#[cfg(not(target_arch = "wasm32"))]
160fn mcp_error_from_action(data: &Value) -> JsonRpcError {
161 let code = data
164 .get("code")
165 .and_then(|v| v.as_i64())
166 .and_then(|n| i32::try_from(n).ok())
167 .unwrap_or(ErrorCode::InternalError as i32);
168
169 JsonRpcError {
170 code,
171 message: data
172 .get("message")
173 .and_then(|v| v.as_str())
174 .unwrap_or("Internal error")
175 .to_string(),
176 data: data.get("data").cloned(),
177 }
178}
179
180#[cfg(not(target_arch = "wasm32"))]
202fn llm_failure_error(state: &McpServerState, method: &str, e: anyhow::Error) -> JsonRpcError {
203 let failure = crate::utils::WireFailure::classify(&e);
204 let decision = if failure.is_overloaded() {
205 "fail_closed_llm_error_overloaded"
206 } else {
207 "fail_closed_llm_error_unavailable"
208 };
209 error!("MCP {} decision={}: {}", method, decision, e);
210 Log::new(Some(&state.status_tx)).error(format!("MCP {} decision={}: {}", method, decision, e));
211
212 if failure.is_overloaded() {
215 return JsonRpcError {
216 code: MCP_SERVER_BUSY_CODE,
217 message: failure.text().to_string(),
218 data: Some(serde_json::json!({"retryable": true})),
219 };
220 }
221
222 JsonRpcError {
223 code: ErrorCode::InternalError as i32,
224 message: failure.text().to_string(),
225 data: Some(serde_json::json!({"retryable": false})),
226 }
227}
228
229#[cfg(not(target_arch = "wasm32"))]
233fn log_model_reject(state: &McpServerState, method: &str) {
234 warn!(
235 "MCP {} decision=model_reject: handler returned mcp_error",
236 method
237 );
238 Log::new(Some(&state.status_tx)).warn(format!(
239 "MCP {} decision=model_reject (handler returned mcp_error)",
240 method
241 ));
242}
243
244#[cfg(not(target_arch = "wasm32"))]
247fn log_model_no_answer(state: &McpServerState, method: &str, fallback: &str) {
248 warn!(
249 "MCP {} decision=model_no_answer: replying with {}",
250 method, fallback
251 );
252 Log::new(Some(&state.status_tx)).warn(format!(
253 "MCP {} decision=model_no_answer (replying with {})",
254 method, fallback
255 ));
256}
257
258#[cfg(not(target_arch = "wasm32"))]
265fn recover_request_id(payload: &Value) -> Option<RequestId> {
266 match payload.get("id")? {
267 Value::String(s) => Some(RequestId::String(s.clone())),
268 Value::Number(n) => n.as_i64().map(RequestId::Number),
269 _ => None,
270 }
271}
272
273pub struct McpServer;
275
276#[cfg(not(target_arch = "wasm32"))]
277impl McpServer {
278 pub async fn spawn_with_llm_actions(
280 listen_addr: SocketAddr,
281 llm_client: OllamaClient,
282 app_state: Arc<AppState>,
283 status_tx: mpsc::UnboundedSender<String>,
284 server_id: ServerId,
285 ) -> Result<SocketAddr> {
286 let listener = tokio::net::TcpListener::bind(listen_addr).await?;
287 let local_addr = listener.local_addr()?;
288
289 info!("MCP server (JSON-RPC 2.0) listening on {}", local_addr);
290 Log::new(Some(&status_tx)).info(format!("MCP server listening on {}", local_addr));
291
292 let protocol = Arc::new(McpProtocol::new());
293
294 let task_registrar = app_state.clone();
295 let server_state = McpServerState {
296 llm_client,
297 app_state,
298 status_tx: status_tx.clone(),
299 server_id,
300 protocol,
301 local_addr,
302 };
303
304 let app = Router::new()
306 .route("/", post(handle_jsonrpc))
307 .layer(DefaultBodyLimit::max(MAX_REQUEST_BODY_BYTES))
308 .with_state(server_state);
309
310 let backend = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
320 let backend_addr = backend.local_addr()?;
321
322 let backend_status = status_tx.clone();
323 let backend_handle = tokio::spawn(async move {
324 if let Err(e) = axum::serve(backend, app).await {
325 console_error!(backend_status, "MCP server error: {}", e);
326 }
327 });
328
329 let relay_state = task_registrar.clone();
330 let accept_handle = tokio::spawn(async move {
331 serve_screened_mcp(listener, backend_addr, status_tx, relay_state, server_id).await;
332 });
333
334 task_registrar
337 .register_server_task(server_id, backend_handle)
338 .await;
339 task_registrar
340 .register_server_task(server_id, accept_handle)
341 .await;
342
343 Ok(local_addr)
344 }
345}
346
347#[cfg(not(target_arch = "wasm32"))]
349async fn handle_jsonrpc(
350 AxumState(state): AxumState<McpServerState>,
351 payload: Result<Json<Value>, JsonRejection>,
352) -> Response {
353 let payload = match payload {
354 Ok(Json(payload)) => payload,
355 Err(rejection) if rejection.status() == StatusCode::PAYLOAD_TOO_LARGE => {
356 warn!(
357 "MCP request body over {} bytes decision=fail_closed_body_too_large",
358 MAX_REQUEST_BODY_BYTES
359 );
360 Log::new(Some(&state.status_tx)).warn(format!(
361 "MCP refused a request body over {} bytes (decision=fail_closed_body_too_large)",
362 MAX_REQUEST_BODY_BYTES
363 ));
364 let body = serde_json::json!({
365 "jsonrpc": "2.0",
366 "id": null,
367 "error": {
368 "code": ErrorCode::InvalidRequest.as_i32(),
369 "message": BODY_TOO_LARGE_MESSAGE,
370 },
371 });
372 return (StatusCode::PAYLOAD_TOO_LARGE, Json(body)).into_response();
373 }
374 Err(rejection) => return rejection.into_response(),
376 };
377 trace!(
378 "MCP received JSON-RPC request: {}",
379 serde_json::to_string_pretty(&payload).unwrap_or_else(|_| payload.to_string())
380 );
381
382 let mut trace_body = serde_json::to_string(&payload).unwrap_or_default();
383 if trace_body.len() > MAX_TRACE_BYTES {
384 let cut = trace_body
387 .char_indices()
388 .take_while(|(i, _)| *i <= MAX_TRACE_BYTES)
389 .last()
390 .map(|(i, _)| i)
391 .unwrap_or(0);
392 trace_body.truncate(cut);
393 trace_body.push_str("… (truncated)");
394 }
395 Log::new(Some(&state.status_tx)).trace(format!("MCP received: {}", trace_body));
396
397 let message = match JsonRpcMessage::from_value(payload.clone()) {
399 Ok(msg) => msg,
400 Err(e) => {
401 error!("Failed to parse JSON-RPC message: {:?}", e);
402 let response = JsonRpcResponse::error(recover_request_id(&payload), e);
403 return Json(response).into_response();
404 }
405 };
406
407 match message {
409 JsonRpcMessage::Request(req) => {
410 let request_id = req.id.clone();
411 let method = req.method.clone();
412
413 debug!("MCP request: method={}, id={:?}", method, request_id);
414
415 let result = match method.as_str() {
417 "initialize" => handle_initialize(&state, req.params).await,
418 "ping" => handle_ping(),
419 "resources/list" => handle_resources_list(&state).await,
420 "resources/read" => handle_resources_read(&state, req.params).await,
421 "resources/subscribe" => handle_resources_subscribe(&state, req.params).await,
422 "resources/unsubscribe" => handle_resources_unsubscribe(&state, req.params).await,
423 "resources/templates/list" => handle_resources_templates_list(&state).await,
424 "tools/list" => handle_tools_list(&state).await,
425 "tools/call" => handle_tools_call(&state, req.params).await,
426 "prompts/list" => handle_prompts_list(&state).await,
427 "prompts/get" => handle_prompts_get(&state, req.params).await,
428 "logging/setLevel" => handle_logging_set_level(&state, req.params).await,
429 "completion/complete" => handle_completion(&state, req.params).await,
430 _ => Err(JsonRpcError::new(ErrorCode::MethodNotFound)),
431 };
432
433 let response = match result {
434 Ok(value) => {
435 trace!(
436 "MCP response success: {}",
437 serde_json::to_string_pretty(&value).unwrap_or_default()
438 );
439 JsonRpcResponse::success(request_id, value)
440 }
441 Err(e) => {
442 error!("MCP error: code={}, message={}", e.code, e.message);
443 Log::new(Some(&state.status_tx)).error(format!("MCP error: {}", e.message));
444 JsonRpcResponse::error(request_id, e)
445 }
446 };
447
448 Json(response).into_response()
449 }
450 JsonRpcMessage::Notification(notif) => {
451 let method = notif.method.clone();
452 debug!("MCP notification: method={}", method);
453
454 match method.as_str() {
456 "notifications/initialized" => {
457 handle_initialized(&state).await;
458 }
459 "notifications/cancelled" => {
460 handle_cancelled(&state, notif.params).await;
461 }
462 "notifications/progress" => {
463 handle_progress(&state, notif.params).await;
464 }
465 _ => {
466 debug!("Unknown MCP notification: {}", method);
467 }
468 }
469
470 StatusCode::NO_CONTENT.into_response()
472 }
473 }
474}
475
476#[cfg(not(target_arch = "wasm32"))]
478async fn handle_initialize(
479 state: &McpServerState,
480 params: Option<Value>,
481) -> Result<Value, JsonRpcError> {
482 let connection_id = ConnectionId::new(state.app_state.get_next_unified_id().await);
483 let result = handle_initialize_inner(state, params, connection_id).await;
484 state
486 .app_state
487 .close_connection_on_server(state.server_id, connection_id)
488 .await;
489 result
490}
491
492#[cfg(not(target_arch = "wasm32"))]
493async fn handle_initialize_inner(
494 state: &McpServerState,
495 params: Option<Value>,
496 connection_id: ConnectionId,
497) -> Result<Value, JsonRpcError> {
498 info!("MCP initialize request");
499
500 let client_info = params
502 .as_ref()
503 .and_then(|p| p.get("clientInfo"))
504 .map(|c| c.to_string())
505 .unwrap_or_else(|| "unknown".to_string());
506
507 debug!("MCP client: {}", client_info);
508 let _ = state
509 .status_tx
510 .send(format!("→ MCP client initializing: {}", client_info));
511
512 state
525 .app_state
526 .add_connection_to_server(
527 state.server_id,
528 crate::state::ConnectionState {
529 id: connection_id,
530 remote_addr: state.local_addr, local_addr: state.local_addr,
532 bytes_sent: 0,
533 bytes_received: 0,
534 packets_sent: 0,
535 packets_received: 0,
536 last_activity: crate::utils::clock::Instant::now(),
537 status: ConnectionStatus::Active,
538 status_changed_at: crate::utils::clock::Instant::now(),
539 protocol_info: ProtocolConnectionInfo::empty(),
540 },
541 )
542 .await;
543
544 let requested_version = params
545 .as_ref()
546 .and_then(|p| p.get("protocolVersion"))
547 .and_then(|v| v.as_str())
548 .unwrap_or("unknown")
549 .to_string();
550
551 let event = Event::new(
553 &MCP_INITIALIZE_EVENT,
554 serde_json::json!({
555 "method": "initialize",
556 "client_info": client_info,
557 "protocol_version": requested_version,
558 "capabilities": params.as_ref().and_then(|p| p.get("capabilities")),
559 }),
560 );
561
562 let protocol = state.protocol.clone();
564
565 Log::new(Some(&state.status_tx)).debug("MCP calling LLM for initialize request");
566
567 let execution_result = match call_llm(
569 &state.llm_client,
570 &state.app_state,
571 state.server_id,
572 Some(connection_id),
573 &event,
574 protocol.as_ref(),
575 )
576 .await
577 {
578 Ok(result) => result,
579 Err(e) => {
580 return Err(llm_failure_error(state, "initialize", e));
581 }
582 };
583
584 let log = Log::new(Some(&state.status_tx));
586 for message in &execution_result.messages {
587 log.info(format!("{}", message));
588 }
589
590 log.debug(format!(
591 "MCP got {} protocol results",
592 execution_result.protocol_results.len()
593 ));
594
595 for protocol_result in &execution_result.protocol_results {
597 use crate::llm::actions::protocol_trait::ActionResult;
598 if let ActionResult::Custom { name, data } = protocol_result {
599 if name == "mcp_error" {
603 log_model_reject(state, "initialize");
604 return Err(mcp_error_from_action(data));
605 }
606 if name == "mcp_initialize" {
607 if let Some(response) = data.get("response") {
608 return Ok(response.clone());
609 }
610 }
611 }
612 }
613
614 log_model_no_answer(state, "initialize", "the negotiated protocol version");
621 let agreed_version = if SUPPORTED_PROTOCOL_VERSIONS.contains(&requested_version.as_str()) {
622 requested_version.as_str()
623 } else {
624 DEFAULT_PROTOCOL_VERSION
625 };
626
627 Ok(serde_json::json!({
628 "protocolVersion": agreed_version,
629 "capabilities": {
630 "resources": {},
631 "tools": {},
632 "prompts": {}
633 },
634 "serverInfo": {
635 "name": "netget-mcp",
636 "version": "0.1.0"
637 }
638 }))
639}
640
641#[cfg(not(target_arch = "wasm32"))]
643fn handle_ping() -> Result<Value, JsonRpcError> {
644 Ok(serde_json::json!({}))
645}
646
647#[cfg(not(target_arch = "wasm32"))]
649async fn handle_resources_list(state: &McpServerState) -> Result<Value, JsonRpcError> {
650 Log::new(Some(&state.status_tx)).debug("MCP resources/list request");
651
652 let event = Event::new(
654 &MCP_RESOURCES_LIST_EVENT,
655 serde_json::json!({
656 "method": "resources/list",
657 }),
658 );
659
660 let protocol = state.protocol.clone();
662
663 let execution_result = match call_llm(
665 &state.llm_client,
666 &state.app_state,
667 state.server_id,
668 None,
669 &event,
670 protocol.as_ref(),
671 )
672 .await
673 {
674 Ok(result) => result,
675 Err(e) => {
676 return Err(llm_failure_error(state, "resources/list", e));
677 }
678 };
679
680 for protocol_result in &execution_result.protocol_results {
682 use crate::llm::actions::protocol_trait::ActionResult;
683 if let ActionResult::Custom { name, data } = protocol_result {
684 if name == "mcp_error" {
688 log_model_reject(state, "resources/list");
689 return Err(mcp_error_from_action(data));
690 }
691 if name == "mcp_resources_list" {
692 if let Some(response) = data.get("response") {
693 return Ok(response.clone());
694 }
695 }
696 }
697 }
698
699 log_model_no_answer(state, "resources/list", "an empty resources list");
701 Ok(serde_json::json!({"resources": []}))
702}
703
704#[cfg(not(target_arch = "wasm32"))]
706async fn handle_resources_read(
707 state: &McpServerState,
708 params: Option<Value>,
709) -> Result<Value, JsonRpcError> {
710 let uri = params
711 .as_ref()
712 .and_then(|p| p.get("uri"))
713 .and_then(|u| u.as_str())
714 .ok_or_else(|| JsonRpcError::new(ErrorCode::InvalidParams))?;
715
716 Log::new(Some(&state.status_tx)).debug(format!("MCP resources/read: {}", uri));
717
718 let event = Event::new(
720 &MCP_RESOURCES_READ_EVENT,
721 serde_json::json!({
722 "method": "resources/read",
723 "uri": uri,
724 }),
725 );
726
727 let protocol = state.protocol.clone();
729
730 let execution_result = match call_llm(
732 &state.llm_client,
733 &state.app_state,
734 state.server_id,
735 None,
736 &event,
737 protocol.as_ref(),
738 )
739 .await
740 {
741 Ok(result) => result,
742 Err(e) => {
743 return Err(llm_failure_error(state, "resources/read", e));
744 }
745 };
746
747 for protocol_result in &execution_result.protocol_results {
749 use crate::llm::actions::protocol_trait::ActionResult;
750 if let ActionResult::Custom { name, data } = protocol_result {
751 if name == "mcp_error" {
755 log_model_reject(state, "resources/read");
756 return Err(mcp_error_from_action(data));
757 }
758 if name == "mcp_resources_read" {
759 if let Some(response) = data.get("response") {
760 return Ok(response.clone());
761 }
762 }
763 }
764 }
765
766 log_model_no_answer(state, "resources/read", "a JSON-RPC error");
768 Err(JsonRpcError::custom(
769 ErrorCode::InternalError,
770 format!("Resource not found: {}", uri),
771 ))
772}
773
774#[cfg(not(target_arch = "wasm32"))]
776async fn handle_resources_subscribe(
777 _state: &McpServerState,
778 params: Option<Value>,
779) -> Result<Value, JsonRpcError> {
780 let uri = params
781 .as_ref()
782 .and_then(|p| p.get("uri"))
783 .and_then(|u| u.as_str())
784 .ok_or_else(|| JsonRpcError::new(ErrorCode::InvalidParams))?;
785
786 debug!("MCP resources/subscribe: uri={}", uri);
787
788 Ok(serde_json::json!({}))
790}
791
792#[cfg(not(target_arch = "wasm32"))]
794async fn handle_resources_unsubscribe(
795 _state: &McpServerState,
796 params: Option<Value>,
797) -> Result<Value, JsonRpcError> {
798 let uri = params
799 .as_ref()
800 .and_then(|p| p.get("uri"))
801 .and_then(|u| u.as_str())
802 .ok_or_else(|| JsonRpcError::new(ErrorCode::InvalidParams))?;
803
804 debug!("MCP resources/unsubscribe: uri={}", uri);
805 Ok(serde_json::json!({}))
806}
807
808#[cfg(not(target_arch = "wasm32"))]
810async fn handle_resources_templates_list(_state: &McpServerState) -> Result<Value, JsonRpcError> {
811 debug!("MCP resources/templates/list");
812
813 Ok(serde_json::json!({
815 "resourceTemplates": []
816 }))
817}
818
819#[cfg(not(target_arch = "wasm32"))]
821async fn handle_tools_list(state: &McpServerState) -> Result<Value, JsonRpcError> {
822 Log::new(Some(&state.status_tx)).debug("MCP tools/list request");
823
824 let event = Event::new(
826 &MCP_TOOLS_LIST_EVENT,
827 serde_json::json!({
828 "method": "tools/list",
829 }),
830 );
831
832 let protocol = state.protocol.clone();
834
835 let execution_result = match call_llm(
837 &state.llm_client,
838 &state.app_state,
839 state.server_id,
840 None,
841 &event,
842 protocol.as_ref(),
843 )
844 .await
845 {
846 Ok(result) => result,
847 Err(e) => {
848 return Err(llm_failure_error(state, "tools/list", e));
849 }
850 };
851
852 for protocol_result in &execution_result.protocol_results {
854 use crate::llm::actions::protocol_trait::ActionResult;
855 if let ActionResult::Custom { name, data } = protocol_result {
856 if name == "mcp_error" {
860 log_model_reject(state, "tools/list");
861 return Err(mcp_error_from_action(data));
862 }
863 if name == "mcp_tools_list" {
864 if let Some(response) = data.get("response") {
865 return Ok(response.clone());
866 }
867 }
868 }
869 }
870
871 log_model_no_answer(state, "tools/list", "an empty tools list");
873 Ok(serde_json::json!({"tools": []}))
874}
875
876#[cfg(not(target_arch = "wasm32"))]
878async fn handle_tools_call(
879 state: &McpServerState,
880 params: Option<Value>,
881) -> Result<Value, JsonRpcError> {
882 let tool_name = params
883 .as_ref()
884 .and_then(|p| p.get("name"))
885 .and_then(|n| n.as_str())
886 .ok_or_else(|| JsonRpcError::new(ErrorCode::InvalidParams))?;
887
888 let tool_arguments = params.as_ref().and_then(|p| p.get("arguments"));
889
890 Log::new(Some(&state.status_tx)).debug(format!("MCP tools/call: {}", tool_name));
891
892 let event = Event::new(
894 &MCP_TOOLS_CALL_EVENT,
895 serde_json::json!({
896 "method": "tools/call",
897 "name": tool_name,
898 "arguments": tool_arguments,
899 }),
900 );
901
902 let protocol = state.protocol.clone();
904
905 let execution_result = match call_llm(
907 &state.llm_client,
908 &state.app_state,
909 state.server_id,
910 None,
911 &event,
912 protocol.as_ref(),
913 )
914 .await
915 {
916 Ok(result) => result,
917 Err(e) => {
918 return Err(llm_failure_error(state, "tools/call", e));
919 }
920 };
921
922 for protocol_result in &execution_result.protocol_results {
924 use crate::llm::actions::protocol_trait::ActionResult;
925 if let ActionResult::Custom { name, data } = protocol_result {
926 if name == "mcp_error" {
930 log_model_reject(state, "tools/call");
931 return Err(mcp_error_from_action(data));
932 }
933 if name == "mcp_tools_call" {
934 if let Some(response) = data.get("response") {
935 return Ok(response.clone());
936 }
937 }
938 }
939 }
940
941 log_model_no_answer(state, "tools/call", "a JSON-RPC error");
943 Err(JsonRpcError::custom(
944 ErrorCode::InternalError,
945 format!("Tool execution failed: {}", tool_name),
946 ))
947}
948
949#[cfg(not(target_arch = "wasm32"))]
951async fn handle_prompts_list(state: &McpServerState) -> Result<Value, JsonRpcError> {
952 Log::new(Some(&state.status_tx)).debug("MCP prompts/list request");
953
954 let event = Event::new(
956 &MCP_PROMPTS_LIST_EVENT,
957 serde_json::json!({
958 "method": "prompts/list",
959 }),
960 );
961
962 let protocol = state.protocol.clone();
964
965 let execution_result = match call_llm(
967 &state.llm_client,
968 &state.app_state,
969 state.server_id,
970 None,
971 &event,
972 protocol.as_ref(),
973 )
974 .await
975 {
976 Ok(result) => result,
977 Err(e) => {
978 return Err(llm_failure_error(state, "prompts/list", e));
979 }
980 };
981
982 for protocol_result in &execution_result.protocol_results {
984 use crate::llm::actions::protocol_trait::ActionResult;
985 if let ActionResult::Custom { name, data } = protocol_result {
986 if name == "mcp_error" {
990 log_model_reject(state, "prompts/list");
991 return Err(mcp_error_from_action(data));
992 }
993 if name == "mcp_prompts_list" {
994 if let Some(response) = data.get("response") {
995 return Ok(response.clone());
996 }
997 }
998 }
999 }
1000
1001 log_model_no_answer(state, "prompts/list", "an empty prompts list");
1003 Ok(serde_json::json!({"prompts": []}))
1004}
1005
1006#[cfg(not(target_arch = "wasm32"))]
1008async fn handle_prompts_get(
1009 state: &McpServerState,
1010 params: Option<Value>,
1011) -> Result<Value, JsonRpcError> {
1012 let prompt_name = params
1013 .as_ref()
1014 .and_then(|p| p.get("name"))
1015 .and_then(|n| n.as_str())
1016 .ok_or_else(|| JsonRpcError::new(ErrorCode::InvalidParams))?;
1017
1018 let prompt_arguments = params.as_ref().and_then(|p| p.get("arguments"));
1019
1020 Log::new(Some(&state.status_tx)).debug(format!("MCP prompts/get: {}", prompt_name));
1021
1022 let event = Event::new(
1024 &MCP_PROMPTS_GET_EVENT,
1025 serde_json::json!({
1026 "method": "prompts/get",
1027 "name": prompt_name,
1028 "arguments": prompt_arguments,
1029 }),
1030 );
1031
1032 let protocol = state.protocol.clone();
1034
1035 let execution_result = match call_llm(
1037 &state.llm_client,
1038 &state.app_state,
1039 state.server_id,
1040 None,
1041 &event,
1042 protocol.as_ref(),
1043 )
1044 .await
1045 {
1046 Ok(result) => result,
1047 Err(e) => {
1048 return Err(llm_failure_error(state, "prompts/get", e));
1049 }
1050 };
1051
1052 for protocol_result in &execution_result.protocol_results {
1054 use crate::llm::actions::protocol_trait::ActionResult;
1055 if let ActionResult::Custom { name, data } = protocol_result {
1056 if name == "mcp_error" {
1060 log_model_reject(state, "prompts/get");
1061 return Err(mcp_error_from_action(data));
1062 }
1063 if name == "mcp_prompts_get" {
1064 if let Some(response) = data.get("response") {
1065 return Ok(response.clone());
1066 }
1067 }
1068 }
1069 }
1070
1071 log_model_no_answer(state, "prompts/get", "a JSON-RPC error");
1073 Err(JsonRpcError::custom(
1074 ErrorCode::InternalError,
1075 format!("Prompt not found: {}", prompt_name),
1076 ))
1077}
1078
1079#[cfg(not(target_arch = "wasm32"))]
1081async fn handle_logging_set_level(
1082 state: &McpServerState,
1083 params: Option<Value>,
1084) -> Result<Value, JsonRpcError> {
1085 let level = params
1086 .as_ref()
1087 .and_then(|p| p.get("level"))
1088 .and_then(|l| l.as_str())
1089 .unwrap_or("info");
1090
1091 Log::new(Some(&state.status_tx)).info(format!("MCP log level set to: {}", level));
1094
1095 Ok(serde_json::json!({}))
1096}
1097
1098#[cfg(not(target_arch = "wasm32"))]
1100async fn handle_completion(
1101 _state: &McpServerState,
1102 _params: Option<Value>,
1103) -> Result<Value, JsonRpcError> {
1104 debug!("MCP completion/complete");
1105
1106 Ok(serde_json::json!({
1110 "completion": {
1111 "values": [],
1112 "total": 0,
1113 "hasMore": false
1114 }
1115 }))
1116}
1117
1118#[cfg(not(target_arch = "wasm32"))]
1120async fn handle_initialized(state: &McpServerState) {
1121 Log::new(Some(&state.status_tx)).info("MCP client initialized");
1122}
1123
1124#[cfg(not(target_arch = "wasm32"))]
1126async fn handle_cancelled(state: &McpServerState, params: Option<Value>) {
1127 if let Some(req_id) = params.as_ref().and_then(|p| p.get("requestId")) {
1128 Log::new(Some(&state.status_tx)).debug(format!("MCP cancelled: {:?}", req_id));
1129 }
1130}
1131
1132#[cfg(not(target_arch = "wasm32"))]
1134async fn handle_progress(_state: &McpServerState, params: Option<Value>) {
1135 if let Some(progress) = params {
1136 trace!("MCP progress: {}", progress);
1137 }
1138}
1139
1140#[cfg(not(target_arch = "wasm32"))]
1146async fn serve_screened_mcp(
1147 listener: tokio::net::TcpListener,
1148 backend_addr: SocketAddr,
1149 status_tx: mpsc::UnboundedSender<String>,
1150 app_state: std::sync::Arc<crate::state::app_state::AppState>,
1151 server_id: crate::state::ServerId,
1152) {
1153 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
1154 loop {
1155 let (peer_stream, peer_addr, permit) = match crate::server::accept_bounded::accept_bounded(
1156 &listener,
1157 &limiter,
1158 CONNECTION_CAP_REFUSAL,
1159 "MCP",
1160 Some(&status_tx),
1161 )
1162 .await
1163 {
1164 Ok(triple) => triple,
1165 Err(e) => {
1166 console_error!(status_tx, "MCP accept failed, listener stopped: {}", e);
1167 return;
1168 }
1169 };
1170
1171 let status = status_tx.clone();
1172 let task_owner = app_state.clone();
1174 task_owner
1175 .spawn_server_task(server_id, async move {
1176 let _permit = permit;
1177 relay_mcp_connection(peer_stream, peer_addr, backend_addr, status).await;
1178 })
1179 .await;
1180 }
1181}
1182
1183#[cfg(not(target_arch = "wasm32"))]
1193async fn relay_mcp_connection(
1194 peer_stream: tokio::net::TcpStream,
1195 peer_addr: SocketAddr,
1196 backend_addr: SocketAddr,
1197 status_tx: mpsc::UnboundedSender<String>,
1198) {
1199 use std::sync::atomic::{AtomicBool, Ordering};
1200 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1201
1202 let backend_stream = match tokio::net::TcpStream::connect(backend_addr).await {
1203 Ok(stream) => stream,
1204 Err(e) => {
1205 console_error!(status_tx, "MCP could not reach its own backend: {}", e);
1206 return;
1207 }
1208 };
1209
1210 let (mut peer_read, mut peer_write) = tokio::io::split(peer_stream);
1211 let (mut backend_read, mut backend_write) = tokio::io::split(backend_stream);
1212
1213 let awaiting_response = Arc::new(AtomicBool::new(false));
1214
1215 let to_backend_awaiting = Arc::clone(&awaiting_response);
1216 let to_backend_status = status_tx.clone();
1217 let to_backend = tokio::spawn(async move {
1218 let mut buf = vec![0u8; 16 * 1024];
1219 let mut seen_request = false;
1220 loop {
1221 let bound = if seen_request {
1222 IDLE_BETWEEN_REQUESTS_TIMEOUT
1223 } else {
1224 FIRST_REQUEST_READ_TIMEOUT
1225 };
1226 let read = loop {
1227 match tokio::time::timeout(bound, peer_read.read(&mut buf)).await {
1228 Ok(read) => break Some(read),
1229 Err(_) => {
1230 if to_backend_awaiting.load(Ordering::SeqCst) {
1231 continue;
1234 }
1235 Log::new(Some(&to_backend_status)).info(format!(
1236 "MCP peer {} sent nothing for {}s; closing idle connection",
1237 peer_addr,
1238 bound.as_secs()
1239 ));
1240 break None;
1241 }
1242 }
1243 };
1244 let n = match read {
1245 Some(Ok(0)) | None => break,
1246 Some(Ok(n)) => n,
1247 Some(Err(_)) => break,
1248 };
1249 seen_request = true;
1250 to_backend_awaiting.store(true, Ordering::SeqCst);
1251 if backend_write.write_all(&buf[..n]).await.is_err() {
1252 break;
1253 }
1254 }
1255 let _ = backend_write.shutdown().await;
1256 });
1257
1258 let to_peer_awaiting = Arc::clone(&awaiting_response);
1259 let to_peer = tokio::spawn(async move {
1260 let mut buf = vec![0u8; 16 * 1024];
1261 loop {
1262 match backend_read.read(&mut buf).await {
1263 Ok(0) | Err(_) => break,
1264 Ok(n) => {
1265 if peer_write.write_all(&buf[..n]).await.is_err() {
1266 break;
1267 }
1268 to_peer_awaiting.store(false, Ordering::SeqCst);
1271 }
1272 }
1273 }
1274 let _ = peer_write.shutdown().await;
1275 });
1276
1277 tokio::select! {
1280 _ = to_backend => {}
1281 _ = to_peer => {}
1282 }
1283}