Skip to main content

soma_mcp/
rmcp_server.rs

1//! `SomaRmcpServer` — the `ServerHandler` implementation.
2//!
3//! This is the adapter between the rmcp crate and your application. It:
4//!   - Advertises tools, resources, and prompts to MCP clients
5//!   - Enforces auth scopes on every call
6//!   - Delegates business logic to `tools.rs` → `app.rs` → `soma-client`
7//!
8//! **Customize**: rename `SomaRmcpServer`. Update action metadata in
9//! `src/actions.rs` to keep schemas, scope rules, and dispatch in sync.
10
11use std::time::Instant;
12
13use rmcp::{
14    ErrorData, RoleServer, ServerHandler,
15    model::{
16        CacheScope, CallToolRequestParams, CallToolResponse, CancelTaskParams,
17        GetPromptRequestParams, GetPromptResponse, GetTaskParams, GetTaskResult, Implementation,
18        ListPromptsResult, ListResourceTemplatesResult, ListResourcesResult, ListToolsResult,
19        PaginatedRequestParams, ReadResourceRequestParams, ReadResourceResponse,
20        ReadResourceResult, ResourceContents, ResourceTemplate, ServerCapabilities, ServerInfo,
21        SubscriptionFilter, UpdateTaskParams,
22    },
23    service::{Peer, RequestContext, SubscriptionContext},
24};
25use serde_json::{Map, Value};
26
27use soma_application::{ApplicationError, GatewayMcpRoundTrip, ReadResourceRequest};
28use soma_mcp_server::{
29    conformance,
30    response_paging::{
31        response_page_request, strip_response_page_params, tool_result_from_cached_page,
32        tool_result_from_json,
33    },
34};
35
36use super::{
37    gateway_proxy, prompts,
38    protocol_errors::{application_error_payload, tool_error_result, unknown_tool_error},
39    rmcp_auth::{protected_route_scope, protected_scope_allows_service, require_auth_context},
40    state::McpState,
41    tools::execute_tool,
42    trace_resolution,
43};
44
45macro_rules! trace_summary_event {
46    ($level:ident, $trace_resolution:expr_2021, $trace_context_conflict:expr_2021, $message:literal, $($field:tt)*) => {
47        tracing::$level!(
48            $($field)*
49            trace_id_prefix = ?$trace_resolution.summary.trace_id_prefix(),
50            span_id_prefix = ?$trace_resolution.summary.span_id_prefix(),
51            trace_sampled = ?$trace_resolution.summary.sampled(),
52            trace_trust = ?$trace_resolution.summary.trust(),
53            has_tracestate = $trace_resolution.summary.has_tracestate(),
54            baggage_member_count = $trace_resolution.summary.baggage_member_count(),
55            sensitive_baggage_member_count = $trace_resolution.summary.sensitive_baggage_member_count(),
56            trace_invalid_count = $trace_resolution.summary.invalid_count(),
57            trace_invalid_reasons = ?$trace_resolution.summary.invalid_reasons(),
58            http_trace_headers_present = $trace_resolution.http_trace_headers_present,
59            trace_context_conflict = $trace_context_conflict,
60            $message
61        );
62    };
63}
64
65mod catalog_subscriptions;
66mod destructive_mrtr;
67mod support;
68
69use support::{
70    SCHEMA_RESOURCE_URI, SERVER_INSTRUCTIONS, empty_action_as_none, execution_context,
71    execution_context_with_trace, private_dynamic_read_response, private_prompts_result,
72    private_resource_templates_result, private_resources_result, private_tools_result,
73    refresh_file_providers, request_meta_value, resource_contents_from_output, resource_read_error,
74    response_paging_options, rmcp_resource_from_catalog_resource, rmcp_tool_definitions,
75    schema_resource, task_application_error, tool_definitions_for_state, trace_resolution_for_call,
76};
77
78// ── server ────────────────────────────────────────────────────────────────────
79
80#[derive(Clone)]
81pub struct SomaRmcpServer {
82    state: McpState,
83}
84
85pub fn rmcp_server(state: McpState) -> SomaRmcpServer {
86    SomaRmcpServer { state }
87}
88
89impl ServerHandler for SomaRmcpServer {
90    // ── tools ─────────────────────────────────────────────────────────────────
91
92    async fn list_tools(
93        &self,
94        _request: Option<PaginatedRequestParams>,
95        context: RequestContext<RoleServer>,
96    ) -> Result<ListToolsResult, ErrorData> {
97        let auth = require_auth_context(&self.state, &context)?;
98        let route_scope = protected_route_scope(&context);
99        let execution_context = execution_context(&self.state, &context, auth);
100        let soma_allowed = protected_scope_allows_service(route_scope, "soma");
101        refresh_file_providers(&self.state).await?;
102        let mut tools = if soma_allowed {
103            rmcp_tool_definitions(&self.state)?
104        } else {
105            Vec::new()
106        };
107        tools.extend(
108            gateway_proxy::list_tools_for_subject_and_scope(
109                self.state.application(),
110                route_scope,
111                &execution_context,
112            )
113            .await?,
114        );
115        if soma_allowed && self.state.config().conformance_fixtures {
116            tools.extend(conformance::tool_definitions());
117        }
118        tracing::debug!(tool_count = tools.len(), "MCP tools listed");
119        Ok(private_tools_result(tools))
120    }
121
122    async fn call_tool(
123        &self,
124        request: CallToolRequestParams,
125        context: RequestContext<RoleServer>,
126    ) -> Result<CallToolResponse, ErrorData> {
127        let tool_name = request.name.to_string();
128
129        // Extract action before scope check so a missing action returns the
130        // more useful "action is required" validation error, not DENY_SCOPE.
131        let action_opt: Option<String> = request
132            .arguments
133            .as_ref()
134            .and_then(|m| m.get("action"))
135            .and_then(Value::as_str)
136            .map(ToOwned::to_owned);
137
138        let response_page = match response_page_request(request.arguments.as_ref()) {
139            Ok(response_page) => response_page,
140            Err(error) => {
141                tracing::warn!("MCP tool rejected response paging params");
142                return Err(error);
143            }
144        };
145        let auth = match require_auth_context(&self.state, &context) {
146            Ok(auth) => auth,
147            Err(error) => {
148                tracing::warn!("MCP tool rejected auth context");
149                return Err(error);
150            }
151        };
152        let trace_resolution = trace_resolution_for_call(&self.state, &context);
153        let trace_context_conflict = trace_resolution.http_trace_headers_present
154            && trace_resolution::meta_has_any_trace_key(&context.meta);
155        let route_scope = protected_route_scope(&context);
156        let mut execution_context =
157            execution_context_with_trace(&self.state, auth, trace_resolution.trace_context.clone());
158        let soma_allowed = protected_scope_allows_service(route_scope, "soma");
159        if soma_allowed
160            && self.state.config().conformance_fixtures
161            && let Some(result) = conformance::call_tool(&tool_name)
162        {
163            return Ok(result.into());
164        }
165        if tool_name != "soma" {
166            let Some(requires_confirmation) = gateway_proxy::tool_requires_confirmation(
167                self.state.application(),
168                &tool_name,
169                route_scope,
170                &execution_context,
171            )
172            .await?
173            else {
174                return Err(unknown_tool_error(&tool_name));
175            };
176            let confirmation_action = action_opt
177                .as_deref()
178                .filter(|action| !action.is_empty())
179                .unwrap_or(&tool_name);
180            match destructive_mrtr::destructive_confirmation(
181                &request,
182                &context.meta,
183                confirmation_action,
184                requires_confirmation,
185            ) {
186                destructive_mrtr::DestructiveConfirmation::Proceed(confirmation) => {
187                    execution_context.destructive_confirmation = confirmation;
188                }
189                destructive_mrtr::DestructiveConfirmation::InputRequired(result) => {
190                    return Ok(CallToolResponse::InputRequired(result));
191                }
192                destructive_mrtr::DestructiveConfirmation::Refused => {
193                    return tool_error_result(serde_json::json!({
194                        "kind": "mcp_tool_error",
195                        "schema_version": 1,
196                        "code": "confirmation_required",
197                        "tool": tool_name,
198                        "action": confirmation_action,
199                        "message": "destructive gateway tool requires an accepted MCP form confirmation",
200                        "retryable": true,
201                        "remediation": "Retry with form elicitation support and accept the destructive confirmation, or send legacy confirm=true.",
202                    }))
203                    .map(Into::into);
204                }
205            }
206
207            if let Some(result) = gateway_proxy::call_tool_for_subject_and_scope(
208                self.state.application(),
209                &tool_name,
210                request.arguments.clone(),
211                GatewayMcpRoundTrip {
212                    input_responses: request.input_responses.clone(),
213                    request_state: request.request_state.clone(),
214                    request_meta: request_meta_value(&context),
215                },
216                route_scope,
217                &execution_context,
218            )
219            .await
220            {
221                match &result {
222                    CallToolResponse::Complete(result) if result.is_error == Some(true) => {
223                        trace_summary_event!(
224                            warn,
225                            trace_resolution,
226                            trace_context_conflict,
227                            "MCP gateway tool execution failed",
228                            tool = %tool_name,
229                            action = action_opt.as_deref().unwrap_or_default(),
230                        );
231                    }
232                    CallToolResponse::InputRequired(_) => {
233                        trace_summary_event!(
234                            info,
235                            trace_resolution,
236                            trace_context_conflict,
237                            "MCP gateway tool requires client input",
238                            tool = %tool_name,
239                            action = action_opt.as_deref().unwrap_or_default(),
240                        );
241                    }
242                    CallToolResponse::Task(_) => {
243                        trace_summary_event!(
244                            info,
245                            trace_resolution,
246                            trace_context_conflict,
247                            "MCP gateway tool returned a task",
248                            tool = %tool_name,
249                            action = action_opt.as_deref().unwrap_or_default(),
250                        );
251                    }
252                    _ => {
253                        trace_summary_event!(
254                            info,
255                            trace_resolution,
256                            trace_context_conflict,
257                            "MCP gateway tool execution completed",
258                            tool = %tool_name,
259                            action = action_opt.as_deref().unwrap_or_default(),
260                        );
261                    }
262                }
263                return Ok(result);
264            }
265            trace_summary_event!(
266                warn,
267                trace_resolution,
268                trace_context_conflict,
269                "MCP tool rejected unknown tool",
270                tool = %tool_name,
271                action = action_opt.as_deref().unwrap_or_default(),
272            );
273            return Err(unknown_tool_error(&tool_name));
274        }
275        if !soma_allowed {
276            return Err(unknown_tool_error(&tool_name));
277        }
278        let action: String = action_opt.unwrap_or_default();
279        if let Some(cursor) = response_page.cursor().map(str::to_owned) {
280            trace_summary_event!(
281                info,
282                trace_resolution,
283                trace_context_conflict,
284                "MCP tool returned cached response page",
285                tool = %tool_name,
286                action = empty_action_as_none(&action).unwrap_or_default(),
287            );
288            return tool_result_from_cached_page(
289                self.state.response_pages(),
290                &cursor,
291                response_page,
292                response_paging_options(),
293                &tool_name,
294                empty_action_as_none(&action),
295            )
296            .map(Into::into);
297        }
298
299        let requires_confirmation = self
300            .state
301            .application()
302            .action_requires_confirmation(&action);
303        match destructive_mrtr::destructive_confirmation(
304            &request,
305            &context.meta,
306            &action,
307            requires_confirmation,
308        ) {
309            destructive_mrtr::DestructiveConfirmation::Proceed(confirmation) => {
310                execution_context.destructive_confirmation = confirmation;
311            }
312            destructive_mrtr::DestructiveConfirmation::InputRequired(result) => {
313                return Ok(CallToolResponse::InputRequired(result));
314            }
315            destructive_mrtr::DestructiveConfirmation::Refused => {
316                return tool_error_result(serde_json::json!({
317                    "kind": "mcp_tool_error",
318                    "schema_version": 1,
319                    "code": "confirmation_required",
320                    "tool": tool_name,
321                    "action": action,
322                    "message": "destructive action requires an accepted MCP form confirmation",
323                    "retryable": true,
324                    "remediation": "Retry with form elicitation support and accept the destructive confirmation, or send legacy confirm=true.",
325                }))
326                .map(Into::into);
327            }
328        }
329
330        let mut arguments = request
331            .arguments
332            .map(Value::Object)
333            .unwrap_or_else(|| Value::Object(Map::new()));
334        strip_response_page_params(&mut arguments);
335        let continuation_args = arguments.as_object().cloned();
336
337        // Clone the peer so we can pass it to the tool dispatcher.
338        // The peer is needed for elicitation (asking the client for user input).
339        let peer: Peer<RoleServer> = context.peer.clone();
340        let started = Instant::now();
341        trace_summary_event!(
342            info,
343            trace_resolution,
344            trace_context_conflict,
345            "MCP tool execution started",
346            tool = %tool_name,
347            action = %action,
348        );
349
350        match execute_tool(&self.state, &tool_name, arguments, &peer, execution_context).await {
351            Ok(result) => {
352                trace_summary_event!(
353                    info,
354                    trace_resolution,
355                    trace_context_conflict,
356                    "MCP tool execution completed",
357                    tool = %tool_name,
358                    action = %action,
359                    elapsed_ms = started.elapsed().as_millis(),
360                );
361                tool_result_from_json(
362                    result,
363                    self.state.response_pages(),
364                    response_page,
365                    response_paging_options(),
366                    &tool_name,
367                    empty_action_as_none(&action),
368                    continuation_args.as_ref(),
369                )
370                .map(Into::into)
371            }
372            Err(error) => {
373                let application_error = error.downcast_ref::<ApplicationError>();
374                if application_error.is_some_and(ApplicationError::is_validation) {
375                    trace_summary_event!(
376                        warn,
377                        trace_resolution,
378                        trace_context_conflict,
379                        "MCP tool rejected invalid params",
380                        tool = %tool_name,
381                        action = %action,
382                        elapsed_ms = started.elapsed().as_millis(),
383                    );
384                } else {
385                    trace_summary_event!(
386                        error,
387                        trace_resolution,
388                        trace_context_conflict,
389                        "MCP tool execution failed",
390                        tool = %tool_name,
391                        action = %action,
392                        elapsed_ms = started.elapsed().as_millis(),
393                        service_error_kind = application_error
394                            .and_then(ApplicationError::service_error_kind),
395                        private_diagnostics = application_error
396                            .and_then(ApplicationError::private_diagnostics),
397                        error = %error,
398                    );
399                }
400                tool_error_result(application_error_payload(
401                    &error,
402                    &tool_name,
403                    empty_action_as_none(&action),
404                ))
405                .map(Into::into)
406            }
407        }
408    }
409
410    // ── resources ─────────────────────────────────────────────────────────────
411
412    async fn list_resources(
413        &self,
414        _request: Option<PaginatedRequestParams>,
415        context: RequestContext<RoleServer>,
416    ) -> Result<ListResourcesResult, ErrorData> {
417        let auth = require_auth_context(&self.state, &context)?;
418        let route_scope = protected_route_scope(&context);
419        let execution_context = execution_context(&self.state, &context, auth);
420        refresh_file_providers(&self.state).await?;
421        let soma_allowed = protected_scope_allows_service(route_scope, "soma");
422        let mut resources = if soma_allowed {
423            let mut resources = vec![schema_resource()];
424            resources.extend(
425                self.state
426                    .application()
427                    .list_resources()
428                    .iter()
429                    .map(rmcp_resource_from_catalog_resource),
430            );
431            resources
432        } else {
433            Vec::new()
434        };
435        resources.extend(
436            gateway_proxy::list_resources_for_subject_and_scope(
437                self.state.application(),
438                route_scope,
439                &execution_context,
440            )
441            .await?,
442        );
443        if soma_allowed && self.state.config().conformance_fixtures {
444            resources.extend(conformance::resources());
445        }
446        Ok(private_resources_result(resources))
447    }
448
449    async fn list_resource_templates(
450        &self,
451        _request: Option<PaginatedRequestParams>,
452        context: RequestContext<RoleServer>,
453    ) -> Result<ListResourceTemplatesResult, ErrorData> {
454        require_auth_context(&self.state, &context)?;
455        refresh_file_providers(&self.state).await?;
456        let mut resource_templates: Vec<ResourceTemplate> = self
457            .state
458            .application()
459            .list_resource_templates()
460            .into_iter()
461            .map(|template| {
462                let mut built = ResourceTemplate::new(template.uri_template, template.name)
463                    .with_description(template.description.clone());
464                if let Some(mime_type) = template.mime_type {
465                    built = built.with_mime_type(mime_type);
466                }
467                built
468            })
469            .collect();
470        if self.state.config().conformance_fixtures {
471            resource_templates.extend(conformance::resource_templates());
472        }
473        Ok(private_resource_templates_result(resource_templates))
474    }
475
476    async fn read_resource(
477        &self,
478        request: ReadResourceRequestParams,
479        context: RequestContext<RoleServer>,
480    ) -> Result<ReadResourceResponse, ErrorData> {
481        let auth = require_auth_context(&self.state, &context)?;
482        let route_scope = protected_route_scope(&context);
483        let execution_context = execution_context(&self.state, &context, auth);
484        let soma_allowed = protected_scope_allows_service(route_scope, "soma");
485        if soma_allowed
486            && self.state.config().conformance_fixtures
487            && let Some(result) = conformance::read_resource(&request.uri)
488        {
489            return Ok(private_dynamic_read_response(result.into()));
490        }
491        if let Some(result) = gateway_proxy::read_resource_for_subject_and_scope(
492            self.state.application(),
493            &request.uri,
494            GatewayMcpRoundTrip {
495                input_responses: request.input_responses.clone(),
496                request_state: request.request_state.clone(),
497                request_meta: request_meta_value(&context),
498            },
499            route_scope,
500            &execution_context,
501        )
502        .await?
503        {
504            return Ok(private_dynamic_read_response(result));
505        }
506        if !soma_allowed {
507            return Err(ErrorData::invalid_params(
508                format!("unknown resource: {}", request.uri),
509                None,
510            ));
511        }
512        refresh_file_providers(&self.state).await?;
513        if request.uri == SCHEMA_RESOURCE_URI {
514            let schema = tool_definitions_for_state(&self.state);
515            let text = serde_json::to_string_pretty(&schema).map_err(|e| {
516                ErrorData::internal_error(format!("serialization error: {e}"), None)
517            })?;
518            return Ok(ReadResourceResult::new(vec![
519                ResourceContents::text(text, SCHEMA_RESOURCE_URI)
520                    .with_mime_type("application/json"),
521            ])
522            .with_ttl_ms(0)
523            .with_cache_scope(CacheScope::Private)
524            .into());
525        }
526
527        let output = self
528            .state
529            .application()
530            .read_resource(
531                ReadResourceRequest {
532                    uri: request.uri.clone(),
533                },
534                execution_context,
535            )
536            .await
537            .map_err(|error| resource_read_error(&request.uri, &error))?;
538        Ok(
539            ReadResourceResult::new(vec![resource_contents_from_output(&request.uri, output)])
540                .with_ttl_ms(0)
541                .with_cache_scope(CacheScope::Private)
542                .into(),
543        )
544    }
545
546    // ── prompts ───────────────────────────────────────────────────────────────
547
548    async fn list_prompts(
549        &self,
550        _request: Option<PaginatedRequestParams>,
551        context: RequestContext<RoleServer>,
552    ) -> Result<ListPromptsResult, ErrorData> {
553        let auth = require_auth_context(&self.state, &context)?;
554        let route_scope = protected_route_scope(&context);
555        let execution_context = execution_context(&self.state, &context, auth);
556        let soma_allowed = protected_scope_allows_service(route_scope, "soma");
557        let mut result = if soma_allowed {
558            prompts::list_prompts()
559        } else {
560            ListPromptsResult::default()
561        };
562        refresh_file_providers(&self.state).await?;
563        if soma_allowed {
564            result.prompts.extend(prompts::provider_prompts(
565                &self.state.application().list_prompts(),
566            ));
567        }
568        result.prompts.extend(
569            gateway_proxy::list_prompts_for_subject_and_scope(
570                self.state.application(),
571                route_scope,
572                &execution_context,
573            )
574            .await?,
575        );
576        if soma_allowed && self.state.config().conformance_fixtures {
577            result.prompts.extend(conformance::prompts());
578        }
579        Ok(private_prompts_result(result))
580    }
581
582    async fn get_prompt(
583        &self,
584        request: GetPromptRequestParams,
585        context: RequestContext<RoleServer>,
586    ) -> Result<GetPromptResponse, ErrorData> {
587        let auth = require_auth_context(&self.state, &context)?;
588        let route_scope = protected_route_scope(&context);
589        let execution_context = execution_context(&self.state, &context, auth);
590        let soma_allowed = protected_scope_allows_service(route_scope, "soma");
591        if soma_allowed
592            && self.state.config().conformance_fixtures
593            && let Some(result) = conformance::get_prompt(request.clone())
594        {
595            return Ok(result.into());
596        }
597        if let Some(result) = gateway_proxy::get_prompt_for_subject_and_scope(
598            self.state.application(),
599            &request.name,
600            request.arguments.clone(),
601            GatewayMcpRoundTrip {
602                input_responses: request.input_responses.clone(),
603                request_state: request.request_state.clone(),
604                request_meta: request_meta_value(&context),
605            },
606            route_scope,
607            &execution_context,
608        )
609        .await?
610        {
611            return Ok(result);
612        }
613        if !soma_allowed {
614            return Err(ErrorData::invalid_params(
615                format!("unknown prompt: {}", request.name),
616                None,
617            ));
618        }
619        refresh_file_providers(&self.state).await?;
620        match self
621            .state
622            .application()
623            .get_prompt(&request.name, &execution_context)
624        {
625            Ok(prompt) => return Ok(prompts::provider_prompt_result(prompt).into()),
626            Err(error) if error.code == "insufficient_scope" => {
627                return Err(ErrorData::invalid_request(
628                    format!("forbidden: {}", error.message),
629                    None,
630                ));
631            }
632            Err(error) if error.code == "prompt_not_found" => {}
633            Err(error) => {
634                return Err(ErrorData::internal_error(error.message, None));
635            }
636        }
637        prompts::get_prompt(request)
638            .map(Into::into)
639            .map_err(|e| ErrorData::invalid_params(e.to_string(), None))
640    }
641
642    // ── subscriptions ──────────────────────────────────────────────────────────
643
644    fn accepted_subscription_filter(
645        &self,
646        requested: &SubscriptionFilter,
647    ) -> Option<SubscriptionFilter> {
648        // The rmcp service layer intersects this with Soma's advertised
649        // notification capabilities before acknowledging the subscription.
650        // Returning the requested filter enables the modern lifecycle without
651        // claiming notification categories Soma cannot currently emit.
652        Some(requested.clone())
653    }
654
655    async fn listen(&self, context: SubscriptionContext) -> Result<(), ErrorData> {
656        require_auth_context(&self.state, context.request_context())?;
657        tracing::debug!(
658            accepted = ?context.accepted(),
659            "MCP catalog subscription established"
660        );
661        let result = catalog_subscriptions::listen_for_catalog_changes(self, context).await;
662        tracing::debug!("MCP catalog subscription closed");
663        result
664    }
665
666    // ── tasks ─────────────────────────────────────────────────────────────────
667
668    async fn get_task(
669        &self,
670        request: GetTaskParams,
671        context: RequestContext<RoleServer>,
672    ) -> Result<GetTaskResult, ErrorData> {
673        let auth = require_auth_context(&self.state, &context)?;
674        let execution_context = execution_context(&self.state, &context, auth);
675        let value = self
676            .state
677            .application()
678            .gateway_get_mcp_task(&request.task_id, &execution_context)
679            .await
680            .map_err(task_application_error)?;
681        serde_json::from_value(value).map_err(|error| {
682            ErrorData::internal_error(
683                "gateway returned an invalid tasks/get result",
684                Some(serde_json::json!({"message": error.to_string()})),
685            )
686        })
687    }
688
689    async fn update_task(
690        &self,
691        request: UpdateTaskParams,
692        context: RequestContext<RoleServer>,
693    ) -> Result<(), ErrorData> {
694        let auth = require_auth_context(&self.state, &context)?;
695        let execution_context = execution_context(&self.state, &context, auth);
696        self.state
697            .application()
698            .gateway_update_mcp_task(
699                &request.task_id,
700                request.input_responses,
701                &execution_context,
702            )
703            .await
704            .map_err(task_application_error)
705    }
706
707    async fn cancel_task(
708        &self,
709        request: CancelTaskParams,
710        context: RequestContext<RoleServer>,
711    ) -> Result<(), ErrorData> {
712        let auth = require_auth_context(&self.state, &context)?;
713        let execution_context = execution_context(&self.state, &context, auth);
714        self.state
715            .application()
716            .gateway_cancel_mcp_task(&request.task_id, &execution_context)
717            .await
718            .map_err(task_application_error)
719    }
720
721    // ── server info ───────────────────────────────────────────────────────────
722
723    fn get_info(&self) -> ServerInfo {
724        ServerInfo::new(
725            ServerCapabilities::builder()
726                .enable_tools()
727                .enable_tool_list_changed()
728                .enable_resources()
729                .enable_resources_list_changed()
730                .enable_prompts()
731                .enable_prompts_list_changed()
732                .enable_tasks()
733                .build(),
734        )
735        .with_server_info(Implementation::new(
736            self.state.config().server_name.clone(),
737            env!("CARGO_PKG_VERSION"),
738        ))
739        .with_instructions(SERVER_INSTRUCTIONS)
740    }
741}
742
743#[cfg(test)]
744#[path = "rmcp_server_tests.rs"]
745mod tests;