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 HEALTH_TOOL, INDEX_STATUS_TOOL, INSPECT_GRAPH_TOOL, RETRIEVE_CONTEXT_TOOL, SERVICE_STATUS_TOOL,
64 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 let queued = server.qos.reserve_queue(&policy)?;
601 let permit = server.qos.admit_request(&policy);
602 drop(queued);
603 permit
604}
605
606async fn run_cancellable_tool_call(
607 server: &McpServer,
608 params: ToolCallParams,
609 request_id: String,
610) -> ToolCallOutcome {
611 let started = Instant::now();
612 let operation = params.name.clone();
613 let (mut cancellation, _registration) = server.cancellations.register(request_id.clone());
614 let timeout = Duration::from_millis(server.agent.access_policy.max_runtime_ms);
615 let tool = run_tool_call(server, params, request_id.clone());
616
617 let result = tokio::select! {
618 result = tokio::time::timeout(timeout, tool) => match result {
619 Ok(value) => value,
620 Err(_) => tool_error_result(AgentAdapterError::new(
621 AgentAdapterErrorKind::Timeout,
622 "MCP tool call exceeded max_runtime_ms",
623 )),
624 },
625 _ = wait_for_cancellation(&mut cancellation) => {
626 tool_error_result(AgentAdapterError::new(
627 AgentAdapterErrorKind::Cancelled,
628 "MCP tool call was cancelled",
629 ))
630 }
631 };
632
633 ToolCallOutcome {
634 operation,
635 request_id,
636 result,
637 duration_ms: elapsed_millis(started),
638 }
639}
640
641struct ToolCallOutcome {
642 operation: String,
643 request_id: String,
644 result: Value,
645 duration_ms: u64,
646}
647
648async fn wait_for_cancellation(cancellation: &mut watch::Receiver<bool>) {
649 while cancellation.changed().await.is_ok() {
650 if *cancellation.borrow() {
651 return;
652 }
653 }
654
655 std::future::pending::<()>().await;
656}
657
658async fn run_tool_call(server: &McpServer, params: ToolCallParams, request_id: String) -> Value {
659 match params.name.as_str() {
660 RETRIEVE_CONTEXT_TOOL => retrieve_context_tool(server, params.arguments, request_id).await,
661 INSPECT_GRAPH_TOOL => inspect_graph_tool(server, params.arguments, request_id).await,
662 HEALTH_TOOL => health_tool(server, request_id).await,
663 SERVICE_STATUS_TOOL => service_status_tool(server, request_id).await,
664 INDEX_STATUS_TOOL => index_status_tool(server, request_id).await,
665 CODE_QUERY_TOOL
666 | CODE_FEATURE_FLAGS_TOOL
667 | CODE_IMPACT_TOOL
668 | CODE_REPOSITORY_SET_QUERY_TOOL => {
669 run_code_tool(server, params.name.as_str(), params.arguments, request_id).await
670 }
671 _ => json!({
672 "content": [{"type": "text", "text": "unknown MCP tool"}],
673 "isError": true
674 }),
675 }
676}
677
678async fn retrieve_context_tool(server: &McpServer, arguments: Value, request_id: String) -> Value {
679 let started = Instant::now();
680 let args = match serde_json::from_value::<RetrieveContextArgs>(arguments) {
681 Ok(args) => args,
682 Err(error) => return tool_error_result(invalid_arguments(error)),
683 };
684 let policy = &server.agent.access_policy;
685 let limit = match authorize_limit(args.limit, policy) {
686 Ok(limit) => limit,
687 Err(error) => return tool_error_result(error),
688 };
689 let source_scope = match server
690 .scope_authorizer
691 .authorize_scope(&server.service, policy, args.source_scope)
692 .await
693 {
694 Ok(scope) => scope,
695 Err(error) => return tool_error_result(error),
696 };
697 let freshness = match parse_freshness(args.freshness.as_deref()) {
698 Ok(freshness) => freshness,
699 Err(error) => return tool_error_result(error),
700 };
701 let context = request_context(request_id.clone());
702 let identity = RuntimeIdentity::mcp(Some(request_id));
703
704 match server
705 .service
706 .retrieve_context(
707 HybridRetrievalRequest {
708 query: args.query,
709 source_scope: source_scope.clone(),
710 limit,
711 freshness,
712 },
713 context,
714 )
715 .await
716 {
717 Ok(response) => {
718 let elapsed_ms = elapsed_millis(started);
719 let result = AgentRetrievalResult::from_retrieval(
720 response,
721 identity,
722 policy.max_context_bytes,
723 elapsed_ms,
724 );
725 tool_success_result(
726 format!(
727 "retrieved {} result(s), graph_version={}, freshness={}",
728 result.results.len(),
729 result.metadata.graph_version,
730 freshness_label(freshness)
731 ),
732 json!(result),
733 )
734 }
735 Err(error) => api_error_result(error),
736 }
737}
738
739async fn inspect_graph_tool(server: &McpServer, arguments: Value, request_id: String) -> Value {
740 let args = match serde_json::from_value::<InspectGraphArgs>(arguments) {
741 Ok(args) => args,
742 Err(error) => return tool_error_result(invalid_arguments(error)),
743 };
744 let source_scope = match server
745 .scope_authorizer
746 .authorize_scope(
747 &server.service,
748 &server.agent.access_policy,
749 args.source_scope,
750 )
751 .await
752 {
753 Ok(scope) => scope,
754 Err(error) => return tool_error_result(error),
755 };
756
757 match server
758 .service
759 .inspect_graph(
760 GraphInspectionRequest { source_scope },
761 request_context(request_id),
762 )
763 .await
764 {
765 Ok(response) => tool_success_result("graph inspection completed", json!(response)),
766 Err(error) => api_error_result(error),
767 }
768}
769
770async fn health_tool(server: &McpServer, request_id: String) -> Value {
771 match server.service.health(request_context(request_id)).await {
772 Ok(response) => tool_success_result(
773 format!(
774 "health={}",
775 if response.healthy { "ok" } else { "degraded" }
776 ),
777 json!(response),
778 ),
779 Err(error) => api_error_result(error),
780 }
781}
782
783async fn service_status_tool(server: &McpServer, request_id: String) -> Value {
784 match server
785 .service
786 .service_status(request_context(request_id))
787 .await
788 {
789 Ok(response) => tool_success_result("service status loaded", json!(response)),
790 Err(error) => api_error_result(error),
791 }
792}
793
794async fn index_status_tool(server: &McpServer, request_id: String) -> Value {
795 match server.service.health(request_context(request_id)).await {
796 Ok(response) => tool_success_result(
797 "index status loaded",
798 json!({
799 "metadata": response.metadata,
800 "indexes": response.indexes,
801 }),
802 ),
803 Err(error) => api_error_result(error),
804 }
805}
806
807fn initialize_result() -> Value {
808 json!({
809 "protocolVersion": MCP_PROTOCOL_VERSION,
810 "capabilities": {
811 "tools": {},
812 "resources": {"listChanged": false},
813 "prompts": {"listChanged": false}
814 },
815 "serverInfo": {
816 "name": PROJECT_NAME,
817 "version": env!("CARGO_PKG_VERSION")
818 }
819 })
820}
821
822fn parse_freshness(value: Option<&str>) -> Result<FreshnessPolicy, AgentAdapterError> {
823 match value.unwrap_or("allow-stale") {
824 "allow-stale" => Ok(FreshnessPolicy::AllowStale),
825 "wait-until-fresh" => Ok(FreshnessPolicy::WaitUntilFresh),
826 "graph-only" => Ok(FreshnessPolicy::GraphOnly),
827 other => Err(AgentAdapterError::new(
828 AgentAdapterErrorKind::InvalidArgument,
829 format!("invalid freshness '{other}'"),
830 )),
831 }
832}
833
834fn tool_success_result(summary: impl Into<String>, structured_content: Value) -> Value {
835 json!({
836 "content": [{"type": "text", "text": summary.into()}],
837 "structuredContent": structured_content,
838 "isError": false
839 })
840}
841
842fn api_error_result(error: ApiError) -> Value {
843 tool_error_result(AgentAdapterError::new(
844 agent_error_kind(error.error_kind),
845 error.message,
846 ))
847}
848
849fn agent_error_kind(kind: ErrorKind) -> AgentAdapterErrorKind {
850 match kind {
851 ErrorKind::InvalidArgument => AgentAdapterErrorKind::InvalidArgument,
852 ErrorKind::StorageUnavailable => AgentAdapterErrorKind::StorageUnavailable,
853 ErrorKind::Timeout => AgentAdapterErrorKind::Timeout,
854 ErrorKind::Internal => AgentAdapterErrorKind::Internal,
855 }
856}
857
858fn tool_error_result(error: AgentAdapterError) -> Value {
859 json!({
860 "content": [{
861 "type": "text",
862 "text": format!("{}: {}", error.kind.as_str(), error.message)
863 }],
864 "structuredContent": {
865 "error_kind": error.kind.as_str(),
866 "message": error.message,
867 },
868 "isError": true
869 })
870}
871
872fn invalid_arguments(error: serde_json::Error) -> AgentAdapterError {
873 AgentAdapterError::new(
874 AgentAdapterErrorKind::InvalidArgument,
875 format!("invalid tool arguments: {error}"),
876 )
877}
878
879fn domain_argument_error(error: impl fmt::Display) -> AgentAdapterError {
880 AgentAdapterError::new(AgentAdapterErrorKind::InvalidArgument, error.to_string())
881}
882
883fn json_rpc_success(id: Value, result: Value) -> Response {
884 json_response(
885 StatusCode::OK,
886 json!({ "jsonrpc": "2.0", "id": id, "result": result }),
887 )
888}
889
890fn json_rpc_success_with_session(id: Value, result: Value, session_id: &str) -> Response {
891 let mut response = json_rpc_success(id, result);
892 response.headers_mut().insert(
893 MCP_SESSION_ID_HEADER,
894 HeaderValue::from_str(session_id).expect("generated MCP session id is a valid header"),
895 );
896 response
897}
898
899fn json_rpc_error(id: Value, code: i64, message: impl Into<String>) -> Response {
900 json_response(
901 StatusCode::OK,
902 json!({
903 "jsonrpc": "2.0",
904 "id": id,
905 "error": {
906 "code": code,
907 "message": message.into()
908 }
909 }),
910 )
911}
912
913fn json_response(status: StatusCode, value: Value) -> Response {
914 (
915 status,
916 [(header::CONTENT_TYPE, "application/json")],
917 value.to_string(),
918 )
919 .into_response()
920}
921
922fn request_context(request_id: String) -> RequestContext {
923 RequestContext::with_ids(
924 InterfaceKind::Mcp,
925 format!("mcp-{request_id}"),
926 format!("trace-mcp-{request_id}"),
927 )
928}
929
930fn endpoint_child(endpoint: &str, child: &str) -> String {
931 if endpoint == "/" {
932 format!("/{child}")
933 } else {
934 format!("{}/{child}", endpoint.trim_end_matches('/'))
935 }
936}
937
938fn request_id_key(namespace: &str, value: &Value) -> Option<String> {
939 match value {
940 Value::String(value) => Some(format!("{namespace}|string:{value}")),
941 Value::Number(value) if value.is_i64() || value.is_u64() => {
942 Some(format!("{namespace}|number:{value}"))
943 }
944 _ => None,
945 }
946}
947
948fn is_json_rpc_id(value: &Value) -> bool {
949 match value {
950 Value::String(_) => true,
951 Value::Number(number) => number.is_i64() || number.is_u64(),
952 _ => false,
953 }
954}
955
956fn elapsed_millis(started: Instant) -> u64 {
957 u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
958}
959
960fn qos_message(reason: RejectReason) -> &'static str {
961 match reason {
962 RejectReason::ConnectionBudgetExceeded => "connection budget exhausted",
963 RejectReason::RequestBudgetExceeded => "request budget exhausted",
964 RejectReason::QueueBudgetExceeded => "queue budget exhausted",
965 }
966}
967
968#[cfg(test)]
969#[path = "mcp_test_support.rs"]
970mod mcp_test_support;
971
972#[cfg(test)]
973#[path = "mcp_tests.rs"]
974mod mcp_tests;
975
976#[cfg(test)]
977#[path = "mcp_tool_tests.rs"]
978mod mcp_tool_tests;
979
980#[cfg(test)]
981#[path = "mcp_feature_flag_tool_tests.rs"]
982mod mcp_feature_flag_tool_tests;
983
984#[cfg(test)]
985#[path = "mcp_protocol_tests.rs"]
986mod mcp_protocol_tests;