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