1use std::{
2 error::Error,
3 fmt,
4 future::Future,
5 time::{Duration, Instant},
6};
7
8mod state;
9
10mod audit_bridge;
11mod code_tools;
12mod http_contract;
13mod metrics;
14mod prompts;
15mod resources;
16mod scope_authorization;
17mod tool_registry;
18
19use axum::{
20 Router,
21 body::Bytes,
22 extract::State,
23 http::{HeaderMap, HeaderValue, StatusCode, header},
24 response::{IntoResponse, Response},
25 routing::{get, post},
26};
27use serde::Deserialize;
28use serde_json::{Value, json};
29use tokio::sync::watch;
30use tower_http::{limit::RequestBodyLimitLayer, trace::TraceLayer};
31
32use http_contract::{
33 ensure_remote_bind_allowed, validate_http_headers, validate_origin,
34 validate_protocol_version_header,
35};
36use scope_authorization::RuntimeScopeAuthorizer;
37use state::{CancellationRegistry, SessionCreateError, SessionLookupError, SessionRegistry};
38
39use crate::{
40 api::{
41 AgentRetrievalResult, ApiError, ErrorKind, GraphInspectionRequest, HybridRetrievalRequest,
42 InterfaceKind, RequestContext, RuntimeIdentity, freshness_label,
43 },
44 application::{AgentRuntimeConfig, RelayKnowledgeService},
45 domain::FreshnessPolicy,
46 net::{
47 NetworkRuntime,
48 http::HttpServeError,
49 qos::{QosPermit, QosRuntime, RejectReason},
50 },
51 observability::AgentProtocolMetrics,
52 project::PROJECT_NAME,
53};
54
55use super::{
56 AgentAdapterError, AgentAdapterErrorKind, AgentAuditEvent, AgentAuditLog, AgentAuditSink,
57 authorize_limit,
58};
59use audit_bridge::{record_mcp_qos_rejection, record_mcp_tool_audit};
60use code_tools::run_code_tool;
61use tool_registry::{
62 CODE_IMPACT_TOOL, CODE_QUERY_TOOL, HEALTH_TOOL, INDEX_STATUS_TOOL, INSPECT_GRAPH_TOOL,
63 RETRIEVE_CONTEXT_TOOL, SERVICE_STATUS_TOOL, is_known_tool, tools_list_result,
64};
65
66pub const MCP_PROTOCOL_VERSION: &str = "2025-11-25";
67const MCP_PROTOCOL_VERSION_HEADER: &str = "mcp-protocol-version";
68const MCP_SESSION_ID_HEADER: &str = "mcp-session-id";
69
70#[derive(Clone)]
72pub struct McpServer {
73 service: RelayKnowledgeService,
74 network: NetworkRuntime,
75 agent: AgentRuntimeConfig,
76 qos: QosRuntime,
77 audit: AgentAuditLog,
78 metrics: AgentProtocolMetrics,
79 cancellations: CancellationRegistry,
80 sessions: SessionRegistry,
81 scope_authorizer: RuntimeScopeAuthorizer,
82}
83
84impl McpServer {
85 pub fn new(
87 service: RelayKnowledgeService,
88 network: NetworkRuntime,
89 agent: AgentRuntimeConfig,
90 ) -> Self {
91 let metrics = service.observability().agent_metrics();
92 let audit = if agent.audit_sink_enabled {
93 AgentAuditSink::jsonl(service.agent_audit_log_path(), agent.audit_queue_depth)
94 .map(AgentAuditLog::with_sink)
95 .unwrap_or_default()
96 } else {
97 AgentAuditLog::default()
98 };
99
100 Self {
101 service,
102 network,
103 agent,
104 qos: QosRuntime::default(),
105 audit,
106 metrics,
107 cancellations: CancellationRegistry::default(),
108 sessions: SessionRegistry::default(),
109 scope_authorizer: RuntimeScopeAuthorizer::default(),
110 }
111 }
112
113 pub fn router(self) -> Router {
115 let config = self.network.current();
116 let endpoint = self.agent.mcp_endpoint.clone();
117 let metrics_endpoint = metrics::metrics_endpoint(&endpoint);
118 let body_limit = usize::try_from(config.http.max_request_body_bytes).unwrap_or(usize::MAX);
119
120 Router::new()
121 .route(&endpoint, post(handle_mcp_post))
122 .route(&endpoint, axum::routing::delete(handle_mcp_delete))
123 .route(&metrics_endpoint, get(metrics::handle_metrics_get))
124 .with_state(self)
125 .layer(TraceLayer::new_for_http())
126 .layer(RequestBodyLimitLayer::new(body_limit))
127 }
128
129 pub async fn serve_until_shutdown(
131 self,
132 shutdown: impl Future<Output = ()> + Send + 'static,
133 ) -> Result<(), McpServeError> {
134 let network_config = self.network.current();
135 let config = network_config.http;
136 let qos_policy = network_config.qos;
137 let qos = self.qos.clone();
138 let router = self.checked_router()?;
139
140 crate::net::http::serve_router_with_qos(router, config, qos, qos_policy, shutdown)
141 .await
142 .map_err(McpServeError::Http)
143 }
144
145 pub fn checked_router(self) -> Result<Router, McpServeError> {
147 if !self.agent.mcp_streamable_http_enabled {
148 return Err(McpServeError::Disabled);
149 }
150 ensure_remote_bind_allowed(&self.network.current().http, &self.agent.access_policy)?;
151
152 Ok(self.router())
153 }
154
155 #[cfg(test)]
156 pub fn qos_snapshot(&self) -> crate::net::qos::QosSnapshot {
157 self.qos.snapshot()
158 }
159
160 #[cfg(test)]
161 pub fn audit_snapshot(&self) -> Vec<AgentAuditEvent> {
162 self.audit.snapshot()
163 }
164}
165
166#[derive(Debug)]
168pub enum McpServeError {
169 Disabled,
170 RemoteBindDisabled,
171 Http(HttpServeError),
172}
173
174impl fmt::Display for McpServeError {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 match self {
177 Self::Disabled => write!(formatter, "MCP Streamable HTTP is not enabled"),
178 Self::RemoteBindDisabled => {
179 write!(
180 formatter,
181 "MCP remote bind requires allow_remote_clients=true"
182 )
183 }
184 Self::Http(error) => write!(formatter, "{error}"),
185 }
186 }
187}
188
189impl Error for McpServeError {}
190
191#[derive(Debug, Deserialize)]
192struct JsonRpcRequest {
193 jsonrpc: Option<String>,
194 id: Option<Value>,
195 method: Option<String>,
196 #[serde(default)]
197 params: Value,
198}
199
200#[derive(Debug, Deserialize)]
201struct InitializeParams {
202 #[serde(rename = "protocolVersion")]
203 protocol_version: String,
204 capabilities: Value,
205 #[serde(rename = "clientInfo")]
206 client_info: InitializeClientInfo,
207}
208
209#[derive(Debug, Deserialize)]
210struct InitializeClientInfo {
211 name: String,
212 version: String,
213}
214
215#[derive(Debug, Deserialize)]
216struct ToolCallParams {
217 name: String,
218 #[serde(default)]
219 arguments: Value,
220}
221
222#[derive(Debug, Deserialize)]
223struct RetrieveContextArgs {
224 query: String,
225 #[serde(default)]
226 source_scope: Option<String>,
227 #[serde(default)]
228 limit: Option<usize>,
229 #[serde(default)]
230 freshness: Option<String>,
231}
232
233#[derive(Debug, Deserialize)]
234struct InspectGraphArgs {
235 #[serde(default)]
236 source_scope: Option<String>,
237}
238
239#[derive(Debug, Deserialize)]
240struct CancelParams {
241 #[serde(rename = "requestId")]
242 request_id: Value,
243}
244
245pub(super) struct McpMethodError {
246 code: i64,
247 kind: &'static str,
248 message: String,
249}
250
251impl McpMethodError {
252 fn invalid_params(message: impl Into<String>) -> Self {
253 Self {
254 code: -32602,
255 kind: "invalid_argument",
256 message: message.into(),
257 }
258 }
259
260 fn internal(message: impl Into<String>) -> Self {
261 Self {
262 code: -32603,
263 kind: "internal",
264 message: message.into(),
265 }
266 }
267
268 fn timeout(message: impl Into<String>) -> Self {
269 Self {
270 code: -32000,
271 kind: "timeout",
272 message: message.into(),
273 }
274 }
275
276 fn api(error: ApiError) -> Self {
277 Self {
278 code: -32000,
279 kind: match error.error_kind {
280 ErrorKind::InvalidArgument => "invalid_argument",
281 ErrorKind::StorageUnavailable => "storage_unavailable",
282 ErrorKind::Timeout => "timeout",
283 ErrorKind::Internal => "internal",
284 },
285 message: error.message,
286 }
287 }
288
289 fn adapter(error: AgentAdapterError) -> Self {
290 Self {
291 code: -32000,
292 kind: error.kind.as_str(),
293 message: error.message,
294 }
295 }
296}
297
298async fn handle_mcp_post(
299 State(server): State<McpServer>,
300 headers: HeaderMap,
301 body: Bytes,
302) -> Response {
303 if let Err(status) = validate_http_headers(&server, &headers) {
304 return status.into_response();
305 }
306 if body.len() as u64 > server.network.current().http.max_request_body_bytes {
307 return StatusCode::PAYLOAD_TOO_LARGE.into_response();
308 }
309
310 let payload = match serde_json::from_slice::<Value>(&body) {
311 Ok(payload) => payload,
312 Err(error) => {
313 return json_rpc_error(Value::Null, -32700, format!("parse error: {error}"));
314 }
315 };
316 if payload.is_array() {
317 return json_rpc_error(Value::Null, -32600, "batch requests are not supported");
318 }
319 let request = match serde_json::from_value::<JsonRpcRequest>(payload.clone()) {
320 Ok(request) => request,
321 Err(error) => {
322 return json_rpc_error(Value::Null, -32600, format!("invalid request: {error}"));
323 }
324 };
325 let id = request.id.clone().unwrap_or(Value::Null);
326 if request.jsonrpc.as_deref() != Some("2.0") {
327 return json_rpc_error(id, -32600, "jsonrpc must be 2.0");
328 }
329 let Some(method) = request.method.as_deref() else {
330 if is_valid_json_rpc_response(&payload) {
331 if let Err(status) = validate_protocol_version_header(&headers, true) {
332 return status.into_response();
333 }
334 return response_message_session_response(&server, &headers);
335 }
336 if payload
337 .as_object()
338 .is_some_and(|object| object.contains_key("result") || object.contains_key("error"))
339 {
340 return StatusCode::BAD_REQUEST.into_response();
341 }
342 return json_rpc_error(id, -32600, "method is required");
343 };
344
345 if method == "initialize" {
346 let Some(id) = request.id else {
347 return json_rpc_error(Value::Null, -32600, "requests must include an id");
348 };
349 if !is_json_rpc_id(&id) {
350 return invalid_request_id_response();
351 }
352 if let Err(message) = validate_initialize_params(request.params) {
353 return json_rpc_error(id, -32602, message);
354 }
355 let Ok(permit) = admit_mcp_request(&server) else {
356 return StatusCode::TOO_MANY_REQUESTS.into_response();
357 };
358 let session_id = match server.sessions.require_session(&headers) {
359 Ok(session) => session.session_id().to_owned(),
360 Err(SessionLookupError::Missing) => match server.sessions.create_session() {
361 Ok(session_id) => session_id,
362 Err(error) => {
363 drop(permit);
364 return session_create_error(id, error);
365 }
366 },
367 Err(error) => {
368 drop(permit);
369 return session_lookup_error_response(error);
370 }
371 };
372 drop(permit);
373 return json_rpc_success_with_session(id, initialize_result(), &session_id);
374 }
375
376 if let Err(status) = validate_protocol_version_header(&headers, true) {
377 return status.into_response();
378 }
379
380 let session = match server.sessions.require_session(&headers) {
381 Ok(session) => session,
382 Err(error) => return session_lookup_error_response(error),
383 };
384
385 if method == "notifications/initialized" {
386 if request.id.is_some() {
387 return json_rpc_error(id, -32600, "notifications must not include an id");
388 }
389 let Ok(permit) = admit_mcp_request(&server) else {
390 return StatusCode::TOO_MANY_REQUESTS.into_response();
391 };
392 if let Err(error) = server.sessions.mark_initialized(session.session_id()) {
393 drop(permit);
394 return session_lookup_error_response(error);
395 }
396 drop(permit);
397 return StatusCode::ACCEPTED.into_response();
398 }
399
400 if !session.initialized {
401 return uninitialized_session_response(request.id);
402 }
403
404 let namespace = session.namespace();
405 if method.starts_with("notifications/") {
406 if request.id.is_some() {
407 return json_rpc_error(id, -32600, "notifications must not include an id");
408 }
409 let Ok(permit) = admit_mcp_request(&server) else {
410 return StatusCode::TOO_MANY_REQUESTS.into_response();
411 };
412 handle_notification(&server, method, request.params, &namespace);
413 drop(permit);
414 return StatusCode::ACCEPTED.into_response();
415 }
416
417 let Some(id) = request.id else {
418 return json_rpc_error(Value::Null, -32600, "requests must include an id");
419 };
420 let Some(request_id) = request_id_key(&namespace, &id) else {
421 return invalid_request_id_response();
422 };
423 let permit = match admit_mcp_request(&server) {
424 Ok(permit) => permit,
425 Err(reason) => {
426 let error =
427 AgentAdapterError::new(AgentAdapterErrorKind::QosRejected, qos_message(reason));
428 record_mcp_qos_rejection(&server, method, &id, error.kind.as_str());
429 server.metrics.record_rejection("mcp", error.kind.as_str());
430 return if method == "tools/call" {
431 json_rpc_success(id, tool_error_result(error))
432 } else {
433 json_rpc_error(id, -32000, error.to_string())
434 };
435 }
436 };
437
438 let started = Instant::now();
439 let mut pending_tool_audit = None;
440 let result = match method {
441 "ping" => json!({}),
442 "tools/list" => json!(tools_list_result()),
443 "resources/list" => resources::list_resources(&server),
444 "resources/read" => {
445 match resources::read_resource_with_timeout(&server, request.params, &request_id).await
446 {
447 Ok(result) => result,
448 Err(error) => return json_rpc_error(id, error.code, error.message),
449 }
450 }
451 "prompts/list" => prompts::list_prompts(),
452 "prompts/get" => match prompts::get_prompt(&server, request.params, &request_id).await {
453 Ok(result) => result,
454 Err(error) => return json_rpc_error(id, error.code, error.message),
455 },
456 "tools/call" => {
457 let params = match serde_json::from_value::<ToolCallParams>(request.params) {
458 Ok(params) => params,
459 Err(error) => {
460 return json_rpc_error(
461 id,
462 -32602,
463 format!("invalid tools/call params: {error}"),
464 );
465 }
466 };
467 if !is_known_tool(¶ms.name) {
468 return json_rpc_error(id, -32602, "unknown tool name");
469 }
470 let outcome = run_cancellable_tool_call(&server, params, request_id).await;
471 pending_tool_audit = Some((
472 outcome.operation,
473 outcome.request_id,
474 outcome.result.clone(),
475 outcome.duration_ms,
476 ));
477 outcome.result
478 }
479 _ => return json_rpc_error(id, -32601, "method not found"),
480 };
481
482 drop(permit);
483 if let Some((operation, request_id, result, duration_ms)) = pending_tool_audit {
484 record_mcp_tool_audit(&server, &operation, &request_id, &result, duration_ms).await;
485 } else if !matches!(method, "resources/read" | "prompts/get") {
486 server
487 .metrics
488 .record_request("mcp", method, "completed", elapsed_millis(started), false);
489 }
490 json_rpc_success(id, result)
491}
492
493async fn handle_mcp_delete(State(server): State<McpServer>, headers: HeaderMap) -> Response {
494 if let Err(status) = validate_origin(&server, &headers) {
495 return status.into_response();
496 }
497 if let Err(status) = validate_protocol_version_header(&headers, true) {
498 return status.into_response();
499 }
500 let permit = match admit_mcp_request(&server) {
501 Ok(permit) => permit,
502 Err(_) => {
503 server.metrics.record_rejection("mcp", "qos_rejected");
504 return StatusCode::TOO_MANY_REQUESTS.into_response();
505 }
506 };
507 match server.sessions.terminate_session(&headers) {
508 Ok(()) => {
509 drop(permit);
510 StatusCode::ACCEPTED.into_response()
511 }
512 Err(error) => {
513 drop(permit);
514 session_lookup_error_response(error)
515 }
516 }
517}
518
519fn is_valid_json_rpc_response(payload: &Value) -> bool {
520 let Some(object) = payload.as_object() else {
521 return false;
522 };
523 let has_result = object.contains_key("result");
524 let has_error = object.contains_key("error");
525 if has_result == has_error {
526 return false;
527 }
528
529 object.get("id").is_some_and(is_json_rpc_id)
530}
531
532fn validate_initialize_params(params: Value) -> Result<(), String> {
533 let params = serde_json::from_value::<InitializeParams>(params)
534 .map_err(|error| format!("invalid initialize params: {error}"))?;
535 if params.protocol_version != MCP_PROTOCOL_VERSION {
536 return Err(format!(
537 "unsupported MCP protocol version '{}'",
538 params.protocol_version
539 ));
540 }
541 if !params.capabilities.is_object() {
542 return Err("initialize capabilities must be an object".to_owned());
543 }
544 if params.client_info.name.trim().is_empty() || params.client_info.version.trim().is_empty() {
545 return Err("initialize clientInfo requires name and version".to_owned());
546 }
547
548 Ok(())
549}
550
551fn response_message_session_response(server: &McpServer, headers: &HeaderMap) -> Response {
552 match server.sessions.require_session(headers) {
553 Ok(session) if session.initialized => StatusCode::ACCEPTED.into_response(),
554 Ok(_) => StatusCode::BAD_REQUEST.into_response(),
555 Err(error) => session_lookup_error_response(error),
556 }
557}
558
559fn session_lookup_error_response(error: SessionLookupError) -> Response {
560 match error {
561 SessionLookupError::Missing | SessionLookupError::InvalidHeader => {
562 StatusCode::BAD_REQUEST.into_response()
563 }
564 SessionLookupError::Unknown => StatusCode::NOT_FOUND.into_response(),
565 }
566}
567
568fn uninitialized_session_response(id: Option<Value>) -> Response {
569 let Some(id) = id else {
570 return StatusCode::BAD_REQUEST.into_response();
571 };
572 if is_json_rpc_id(&id) {
573 json_rpc_error(id, -32002, "MCP session is not initialized")
574 } else {
575 invalid_request_id_response()
576 }
577}
578
579fn invalid_request_id_response() -> Response {
580 json_rpc_error(Value::Null, -32600, "request id must be a string or number")
581}
582
583fn session_create_error(id: Value, error: SessionCreateError) -> Response {
584 json_rpc_error(id, -32603, format!("failed to create MCP session: {error}"))
585}
586
587fn handle_notification(server: &McpServer, method: &str, params: Value, namespace: &str) {
588 if method == "notifications/cancelled" {
589 if let Ok(cancel) = serde_json::from_value::<CancelParams>(params) {
590 if let Some(request_id) = request_id_key(namespace, &cancel.request_id) {
591 server.cancellations.cancel(&request_id);
592 }
593 }
594 }
595}
596
597fn admit_mcp_request(server: &McpServer) -> Result<QosPermit, RejectReason> {
598 let policy = server.network.current().qos;
599 let queued = server.qos.reserve_queue(&policy)?;
600 let permit = server.qos.admit_request(&policy);
601 drop(queued);
602 permit
603}
604
605async fn run_cancellable_tool_call(
606 server: &McpServer,
607 params: ToolCallParams,
608 request_id: String,
609) -> ToolCallOutcome {
610 let started = Instant::now();
611 let operation = params.name.clone();
612 let (mut cancellation, _registration) = server.cancellations.register(request_id.clone());
613 let timeout = Duration::from_millis(server.agent.access_policy.max_runtime_ms);
614 let tool = run_tool_call(server, params, request_id.clone());
615
616 let result = tokio::select! {
617 result = tokio::time::timeout(timeout, tool) => match result {
618 Ok(value) => value,
619 Err(_) => tool_error_result(AgentAdapterError::new(
620 AgentAdapterErrorKind::Timeout,
621 "MCP tool call exceeded max_runtime_ms",
622 )),
623 },
624 _ = wait_for_cancellation(&mut cancellation) => {
625 tool_error_result(AgentAdapterError::new(
626 AgentAdapterErrorKind::Cancelled,
627 "MCP tool call was cancelled",
628 ))
629 }
630 };
631
632 ToolCallOutcome {
633 operation,
634 request_id,
635 result,
636 duration_ms: elapsed_millis(started),
637 }
638}
639
640struct ToolCallOutcome {
641 operation: String,
642 request_id: String,
643 result: Value,
644 duration_ms: u64,
645}
646
647async fn wait_for_cancellation(cancellation: &mut watch::Receiver<bool>) {
648 while cancellation.changed().await.is_ok() {
649 if *cancellation.borrow() {
650 return;
651 }
652 }
653
654 std::future::pending::<()>().await;
655}
656
657async fn run_tool_call(server: &McpServer, params: ToolCallParams, request_id: String) -> Value {
658 match params.name.as_str() {
659 RETRIEVE_CONTEXT_TOOL => retrieve_context_tool(server, params.arguments, request_id).await,
660 INSPECT_GRAPH_TOOL => inspect_graph_tool(server, params.arguments, request_id).await,
661 HEALTH_TOOL => health_tool(server, request_id).await,
662 SERVICE_STATUS_TOOL => service_status_tool(server, request_id).await,
663 INDEX_STATUS_TOOL => index_status_tool(server, request_id).await,
664 CODE_QUERY_TOOL | CODE_IMPACT_TOOL => {
665 run_code_tool(server, params.name.as_str(), params.arguments, request_id).await
666 }
667 _ => json!({
668 "content": [{"type": "text", "text": "unknown MCP tool"}],
669 "isError": true
670 }),
671 }
672}
673
674async fn retrieve_context_tool(server: &McpServer, arguments: Value, request_id: String) -> Value {
675 let started = Instant::now();
676 let args = match serde_json::from_value::<RetrieveContextArgs>(arguments) {
677 Ok(args) => args,
678 Err(error) => return tool_error_result(invalid_arguments(error)),
679 };
680 let policy = &server.agent.access_policy;
681 let limit = match authorize_limit(args.limit, policy) {
682 Ok(limit) => limit,
683 Err(error) => return tool_error_result(error),
684 };
685 let source_scope = match server
686 .scope_authorizer
687 .authorize_scope(&server.service, policy, args.source_scope)
688 .await
689 {
690 Ok(scope) => scope,
691 Err(error) => return tool_error_result(error),
692 };
693 let freshness = match parse_freshness(args.freshness.as_deref()) {
694 Ok(freshness) => freshness,
695 Err(error) => return tool_error_result(error),
696 };
697 let context = request_context(request_id.clone());
698 let identity = RuntimeIdentity::mcp(Some(request_id));
699
700 match server
701 .service
702 .retrieve_context(
703 HybridRetrievalRequest {
704 query: args.query,
705 source_scope: source_scope.clone(),
706 limit,
707 freshness,
708 },
709 context,
710 )
711 .await
712 {
713 Ok(response) => {
714 let elapsed_ms = elapsed_millis(started);
715 let result = AgentRetrievalResult::from_retrieval(
716 response,
717 identity,
718 policy.max_context_bytes,
719 elapsed_ms,
720 );
721 tool_success_result(
722 format!(
723 "retrieved {} result(s), graph_version={}, freshness={}",
724 result.results.len(),
725 result.metadata.graph_version,
726 freshness_label(freshness)
727 ),
728 json!(result),
729 )
730 }
731 Err(error) => api_error_result(error),
732 }
733}
734
735async fn inspect_graph_tool(server: &McpServer, arguments: Value, request_id: String) -> Value {
736 let args = match serde_json::from_value::<InspectGraphArgs>(arguments) {
737 Ok(args) => args,
738 Err(error) => return tool_error_result(invalid_arguments(error)),
739 };
740 let source_scope = match server
741 .scope_authorizer
742 .authorize_scope(
743 &server.service,
744 &server.agent.access_policy,
745 args.source_scope,
746 )
747 .await
748 {
749 Ok(scope) => scope,
750 Err(error) => return tool_error_result(error),
751 };
752
753 match server
754 .service
755 .inspect_graph(
756 GraphInspectionRequest { source_scope },
757 request_context(request_id),
758 )
759 .await
760 {
761 Ok(response) => tool_success_result("graph inspection completed", json!(response)),
762 Err(error) => api_error_result(error),
763 }
764}
765
766async fn health_tool(server: &McpServer, request_id: String) -> Value {
767 match server.service.health(request_context(request_id)).await {
768 Ok(response) => tool_success_result(
769 format!(
770 "health={}",
771 if response.healthy { "ok" } else { "degraded" }
772 ),
773 json!(response),
774 ),
775 Err(error) => api_error_result(error),
776 }
777}
778
779async fn service_status_tool(server: &McpServer, request_id: String) -> Value {
780 match server
781 .service
782 .service_status(request_context(request_id))
783 .await
784 {
785 Ok(response) => tool_success_result("service status loaded", json!(response)),
786 Err(error) => api_error_result(error),
787 }
788}
789
790async fn index_status_tool(server: &McpServer, request_id: String) -> Value {
791 match server.service.health(request_context(request_id)).await {
792 Ok(response) => tool_success_result(
793 "index status loaded",
794 json!({
795 "metadata": response.metadata,
796 "indexes": response.indexes,
797 }),
798 ),
799 Err(error) => api_error_result(error),
800 }
801}
802
803fn initialize_result() -> Value {
804 json!({
805 "protocolVersion": MCP_PROTOCOL_VERSION,
806 "capabilities": {
807 "tools": {},
808 "resources": {"listChanged": false},
809 "prompts": {"listChanged": false}
810 },
811 "serverInfo": {
812 "name": PROJECT_NAME,
813 "version": env!("CARGO_PKG_VERSION")
814 }
815 })
816}
817
818fn parse_freshness(value: Option<&str>) -> Result<FreshnessPolicy, AgentAdapterError> {
819 match value.unwrap_or("allow-stale") {
820 "allow-stale" => Ok(FreshnessPolicy::AllowStale),
821 "wait-until-fresh" => Ok(FreshnessPolicy::WaitUntilFresh),
822 "graph-only" => Ok(FreshnessPolicy::GraphOnly),
823 other => Err(AgentAdapterError::new(
824 AgentAdapterErrorKind::InvalidArgument,
825 format!("invalid freshness '{other}'"),
826 )),
827 }
828}
829
830fn tool_success_result(summary: impl Into<String>, structured_content: Value) -> Value {
831 json!({
832 "content": [{"type": "text", "text": summary.into()}],
833 "structuredContent": structured_content,
834 "isError": false
835 })
836}
837
838fn api_error_result(error: ApiError) -> Value {
839 tool_error_result(AgentAdapterError::new(
840 agent_error_kind(error.error_kind),
841 error.message,
842 ))
843}
844
845fn agent_error_kind(kind: ErrorKind) -> AgentAdapterErrorKind {
846 match kind {
847 ErrorKind::InvalidArgument => AgentAdapterErrorKind::InvalidArgument,
848 ErrorKind::StorageUnavailable => AgentAdapterErrorKind::StorageUnavailable,
849 ErrorKind::Timeout => AgentAdapterErrorKind::Timeout,
850 ErrorKind::Internal => AgentAdapterErrorKind::Internal,
851 }
852}
853
854fn tool_error_result(error: AgentAdapterError) -> Value {
855 json!({
856 "content": [{
857 "type": "text",
858 "text": format!("{}: {}", error.kind.as_str(), error.message)
859 }],
860 "structuredContent": {
861 "error_kind": error.kind.as_str(),
862 "message": error.message,
863 },
864 "isError": true
865 })
866}
867
868fn invalid_arguments(error: serde_json::Error) -> AgentAdapterError {
869 AgentAdapterError::new(
870 AgentAdapterErrorKind::InvalidArgument,
871 format!("invalid tool arguments: {error}"),
872 )
873}
874
875fn domain_argument_error(error: impl fmt::Display) -> AgentAdapterError {
876 AgentAdapterError::new(AgentAdapterErrorKind::InvalidArgument, error.to_string())
877}
878
879fn json_rpc_success(id: Value, result: Value) -> Response {
880 json_response(
881 StatusCode::OK,
882 json!({ "jsonrpc": "2.0", "id": id, "result": result }),
883 )
884}
885
886fn json_rpc_success_with_session(id: Value, result: Value, session_id: &str) -> Response {
887 let mut response = json_rpc_success(id, result);
888 response.headers_mut().insert(
889 MCP_SESSION_ID_HEADER,
890 HeaderValue::from_str(session_id).expect("generated MCP session id is a valid header"),
891 );
892 response
893}
894
895fn json_rpc_error(id: Value, code: i64, message: impl Into<String>) -> Response {
896 json_response(
897 StatusCode::OK,
898 json!({
899 "jsonrpc": "2.0",
900 "id": id,
901 "error": {
902 "code": code,
903 "message": message.into()
904 }
905 }),
906 )
907}
908
909fn json_response(status: StatusCode, value: Value) -> Response {
910 (
911 status,
912 [(header::CONTENT_TYPE, "application/json")],
913 value.to_string(),
914 )
915 .into_response()
916}
917
918fn request_context(request_id: String) -> RequestContext {
919 RequestContext::with_ids(
920 InterfaceKind::Mcp,
921 format!("mcp-{request_id}"),
922 format!("trace-mcp-{request_id}"),
923 )
924}
925
926fn endpoint_child(endpoint: &str, child: &str) -> String {
927 if endpoint == "/" {
928 format!("/{child}")
929 } else {
930 format!("{}/{child}", endpoint.trim_end_matches('/'))
931 }
932}
933
934fn request_id_key(namespace: &str, value: &Value) -> Option<String> {
935 match value {
936 Value::String(value) => Some(format!("{namespace}|string:{value}")),
937 Value::Number(value) if value.is_i64() || value.is_u64() => {
938 Some(format!("{namespace}|number:{value}"))
939 }
940 _ => None,
941 }
942}
943
944fn is_json_rpc_id(value: &Value) -> bool {
945 match value {
946 Value::String(_) => true,
947 Value::Number(number) => number.is_i64() || number.is_u64(),
948 _ => false,
949 }
950}
951
952fn elapsed_millis(started: Instant) -> u64 {
953 u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
954}
955
956fn qos_message(reason: RejectReason) -> &'static str {
957 match reason {
958 RejectReason::ConnectionBudgetExceeded => "connection budget exhausted",
959 RejectReason::RequestBudgetExceeded => "request budget exhausted",
960 RejectReason::QueueBudgetExceeded => "queue budget exhausted",
961 }
962}
963
964#[cfg(test)]
965#[path = "mcp_test_support.rs"]
966mod mcp_test_support;
967
968#[cfg(test)]
969#[path = "mcp_tests.rs"]
970mod mcp_tests;
971
972#[cfg(test)]
973#[path = "mcp_tool_tests.rs"]
974mod mcp_tool_tests;
975
976#[cfg(test)]
977#[path = "mcp_protocol_tests.rs"]
978mod mcp_protocol_tests;