Skip to main content

fastmcp_server/
router.rs

1//! Request router for MCP servers.
2
3use std::collections::{BTreeMap, HashMap, HashSet};
4use std::io::{self, Write};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::{Arc, Weak};
7use std::task::Poll;
8use std::time::Duration;
9
10#[cfg(feature = "tasks")]
11use crate::FinalTaskRuntime;
12use crate::Session;
13use crate::auth::AuthRequest;
14use crate::bidirectional::{
15    MrtrCompletedInputs, MrtrExchangeBinding, MrtrExchangeRegistry, MrtrInputKind,
16    MrtrInputRequest, MrtrInputRequests, MrtrInputRequired, MrtrRetry,
17};
18use crate::handler::{
19    BidirectionalSenders, BoxFuture, FinalMethodOutcome, FinalResourceReadCacheHintProvenance,
20    FinalResourceUriUse, FinalToolOutcome, ProgressNotificationSender, ResourceUriUsePolicy,
21    UriParams, empty_final_result_meta, encode_final_complete_result,
22};
23use crate::handler::{
24    BoxedCompletionHandler, BoxedPromptHandler, BoxedResourceHandler, BoxedToolHandler,
25    CompletionHandler, PromptHandler, ResourceHandler, ToolErrorKind, ToolHandler,
26};
27#[cfg(all(feature = "proxy", feature = "tasks"))]
28use crate::proxy::ProxyFinalTaskRelay;
29use crate::session::SessionPrincipalBinding;
30#[cfg(test)]
31use asupersync::time::wall_now;
32use asupersync::types::Time;
33use asupersync::{Budget, Cx, Outcome};
34use base64::Engine as _;
35use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
36use fastmcp_core::logging::{debug, targets, trace};
37use fastmcp_core::{
38    McpContext, McpError, McpErrorCode, McpOutcome, McpResult, PromptCaller, PromptGetResult,
39    PromptMessageItem, PromptMessageRole, SessionState, block_on, sha256_bounded,
40};
41#[cfg(feature = "tasks")]
42use fastmcp_protocol::MissingRequiredClientCapabilityError;
43use fastmcp_protocol::common_types::{
44    AbsoluteUri, Annotations, EmbeddedResourceContents, OpenMetadata, RawIcon,
45};
46#[cfg(feature = "tasks")]
47use fastmcp_protocol::extensions::OFFICIAL_TASKS_EXTENSION_ID;
48use fastmcp_protocol::methods::COMPLETION_COMPLETE;
49use fastmcp_protocol::protocol_policy::ProtocolEra;
50use fastmcp_protocol::uri_template::{ReversibleResourceTemplate, UriTemplatePart};
51use fastmcp_protocol::{
52    AdmittedSchema, CacheScope, CacheTtl, CallToolParams, CallToolResult, CompleteResult, Content,
53    CoreRequest, CoreResult, FinalCallToolParams, FinalCallToolResult, FinalCompletionParams,
54    FinalCompletionReference, FinalCompletionResult, FinalCoreRequest, FinalCoreResult,
55    FinalGetPromptParams, FinalGetPromptResult, FinalInputResponses, FinalListParams,
56    FinalListPromptsResult, FinalListResourceTemplatesResult, FinalListResourcesResult,
57    FinalListToolsResult, FinalPrompt, FinalPromptArgument, FinalReadResourceParams,
58    FinalReadResourceResult, FinalResource, FinalResourceTemplate, FinalTool, FinalToolAnnotations,
59    GetPromptParams, GetPromptResult, InitializeParams, InitializeResult, InputRequiredResult,
60    JsonRpcRequest, LegacyCompletionParams, LegacyCompletionResult, LegacyContent,
61    LegacyCoreRequest, LegacyPromptMessage, LegacyResourceContent, ListPromptsParams,
62    ListPromptsResult, ListResourceTemplatesParams, ListResourceTemplatesResult,
63    ListResourcesParams, ListResourcesResult, ListToolsParams, ListToolsResult, PROTOCOL_VERSION,
64    ProgressMarker, Prompt, PromptMessage, ReadResourceParams, ReadResourceResult, Resource,
65    ResourceContent, ResourceTemplate, Role, ServerBehavior, ServerBehaviorRegistry, TemplateValue,
66    Tool, admit_final_schema, exact_json_to_serde, validate, validate_strict,
67};
68
69/// Type alias for a notification sender callback.
70///
71/// This callback is used to send notifications (like progress updates) back to the client
72/// during request handling. The callback receives a JSON-RPC request (notification format).
73pub type NotificationSender = Arc<dyn Fn(JsonRpcRequest) + Send + Sync>;
74
75/// Allowlisted transport provenance attached to a sanitized inbound request.
76///
77/// This deliberately contains no peer address, headers, cookies, or
78/// credentials. Transport implementations retain those raw values inside their
79/// authentication boundary and pass only one of these validated facts to the
80/// server dispatch layer.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum InboundRequestTransport {
83    /// Standard input/output framing.
84    Stdio,
85    /// Streamable HTTP request handling.
86    Http,
87    /// Server-sent events transport.
88    Sse,
89    /// WebSocket transport.
90    WebSocket,
91    /// In-process transport used by embeddings and tests.
92    Memory,
93}
94
95/// Opaque custody for one transport's singleton `Authorization` field.
96///
97/// This deliberately has no `Debug` implementation and no raw-value getter.
98/// Only server admission can convert it into an [`AuthRequest`], so extension
99/// middleware and handlers cannot observe a native transport credential.
100#[derive(Clone, Default)]
101pub(crate) struct TransportAuthorization(Option<String>);
102
103impl TransportAuthorization {
104    /// Captures an already cardinality-validated native authorization field.
105    #[must_use]
106    pub(crate) fn from_singleton_header(value: Option<&str>) -> Self {
107        Self(value.map(ToOwned::to_owned))
108    }
109
110    pub(crate) fn auth_request<'a>(
111        &'a self,
112        method: &'a str,
113        params: Option<&'a serde_json::Value>,
114        request_id: u64,
115    ) -> AuthRequest<'a> {
116        AuthRequest {
117            method,
118            params,
119            transport_authorization: self.0.as_deref(),
120            request_id,
121        }
122    }
123}
124
125/// Sanitized, immutable ingress facts for one server dispatch.
126///
127/// The type intentionally has no `Clone`, `Serialize`, or `Debug`
128/// implementation. In particular, it offers no channel for raw headers or
129/// credentials. A native transport may retain its singleton authorization
130/// field in crate-private custody for server admission, but it never exposes
131/// that field through this public context. The server creates a fresh
132/// request-scoped [`McpContext`] from these facts for every dispatch.
133pub struct InboundRequestContext {
134    cx: Cx,
135    request_id: u64,
136    transport: InboundRequestTransport,
137    state: Option<SessionState>,
138    mrtr_continuation_cancellation: Option<fastmcp_core::McpRequestCancellation>,
139    transport_authorization: TransportAuthorization,
140    principal_binding: Option<SessionPrincipalBinding>,
141}
142
143impl InboundRequestContext {
144    /// Creates sanitized facts after transport metadata validation has
145    /// completed.
146    #[must_use]
147    pub fn new(cx: Cx, request_id: u64, transport: InboundRequestTransport) -> Self {
148        Self {
149            cx,
150            request_id,
151            transport,
152            state: None,
153            mrtr_continuation_cancellation: None,
154            transport_authorization: TransportAuthorization::default(),
155            principal_binding: None,
156        }
157    }
158
159    /// Creates sanitized facts for a request that belongs to one live modern
160    /// transport connection. The connection owns both the durable partition
161    /// used to bind MRTR retries and the cancellation authority that makes
162    /// retained continuations unusable after peer disconnect.
163    #[must_use]
164    pub(crate) fn with_modern_connection(
165        cx: Cx,
166        request_id: u64,
167        transport: InboundRequestTransport,
168        connection: &ModernConnection,
169    ) -> Self {
170        Self::with_modern_connection_and_transport_authorization(
171            cx,
172            request_id,
173            transport,
174            connection,
175            TransportAuthorization::default(),
176        )
177    }
178
179    /// Creates sanitized modern connection facts while retaining the native
180    /// authorization field solely for server admission.
181    #[must_use]
182    pub(crate) fn with_modern_connection_and_transport_authorization(
183        cx: Cx,
184        request_id: u64,
185        transport: InboundRequestTransport,
186        connection: &ModernConnection,
187        transport_authorization: TransportAuthorization,
188    ) -> Self {
189        Self::with_modern_connection_context(
190            cx,
191            request_id,
192            transport,
193            &connection.request_context(),
194            transport_authorization,
195        )
196    }
197
198    #[must_use]
199    pub(crate) fn with_modern_connection_context(
200        cx: Cx,
201        request_id: u64,
202        transport: InboundRequestTransport,
203        connection: &ModernConnectionRequestContext,
204        transport_authorization: TransportAuthorization,
205    ) -> Self {
206        Self {
207            cx,
208            request_id,
209            transport,
210            state: Some(connection.state.clone()),
211            mrtr_continuation_cancellation: Some(connection.continuation_cancellation.clone()),
212            transport_authorization,
213            principal_binding: Some(connection.principal_binding.clone()),
214        }
215    }
216
217    /// Returns the transport's allowlisted provenance fact.
218    #[must_use]
219    pub const fn transport(&self) -> InboundRequestTransport {
220        self.transport
221    }
222
223    /// Returns the request identity selected by the transport.
224    #[must_use]
225    pub const fn request_id(&self) -> u64 {
226        self.request_id
227    }
228
229    pub(crate) fn request_context(&self) -> McpContext {
230        // Anonymous modern HTTP POSTs are request-local. Give each one a
231        // fresh SessionState so disable_*/enable_* can mutate and publish
232        // list_changed to live subscriptions/listen streams without inventing
233        // a durable Mcp-Session-Id.
234        self.state.clone().map_or_else(
235            || McpContext::with_state(self.cx.clone(), self.request_id, SessionState::new()),
236            |state| McpContext::with_state(self.cx.clone(), self.request_id, state),
237        )
238    }
239
240    pub(crate) fn mrtr_continuation_cancellation(
241        &self,
242    ) -> Option<fastmcp_core::McpRequestCancellation> {
243        self.mrtr_continuation_cancellation.clone()
244    }
245
246    pub(crate) fn auth_request<'a>(
247        &'a self,
248        method: &'a str,
249        params: Option<&'a serde_json::Value>,
250    ) -> AuthRequest<'a> {
251        self.transport_authorization
252            .auth_request(method, params, self.request_id)
253    }
254
255    pub(crate) fn bind_or_verify_principal(&self, fingerprint: fastmcp_core::Sha256Digest) -> bool {
256        self.principal_binding
257            .as_ref()
258            .is_none_or(|binding| binding.bind_or_verify(fingerprint))
259    }
260
261    pub(crate) fn with_cx(mut self, cx: Cx) -> Self {
262        self.cx = cx;
263        self
264    }
265}
266
267/// Durable state and retained-continuation ownership for one modern stdio
268/// connection or retained modern HTTP session.
269///
270/// Dropping the owner is a terminal peer-disconnect event: it cancels every
271/// MRTR continuation minted by requests on this connection or session.
272/// Request contexts retain only clones of its state and cancellation capability, so a retained
273/// continuation cannot outlive the connection or session that issued it.
274pub(crate) struct ModernConnection {
275    state: SessionState,
276    continuation_cancellation: fastmcp_core::McpRequestCancellation,
277    principal_binding: SessionPrincipalBinding,
278}
279
280#[derive(Clone)]
281pub(crate) struct ModernConnectionRequestContext {
282    state: SessionState,
283    continuation_cancellation: fastmcp_core::McpRequestCancellation,
284    principal_binding: SessionPrincipalBinding,
285}
286
287impl ModernConnection {
288    pub(crate) fn new() -> Self {
289        Self::with_state(SessionState::new())
290    }
291
292    /// One modern HTTP POST. The bag is request-local: disable/enable can
293    /// publish `list_changed` without a durable `Mcp-Session-Id`, and the
294    /// response cache must not treat it as a partition identity.
295    pub(crate) fn new_request_local() -> Self {
296        Self::with_state(SessionState::ephemeral())
297    }
298
299    fn with_state(state: SessionState) -> Self {
300        Self {
301            state,
302            continuation_cancellation: fastmcp_core::McpRequestCancellation::new(),
303            principal_binding: SessionPrincipalBinding::default(),
304        }
305    }
306
307    pub(crate) fn disconnect(&self) {
308        self.continuation_cancellation.cancel();
309    }
310
311    pub(crate) fn request_context(&self) -> ModernConnectionRequestContext {
312        ModernConnectionRequestContext {
313            state: self.state.clone(),
314            continuation_cancellation: self.continuation_cancellation.clone(),
315            principal_binding: self.principal_binding.clone(),
316        }
317    }
318}
319
320impl Drop for ModernConnection {
321    fn drop(&mut self) {
322        self.disconnect();
323    }
324}
325
326/// Tag filtering parameters for list operations.
327#[derive(Debug, Clone, Default)]
328pub struct TagFilters<'a> {
329    /// Only include components with ALL of these tags (AND logic).
330    pub include: Option<&'a [String]>,
331    /// Exclude components with ANY of these tags (OR logic).
332    pub exclude: Option<&'a [String]>,
333}
334
335impl<'a> TagFilters<'a> {
336    /// Creates tag filters from include and exclude vectors.
337    pub fn new(include: Option<&'a Vec<String>>, exclude: Option<&'a Vec<String>>) -> Self {
338        Self {
339            include: include.map(|v| v.as_slice()),
340            exclude: exclude.map(|v| v.as_slice()),
341        }
342    }
343
344    /// Returns true if the given component tags pass the filter.
345    ///
346    /// - Include filter: component must have ALL include tags (AND logic)
347    /// - Exclude filter: component is rejected if it has ANY exclude tag (OR logic)
348    /// - Tag matching is case-insensitive
349    pub fn matches(&self, component_tags: &[String]) -> bool {
350        // Normalize component tags to lowercase for comparison
351        let component_tags_lower: Vec<String> =
352            component_tags.iter().map(|t| t.to_lowercase()).collect();
353
354        // Include filter: must have ALL specified tags
355        if let Some(include) = self.include {
356            // Empty include array means no filter (all pass)
357            if !include.is_empty() {
358                for tag in include {
359                    let tag_lower = tag.to_lowercase();
360                    if !component_tags_lower.contains(&tag_lower) {
361                        return false;
362                    }
363                }
364            }
365        }
366
367        // Exclude filter: rejected if has ANY specified tag
368        if let Some(exclude) = self.exclude {
369            for tag in exclude {
370                let tag_lower = tag.to_lowercase();
371                if component_tags_lower.contains(&tag_lower) {
372                    return false;
373                }
374            }
375        }
376
377        true
378    }
379}
380
381fn decode_cursor_offset(cursor: Option<&str>) -> McpResult<usize> {
382    let Some(cursor) = cursor else {
383        return Ok(0);
384    };
385
386    let decoded = BASE64_STANDARD.decode(cursor).map_err(|_| {
387        McpError::invalid_params("Invalid cursor (base64 decode failed)".to_string())
388    })?;
389    let v: serde_json::Value = serde_json::from_slice(&decoded)
390        .map_err(|_| McpError::invalid_params("Invalid cursor (JSON parse failed)".to_string()))?;
391    let offset = v
392        .get("offset")
393        .and_then(serde_json::Value::as_u64)
394        .ok_or_else(|| McpError::invalid_params("Invalid cursor (missing offset)".to_string()))?;
395
396    usize::try_from(offset)
397        .map_err(|_| McpError::invalid_params("Invalid cursor (offset too large)".to_string()))
398}
399
400/// The catalog a continuation cursor was minted for.
401///
402/// Paged list cursors bind the offset to this catalog discriminator, the
403/// router catalog revision, and the normalized query filters so a
404/// continuation cannot cross list methods, observe a changed catalog, or
405/// replay under different tag filters.
406#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
407#[serde(rename_all = "snake_case")]
408enum FinalCatalogKind {
409    Tools,
410    Resources,
411    ResourceTemplates,
412    Prompts,
413}
414
415/// Canonical identity for the final list filters whose semantics affect a page.
416///
417/// Tag matching is case-insensitive and ignores duplicate/order differences,
418/// so cursors bind those normalized semantics rather than incidental input
419/// spelling. `None` and an explicitly empty tag array consequently identify
420/// the same unfiltered catalog.
421#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
422#[serde(deny_unknown_fields)]
423struct FinalCatalogQuery {
424    include_tags: Vec<String>,
425    exclude_tags: Vec<String>,
426}
427
428impl FinalCatalogQuery {
429    fn from_final_list_params(params: &FinalListParams) -> Self {
430        Self::from_tag_filters(
431            params.include_tags.as_deref(),
432            params.exclude_tags.as_deref(),
433        )
434    }
435
436    fn from_tag_filters(include_tags: Option<&[String]>, exclude_tags: Option<&[String]>) -> Self {
437        Self {
438            include_tags: canonical_final_catalog_tags(include_tags),
439            exclude_tags: canonical_final_catalog_tags(exclude_tags),
440        }
441    }
442}
443
444fn canonical_final_catalog_tags(tags: Option<&[String]>) -> Vec<String> {
445    let Some(tags) = tags else {
446        return Vec::new();
447    };
448    let mut canonical = tags
449        .iter()
450        .map(|tag| tag.to_lowercase())
451        .collect::<Vec<_>>();
452    canonical.sort_unstable();
453    canonical.dedup();
454    canonical
455}
456
457#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
458#[serde(deny_unknown_fields)]
459struct FinalCatalogCursor {
460    catalog: FinalCatalogKind,
461    revision: u64,
462    query: FinalCatalogQuery,
463    offset: u64,
464}
465
466fn decode_final_catalog_cursor_offset(
467    cursor: Option<&str>,
468    expected_catalog: FinalCatalogKind,
469    expected_revision: u64,
470    expected_query: &FinalCatalogQuery,
471    catalog_length: usize,
472) -> McpResult<usize> {
473    let Some(cursor) = cursor else {
474        return Ok(0);
475    };
476
477    let decoded = BASE64_STANDARD.decode(cursor).map_err(|_| {
478        McpError::invalid_params("Invalid final catalog cursor (base64 decode failed)")
479    })?;
480    let cursor = serde_json::from_slice::<FinalCatalogCursor>(&decoded).map_err(|_| {
481        McpError::invalid_params("Invalid final catalog cursor (JSON parse failed)")
482    })?;
483    if cursor.catalog != expected_catalog {
484        return Err(McpError::invalid_params(
485            "final catalog cursor belongs to another list method",
486        ));
487    }
488    if cursor.revision != expected_revision {
489        return Err(McpError::invalid_params(
490            "final catalog cursor references a stale catalog revision",
491        ));
492    }
493    if &cursor.query != expected_query {
494        return Err(McpError::invalid_params(
495            "final catalog cursor does not match the requested query filters",
496        ));
497    }
498    let offset = usize::try_from(cursor.offset)
499        .map_err(|_| McpError::invalid_params("Invalid final catalog cursor (offset too large)"))?;
500    if offset >= catalog_length {
501        return Err(McpError::invalid_params(
502            "final catalog cursor offset is outside the requested catalog page",
503        ));
504    }
505    Ok(offset)
506}
507
508fn parse_stateless_params<T: serde::de::DeserializeOwned>(
509    params: Option<serde_json::Value>,
510) -> McpResult<T> {
511    let value = params.ok_or_else(|| McpError::invalid_params("Missing required parameters"))?;
512    serde_json::from_value(value).map_err(|error| McpError::invalid_params(error.to_string()))
513}
514
515fn parse_stateless_params_or_default<T: serde::de::DeserializeOwned + Default>(
516    params: Option<serde_json::Value>,
517) -> McpResult<T> {
518    match params {
519        Some(value) => serde_json::from_value(value)
520            .map_err(|error| McpError::invalid_params(error.to_string())),
521        None => Ok(T::default()),
522    }
523}
524
525/// Converts a completed stateless handler result through the final result
526/// contract while preserving the original typed [`McpError`] on refusal.
527fn encode_stateless_handler_result<T: serde::Serialize>(
528    result: McpResult<T>,
529) -> McpResult<serde_json::Value> {
530    encode_final_complete_result(result?)
531}
532
533/// Encodes a handler-authored final tool result without reprojecting it through
534/// the legacy result surface. This preserves the complete result's metadata
535/// and inert open members under the protocol-owned final codec.
536fn encode_final_tools_call_result(
537    result: McpResult<CompleteResult<FinalCallToolResult>>,
538) -> McpResult<serde_json::Value> {
539    let result = result?;
540    let encoded = CoreResult::Final(FinalCoreResult::ToolsCall {
541        result,
542        diagnostic: None,
543    })
544    .encode()
545    .map_err(|error| McpError::internal_error(error.to_string()))?;
546    serde_json::from_str(&encoded).map_err(McpError::from)
547}
548
549/// Encodes a framework-minted MRTR continuation without allowing a handler to
550/// select or replay its opaque request state.
551fn encode_mrtr_input_required_result(result: MrtrInputRequired) -> McpResult<serde_json::Value> {
552    serde_json::to_value(result)
553        .map_err(|_| McpError::internal_error("failed to encode MRTR input_required result"))
554}
555
556const MAX_MRTR_BINDING_BYTES: usize = 64 * 1024;
557const MAX_MRTR_RAW_PARAMS_BYTES: usize = 256 * 1024;
558const MAX_MRTR_RAW_INPUT_RESPONSES_BYTES: usize = 192 * 1024;
559const MAX_MRTR_RAW_JSON_DEPTH: usize = 32;
560const MAX_MRTR_RAW_JSON_VALUES: usize = 4_096;
561const MRTR_REQUIRES_BOUND_MODERN_CONNECTION: &str =
562    "MRTR-capable handlers require a bound modern connection";
563
564struct MrtrRawJsonCounter {
565    max_bytes: usize,
566    bytes: usize,
567}
568
569impl Write for MrtrRawJsonCounter {
570    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
571        let remaining = self.max_bytes.saturating_sub(self.bytes);
572        if buffer.len() > remaining {
573            return Err(io::Error::other("MRTR JSON byte limit exceeded"));
574        }
575        self.bytes += buffer.len();
576        Ok(buffer.len())
577    }
578
579    fn flush(&mut self) -> io::Result<()> {
580        Ok(())
581    }
582}
583
584fn admit_mrtr_raw_json_value(value: &serde_json::Value, max_bytes: usize) -> McpResult<()> {
585    fn count_values(value: &serde_json::Value, depth: usize, values: &mut usize) -> McpResult<()> {
586        if depth > MAX_MRTR_RAW_JSON_DEPTH {
587            return Err(McpError::invalid_params(
588                "MRTR JSON exceeds its nesting limit",
589            ));
590        }
591        *values = values.saturating_add(1);
592        if *values > MAX_MRTR_RAW_JSON_VALUES {
593            return Err(McpError::invalid_params(
594                "MRTR JSON exceeds its value limit",
595            ));
596        }
597        match value {
598            serde_json::Value::Array(items) => {
599                for value in items {
600                    count_values(value, depth + 1, values)?;
601                }
602            }
603            serde_json::Value::Object(members) => {
604                for value in members.values() {
605                    count_values(value, depth + 1, values)?;
606                }
607            }
608            serde_json::Value::Null
609            | serde_json::Value::Bool(_)
610            | serde_json::Value::Number(_)
611            | serde_json::Value::String(_) => {}
612        }
613        Ok(())
614    }
615
616    let mut values = 0;
617    count_values(value, 0, &mut values)?;
618    let mut counter = MrtrRawJsonCounter {
619        max_bytes,
620        bytes: 0,
621    };
622    serde_json::to_writer(&mut counter, value)
623        .map_err(|_| McpError::invalid_params("MRTR JSON exceeds its byte limit"))
624}
625
626enum FinalMrtrDispatch {
627    Fresh,
628    Resume(MrtrCompletedInputs),
629    InputRequired(serde_json::Value),
630}
631
632fn mrtr_digest(value: &impl serde::Serialize) -> McpResult<[u8; 32]> {
633    let bytes = serde_json::to_vec(value)
634        .map_err(|_| McpError::invalid_params("invalid MRTR operation binding"))?;
635    let digest = sha256_bounded(&bytes, MAX_MRTR_BINDING_BYTES)
636        .map_err(|_| McpError::invalid_params("MRTR operation binding exceeds its limit"))?;
637    Ok(*digest.as_bytes())
638}
639
640fn final_mrtr_binding(
641    request_ctx: &McpContext,
642    method: &'static str,
643    target: String,
644    arguments: &impl serde::Serialize,
645) -> McpResult<Option<MrtrExchangeBinding>> {
646    if target.len() > MAX_MRTR_BINDING_BYTES {
647        return Err(McpError::invalid_params("MRTR target exceeds its limit"));
648    }
649    // A binding is only consumable where retry state can live. Fresh
650    // dispatches on a session-less context stay valid; the binding is
651    // demanded again at the points that actually need it (retry resolution
652    // and input_required issuance).
653    let Some((session_partition, _revision)) = request_ctx.session_cache_partition() else {
654        return Ok(None);
655    };
656    let principal_digest = request_ctx
657        .auth()
658        .map(|auth| {
659            auth.session_owner()
660                .map(|owner| Ok(*owner.as_bytes()))
661                .unwrap_or_else(|| mrtr_digest(&auth))
662        })
663        .transpose()?;
664    Ok(Some(MrtrExchangeBinding::new(
665        method,
666        target,
667        mrtr_digest(arguments)?,
668        session_partition,
669        principal_digest,
670    )))
671}
672
673fn handler_mrtr_input_requests(
674    request_ctx: &McpContext,
675    result: &InputRequiredResult,
676) -> McpResult<MrtrInputRequests> {
677    let Some(input_requests) = result.input_requests() else {
678        return Ok(MrtrInputRequests::default());
679    };
680    if input_requests.members().is_empty() {
681        return Err(McpError::invalid_params(
682            "MRTR inputRequests must not be empty",
683        ));
684    }
685    MrtrInputRequests::new(
686        input_requests
687            .members()
688            .iter()
689            .map(|member| {
690                let value = exact_json_to_serde(&member.value)
691                    .map_err(|_| McpError::invalid_params("invalid MRTR input request"))?;
692                let request = MrtrInputRequest::from_wire(&value)?;
693                if request.kind() == MrtrInputKind::Sampling
694                    && !request_ctx.client_supports_sampling()
695                {
696                    return Err(McpError::invalid_request(
697                        "Final sampling is not advertised by the client",
698                    ));
699                }
700                Ok((member.name.clone(), request))
701            })
702            .collect::<McpResult<Vec<_>>>()?,
703    )
704}
705
706#[cfg(feature = "tasks")]
707fn encode_final_task_result(
708    result: fastmcp_protocol::tasks_extension::CreateTaskResult,
709) -> McpResult<serde_json::Value> {
710    let encoded = CoreResult::Final(FinalCoreResult::ToolsCallTask { result })
711        .encode()
712        .map_err(|error| McpError::internal_error(error.to_string()))?;
713    serde_json::from_str(&encoded).map_err(McpError::from)
714}
715
716#[cfg(feature = "tasks")]
717fn require_final_tasks_capability(metadata: &OpenMetadata) -> McpResult<()> {
718    let declared = metadata
719        .client_capabilities()
720        .map_err(|_| McpError::invalid_params("invalid final client capabilities"))?
721        .and_then(|capabilities| capabilities.get("extensions"))
722        .and_then(serde_json::Value::as_object)
723        .and_then(|extensions| extensions.get(OFFICIAL_TASKS_EXTENSION_ID))
724        .and_then(serde_json::Value::as_object)
725        .is_some_and(serde_json::Map::is_empty);
726    if declared {
727        return Ok(());
728    }
729
730    let mut extensions = serde_json::Map::new();
731    extensions.insert(
732        OFFICIAL_TASKS_EXTENSION_ID.to_owned(),
733        serde_json::json!({}),
734    );
735    let missing = MissingRequiredClientCapabilityError::new(serde_json::json!({
736        "extensions": serde_json::Value::Object(extensions)
737    }))
738    .map_err(|_| McpError::internal_error("failed to encode required Tasks capability"))?;
739    Err(McpError::with_data(
740        McpErrorCode::Custom(missing.jsonrpc_error_code()),
741        "Required client capability is missing",
742        missing.canonical_error_data(),
743    ))
744}
745
746/// Admits the schemas a local tool declares for a final-dialect call.
747///
748/// The returned values own immutable copies of the declarations so one exact
749/// admitted pair is used for both the pre-handler input check and the
750/// post-handler output check. Legacy dispatch intentionally retains its raw
751/// compatibility validators.
752#[derive(Clone)]
753struct FinalToolSchemas {
754    input: Option<AdmittedSchema>,
755    output: Option<AdmittedSchema>,
756    errors: Option<FinalToolErrorStructuredContent>,
757}
758
759#[derive(Clone)]
760struct FinalToolErrorStructuredContent {
761    input_validation: serde_json::Value,
762    handler: serde_json::Value,
763}
764
765const MAX_FINAL_TOOL_ERROR_STRUCTURED_CONTENT_BYTES: usize = 16 * 1024;
766
767fn admit_final_tool_error_structured_content<H: ToolHandler + ?Sized>(
768    handler: &H,
769    output: &AdmittedSchema,
770    kind: ToolErrorKind,
771) -> McpResult<serde_json::Value> {
772    let mapped = crate::catch_extension_unwind(|| {
773        handler.final_tool_error_structured_content(kind)
774    })
775    .map_err(|_payload| {
776        McpError::internal_error("tool error structured-content mapper panicked during admission")
777    })?
778    .ok_or_else(|| {
779        McpError::internal_error(
780            "tool declares outputSchema without a complete tool-error structured-content mapper",
781        )
782    })?;
783    let encoded = serde_json::to_vec(&mapped).map_err(|_error| {
784        McpError::internal_error("tool error structured-content mapper returned invalid JSON")
785    })?;
786    if encoded.len() > MAX_FINAL_TOOL_ERROR_STRUCTURED_CONTENT_BYTES {
787        return Err(McpError::internal_error(
788            "tool error structured-content mapper exceeded the registration limit",
789        ));
790    }
791    if output.validate(&mapped).is_err() {
792        return Err(McpError::internal_error(
793            "tool error structured-content mapper does not satisfy outputSchema",
794        ));
795    }
796    Ok(mapped)
797}
798
799fn admit_final_tool_schemas<H: ToolHandler + ?Sized>(
800    upstream_schema_registered: bool,
801    input_schema: &serde_json::Value,
802    output_schema: Option<&serde_json::Value>,
803    handler: &H,
804) -> McpResult<FinalToolSchemas> {
805    if upstream_schema_registered {
806        // The upstream selected and already admitted this exact schema. In
807        // particular, a proxy must retain a valid non-object JSON Schema
808        // rather than treating it as a local framework schema or inventing a
809        // `{}` error payload to satisfy it.
810        return Ok(FinalToolSchemas {
811            input: None,
812            output: None,
813            errors: None,
814        });
815    }
816    if !input_schema.is_object() {
817        return Err(McpError::internal_error(
818            "tool declares a final input schema that is not an object",
819        ));
820    }
821    if input_schema.get("type").and_then(serde_json::Value::as_str) != Some("object") {
822        return Err(McpError::internal_error(
823            "tool declares a final input schema without type object",
824        ));
825    }
826    let input = admit_final_schema(input_schema.clone()).map_err(|_error| {
827        McpError::internal_error("tool declares an invalid final input schema")
828    })?;
829    let output = output_schema
830        .cloned()
831        .map(|schema| {
832            if !schema.is_object() {
833                return Err(McpError::internal_error(
834                    "tool declares a final output schema that is not an object",
835                ));
836            }
837            admit_final_schema(schema).map_err(|_error| {
838                McpError::internal_error("tool declares an invalid final output schema")
839            })
840        })
841        .transpose()?;
842    let errors = output
843        .as_ref()
844        .map(|output| -> McpResult<FinalToolErrorStructuredContent> {
845            Ok(FinalToolErrorStructuredContent {
846                input_validation: admit_final_tool_error_structured_content(
847                    handler,
848                    output,
849                    ToolErrorKind::InputValidation,
850                )?,
851                handler: admit_final_tool_error_structured_content(
852                    handler,
853                    output,
854                    ToolErrorKind::Handler,
855                )?,
856            })
857        })
858        .transpose()?;
859    Ok(FinalToolSchemas {
860        input: Some(input),
861        output,
862        errors,
863    })
864}
865
866/// One immutable catalog snapshot committed together with its dispatch target.
867/// No list or validation path re-invokes the handler's definition hooks.
868struct AdmittedToolRegistration {
869    handler: BoxedToolHandler,
870    definition: Tool,
871    final_registration: Option<AdmittedFinalToolRegistration>,
872    legacy_enabled: bool,
873}
874
875struct AdmittedFinalToolRegistration {
876    final_definition: FinalTool,
877    schemas: FinalToolSchemas,
878    declares_final_tasks: bool,
879}
880
881/// Immutable final resource catalog data, including the legacy tag snapshot
882/// used only for server-side list filtering.
883struct AdmittedFinalResourceRegistration {
884    definition: FinalResource,
885    tags: Vec<String>,
886    uri_use_policy: ResourceUriUsePolicy,
887}
888
889/// Immutable final prompt catalog data, including the legacy tag snapshot
890/// used only for server-side list filtering.
891struct AdmittedFinalPromptRegistration {
892    definition: FinalPrompt,
893    tags: Vec<String>,
894    uri_use_policy: ResourceUriUsePolicy,
895}
896
897impl AdmittedToolRegistration {
898    fn admit<H: ToolHandler + 'static>(
899        handler: H,
900        definition: Tool,
901        legacy_enabled: bool,
902    ) -> McpResult<Self> {
903        let (exact_final_definition, declares_final_tasks, upstream_schema_registered) =
904            crate::catch_extension_unwind(|| {
905                (
906                    handler.final_definition(),
907                    handler.declares_final_tasks(),
908                    handler.upstream_final_tool_schema_registration().is_some(),
909                )
910            })
911            .map_err(|_payload| {
912                McpError::internal_error("tool metadata hook panicked during admission")
913            })?;
914        let final_definition = match exact_final_definition {
915            Some(definition) => definition,
916            None => {
917                let (title, icons, metadata) = crate::catch_extension_unwind(|| {
918                    (
919                        handler.final_title().map(str::to_owned),
920                        handler.final_icons().map(|icons| icons.to_vec()),
921                        handler.final_metadata().cloned(),
922                    )
923                })
924                .map_err(|_payload| {
925                    McpError::internal_error("tool metadata hook panicked during admission")
926                })?;
927                FinalTool {
928                    name: definition.name.clone(),
929                    title,
930                    description: definition.description.clone(),
931                    input_schema: definition.input_schema.clone(),
932                    output_schema: definition.output_schema.clone(),
933                    annotations: definition
934                        .annotations
935                        .clone()
936                        .map(project_final_tool_annotations),
937                    icons,
938                    meta: metadata,
939                }
940            }
941        };
942        if final_definition.name != definition.name {
943            return Err(McpError::internal_error(
944                "tool's exact final definition name differs from its legacy definition name",
945            ));
946        }
947        let schemas = admit_final_tool_schemas(
948            upstream_schema_registered,
949            &final_definition.input_schema,
950            final_definition.output_schema.as_ref(),
951            &handler,
952        )?;
953        Ok(Self {
954            handler: Box::new(handler),
955            definition,
956            final_registration: Some(AdmittedFinalToolRegistration {
957                final_definition,
958                schemas,
959                declares_final_tasks,
960            }),
961            legacy_enabled,
962        })
963    }
964
965    fn legacy_only<H: ToolHandler + 'static>(handler: H, definition: Tool) -> Self {
966        Self {
967            handler: Box::new(handler),
968            definition,
969            final_registration: None,
970            legacy_enabled: true,
971        }
972    }
973
974    fn with_mounted_name(self, mounted_name: String) -> Self {
975        use crate::handler::MountedToolHandler;
976
977        let Self {
978            handler,
979            mut definition,
980            mut final_registration,
981            legacy_enabled,
982        } = self;
983        definition.name.clone_from(&mounted_name);
984        if let Some(final_registration) = final_registration.as_mut() {
985            final_registration
986                .final_definition
987                .name
988                .clone_from(&mounted_name);
989        }
990        Self {
991            handler: Box::new(MountedToolHandler::new(handler, mounted_name)),
992            definition,
993            final_registration,
994            legacy_enabled,
995        }
996    }
997}
998
999/// Encodes a handler-authored final resource result without reprojecting it
1000/// through the legacy resource result surface. This preserves final embedded
1001/// resource fields, cache policy, metadata, and open members selected by the
1002/// handler.
1003fn encode_final_resources_read_result(
1004    result: McpResult<CompleteResult<FinalReadResourceResult>>,
1005) -> McpResult<serde_json::Value> {
1006    let result = result?;
1007    let encoded = CoreResult::Final(FinalCoreResult::ResourcesRead {
1008        result,
1009        diagnostic: None,
1010    })
1011    .encode()
1012    .map_err(|error| McpError::internal_error(error.to_string()))?;
1013    serde_json::from_str(&encoded).map_err(McpError::from)
1014}
1015
1016/// Encodes a handler-authored final prompt result without reprojecting it
1017/// through the legacy prompt surface. This preserves final common content and
1018/// result metadata selected by the handler.
1019fn encode_final_prompts_get_result(
1020    result: McpResult<CompleteResult<FinalGetPromptResult>>,
1021) -> McpResult<serde_json::Value> {
1022    let result = result?;
1023    let encoded = CoreResult::Final(FinalCoreResult::PromptsGet {
1024        result,
1025        diagnostic: None,
1026    })
1027    .encode()
1028    .map_err(|error| McpError::internal_error(error.to_string()))?;
1029    serde_json::from_str(&encoded).map_err(McpError::from)
1030}
1031
1032#[derive(Clone, Debug, PartialEq, Eq)]
1033struct FinalCacheHintPolicy {
1034    list_ttl_ms: CacheTtl,
1035    resource_read_ttl_ms: CacheTtl,
1036    scope: CacheScope,
1037}
1038
1039impl Default for FinalCacheHintPolicy {
1040    fn default() -> Self {
1041        Self {
1042            list_ttl_ms: CacheTtl::milliseconds(5 * 60 * 1_000),
1043            resource_read_ttl_ms: CacheTtl::milliseconds(60 * 60 * 1_000),
1044            scope: CacheScope::Private,
1045        }
1046    }
1047}
1048
1049/// Encodes a server-authored final payload through the selected method's exact
1050/// `FinalCoreResult` composition. This preserves typed catalog and cache
1051/// fields instead of inserting unvalidated JSON after serialization.
1052fn encode_final_core_result<T>(
1053    result: McpResult<T>,
1054    select: impl FnOnce(CompleteResult<T>) -> FinalCoreResult,
1055) -> McpResult<serde_json::Value> {
1056    let result = CompleteResult::new(result?, empty_final_result_meta()?);
1057    let encoded = CoreResult::Final(select(result))
1058        .encode()
1059        .map_err(|error| McpError::internal_error(error.to_string()))?;
1060    serde_json::from_str(&encoded).map_err(McpError::from)
1061}
1062
1063fn project_final_tool_annotations(
1064    annotations: fastmcp_protocol::ToolAnnotations,
1065) -> FinalToolAnnotations {
1066    FinalToolAnnotations {
1067        title: None,
1068        destructive: annotations.destructive,
1069        idempotent: annotations.idempotent,
1070        read_only: annotations.read_only,
1071        open_world_hint: annotations.open_world_hint,
1072    }
1073}
1074
1075fn project_final_resource_catalog_entry(
1076    resource: Resource,
1077    title: Option<String>,
1078    icons: Option<Vec<RawIcon>>,
1079    annotations: Option<Annotations>,
1080    meta: Option<OpenMetadata>,
1081) -> McpResult<FinalResource> {
1082    let uri = AbsoluteUri::parse(resource.uri).map_err(|error| {
1083        McpError::internal_error(format!(
1084            "legacy resource URI cannot be projected into the final catalog: {error}",
1085        ))
1086    })?;
1087    Ok(FinalResource {
1088        uri,
1089        name: resource.name,
1090        title,
1091        description: resource.description,
1092        icons,
1093        mime_type: resource.mime_type,
1094        size: None,
1095        annotations,
1096        meta,
1097    })
1098}
1099
1100const FINAL_RESOURCE_URI_USE_REJECTED: &str =
1101    "final resource URI is not admitted for this local use site";
1102const FINAL_RESOURCE_URI_USE_EMISSION_REJECTED: &str =
1103    "handler emitted a final resource URI that is not admitted for this local use site";
1104
1105fn admit_final_resource_uri(
1106    policy: ResourceUriUsePolicy,
1107    uri: &AbsoluteUri,
1108    use_site: FinalResourceUriUse,
1109) -> McpResult<()> {
1110    policy
1111        .admits(uri, use_site)
1112        .then_some(())
1113        .ok_or_else(|| McpError::invalid_params(FINAL_RESOURCE_URI_USE_REJECTED))
1114}
1115
1116fn admit_final_resource_template_uri(
1117    policy: ResourceUriUsePolicy,
1118    uri_template: &str,
1119) -> McpResult<()> {
1120    policy
1121        .admits_template(uri_template)
1122        .then_some(())
1123        .ok_or_else(|| McpError::invalid_params(FINAL_RESOURCE_URI_USE_REJECTED))
1124}
1125
1126fn enforce_final_resource_uri_emission(
1127    policy: ResourceUriUsePolicy,
1128    uri: &AbsoluteUri,
1129    use_site: FinalResourceUriUse,
1130) -> McpResult<()> {
1131    policy
1132        .admits(uri, use_site)
1133        .then_some(())
1134        .ok_or_else(|| McpError::internal_error(FINAL_RESOURCE_URI_USE_EMISSION_REJECTED))
1135}
1136
1137fn embedded_resource_uri(contents: &EmbeddedResourceContents) -> &AbsoluteUri {
1138    match contents {
1139        EmbeddedResourceContents::Text { uri, .. } | EmbeddedResourceContents::Blob { uri, .. } => {
1140            uri
1141        }
1142    }
1143}
1144
1145fn admit_final_resource_read_outcome(
1146    policy: ResourceUriUsePolicy,
1147    outcome: &FinalMethodOutcome<FinalReadResourceResult>,
1148) -> McpResult<()> {
1149    let FinalMethodOutcome::Complete(result) = outcome else {
1150        return Ok(());
1151    };
1152    for contents in &result.payload.contents {
1153        enforce_final_resource_uri_emission(
1154            policy,
1155            embedded_resource_uri(contents),
1156            FinalResourceUriUse::ResourceReadContents,
1157        )?;
1158    }
1159    Ok(())
1160}
1161
1162fn admit_final_prompt_content(
1163    policy: ResourceUriUsePolicy,
1164    content: &fastmcp_protocol::common_types::ContentBlock,
1165) -> McpResult<()> {
1166    match content {
1167        fastmcp_protocol::common_types::ContentBlock::ResourceLink { uri, .. } => {
1168            enforce_final_resource_uri_emission(
1169                policy,
1170                uri,
1171                FinalResourceUriUse::PromptResourceLink,
1172            )
1173        }
1174        fastmcp_protocol::common_types::ContentBlock::Resource { resource, .. } => {
1175            enforce_final_resource_uri_emission(
1176                policy,
1177                embedded_resource_uri(resource),
1178                FinalResourceUriUse::PromptEmbeddedResource,
1179            )
1180        }
1181        fastmcp_protocol::common_types::ContentBlock::Text { .. }
1182        | fastmcp_protocol::common_types::ContentBlock::Image { .. }
1183        | fastmcp_protocol::common_types::ContentBlock::Audio { .. } => Ok(()),
1184    }
1185}
1186
1187fn admit_final_prompt_outcome(
1188    policy: ResourceUriUsePolicy,
1189    outcome: &FinalMethodOutcome<FinalGetPromptResult>,
1190) -> McpResult<()> {
1191    let FinalMethodOutcome::Complete(result) = outcome else {
1192        return Ok(());
1193    };
1194    for message in &result.payload.messages {
1195        admit_final_prompt_content(policy, &message.content)?;
1196    }
1197    Ok(())
1198}
1199
1200/// Freezes one resource's modern catalog entry during registration.
1201///
1202/// Discovery must not call application hooks: a catalog observed by a final
1203/// peer has to remain the one that was admitted alongside its dispatch target.
1204fn admit_final_resource_definition<H: ResourceHandler + ?Sized>(
1205    handler: &H,
1206    resource: &Resource,
1207    uri_use_policy: ResourceUriUsePolicy,
1208) -> McpResult<FinalResource> {
1209    if let Some(definition) =
1210        crate::catch_extension_unwind(|| handler.final_definition()).map_err(|_payload| {
1211            McpError::internal_error("resource final-definition hook panicked during admission")
1212        })?
1213    {
1214        if definition.uri.as_str() != resource.uri {
1215            return Err(McpError::internal_error(
1216                "resource exact final URI differs from its legacy definition URI",
1217            ));
1218        }
1219        admit_final_resource_uri(
1220            uri_use_policy,
1221            &definition.uri,
1222            FinalResourceUriUse::CatalogResource,
1223        )?;
1224        return Ok(definition);
1225    }
1226    let (title, icons, annotations, meta) = crate::catch_extension_unwind(|| {
1227        (
1228            handler.final_title().map(str::to_owned),
1229            handler.final_icons().map(|icons| icons.to_vec()),
1230            handler.final_annotations().cloned(),
1231            handler.final_metadata().cloned(),
1232        )
1233    })
1234    .map_err(|_payload| {
1235        McpError::internal_error("resource metadata hook panicked during admission")
1236    })?;
1237    let definition =
1238        project_final_resource_catalog_entry(resource.clone(), title, icons, annotations, meta)?;
1239    admit_final_resource_uri(
1240        uri_use_policy,
1241        &definition.uri,
1242        FinalResourceUriUse::CatalogResource,
1243    )?;
1244    Ok(definition)
1245}
1246
1247/// Freezes one resource template's final catalog entry during registration.
1248fn admit_final_resource_template_definition<H: ResourceHandler + ?Sized>(
1249    handler: Option<&H>,
1250    template: &ResourceTemplate,
1251    uri_use_policy: ResourceUriUsePolicy,
1252) -> McpResult<FinalResourceTemplate> {
1253    if let Some(handler) = handler {
1254        if let Some(definition) =
1255            crate::catch_extension_unwind(|| handler.final_template_definition()).map_err(
1256                |_payload| {
1257                    McpError::internal_error(
1258                        "resource final-template-definition hook panicked during admission",
1259                    )
1260                },
1261            )?
1262        {
1263            if definition.uri_template != template.uri_template {
1264                return Err(McpError::internal_error(
1265                    "resource exact final template differs from its legacy template URI",
1266                ));
1267            }
1268            admit_final_resource_template_uri(uri_use_policy, &definition.uri_template)?;
1269            return Ok(definition);
1270        }
1271    }
1272    let (title, icons, annotations, meta) = match handler {
1273        Some(handler) => crate::catch_extension_unwind(|| {
1274            (
1275                handler.final_template_title().map(str::to_owned),
1276                handler.final_template_icons().map(|icons| icons.to_vec()),
1277                handler.final_template_annotations().cloned(),
1278                handler.final_template_metadata().cloned(),
1279            )
1280        })
1281        .map_err(|_payload| {
1282            McpError::internal_error("resource template metadata hook panicked during admission")
1283        })?,
1284        None => (None, None, None, None),
1285    };
1286    let definition = FinalResourceTemplate {
1287        uri_template: template.uri_template.clone(),
1288        name: template.name.clone(),
1289        title,
1290        description: template.description.clone(),
1291        icons,
1292        mime_type: template.mime_type.clone(),
1293        annotations,
1294        meta,
1295    };
1296    admit_final_resource_template_uri(uri_use_policy, &definition.uri_template)?;
1297    Ok(definition)
1298}
1299
1300/// Freezes one prompt's final catalog entry during registration.
1301fn admit_final_prompt_definition<H: PromptHandler + ?Sized>(
1302    handler: &H,
1303    prompt: &Prompt,
1304) -> McpResult<FinalPrompt> {
1305    if let Some(definition) =
1306        crate::catch_extension_unwind(|| handler.final_definition()).map_err(|_payload| {
1307            McpError::internal_error("prompt final-definition hook panicked during admission")
1308        })?
1309    {
1310        if definition.name != prompt.name {
1311            return Err(McpError::internal_error(
1312                "prompt exact final name differs from its legacy definition name",
1313            ));
1314        }
1315        return Ok(definition);
1316    }
1317    let (title, icons, meta) = crate::catch_extension_unwind(|| {
1318        (
1319            handler.final_title().map(str::to_owned),
1320            handler.final_icons().map(|icons| icons.to_vec()),
1321            handler.final_metadata().cloned(),
1322        )
1323    })
1324    .map_err(|_payload| {
1325        McpError::internal_error("prompt metadata hook panicked during admission")
1326    })?;
1327    let arguments = (!prompt.arguments.is_empty()).then(|| {
1328        prompt
1329            .arguments
1330            .iter()
1331            .map(|argument| FinalPromptArgument {
1332                name: argument.name.clone(),
1333                title: None,
1334                description: argument.description.clone(),
1335                required: Some(argument.required),
1336            })
1337            .collect()
1338    });
1339    Ok(FinalPrompt {
1340        name: prompt.name.clone(),
1341        title,
1342        description: prompt.description.clone(),
1343        icons,
1344        arguments,
1345        meta,
1346    })
1347}
1348
1349/// Converts handler-owned content into the exact legacy result union.
1350///
1351/// Audio is valid only in the broader handler surface, never in the exact
1352/// 2024-11-05 content union. Refuse it rather than emitting an invalid legacy
1353/// response or silently changing the content type.
1354fn legacy_content_from_handler(content: Content) -> McpResult<LegacyContent> {
1355    match content {
1356        Content::Text { text } => Ok(LegacyContent::Text {
1357            text,
1358            annotations: None,
1359            additional: BTreeMap::new(),
1360        }),
1361        Content::Image { data, mime_type } => Ok(LegacyContent::Image {
1362            data,
1363            mime_type,
1364            annotations: None,
1365            additional: BTreeMap::new(),
1366        }),
1367        Content::Resource { resource } => Ok(LegacyContent::Resource {
1368            resource: legacy_resource_content_from_handler(resource)?,
1369            annotations: None,
1370            additional: BTreeMap::new(),
1371        }),
1372        Content::Audio { .. } => Err(McpError::internal_error(
1373            "legacy 2024 result content does not support audio",
1374        )),
1375    }
1376}
1377
1378fn legacy_contents_from_handler(content: Vec<Content>) -> McpResult<Vec<LegacyContent>> {
1379    content
1380        .into_iter()
1381        .map(legacy_content_from_handler)
1382        .collect()
1383}
1384
1385/// Converts handler-owned resource content into an exact legacy result item.
1386fn legacy_resource_content_from_handler(
1387    resource: ResourceContent,
1388) -> McpResult<LegacyResourceContent> {
1389    match (resource.text, resource.blob) {
1390        (Some(text), None) => Ok(LegacyResourceContent::Text {
1391            uri: resource.uri,
1392            text,
1393            mime_type: resource.mime_type,
1394            additional: BTreeMap::new(),
1395        }),
1396        (None, Some(blob)) => Ok(LegacyResourceContent::Blob {
1397            uri: resource.uri,
1398            blob,
1399            mime_type: resource.mime_type,
1400            additional: BTreeMap::new(),
1401        }),
1402        _ => Err(McpError::internal_error(
1403            "legacy 2024 resource content requires exactly one text or blob payload",
1404        )),
1405    }
1406}
1407
1408fn legacy_resource_contents_from_handler(
1409    contents: Vec<ResourceContent>,
1410) -> McpResult<Vec<LegacyResourceContent>> {
1411    contents
1412        .into_iter()
1413        .map(legacy_resource_content_from_handler)
1414        .collect()
1415}
1416
1417fn legacy_prompt_messages_from_handler(
1418    messages: Vec<PromptMessage>,
1419) -> McpResult<Vec<LegacyPromptMessage>> {
1420    messages
1421        .into_iter()
1422        .map(|PromptMessage { role, content }| {
1423            Ok(LegacyPromptMessage {
1424                role,
1425                content: legacy_content_from_handler(content)?,
1426                additional: BTreeMap::new(),
1427            })
1428        })
1429        .collect()
1430}
1431
1432/// Promotes exact legacy resource content into a final resource result.
1433///
1434/// Legacy open members remain inert but are retained verbatim. In particular,
1435/// an untyped legacy `_meta` value stays in `additional` rather than acquiring
1436/// final-era metadata authority during the cross-era projection.
1437fn promote_legacy_resource_content(
1438    resource: LegacyResourceContent,
1439) -> McpResult<EmbeddedResourceContents> {
1440    let (uri, content, mime_type, additional) = match resource {
1441        LegacyResourceContent::Text {
1442            uri,
1443            text,
1444            mime_type,
1445            additional,
1446        } => (
1447            uri,
1448            LegacyEmbeddedContent::Text(text),
1449            mime_type,
1450            additional,
1451        ),
1452        LegacyResourceContent::Blob {
1453            uri,
1454            blob,
1455            mime_type,
1456            additional,
1457        } => (
1458            uri,
1459            LegacyEmbeddedContent::Blob(blob),
1460            mime_type,
1461            additional,
1462        ),
1463    };
1464    let uri = AbsoluteUri::parse(uri).map_err(|error| {
1465        McpError::internal_error(format!(
1466            "legacy resource content cannot be projected into the final result: {error}",
1467        ))
1468    })?;
1469
1470    match content {
1471        LegacyEmbeddedContent::Text(text) => Ok(EmbeddedResourceContents::Text {
1472            uri,
1473            text,
1474            mime_type,
1475            meta: None,
1476            additional,
1477        }),
1478        LegacyEmbeddedContent::Blob(blob) => Ok(EmbeddedResourceContents::Blob {
1479            uri,
1480            blob,
1481            mime_type,
1482            meta: None,
1483            additional,
1484        }),
1485    }
1486}
1487
1488enum LegacyEmbeddedContent {
1489    Text(String),
1490    Blob(String),
1491}
1492
1493fn legacy_read_resource_params(params: FinalReadResourceParams) -> ReadResourceParams {
1494    ReadResourceParams {
1495        uri: params.uri.as_str().to_owned(),
1496        meta: None,
1497    }
1498}
1499
1500fn encode_cursor_offset(offset: usize) -> String {
1501    let payload = serde_json::json!({ "offset": offset });
1502    let bytes = serde_json::to_vec(&payload).expect("cursor state must serialize");
1503    BASE64_STANDARD.encode(bytes)
1504}
1505
1506fn encode_final_catalog_cursor(
1507    catalog: FinalCatalogKind,
1508    revision: u64,
1509    query: &FinalCatalogQuery,
1510    offset: usize,
1511) -> String {
1512    let offset = u64::try_from(offset).expect("usize cursor offsets always fit u64");
1513    let payload = FinalCatalogCursor {
1514        catalog,
1515        revision,
1516        query: query.clone(),
1517        offset,
1518    };
1519    let bytes = serde_json::to_vec(&payload).expect("final catalog cursor must serialize");
1520    BASE64_STANDARD.encode(bytes)
1521}
1522
1523/// Pages an already-filtered final catalog snapshot.
1524///
1525/// Filtering must precede cursor arithmetic so a legacy-only or tag-filtered
1526/// entry cannot create an empty modern page or shift a final peer's cursor.
1527fn page_final_catalog<T: Clone>(
1528    items: Vec<T>,
1529    cursor: Option<&str>,
1530    page_size: Option<usize>,
1531    catalog: FinalCatalogKind,
1532    revision: u64,
1533    query: &FinalCatalogQuery,
1534) -> McpResult<(Vec<T>, Option<String>)> {
1535    let offset = decode_final_catalog_cursor_offset(cursor, catalog, revision, query, items.len())?;
1536    let Some(page_size) = page_size else {
1537        return Ok((items, None));
1538    };
1539    let end = offset.saturating_add(page_size).min(items.len());
1540    Ok((
1541        items.get(offset..end).unwrap_or_default().to_vec(),
1542        (end < items.len()).then(|| encode_final_catalog_cursor(catalog, revision, query, end)),
1543    ))
1544}
1545
1546const SANITIZED_HANDLER_PANIC_MESSAGE: &str = "Internal server error";
1547static NEXT_HANDLER_INCIDENT_ID: AtomicU64 = AtomicU64::new(1);
1548
1549/// Maximum number of peer-controlled label bytes admitted to the log-key hash.
1550///
1551/// Labels longer than this retain their exact byte length in logs, but their
1552/// correlation key covers only this bounded prefix. This keeps observability
1553/// useful without allowing an attacker to turn debug logging into unbounded
1554/// hashing work.
1555const LOG_LABEL_HASH_INPUT_LIMIT: usize = 4 * 1024;
1556const LOG_LABEL_DIGEST_PREFIX_BYTES: usize = 8;
1557
1558#[derive(Clone, Copy)]
1559struct SafeLogLabel {
1560    byte_len: usize,
1561    hashed_bytes: usize,
1562    digest_prefix: [u8; LOG_LABEL_DIGEST_PREFIX_BYTES],
1563}
1564
1565impl std::fmt::Display for SafeLogLabel {
1566    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1567        write!(f, "bytes={},sha256_prefix=", self.byte_len)?;
1568        for byte in self.digest_prefix {
1569            write!(f, "{byte:02x}")?;
1570        }
1571        if self.hashed_bytes < self.byte_len {
1572            write!(f, ",hashed_prefix_bytes={}", self.hashed_bytes)?;
1573        }
1574        Ok(())
1575    }
1576}
1577
1578impl std::fmt::Debug for SafeLogLabel {
1579    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1580        std::fmt::Display::fmt(self, f)
1581    }
1582}
1583
1584fn safe_log_label(value: &str) -> SafeLogLabel {
1585    let bytes = value.as_bytes();
1586    let hashed_bytes = bytes.len().min(LOG_LABEL_HASH_INPUT_LIMIT);
1587    let bounded_prefix = &bytes[..hashed_bytes];
1588    let mut digest_prefix = [0_u8; LOG_LABEL_DIGEST_PREFIX_BYTES];
1589    if let Ok(digest) = sha256_bounded(bounded_prefix, LOG_LABEL_HASH_INPUT_LIMIT) {
1590        digest_prefix.copy_from_slice(&digest.as_bytes()[..LOG_LABEL_DIGEST_PREFIX_BYTES]);
1591    }
1592
1593    SafeLogLabel {
1594        byte_len: bytes.len(),
1595        hashed_bytes,
1596        digest_prefix,
1597    }
1598}
1599
1600fn duplicate_registration_error(component: &'static str, key: &str) -> McpError {
1601    McpError::invalid_request(format!(
1602        "{component} already exists; component_key={}",
1603        safe_log_label(key)
1604    ))
1605}
1606
1607fn compose_handler_budget(
1608    ambient: Budget,
1609    server_or_request: Budget,
1610    handler_timeout: Option<Duration>,
1611    now: Time,
1612) -> Budget {
1613    let inherited = ambient.meet(server_or_request);
1614    match handler_timeout {
1615        Some(timeout) if !timeout.is_zero() => inherited.tightened_by_timeout(now, timeout),
1616        Some(_) | None => inherited,
1617    }
1618}
1619
1620fn budget_error(ctx: &McpContext) -> Option<McpError> {
1621    if ctx.ensure_live().is_err() {
1622        return Some(McpError::request_cancelled());
1623    }
1624    None
1625}
1626
1627fn sanitized_handler_panic(_request_lifetime: &Cx, handler_class: &'static str) -> McpError {
1628    let incident_id = NEXT_HANDLER_INCIDENT_ID.fetch_add(1, Ordering::Relaxed);
1629    log::error!(
1630        target: "fastmcp_rust::handler",
1631        "handler terminated unexpectedly; incident_id={incident_id}; class={handler_class}; detail=panic_payload_redacted"
1632    );
1633    McpError::internal_error(SANITIZED_HANDLER_PANIC_MESSAGE)
1634}
1635
1636fn sanitized_handler_internal_error(
1637    _request_lifetime: &Cx,
1638    handler_class: &'static str,
1639) -> McpError {
1640    let incident_id = NEXT_HANDLER_INCIDENT_ID.fetch_add(1, Ordering::Relaxed);
1641    log::error!(
1642        target: "fastmcp_rust::handler",
1643        "handler returned an opaque internal failure; incident_id={incident_id}; class={handler_class}; detail=internal_error_redacted"
1644    );
1645    McpError::internal_error(SANITIZED_HANDLER_PANIC_MESSAGE)
1646}
1647
1648fn sanitize_handler_error(cx: &Cx, handler_class: &'static str, error: McpError) -> McpError {
1649    if error.code == McpErrorCode::InternalError {
1650        sanitized_handler_internal_error(cx, handler_class)
1651    } else {
1652        error
1653    }
1654}
1655
1656const fn is_framework_terminal_tool_error(code: McpErrorCode) -> bool {
1657    matches!(
1658        code,
1659        McpErrorCode::InternalError | McpErrorCode::RequestCancelled
1660    )
1661}
1662
1663fn read_handler_timeout(
1664    cx: &Cx,
1665    handler_class: &'static str,
1666    read: impl FnOnce() -> Option<Duration>,
1667) -> McpResult<Option<Duration>> {
1668    crate::catch_extension_unwind(read)
1669        .map_err(|_payload| sanitized_handler_panic(cx, handler_class))
1670}
1671
1672#[allow(
1673    dead_code,
1674    reason = "retained as the blocking dispatcher if a remaining session entry cannot yet take a request-owned child Cx"
1675)]
1676fn run_handler<'a, T>(
1677    ctx: &McpContext,
1678    budget: Budget,
1679    handler_class: &'static str,
1680    make_future: impl FnOnce() -> BoxFuture<'a, McpOutcome<T>>,
1681) -> McpResult<McpOutcome<T>> {
1682    if let Some(error) = budget_error(ctx) {
1683        return Err(error);
1684    }
1685
1686    let execution = crate::catch_extension_unwind(|| {
1687        let future = make_future();
1688        match budget.deadline {
1689            Some(deadline) => block_on(async move {
1690                asupersync::time::timeout_at(deadline, future)
1691                    .await
1692                    .map_err(|_elapsed| ())
1693            }),
1694            None => Ok(block_on(future)),
1695        }
1696    });
1697
1698    match execution {
1699        Err(_payload) => Err(sanitized_handler_panic(ctx.cx(), handler_class)),
1700        Ok(Err(())) => Err(McpError::new(
1701            McpErrorCode::RequestCancelled,
1702            "Request timeout exceeded",
1703        )),
1704        Ok(Ok(outcome)) => {
1705            if budget_error(ctx).is_some() {
1706                // A synchronous handler cannot be preempted by timeout_at, so
1707                // a late completion surfaces here; deadline expiry keeps its
1708                // distinguishable timeout message.
1709                if budget.is_past_deadline(ctx.cx().now()) {
1710                    Err(McpError::new(
1711                        McpErrorCode::RequestCancelled,
1712                        "Request timeout exceeded",
1713                    ))
1714                } else {
1715                    Err(McpError::request_cancelled())
1716                }
1717            } else {
1718                Ok(outcome)
1719            }
1720        }
1721    }
1722}
1723
1724/// Drives one handler future without entering the legacy blocking dispatcher.
1725///
1726/// The future stays inside its request-owned child task: timeout drops the
1727/// pending future, and dropping the parent task's join cancels the child before
1728/// the parent can complete. This helper deliberately receives the child Cx
1729/// separately from the framework context so modern handlers can propagate that
1730/// structured capability to their own nested work.
1731async fn run_handler_in_request<'a, T>(
1732    ctx: &'a McpContext,
1733    request_cx: &'a Cx,
1734    budget: Budget,
1735    handler_class: &'static str,
1736    make_future: impl FnOnce(&'a Cx) -> BoxFuture<'a, McpOutcome<T>>,
1737) -> McpResult<McpOutcome<T>> {
1738    if request_cx.is_cancel_requested() || budget_error(ctx).is_some() {
1739        return Err(McpError::request_cancelled());
1740    }
1741
1742    let future = crate::catch_extension_unwind(|| make_future(request_cx))
1743        .map_err(|_payload| sanitized_handler_panic(ctx.cx(), handler_class))?;
1744    let mut future = future;
1745    let poll_handler = std::future::poll_fn(|task_cx| {
1746        match crate::catch_extension_unwind(|| future.as_mut().poll(task_cx)) {
1747            Ok(Poll::Ready(outcome)) => Poll::Ready(Ok(outcome)),
1748            Ok(Poll::Pending) => Poll::Pending,
1749            Err(_payload) => Poll::Ready(Err(())),
1750        }
1751    });
1752
1753    let outcome = match budget.deadline {
1754        Some(deadline) => match asupersync::time::timeout_at(deadline, poll_handler).await {
1755            Ok(Ok(outcome)) => outcome,
1756            Ok(Err(())) => return Err(sanitized_handler_panic(ctx.cx(), handler_class)),
1757            Err(_elapsed) => {
1758                return Err(McpError::new(
1759                    McpErrorCode::RequestCancelled,
1760                    "Request timeout exceeded",
1761                ));
1762            }
1763        },
1764        None => match poll_handler.await {
1765            Ok(outcome) => outcome,
1766            Err(()) => return Err(sanitized_handler_panic(ctx.cx(), handler_class)),
1767        },
1768    };
1769
1770    if request_cx.is_cancel_requested() || budget_error(ctx).is_some() {
1771        Err(McpError::request_cancelled())
1772    } else {
1773        Ok(outcome)
1774    }
1775}
1776
1777fn derive_handler_context(
1778    request_ctx: &McpContext,
1779    progress_marker: Option<ProgressMarker>,
1780    notification_sender: Option<&NotificationSender>,
1781    bidirectional_senders: Option<&BidirectionalSenders>,
1782    protocol_era: ProtocolEra,
1783) -> McpContext {
1784    trace!(
1785        target: targets::HANDLER,
1786        "Deriving handler context for request {}",
1787        request_ctx.request_id()
1788    );
1789    let mut handler_ctx = request_ctx.clone();
1790
1791    // The owned modern dispatch path may already have installed a
1792    // request-scoped final-progress runtime. Preserve that reporter so its
1793    // marker, monotonic high-water mark, coalescing slot, and eventual
1794    // terminal finalization remain one request-owned authority.
1795    let reuse_installed_final_reporter =
1796        matches!(protocol_era, ProtocolEra::Modern2026) && handler_ctx.has_progress_reporter();
1797
1798    if let (Some(marker), Some(sender)) = (progress_marker, notification_sender)
1799        && !reuse_installed_final_reporter
1800    {
1801        let sender = sender.clone();
1802        let reporter = match protocol_era {
1803            ProtocolEra::Legacy2024 => ProgressNotificationSender::new(marker, move |request| {
1804                sender(request);
1805            })
1806            .into_reporter(),
1807            ProtocolEra::Modern2026 => {
1808                ProgressNotificationSender::new_final(marker, move |request| {
1809                    sender(request);
1810                })
1811                .into_reporter()
1812            }
1813        };
1814        handler_ctx = handler_ctx.with_progress_reporter(reporter);
1815    }
1816
1817    if let Some(sender) = notification_sender {
1818        let sender = sender.clone();
1819        handler_ctx = handler_ctx.with_log_sender(Arc::new(
1820            crate::handler::LogNotificationSender::new(move |request| {
1821                sender(request);
1822            }),
1823        ));
1824    }
1825
1826    if let Some(senders) = bidirectional_senders {
1827        if let Some(ref sampling) = senders.sampling {
1828            handler_ctx = handler_ctx.with_sampling(sampling.clone());
1829        }
1830        if let Some(ref elicitation) = senders.elicitation {
1831            handler_ctx = handler_ctx.with_elicitation(elicitation.clone());
1832        }
1833        if let Some(ref roots) = senders.roots {
1834            handler_ctx = handler_ctx.with_roots_provider(roots.clone());
1835        }
1836    }
1837
1838    handler_ctx
1839}
1840
1841fn final_progress_marker(metadata: &OpenMetadata) -> McpResult<Option<ProgressMarker>> {
1842    metadata
1843        .get("progressToken")
1844        .map(|value| {
1845            serde_json::from_value(value.clone()).map_err(|_| {
1846                McpError::invalid_params("final progressToken must be a string or integer")
1847            })
1848        })
1849        .transpose()
1850}
1851
1852/// Routes MCP requests to the appropriate handlers.
1853pub struct Router {
1854    tools: HashMap<String, AdmittedToolRegistration>,
1855    tool_order: Vec<String>,
1856    completion_handler: Option<BoxedCompletionHandler>,
1857    /// Whether the server-wide completion fallback is admitted for final dispatch.
1858    default_final_completion_enabled: bool,
1859    /// Legacy completion providers selected by exact prompt name.
1860    legacy_prompt_completion_handlers: HashMap<String, BoxedCompletionHandler>,
1861    /// Legacy completion providers selected by exact resource-template URI.
1862    legacy_resource_template_completion_handlers: HashMap<String, BoxedCompletionHandler>,
1863    /// Final completion providers selected by exact prompt name.
1864    final_prompt_completion_handlers: HashMap<String, BoxedCompletionHandler>,
1865    /// Final completion providers selected by exact resource-template URI.
1866    final_resource_template_completion_handlers: HashMap<String, BoxedCompletionHandler>,
1867    resources: HashMap<String, BoxedResourceHandler>,
1868    /// Static resources visible to exact MCP 2024-11-05 only.
1869    final_only_resources: HashSet<String>,
1870    /// Immutable final catalog entries. Absence means exact-legacy-only.
1871    final_resources: HashMap<String, AdmittedFinalResourceRegistration>,
1872    resource_order: Vec<String>,
1873    prompts: HashMap<String, BoxedPromptHandler>,
1874    /// Prompts visible to exact MCP 2024-11-05 only.
1875    final_only_prompts: HashSet<String>,
1876    /// Immutable final catalog entries. Absence means exact-legacy-only.
1877    final_prompts: HashMap<String, AdmittedFinalPromptRegistration>,
1878    prompt_order: Vec<String>,
1879    resource_templates: HashMap<String, ResourceTemplateEntry>,
1880    resource_template_order: Vec<String>,
1881    /// Pre-sorted template keys by specificity (most specific first).
1882    /// Updated whenever templates are added/modified.
1883    sorted_template_keys: Vec<String>,
1884    /// Whether to enforce strict input validation (reject extra properties).
1885    strict_input_validation: bool,
1886    /// Optional list page size for cursor-based pagination.
1887    ///
1888    /// When `None`, list methods return all items in a single response and
1889    /// `nextCursor` is always omitted.
1890    list_page_size: Option<usize>,
1891    /// Monotonic revision bound into every paged catalog continuation cursor.
1892    ///
1893    /// This advances after each successful catalog mutation so a retained
1894    /// cursor cannot observe a later catalog snapshot.
1895    final_catalog_revision: u64,
1896    /// Cache policy emitted on exact modern catalog and resource-read results.
1897    final_cache_hints: FinalCacheHintPolicy,
1898    /// Application-owned durable final Tasks runtime used only after the
1899    /// request metadata has admitted the official extension.
1900    #[cfg(feature = "tasks")]
1901    final_task_runtime: Option<FinalTaskRuntime>,
1902    /// One route-bound upstream final Tasks relay. This is distinct from the
1903    /// local runtime because upstream task IDs must never be recreated or
1904    /// translated into local task state.
1905    #[cfg(all(feature = "proxy", feature = "tasks"))]
1906    final_task_relay: Option<Arc<ProxyFinalTaskRelay>>,
1907    /// Bounded, process-local final request-state records for tool, resource,
1908    /// and prompt retries. A state is accepted only if this router issued it.
1909    mrtr_exchanges: Arc<MrtrExchangeRegistry>,
1910}
1911
1912impl Router {
1913    /// Creates a new empty router.
1914    #[must_use]
1915    pub fn new() -> Self {
1916        Self {
1917            tools: HashMap::new(),
1918            tool_order: Vec::new(),
1919            completion_handler: None,
1920            default_final_completion_enabled: false,
1921            legacy_prompt_completion_handlers: HashMap::new(),
1922            legacy_resource_template_completion_handlers: HashMap::new(),
1923            final_prompt_completion_handlers: HashMap::new(),
1924            final_resource_template_completion_handlers: HashMap::new(),
1925            resources: HashMap::new(),
1926            final_only_resources: HashSet::new(),
1927            final_resources: HashMap::new(),
1928            resource_order: Vec::new(),
1929            prompts: HashMap::new(),
1930            final_only_prompts: HashSet::new(),
1931            final_prompts: HashMap::new(),
1932            prompt_order: Vec::new(),
1933            resource_templates: HashMap::new(),
1934            resource_template_order: Vec::new(),
1935            sorted_template_keys: Vec::new(),
1936            strict_input_validation: false,
1937            list_page_size: None,
1938            final_catalog_revision: 0,
1939            final_cache_hints: FinalCacheHintPolicy::default(),
1940            #[cfg(feature = "tasks")]
1941            final_task_runtime: None,
1942            #[cfg(all(feature = "proxy", feature = "tasks"))]
1943            final_task_relay: None,
1944            mrtr_exchanges: Arc::new(MrtrExchangeRegistry::new()),
1945        }
1946    }
1947
1948    /// Returns the number of framework-minted MRTR exchanges for crate tests.
1949    #[cfg(test)]
1950    pub(crate) fn test_active_mrtr_exchange_count(&self) -> usize {
1951        self.mrtr_exchanges.active_len()
1952    }
1953
1954    #[cfg(feature = "tasks")]
1955    pub(crate) fn set_final_task_runtime(&mut self, runtime: Option<FinalTaskRuntime>) {
1956        self.final_task_runtime = runtime;
1957    }
1958
1959    #[cfg(all(feature = "proxy", feature = "tasks"))]
1960    pub(crate) fn set_final_task_relay(&mut self, relay: Option<Arc<ProxyFinalTaskRelay>>) {
1961        self.final_task_relay = relay;
1962    }
1963
1964    /// Sets the list pagination page size.
1965    ///
1966    /// When set, list methods (`tools/list`, `resources/list`,
1967    /// `resources/templates/list`, and `prompts/list`) will page results using
1968    /// opaque base64 cursors.
1969    pub fn set_list_page_size(&mut self, page_size: Option<usize>) {
1970        self.list_page_size = page_size.filter(|n| *n > 0);
1971    }
1972
1973    fn advance_final_catalog_revision(&mut self) {
1974        self.final_catalog_revision = self
1975            .final_catalog_revision
1976            .checked_add(1)
1977            .expect("final catalog revision cannot overflow");
1978    }
1979
1980    /// Sets the cache hints emitted by final catalog and resource-read
1981    /// responses. The default is a five-minute private catalog TTL and a
1982    /// one-hour private resource-read TTL.
1983    pub fn set_final_cache_hint_policy(
1984        &mut self,
1985        list_ttl_ms: CacheTtl,
1986        resource_read_ttl_ms: CacheTtl,
1987        scope: CacheScope,
1988    ) {
1989        self.final_cache_hints = FinalCacheHintPolicy {
1990            list_ttl_ms,
1991            resource_read_ttl_ms,
1992            scope,
1993        };
1994    }
1995
1996    /// Returns the active final cache-hint policy as
1997    /// `(&list_ttl_ms, &resource_read_ttl_ms, scope)`.
1998    #[must_use]
1999    pub fn final_cache_hint_policy(&self) -> (&CacheTtl, &CacheTtl, CacheScope) {
2000        (
2001            &self.final_cache_hints.list_ttl_ms,
2002            &self.final_cache_hints.resource_read_ttl_ms,
2003            self.final_cache_hints.scope,
2004        )
2005    }
2006
2007    /// Sets whether to use strict input validation.
2008    ///
2009    /// When enabled, tool input validation will reject any properties not
2010    /// explicitly defined in the tool's input schema (enforces `additionalProperties: false`).
2011    ///
2012    /// When disabled (default), extra properties are allowed unless the schema
2013    /// explicitly sets `additionalProperties: false`.
2014    pub fn set_strict_input_validation(&mut self, strict: bool) {
2015        self.strict_input_validation = strict;
2016    }
2017
2018    /// Returns whether strict input validation is enabled.
2019    #[must_use]
2020    pub fn strict_input_validation(&self) -> bool {
2021        self.strict_input_validation
2022    }
2023
2024    /// Rebuilds the sorted template keys vector.
2025    /// Called after any modification to resource_templates.
2026    fn rebuild_sorted_template_keys(&mut self) {
2027        self.sorted_template_keys = self.resource_templates.keys().cloned().collect();
2028        self.sorted_template_keys.sort_by(|a, b| {
2029            let entry_a = &self.resource_templates[a];
2030            let entry_b = &self.resource_templates[b];
2031            let (a_literals, a_literal_segments, a_segments) = entry_a.specificity;
2032            let (b_literals, b_literal_segments, b_segments) = entry_b.specificity;
2033            b_literals
2034                .cmp(&a_literals)
2035                .then(b_literal_segments.cmp(&a_literal_segments))
2036                .then(b_segments.cmp(&a_segments))
2037                .then_with(|| a.cmp(b))
2038        });
2039    }
2040
2041    /// Rejects a modern handler template that could select an already-admitted
2042    /// modern resource route. Dispatch must never resolve such a collision by
2043    /// registration order or by the specificity tie-breaker.
2044    fn reject_final_template_collisions(
2045        &self,
2046        candidate: &ReversibleResourceTemplate,
2047        replacing_source: Option<&str>,
2048    ) -> McpResult<()> {
2049        for uri in self.final_resources.keys() {
2050            if candidate
2051                .match_uri(uri)
2052                .map_err(|_| McpError::invalid_params("resource template match admission failed"))?
2053                .is_some()
2054            {
2055                return Err(McpError::invalid_params(
2056                    "resource template collides with an exact final resource",
2057                ));
2058            }
2059        }
2060
2061        for (source, entry) in &self.resource_templates {
2062            if replacing_source == Some(source.as_str()) {
2063                continue;
2064            }
2065            let Some(existing) = entry.matcher.as_ref() else {
2066                continue;
2067            };
2068            if reversible_templates_may_overlap(candidate, existing)? {
2069                return Err(McpError::invalid_params(
2070                    "resource template collides with an admitted final resource template",
2071                ));
2072            }
2073        }
2074
2075        Ok(())
2076    }
2077
2078    /// Rejects a modern exact resource whose byte-exact URI is matched by an
2079    /// already-admitted modern resource template.
2080    fn reject_final_exact_resource_template_collisions(&self, uri: &str) -> McpResult<()> {
2081        for entry in self.resource_templates.values() {
2082            let Some(matcher) = entry.matcher.as_ref() else {
2083                continue;
2084            };
2085            if matcher
2086                .match_uri(uri)
2087                .map_err(|_| McpError::invalid_params("resource template match admission failed"))?
2088                .is_some()
2089            {
2090                return Err(McpError::invalid_params(
2091                    "exact final resource collides with an admitted resource template",
2092                ));
2093            }
2094        }
2095
2096        Ok(())
2097    }
2098
2099    /// Adds a tool handler.
2100    ///
2101    /// If a tool with the same name already exists, it will be replaced.
2102    /// Use [`add_tool_with_behavior`](Self::add_tool_with_behavior) for
2103    /// finer control over duplicate handling.
2104    pub fn add_tool<H: ToolHandler + 'static>(&mut self, handler: H) -> McpResult<()> {
2105        self.add_tool_with_behavior(handler, crate::DuplicateBehavior::Replace)
2106    }
2107
2108    /// Adds an intentionally exact-2024-only tool handler.
2109    ///
2110    /// This is the explicit escape hatch for a legacy definition that cannot
2111    /// satisfy final schema admission. The tool remains available to exact
2112    /// MCP 2024-11-05 list and call routes, but is absent from every modern
2113    /// catalog and modern dispatch lookup. Ordinary [`Self::add_tool`] never
2114    /// falls back to this path.
2115    pub fn add_legacy_tool<H: ToolHandler + 'static>(&mut self, handler: H) -> McpResult<()> {
2116        self.add_legacy_tool_with_behavior(handler, crate::DuplicateBehavior::Replace)
2117    }
2118
2119    /// Adds a tool handler with specified duplicate behavior.
2120    ///
2121    /// Returns `Err` if duplicate policy rejects the name or if the candidate's
2122    /// immutable definition, schemas, final metadata, or required error mapper
2123    /// cannot be admitted. Every error is returned before catalog mutation.
2124    pub fn add_tool_with_behavior<H: ToolHandler + 'static>(
2125        &mut self,
2126        handler: H,
2127        behavior: crate::DuplicateBehavior,
2128    ) -> Result<(), McpError> {
2129        self.add_tool_registration_with_behavior(handler, behavior, true, true, false)
2130    }
2131
2132    /// Adds an exact-final-only tool with duplicate handling.
2133    pub(crate) fn add_final_tool_with_behavior<H: ToolHandler + 'static>(
2134        &mut self,
2135        handler: H,
2136        behavior: crate::DuplicateBehavior,
2137    ) -> Result<(), McpError> {
2138        self.add_tool_registration_with_behavior(handler, behavior, true, false, false)
2139    }
2140
2141    /// Adds a final-only tool carrying validated MCP Apps metadata.
2142    ///
2143    /// This is deliberately narrower than ordinary tool registration: callers
2144    /// must first install the Apps extension through the builder, and the tool
2145    /// is never projected into exact MCP 2024-11-05 discovery or dispatch.
2146    pub(crate) fn add_mcp_apps_tool_with_behavior<H: ToolHandler + 'static>(
2147        &mut self,
2148        handler: H,
2149        behavior: crate::DuplicateBehavior,
2150    ) -> Result<(), McpError> {
2151        self.add_tool_registration_with_behavior(handler, behavior, true, false, true)
2152    }
2153
2154    /// Adds an intentionally exact-2024-only tool with duplicate policy.
2155    ///
2156    /// The definition is snapshotted before mutation, but no final definition,
2157    /// schema, metadata, or error-mapper hook is read. This prevents an
2158    /// explicitly legacy-only registration from accidentally claiming modern
2159    /// support while retaining the same duplicate semantics as ordinary tools.
2160    pub fn add_legacy_tool_with_behavior<H: ToolHandler + 'static>(
2161        &mut self,
2162        handler: H,
2163        behavior: crate::DuplicateBehavior,
2164    ) -> Result<(), McpError> {
2165        self.add_tool_registration_with_behavior(handler, behavior, false, true, false)
2166    }
2167
2168    fn add_tool_registration_with_behavior<H: ToolHandler + 'static>(
2169        &mut self,
2170        handler: H,
2171        behavior: crate::DuplicateBehavior,
2172        admit_final: bool,
2173        legacy_enabled: bool,
2174        allow_mcp_apps: bool,
2175    ) -> Result<(), McpError> {
2176        let def = crate::catch_extension_unwind(|| handler.definition()).map_err(|_payload| {
2177            McpError::internal_error("tool definition hook panicked during admission")
2178        })?;
2179        let name = &def.name;
2180
2181        let existed = self.tools.contains_key(name);
2182        if existed {
2183            match behavior {
2184                crate::DuplicateBehavior::Error => {
2185                    return Err(duplicate_registration_error("Tool", name));
2186                }
2187                crate::DuplicateBehavior::Warn => {
2188                    log::warn!(
2189                        target: "fastmcp_rust::router",
2190                        "tool already exists, keeping original; tool_key={}",
2191                        safe_log_label(name)
2192                    );
2193                    return Ok(());
2194                }
2195                crate::DuplicateBehavior::Replace => {
2196                    log::debug!(
2197                        target: "fastmcp_rust::router",
2198                        "replacing tool; tool_key={}",
2199                        safe_log_label(name)
2200                    );
2201                    // Fall through to insert
2202                }
2203                crate::DuplicateBehavior::Ignore => {
2204                    return Ok(());
2205                }
2206            }
2207        }
2208
2209        // Admission must finish before any map or ordering mutation. In
2210        // particular, a rejected replacement must retain the prior handler
2211        // and its admitted schemas for both protocol eras.
2212        let name = def.name.clone();
2213        let admitted = if admit_final {
2214            AdmittedToolRegistration::admit(handler, def, legacy_enabled)?
2215        } else {
2216            AdmittedToolRegistration::legacy_only(handler, def)
2217        };
2218        if let Some(final_registration) = admitted.final_registration.as_ref() {
2219            self.validate_mcp_apps_tool_admission(
2220                &final_registration.final_definition,
2221                allow_mcp_apps,
2222            )?;
2223        }
2224        self.tools.insert(name.clone(), admitted);
2225        if !existed {
2226            self.tool_order.push(name);
2227        }
2228        self.advance_final_catalog_revision();
2229        Ok(())
2230    }
2231
2232    /// Keeps Apps metadata on its explicit, negotiated, exact-final path.
2233    fn validate_mcp_apps_tool_admission(
2234        &self,
2235        tool: &FinalTool,
2236        allow_mcp_apps: bool,
2237    ) -> McpResult<()> {
2238        let metadata = tool.mcp_apps_metadata().map_err(|error| {
2239            McpError::invalid_request(format!("invalid MCP Apps tool metadata: {error}"))
2240        })?;
2241        if metadata.is_some() && !allow_mcp_apps {
2242            return Err(McpError::invalid_request(
2243                "MCP Apps tool metadata requires ServerBuilder::mcp_apps_tool",
2244            ));
2245        }
2246        self.validate_mcp_apps_tool_resource_binding(tool)
2247    }
2248
2249    /// Verifies that a final tool's optional Apps UI binding selects a
2250    /// registered final Apps HTML resource before either catalog is mutated.
2251    fn validate_mcp_apps_tool_resource_binding(&self, tool: &FinalTool) -> McpResult<()> {
2252        let binding = tool.mcp_apps_resource_binding().map_err(|error| {
2253            McpError::invalid_request(format!("invalid MCP Apps tool metadata: {error}"))
2254        })?;
2255        let Some(binding) = binding else {
2256            return Ok(());
2257        };
2258        let resource = self
2259            .final_resources
2260            .get(binding.resource_uri.as_str())
2261            .ok_or_else(|| {
2262                McpError::invalid_request(format!(
2263                    "MCP Apps UI resource is not registered: {}",
2264                    binding.resource_uri.as_str()
2265                ))
2266            })?;
2267        binding
2268            .validate_resource(&resource.definition)
2269            .map_err(|error| {
2270                McpError::invalid_request(format!("invalid MCP Apps UI resource binding: {error}"))
2271            })
2272    }
2273
2274    /// Prevents replacement of one final Apps resource with a definition that
2275    /// would invalidate an already-admitted tool binding.
2276    fn validate_mcp_apps_resource_bindings(&self, resource: &FinalResource) -> McpResult<()> {
2277        for tool in self.tools.values() {
2278            let Some(final_registration) = tool.final_registration.as_ref() else {
2279                continue;
2280            };
2281            let binding = final_registration
2282                .final_definition
2283                .mcp_apps_resource_binding()
2284                .map_err(|error| {
2285                    McpError::invalid_request(format!("invalid MCP Apps tool metadata: {error}"))
2286                })?;
2287            if let Some(binding) = binding
2288                && binding.resource_uri == resource.uri
2289            {
2290                binding.validate_resource(resource).map_err(|error| {
2291                    McpError::invalid_request(format!(
2292                        "invalid MCP Apps UI resource replacement: {error}"
2293                    ))
2294                })?;
2295            }
2296        }
2297        Ok(())
2298    }
2299
2300    /// Prevents generic resource registration from projecting an Apps View
2301    /// into exact MCP 2024-11-05. The typed provider is admitted only through
2302    /// the builder's negotiated final-only registration path.
2303    fn validate_mcp_apps_ui_resource_admission(
2304        &self,
2305        resource: &FinalResource,
2306        allow_mcp_apps: bool,
2307    ) -> McpResult<()> {
2308        let requires_mcp_apps = Self::mcp_apps_ui_resource_requires_special_admission(resource)?;
2309        if !requires_mcp_apps {
2310            return Ok(());
2311        }
2312        if !allow_mcp_apps {
2313            return Err(McpError::invalid_request(
2314                "MCP Apps UI resources require ServerBuilder::mcp_apps_ui_resource",
2315            ));
2316        }
2317        let is_apps_html = resource.uri.as_str().starts_with("ui://")
2318            && resource.mime_type.as_deref() == Some(fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE);
2319        if !is_apps_html {
2320            return Err(McpError::invalid_request(
2321                "MCP Apps UI resources must use a ui:// URI and the MCP Apps HTML MIME type",
2322            ));
2323        }
2324        Ok(())
2325    }
2326
2327    fn mcp_apps_ui_resource_requires_special_admission(
2328        resource: &FinalResource,
2329    ) -> McpResult<bool> {
2330        let metadata = resource.mcp_apps_metadata().map_err(|error| {
2331            McpError::invalid_request(format!("invalid MCP Apps resource metadata: {error}"))
2332        })?;
2333        let is_apps_html = resource.uri.as_str().starts_with("ui://")
2334            && resource.mime_type.as_deref() == Some(fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE);
2335        Ok(is_apps_html || metadata.is_some())
2336    }
2337
2338    /// Returns whether this router contains a final-only MCP Apps component.
2339    ///
2340    /// Builder-level composition uses this inventory before consuming a child
2341    /// server. Invalid retained final metadata is treated as Apps-bound so a
2342    /// malformed child cannot bypass the destination's Apps opt-in gate.
2343    #[must_use]
2344    pub(crate) fn has_mcp_apps_bound_components(&self) -> bool {
2345        self.final_resources.values().any(|registration| {
2346            Self::mcp_apps_ui_resource_requires_special_admission(&registration.definition)
2347                .unwrap_or(true)
2348        }) || self.tools.values().any(|registration| {
2349            registration
2350                .final_registration
2351                .as_ref()
2352                .is_some_and(|final_registration| {
2353                    final_registration
2354                        .final_definition
2355                        .mcp_apps_metadata()
2356                        .map_or(true, |metadata| metadata.is_some())
2357                })
2358        })
2359    }
2360
2361    /// Registers the handler for `completion/complete`.
2362    ///
2363    /// This is the server-wide fallback for final completion dispatch and the
2364    /// sole route for exact MCP 2024-11-05. A final provider registered for a
2365    /// specific prompt or resource template takes precedence. Re-registering
2366    /// replaces the prior fallback, matching ordinary component registration
2367    /// semantics.
2368    pub fn add_completion_handler<H: CompletionHandler + 'static>(&mut self, handler: H) {
2369        self.completion_handler = Some(Box::new(handler));
2370        self.default_final_completion_enabled = true;
2371    }
2372
2373    /// Registers a completion handler for exact MCP 2024-11-05 dispatch only.
2374    pub fn add_legacy_completion_handler<H: CompletionHandler + 'static>(&mut self, handler: H) {
2375        self.completion_handler = Some(Box::new(handler));
2376        self.default_final_completion_enabled = false;
2377    }
2378
2379    /// Registers a legacy completion provider for one exact prompt name.
2380    ///
2381    /// This route takes precedence over the legacy server-wide fallback and is
2382    /// removed atomically when `Replace` admits a new prompt at the same name.
2383    pub(crate) fn add_legacy_prompt_completion_handler<H: CompletionHandler + 'static>(
2384        &mut self,
2385        prompt_name: impl Into<String>,
2386        handler: H,
2387    ) {
2388        self.legacy_prompt_completion_handlers
2389            .insert(prompt_name.into(), Box::new(handler));
2390    }
2391
2392    /// Registers a legacy completion provider for one exact resource-template URI.
2393    ///
2394    /// This route takes precedence over the legacy server-wide fallback and is
2395    /// removed atomically when `Replace` admits a new template at the same URI.
2396    pub(crate) fn add_legacy_resource_template_completion_handler<
2397        H: CompletionHandler + 'static,
2398    >(
2399        &mut self,
2400        uri_template: impl Into<String>,
2401        handler: H,
2402    ) {
2403        self.legacy_resource_template_completion_handlers
2404            .insert(uri_template.into(), Box::new(handler));
2405    }
2406
2407    /// Registers a final completion provider for one exact prompt name.
2408    ///
2409    /// The provider is selected only after final prompt and argument admission
2410    /// succeeds. It never changes exact MCP 2024-11-05's server-wide route.
2411    pub fn add_prompt_completion_handler<H: CompletionHandler + 'static>(
2412        &mut self,
2413        prompt_name: impl Into<String>,
2414        handler: H,
2415    ) {
2416        self.final_prompt_completion_handlers
2417            .insert(prompt_name.into(), Box::new(handler));
2418    }
2419
2420    /// Registers a final completion provider for one exact resource-template URI.
2421    ///
2422    /// The provider is selected only after the resource template and requested
2423    /// template variable have been admitted for final dispatch.
2424    pub fn add_resource_template_completion_handler<H: CompletionHandler + 'static>(
2425        &mut self,
2426        uri_template: impl Into<String>,
2427        handler: H,
2428    ) {
2429        self.final_resource_template_completion_handlers
2430            .insert(uri_template.into(), Box::new(handler));
2431    }
2432
2433    /// Returns whether a `completion/complete` handler is installed.
2434    #[must_use]
2435    pub fn has_completion_handler(&self) -> bool {
2436        self.completion_handler.is_some()
2437            || !self.final_prompt_completion_handlers.is_empty()
2438            || !self.final_resource_template_completion_handlers.is_empty()
2439    }
2440
2441    fn has_final_completion_handler(&self) -> bool {
2442        self.default_final_completion_enabled || self.has_admitted_final_completion_provider()
2443    }
2444
2445    fn has_admitted_final_completion_provider(&self) -> bool {
2446        self.final_prompt_completion_handlers
2447            .keys()
2448            .any(|name| self.final_prompts.contains_key(name))
2449            || self
2450                .final_resource_template_completion_handlers
2451                .keys()
2452                .any(|uri| {
2453                    self.resource_templates
2454                        .get(uri)
2455                        .is_some_and(|entry| entry.final_definition.is_some())
2456                })
2457    }
2458
2459    /// Adds a resource handler.
2460    ///
2461    /// If a resource with the same URI already exists, it will be replaced.
2462    /// Use [`add_resource_with_behavior`](Self::add_resource_with_behavior) for
2463    /// finer control over duplicate handling.
2464    pub fn add_resource<H: ResourceHandler + 'static>(&mut self, handler: H) {
2465        if let Err(error) = self.add_resource_registration_with_behavior(
2466            handler,
2467            crate::DuplicateBehavior::Replace,
2468            true,
2469            true,
2470            false,
2471        ) {
2472            log::warn!(
2473                target: "fastmcp_rust::router",
2474                "rejected resource registration; code={:?}",
2475                error.code
2476            );
2477        }
2478    }
2479
2480    /// Adds an intentionally exact-2024-only resource handler.
2481    pub fn add_legacy_resource<H: ResourceHandler + 'static>(&mut self, handler: H) {
2482        if let Err(error) = self.add_resource_registration_with_behavior(
2483            handler,
2484            crate::DuplicateBehavior::Replace,
2485            false,
2486            true,
2487            false,
2488        ) {
2489            log::warn!(
2490                target: "fastmcp_rust::router",
2491                "rejected exact-2024-only resource registration; code={:?}",
2492                error.code
2493            );
2494        }
2495    }
2496
2497    /// Adds a resource handler with specified duplicate behavior.
2498    ///
2499    /// Returns `Err` if behavior is [`crate::DuplicateBehavior::Error`] and the
2500    /// resource URI already exists.
2501    pub fn add_resource_with_behavior<H: ResourceHandler + 'static>(
2502        &mut self,
2503        handler: H,
2504        behavior: crate::DuplicateBehavior,
2505    ) -> Result<(), McpError> {
2506        self.add_resource_registration_with_behavior(handler, behavior, true, true, false)
2507    }
2508
2509    /// Adds an exact-final-only resource or resource template with duplicate handling.
2510    pub(crate) fn add_final_resource_with_behavior<H: ResourceHandler + 'static>(
2511        &mut self,
2512        handler: H,
2513        behavior: crate::DuplicateBehavior,
2514    ) -> Result<(), McpError> {
2515        self.add_resource_registration_with_behavior(handler, behavior, true, false, false)
2516    }
2517
2518    /// Adds one final-only MCP Apps HTML resource after builder-level Apps opt-in.
2519    pub(crate) fn add_mcp_apps_ui_resource_with_behavior<H: ResourceHandler + 'static>(
2520        &mut self,
2521        handler: H,
2522        behavior: crate::DuplicateBehavior,
2523    ) -> Result<(), McpError> {
2524        self.add_resource_registration_with_behavior(handler, behavior, true, false, true)
2525    }
2526
2527    /// Adds an exact-2024-only resource handler with duplicate handling.
2528    pub fn add_legacy_resource_with_behavior<H: ResourceHandler + 'static>(
2529        &mut self,
2530        handler: H,
2531        behavior: crate::DuplicateBehavior,
2532    ) -> Result<(), McpError> {
2533        self.add_resource_registration_with_behavior(handler, behavior, false, true, false)
2534    }
2535
2536    fn add_resource_registration_with_behavior<H: ResourceHandler + 'static>(
2537        &mut self,
2538        handler: H,
2539        behavior: crate::DuplicateBehavior,
2540        admit_final: bool,
2541        legacy_enabled: bool,
2542        allow_mcp_apps: bool,
2543    ) -> Result<(), McpError> {
2544        let (template, def) =
2545            crate::catch_extension_unwind(|| (handler.template(), handler.definition())).map_err(
2546                |_payload| {
2547                    McpError::internal_error("resource definition hook panicked during admission")
2548                },
2549            )?;
2550
2551        // Check for duplicates
2552        let key = match template.as_ref() {
2553            Some(template) => template.uri_template.clone(),
2554            None => def.uri.clone(),
2555        };
2556
2557        let exists = if template.is_some() {
2558            self.resource_templates.contains_key(&key)
2559        } else {
2560            self.resources.contains_key(&key)
2561        };
2562
2563        if exists {
2564            match behavior {
2565                crate::DuplicateBehavior::Error => {
2566                    return Err(duplicate_registration_error("Resource", &key));
2567                }
2568                crate::DuplicateBehavior::Warn => {
2569                    log::warn!(
2570                        target: "fastmcp_rust::router",
2571                        "resource already exists, keeping original; resource_key={}",
2572                        safe_log_label(&key)
2573                    );
2574                    return Ok(());
2575                }
2576                crate::DuplicateBehavior::Replace => {
2577                    log::debug!(
2578                        target: "fastmcp_rust::router",
2579                        "replacing resource; resource_key={}",
2580                        safe_log_label(&key)
2581                    );
2582                    // Fall through to insert
2583                }
2584                crate::DuplicateBehavior::Ignore => {
2585                    return Ok(());
2586                }
2587            }
2588        }
2589
2590        if let Some(template) = template {
2591            let legacy_matcher = if legacy_enabled {
2592                match admit_legacy_resource_template(&template.uri_template) {
2593                    Ok(matcher) => Some(matcher),
2594                    Err(error) if !admit_final => return Err(error),
2595                    Err(_) => None,
2596                }
2597            } else {
2598                None
2599            };
2600            let (matcher, specificity) = if admit_final {
2601                let (matcher, specificity) = admit_resource_template(&template.uri_template)?;
2602                self.reject_final_template_collisions(
2603                    &matcher,
2604                    Some(template.uri_template.as_str()),
2605                )?;
2606                (Some(matcher), specificity)
2607            } else {
2608                let matcher = legacy_matcher.as_ref().ok_or_else(|| {
2609                    McpError::internal_error(
2610                        "exact-2024 resource template admission lost its matcher",
2611                    )
2612                })?;
2613                (None, matcher.specificity())
2614            };
2615            let uri_use_policy = if admit_final {
2616                ResourceUriUsePolicy::from_client_direct_https(
2617                    crate::catch_extension_unwind(|| handler.final_client_direct_https()).map_err(
2618                        |_payload| {
2619                            McpError::internal_error(
2620                                "resource URI-use policy hook panicked during admission",
2621                            )
2622                        },
2623                    )?,
2624                )
2625            } else {
2626                ResourceUriUsePolicy::server_mediated()
2627            };
2628            let final_definition = admit_final
2629                .then(|| {
2630                    admit_final_resource_template_definition(
2631                        Some(&handler),
2632                        &template,
2633                        uri_use_policy,
2634                    )
2635                })
2636                .transpose()?;
2637            let boxed: BoxedResourceHandler = Box::new(handler);
2638            let is_new = !self.resource_templates.contains_key(&template.uri_template);
2639            if !is_new {
2640                self.legacy_resource_template_completion_handlers
2641                    .remove(&template.uri_template);
2642                self.final_resource_template_completion_handlers
2643                    .remove(&template.uri_template);
2644            }
2645            let entry = ResourceTemplateEntry {
2646                matcher,
2647                specificity,
2648                template: template.clone(),
2649                handler: Some(boxed),
2650                final_definition,
2651                uri_use_policy,
2652                legacy_enabled: legacy_matcher.is_some(),
2653                legacy_matcher,
2654            };
2655            self.resource_templates
2656                .insert(template.uri_template.clone(), entry);
2657            if is_new {
2658                self.resource_template_order.push(template.uri_template);
2659            }
2660            self.rebuild_sorted_template_keys();
2661        } else {
2662            let uri_use_policy = if admit_final {
2663                ResourceUriUsePolicy::from_client_direct_https(
2664                    crate::catch_extension_unwind(|| handler.final_client_direct_https()).map_err(
2665                        |_payload| {
2666                            McpError::internal_error(
2667                                "resource URI-use policy hook panicked during admission",
2668                            )
2669                        },
2670                    )?,
2671                )
2672            } else {
2673                ResourceUriUsePolicy::server_mediated()
2674            };
2675            let final_definition = admit_final
2676                .then(|| admit_final_resource_definition(&handler, &def, uri_use_policy))
2677                .transpose()?;
2678            if let Some(final_definition) = final_definition.as_ref() {
2679                self.validate_mcp_apps_ui_resource_admission(final_definition, allow_mcp_apps)?;
2680                self.validate_mcp_apps_resource_bindings(final_definition)?;
2681                self.reject_final_exact_resource_template_collisions(&def.uri)?;
2682            }
2683            let boxed: BoxedResourceHandler = Box::new(handler);
2684            let is_new = !self.resources.contains_key(&def.uri);
2685            self.resources.insert(def.uri.clone(), boxed);
2686            if legacy_enabled {
2687                self.final_only_resources.remove(&def.uri);
2688            } else {
2689                self.final_only_resources.insert(def.uri.clone());
2690            }
2691            match final_definition {
2692                Some(final_definition) => {
2693                    self.final_resources.insert(
2694                        def.uri.clone(),
2695                        AdmittedFinalResourceRegistration {
2696                            definition: final_definition,
2697                            tags: def.tags.clone(),
2698                            uri_use_policy,
2699                        },
2700                    );
2701                }
2702                None => {
2703                    self.final_resources.remove(&def.uri);
2704                }
2705            }
2706            if is_new {
2707                self.resource_order.push(def.uri);
2708            }
2709        }
2710
2711        self.advance_final_catalog_revision();
2712        Ok(())
2713    }
2714
2715    /// Adds a resource template definition.
2716    ///
2717    /// If a template with the same URI template already exists, its definition
2718    /// is replaced while any registered handler is retained. Use
2719    /// [`add_resource_template_with_behavior`](Self::add_resource_template_with_behavior)
2720    /// for finer control over duplicate handling.
2721    pub fn add_resource_template(&mut self, template: ResourceTemplate) {
2722        let key = template.uri_template.clone();
2723        if let Err(error) =
2724            self.add_resource_template_with_behavior(template, crate::DuplicateBehavior::Replace)
2725        {
2726            log::warn!(
2727                target: "fastmcp_rust::router",
2728                "rejected resource template definition; template_key={}; code={:?}",
2729                safe_log_label(&key),
2730                error.code
2731            );
2732        }
2733    }
2734
2735    /// Adds an exact-2024-only resource template definition.
2736    pub fn add_legacy_resource_template(&mut self, template: ResourceTemplate) {
2737        let key = template.uri_template.clone();
2738        if let Err(error) = self.add_resource_template_registration_with_behavior(
2739            template,
2740            crate::DuplicateBehavior::Replace,
2741            false,
2742        ) {
2743            log::warn!(
2744                target: "fastmcp_rust::router",
2745                "rejected exact-2024-only resource template; template_key={}; code={:?}",
2746                safe_log_label(&key),
2747                error.code
2748            );
2749        }
2750    }
2751
2752    /// Adds a resource template definition with specified duplicate behavior.
2753    ///
2754    /// Replacing a definition retains an existing handler registered for the
2755    /// same URI template. Returns `Err` when behavior is
2756    /// [`crate::DuplicateBehavior::Error`] and the URI template already exists.
2757    pub fn add_resource_template_with_behavior(
2758        &mut self,
2759        template: ResourceTemplate,
2760        behavior: crate::DuplicateBehavior,
2761    ) -> Result<(), McpError> {
2762        self.add_resource_template_registration_with_behavior(template, behavior, true)
2763    }
2764
2765    /// Adds an exact-2024-only resource template with duplicate handling.
2766    pub fn add_legacy_resource_template_with_behavior(
2767        &mut self,
2768        template: ResourceTemplate,
2769        behavior: crate::DuplicateBehavior,
2770    ) -> Result<(), McpError> {
2771        self.add_resource_template_registration_with_behavior(template, behavior, false)
2772    }
2773
2774    fn add_resource_template_registration_with_behavior(
2775        &mut self,
2776        template: ResourceTemplate,
2777        behavior: crate::DuplicateBehavior,
2778        admit_final: bool,
2779    ) -> Result<(), McpError> {
2780        let key = template.uri_template.clone();
2781        let existed = self.resource_templates.contains_key(&key);
2782
2783        if existed {
2784            match behavior {
2785                crate::DuplicateBehavior::Error => {
2786                    return Err(duplicate_registration_error("Resource template", &key));
2787                }
2788                crate::DuplicateBehavior::Warn => {
2789                    log::warn!(
2790                        target: "fastmcp_rust::router",
2791                        "resource template already exists, keeping original; template_key={}",
2792                        safe_log_label(&key)
2793                    );
2794                    return Ok(());
2795                }
2796                crate::DuplicateBehavior::Replace => {
2797                    log::debug!(
2798                        target: "fastmcp_rust::router",
2799                        "replacing resource template definition; template_key={}",
2800                        safe_log_label(&key)
2801                    );
2802                }
2803                crate::DuplicateBehavior::Ignore => return Ok(()),
2804            }
2805        }
2806
2807        let legacy_matcher = match admit_legacy_resource_template(&key) {
2808            Ok(matcher) => Some(matcher),
2809            Err(error) if !admit_final => return Err(error),
2810            Err(_) => None,
2811        };
2812        let (matcher, specificity) = if admit_final {
2813            let (matcher, specificity) = admit_resource_template(&key)?;
2814            self.reject_final_template_collisions(&matcher, Some(key.as_str()))?;
2815            (Some(matcher), specificity)
2816        } else {
2817            let matcher = legacy_matcher.as_ref().ok_or_else(|| {
2818                McpError::internal_error("exact-2024 resource template admission lost its matcher")
2819            })?;
2820            (None, matcher.specificity())
2821        };
2822        let uri_use_policy = ResourceUriUsePolicy::server_mediated();
2823        let final_definition = admit_final
2824            .then(|| {
2825                admit_final_resource_template_definition::<dyn ResourceHandler>(
2826                    None,
2827                    &template,
2828                    uri_use_policy,
2829                )
2830            })
2831            .transpose()?;
2832        if existed {
2833            self.legacy_resource_template_completion_handlers
2834                .remove(&key);
2835            self.final_resource_template_completion_handlers
2836                .remove(&key);
2837        }
2838        let needs_rebuild = match self.resource_templates.get_mut(&key) {
2839            Some(existing) => {
2840                existing.template = template;
2841                existing.matcher = matcher;
2842                existing.specificity = specificity;
2843                existing.final_definition = final_definition;
2844                existing.uri_use_policy = uri_use_policy;
2845                existing.legacy_enabled = legacy_matcher.is_some();
2846                existing.legacy_matcher = legacy_matcher;
2847                false // Key already exists, order unchanged
2848            }
2849            None => {
2850                self.resource_templates.insert(
2851                    key.clone(),
2852                    ResourceTemplateEntry {
2853                        matcher,
2854                        specificity,
2855                        template,
2856                        handler: None,
2857                        final_definition,
2858                        uri_use_policy,
2859                        legacy_enabled: legacy_matcher.is_some(),
2860                        legacy_matcher,
2861                    },
2862                );
2863                true // New key added, need to rebuild
2864            }
2865        };
2866        if needs_rebuild {
2867            self.resource_template_order.push(key);
2868            self.rebuild_sorted_template_keys();
2869        }
2870        self.advance_final_catalog_revision();
2871        Ok(())
2872    }
2873
2874    /// Adds a prompt handler.
2875    ///
2876    /// If a prompt with the same name already exists, it will be replaced.
2877    /// Use [`add_prompt_with_behavior`](Self::add_prompt_with_behavior) for
2878    /// finer control over duplicate handling.
2879    pub fn add_prompt<H: PromptHandler + 'static>(&mut self, handler: H) {
2880        if let Err(error) = self.add_prompt_registration_with_behavior(
2881            handler,
2882            crate::DuplicateBehavior::Replace,
2883            true,
2884            true,
2885        ) {
2886            log::warn!(
2887                target: "fastmcp_rust::router",
2888                "rejected prompt registration; code={:?}",
2889                error.code
2890            );
2891        }
2892    }
2893
2894    /// Adds an intentionally exact-2024-only prompt handler.
2895    pub fn add_legacy_prompt<H: PromptHandler + 'static>(&mut self, handler: H) {
2896        if let Err(error) = self.add_prompt_registration_with_behavior(
2897            handler,
2898            crate::DuplicateBehavior::Replace,
2899            false,
2900            true,
2901        ) {
2902            log::warn!(
2903                target: "fastmcp_rust::router",
2904                "rejected exact-2024-only prompt registration; code={:?}",
2905                error.code
2906            );
2907        }
2908    }
2909
2910    /// Adds a prompt handler with specified duplicate behavior.
2911    ///
2912    /// Returns `Err` if behavior is [`crate::DuplicateBehavior::Error`] and the
2913    /// prompt name already exists.
2914    pub fn add_prompt_with_behavior<H: PromptHandler + 'static>(
2915        &mut self,
2916        handler: H,
2917        behavior: crate::DuplicateBehavior,
2918    ) -> Result<(), McpError> {
2919        self.add_prompt_registration_with_behavior(handler, behavior, true, true)
2920    }
2921
2922    /// Adds an exact-final-only prompt with duplicate handling.
2923    pub(crate) fn add_final_prompt_with_behavior<H: PromptHandler + 'static>(
2924        &mut self,
2925        handler: H,
2926        behavior: crate::DuplicateBehavior,
2927    ) -> Result<(), McpError> {
2928        self.add_prompt_registration_with_behavior(handler, behavior, true, false)
2929    }
2930
2931    /// Adds an exact-2024-only prompt with duplicate handling.
2932    pub fn add_legacy_prompt_with_behavior<H: PromptHandler + 'static>(
2933        &mut self,
2934        handler: H,
2935        behavior: crate::DuplicateBehavior,
2936    ) -> Result<(), McpError> {
2937        self.add_prompt_registration_with_behavior(handler, behavior, false, true)
2938    }
2939
2940    fn add_prompt_registration_with_behavior<H: PromptHandler + 'static>(
2941        &mut self,
2942        handler: H,
2943        behavior: crate::DuplicateBehavior,
2944        admit_final: bool,
2945        legacy_enabled: bool,
2946    ) -> Result<(), McpError> {
2947        let def = crate::catch_extension_unwind(|| handler.definition()).map_err(|_payload| {
2948            McpError::internal_error("prompt definition hook panicked during admission")
2949        })?;
2950        let name = &def.name;
2951
2952        let existed = self.prompts.contains_key(name);
2953        if existed {
2954            match behavior {
2955                crate::DuplicateBehavior::Error => {
2956                    return Err(duplicate_registration_error("Prompt", name));
2957                }
2958                crate::DuplicateBehavior::Warn => {
2959                    log::warn!(
2960                        target: "fastmcp_rust::router",
2961                        "prompt already exists, keeping original; prompt_key={}",
2962                        safe_log_label(name)
2963                    );
2964                    return Ok(());
2965                }
2966                crate::DuplicateBehavior::Replace => {
2967                    log::debug!(
2968                        target: "fastmcp_rust::router",
2969                        "replacing prompt; prompt_key={}",
2970                        safe_log_label(name)
2971                    );
2972                    // Fall through to insert
2973                }
2974                crate::DuplicateBehavior::Ignore => {
2975                    return Ok(());
2976                }
2977            }
2978        }
2979
2980        let uri_use_policy = if admit_final {
2981            ResourceUriUsePolicy::from_client_direct_https(
2982                crate::catch_extension_unwind(|| handler.final_client_direct_https()).map_err(
2983                    |_payload| {
2984                        McpError::internal_error(
2985                            "prompt URI-use policy hook panicked during admission",
2986                        )
2987                    },
2988                )?,
2989            )
2990        } else {
2991            ResourceUriUsePolicy::server_mediated()
2992        };
2993        let final_definition = admit_final
2994            .then(|| admit_final_prompt_definition(&handler, &def))
2995            .transpose()?;
2996        if existed {
2997            self.legacy_prompt_completion_handlers.remove(&def.name);
2998            self.final_prompt_completion_handlers.remove(&def.name);
2999        }
3000        self.prompts.insert(def.name.clone(), Box::new(handler));
3001        if legacy_enabled {
3002            self.final_only_prompts.remove(&def.name);
3003        } else {
3004            self.final_only_prompts.insert(def.name.clone());
3005        }
3006        match final_definition {
3007            Some(final_definition) => {
3008                self.final_prompts.insert(
3009                    def.name.clone(),
3010                    AdmittedFinalPromptRegistration {
3011                        definition: final_definition,
3012                        tags: def.tags.clone(),
3013                        uri_use_policy,
3014                    },
3015                );
3016            }
3017            None => {
3018                self.final_prompts.remove(&def.name);
3019            }
3020        }
3021        if !existed {
3022            self.prompt_order.push(def.name);
3023        }
3024        self.advance_final_catalog_revision();
3025        Ok(())
3026    }
3027
3028    /// Returns all tool definitions.
3029    #[must_use]
3030    pub fn tools(&self) -> Vec<Tool> {
3031        self.tool_order
3032            .iter()
3033            .filter_map(|name| self.tools.get(name))
3034            .filter(|entry| entry.legacy_enabled)
3035            .map(|entry| entry.definition.clone())
3036            .collect()
3037    }
3038
3039    /// Returns tool definitions filtered by session state and tags.
3040    ///
3041    /// Tools that have been disabled in the session state will not be included.
3042    /// If tag filters are provided, tools must match the include/exclude criteria.
3043    #[must_use]
3044    pub fn tools_filtered(
3045        &self,
3046        session_state: Option<&SessionState>,
3047        tag_filters: Option<&TagFilters<'_>>,
3048    ) -> Vec<Tool> {
3049        self.tool_order
3050            .iter()
3051            .filter_map(|name| self.tools.get(name))
3052            .filter_map(|entry| {
3053                if !entry.legacy_enabled {
3054                    return None;
3055                }
3056                let def = &entry.definition;
3057                // Check session state filter
3058                if let Some(state) = session_state {
3059                    if !state.is_tool_enabled(&def.name) {
3060                        return None;
3061                    }
3062                }
3063                // Check tag filters
3064                if let Some(filters) = tag_filters {
3065                    if !filters.matches(&def.tags) {
3066                        return None;
3067                    }
3068                }
3069                Some(def.clone())
3070            })
3071            .collect()
3072    }
3073
3074    /// Returns all resource definitions.
3075    #[must_use]
3076    pub fn resources(&self) -> Vec<Resource> {
3077        self.resource_order
3078            .iter()
3079            .filter_map(|uri| self.resources.get(uri))
3080            .filter(|handler| {
3081                let uri = handler.definition().uri;
3082                !self.final_only_resources.contains(&uri)
3083            })
3084            .map(|h| h.definition())
3085            .collect()
3086    }
3087
3088    /// Returns resource definitions filtered by session state and tags.
3089    ///
3090    /// Resources that have been disabled in the session state will not be included.
3091    /// If tag filters are provided, resources must match the include/exclude criteria.
3092    #[must_use]
3093    pub fn resources_filtered(
3094        &self,
3095        session_state: Option<&SessionState>,
3096        tag_filters: Option<&TagFilters<'_>>,
3097    ) -> Vec<Resource> {
3098        self.resource_order
3099            .iter()
3100            .filter_map(|uri| self.resources.get(uri))
3101            .filter_map(|h| {
3102                let def = h.definition();
3103                if self.final_only_resources.contains(&def.uri) {
3104                    return None;
3105                }
3106                // Check session state filter
3107                if let Some(state) = session_state {
3108                    if !state.is_resource_enabled(&def.uri) {
3109                        return None;
3110                    }
3111                }
3112                // Check tag filters
3113                if let Some(filters) = tag_filters {
3114                    if !filters.matches(&def.tags) {
3115                        return None;
3116                    }
3117                }
3118                Some(def)
3119            })
3120            .collect()
3121    }
3122
3123    /// Returns all resource templates.
3124    #[must_use]
3125    pub fn resource_templates(&self) -> Vec<ResourceTemplate> {
3126        self.resource_template_order
3127            .iter()
3128            .filter_map(|t| self.resource_templates.get(t))
3129            .filter(|entry| entry.legacy_enabled)
3130            .map(|entry| entry.template.clone())
3131            .collect()
3132    }
3133
3134    /// Returns resource templates filtered by session state and tags.
3135    ///
3136    /// Templates that have been disabled in the session state will not be included.
3137    /// If tag filters are provided, templates must match the include/exclude criteria.
3138    #[must_use]
3139    pub fn resource_templates_filtered(
3140        &self,
3141        session_state: Option<&SessionState>,
3142        tag_filters: Option<&TagFilters<'_>>,
3143    ) -> Vec<ResourceTemplate> {
3144        self.resource_template_order
3145            .iter()
3146            .filter_map(|t| self.resource_templates.get(t))
3147            .filter_map(|entry| {
3148                if !entry.legacy_enabled {
3149                    return None;
3150                }
3151                // Check session state filter
3152                if let Some(state) = session_state {
3153                    if !state.is_resource_enabled(&entry.template.uri_template) {
3154                        return None;
3155                    }
3156                }
3157                // Check tag filters
3158                if let Some(filters) = tag_filters {
3159                    if !filters.matches(&entry.template.tags) {
3160                        return None;
3161                    }
3162                }
3163                Some(entry.template.clone())
3164            })
3165            .collect()
3166    }
3167
3168    /// Returns all prompt definitions.
3169    #[must_use]
3170    pub fn prompts(&self) -> Vec<Prompt> {
3171        self.prompt_order
3172            .iter()
3173            .filter_map(|name| self.prompts.get(name))
3174            .filter(|handler| {
3175                let name = handler.definition().name;
3176                !self.final_only_prompts.contains(&name)
3177            })
3178            .map(|h| h.definition())
3179            .collect()
3180    }
3181
3182    /// Returns prompt definitions filtered by session state and tags.
3183    ///
3184    /// Prompts that have been disabled in the session state will not be included.
3185    /// If tag filters are provided, prompts must match the include/exclude criteria.
3186    #[must_use]
3187    pub fn prompts_filtered(
3188        &self,
3189        session_state: Option<&SessionState>,
3190        tag_filters: Option<&TagFilters<'_>>,
3191    ) -> Vec<Prompt> {
3192        self.prompt_order
3193            .iter()
3194            .filter_map(|name| self.prompts.get(name))
3195            .filter_map(|h| {
3196                let def = h.definition();
3197                if self.final_only_prompts.contains(&def.name) {
3198                    return None;
3199                }
3200                // Check session state filter
3201                if let Some(state) = session_state {
3202                    if !state.is_prompt_enabled(&def.name) {
3203                        return None;
3204                    }
3205                }
3206                // Check tag filters
3207                if let Some(filters) = tag_filters {
3208                    if !filters.matches(&def.tags) {
3209                        return None;
3210                    }
3211                }
3212                Some(def)
3213            })
3214            .collect()
3215    }
3216
3217    /// Returns the number of registered tools.
3218    #[must_use]
3219    pub fn tools_count(&self) -> usize {
3220        self.tools.len()
3221    }
3222
3223    /// Returns the number of registered resources.
3224    #[must_use]
3225    pub fn resources_count(&self) -> usize {
3226        self.resources.len()
3227    }
3228
3229    /// Returns the number of registered resource templates.
3230    #[must_use]
3231    pub fn resource_templates_count(&self) -> usize {
3232        self.resource_templates.len()
3233    }
3234
3235    /// Returns the number of registered prompts.
3236    #[must_use]
3237    pub fn prompts_count(&self) -> usize {
3238        self.prompts.len()
3239    }
3240
3241    /// Returns the immutable behavior registry for final server discovery.
3242    ///
3243    /// This records only APIs backed by this router's installed catalog. The
3244    /// The server composition supplies final subscription execution and
3245    /// publishes catalog/resource changes through its request-owned listener
3246    /// registry. This router records the catalog branches that can therefore
3247    /// be selected by those final filters; it still does not advertise the
3248    /// removed logging-request emitter.
3249    #[must_use]
3250    pub(crate) fn server_discovery_behavior_registry(&self) -> ServerBehaviorRegistry {
3251        let mut behaviors = Vec::with_capacity(11);
3252        behaviors.push(ServerBehavior::SubscriptionsListen);
3253        if self.has_final_completion_handler() {
3254            behaviors.push(ServerBehavior::CompletionComplete);
3255        }
3256        if self
3257            .tool_order
3258            .iter()
3259            .filter_map(|name| self.tools.get(name))
3260            .any(|entry| entry.final_registration.is_some())
3261        {
3262            behaviors.push(ServerBehavior::ToolsList);
3263            behaviors.push(ServerBehavior::ToolsListChangedNotification);
3264        }
3265        if !self.final_resources.is_empty()
3266            || self
3267                .resource_template_order
3268                .iter()
3269                .filter_map(|key| self.resource_templates.get(key))
3270                .any(|entry| entry.final_definition.is_some())
3271        {
3272            behaviors.push(ServerBehavior::ResourcesList);
3273            behaviors.push(ServerBehavior::ResourcesListChangedNotification);
3274        }
3275        if !self.final_resources.is_empty() {
3276            behaviors.push(ServerBehavior::ResourceUpdateDelivery);
3277        }
3278        if !self.final_prompts.is_empty() {
3279            behaviors.push(ServerBehavior::PromptsList);
3280            behaviors.push(ServerBehavior::PromptsListChangedNotification);
3281        }
3282        ServerBehaviorRegistry::from_behaviors(behaviors)
3283    }
3284
3285    /// Gets a tool handler by name.
3286    #[must_use]
3287    pub fn get_tool(&self, name: &str) -> Option<&BoxedToolHandler> {
3288        self.tools.get(name).map(|entry| &entry.handler)
3289    }
3290
3291    /// Gets a resource handler by URI.
3292    #[must_use]
3293    pub fn get_resource(&self, uri: &str) -> Option<&BoxedResourceHandler> {
3294        self.resources.get(uri)
3295    }
3296
3297    /// Gets a resource template by URI template.
3298    #[must_use]
3299    pub fn get_resource_template(&self, uri_template: &str) -> Option<&ResourceTemplate> {
3300        self.resource_templates
3301            .get(uri_template)
3302            .map(|entry| &entry.template)
3303    }
3304
3305    /// Returns true if a resource exists for the given URI (static or template match).
3306    #[must_use]
3307    pub fn resource_exists(&self, uri: &str) -> bool {
3308        self.resolve_resource(uri).is_some()
3309    }
3310
3311    /// Forwards a session `resources/subscribe` onto the resolved handler.
3312    pub(crate) fn notify_resource_subscribed(&self, ctx: &McpContext, uri: &str) -> McpResult<()> {
3313        if let Some(resolved) = self.resolve_resource(uri) {
3314            resolved.handler.on_subscribe(ctx, uri)?;
3315        }
3316        Ok(())
3317    }
3318
3319    /// Forwards a session `resources/unsubscribe` onto the resolved handler.
3320    pub(crate) fn notify_resource_unsubscribed(
3321        &self,
3322        ctx: &McpContext,
3323        uri: &str,
3324    ) -> McpResult<()> {
3325        if let Some(resolved) = self.resolve_resource(uri) {
3326            resolved.handler.on_unsubscribe(ctx, uri)?;
3327        }
3328        Ok(())
3329    }
3330
3331    fn resolve_resource(&self, uri: &str) -> Option<ResolvedResource<'_>> {
3332        self.resolve_resource_for_era(uri, None)
3333    }
3334
3335    /// Resolves a registered tool visible in one requested protocol era.
3336    ///
3337    /// A final-only tool (Apps-linked, task-capable) is reachable only in the
3338    /// modern era; a legacy-enabled tool is reachable in the exact-2024 era.
3339    #[cfg(test)]
3340    fn resolve_tool_for_era(
3341        &self,
3342        name: &str,
3343        era: Option<ProtocolEra>,
3344    ) -> Option<&AdmittedToolRegistration> {
3345        let registration = self.tools.get(name)?;
3346        let visible = era.is_none_or(|era| match era {
3347            ProtocolEra::Modern2026 => registration.final_registration.is_some(),
3348            ProtocolEra::Legacy2024 => registration.legacy_enabled,
3349        });
3350        visible.then_some(registration)
3351    }
3352
3353    /// Resolves a resource that is visible in one requested protocol era.
3354    ///
3355    /// A static resource takes precedence only when it is visible to that
3356    /// era. Otherwise a resource-template instance from the requested era
3357    /// must remain reachable; registrations intentionally keep the two
3358    /// catalogs separate.
3359    fn resolve_resource_for_era(
3360        &self,
3361        uri: &str,
3362        era: Option<ProtocolEra>,
3363    ) -> Option<ResolvedResource<'_>> {
3364        if let Some(handler) = self.resources.get(uri) {
3365            let resolved = ResolvedResource {
3366                handler,
3367                params: UriParams::new(),
3368                final_enabled: self.final_resources.contains_key(uri),
3369                legacy_enabled: !self.final_only_resources.contains(uri),
3370                uri_use_policy: self
3371                    .final_resources
3372                    .get(uri)
3373                    .map_or_else(ResourceUriUsePolicy::server_mediated, |entry| {
3374                        entry.uri_use_policy
3375                    }),
3376            };
3377            if era.is_none_or(|era| resolved.is_enabled_in(era)) {
3378                return Some(resolved);
3379            }
3380        }
3381
3382        // Use pre-sorted template keys to avoid sorting on every lookup
3383        'templates: for key in &self.sorted_template_keys {
3384            let entry = &self.resource_templates[key];
3385            let Some(handler) = entry.handler.as_ref() else {
3386                continue;
3387            };
3388            let legacy_params = || {
3389                entry
3390                    .legacy_matcher
3391                    .as_ref()
3392                    .and_then(|matcher| matcher.matches(uri))
3393            };
3394            let final_params = || {
3395                let values = entry.matcher.as_ref()?.match_uri(uri).ok()??;
3396                let mut params = UriParams::with_capacity(values.len());
3397                for (name, value) in values {
3398                    let TemplateValue::Scalar(value) = value else {
3399                        return None;
3400                    };
3401                    params.insert(name, value);
3402                }
3403                Some(params)
3404            };
3405            let Some(params) = (match era {
3406                Some(ProtocolEra::Legacy2024) => legacy_params(),
3407                Some(ProtocolEra::Modern2026) => final_params(),
3408                None => final_params().or_else(legacy_params),
3409            }) else {
3410                continue 'templates;
3411            };
3412            let resolved = ResolvedResource {
3413                handler,
3414                params,
3415                final_enabled: entry.final_definition.is_some(),
3416                legacy_enabled: entry.legacy_enabled,
3417                uri_use_policy: entry.uri_use_policy,
3418            };
3419            if era.is_none_or(|era| resolved.is_enabled_in(era)) {
3420                return Some(resolved);
3421            }
3422        }
3423
3424        None
3425    }
3426
3427    /// Gets a prompt handler by name.
3428    #[must_use]
3429    pub fn get_prompt(&self, name: &str) -> Option<&BoxedPromptHandler> {
3430        self.prompts.get(name)
3431    }
3432
3433    // ========================================================================
3434    // Request Dispatch Methods
3435    // ========================================================================
3436
3437    /// Handles the initialize request.
3438    pub fn handle_initialize(
3439        &self,
3440        request_ctx: &McpContext,
3441        session: &mut Session,
3442        params: InitializeParams,
3443        instructions: Option<&str>,
3444    ) -> McpResult<InitializeResult> {
3445        if let Some(error) = budget_error(request_ctx) {
3446            return Err(error);
3447        }
3448
3449        debug!(
3450            target: targets::SESSION,
3451            "preparing session initialization; client_key={}",
3452            safe_log_label(&params.client_info.name)
3453        );
3454
3455        // Initialize the session
3456        session.initialize(
3457            params.client_info,
3458            params.capabilities,
3459            PROTOCOL_VERSION.to_string(),
3460        );
3461
3462        Ok(InitializeResult {
3463            protocol_version: PROTOCOL_VERSION.to_string(),
3464            capabilities: session.server_capabilities().clone(),
3465            server_info: session.server_info().clone(),
3466            instructions: instructions.map(String::from),
3467        })
3468    }
3469
3470    /// Dispatches one exact legacy `completion/complete` request.
3471    ///
3472    /// This route decodes through the dual-era core contract before invoking
3473    /// the installed handler. In particular, a final `_meta` object remains a
3474    /// cross-era error even though the legacy parameter shape is otherwise
3475    /// intentionally open.
3476    pub(crate) fn dispatch_legacy_completion(
3477        &self,
3478        request_ctx: &McpContext,
3479        request: &JsonRpcRequest,
3480    ) -> McpResult<serde_json::Value> {
3481        if request.method != COMPLETION_COMPLETE {
3482            return Err(McpError::method_not_found(&request.method));
3483        }
3484
3485        let request = CoreRequest::decode(
3486            ProtocolEra::Legacy2024,
3487            COMPLETION_COMPLETE,
3488            request.params.as_ref(),
3489        )
3490        .map_err(|error| McpError::invalid_params(error.to_string()))?;
3491        let CoreRequest::Legacy(LegacyCoreRequest::Completion(params)) = request else {
3492            return Err(McpError::internal_error(
3493                "legacy completion dispatch selected another core request",
3494            ));
3495        };
3496
3497        serde_json::to_value(self.handle_completion_legacy(request_ctx, params)?)
3498            .map_err(McpError::from)
3499    }
3500
3501    pub(crate) async fn dispatch_legacy_completion_in_request(
3502        &self,
3503        request_ctx: &McpContext,
3504        request_cx: &Cx,
3505        request: &JsonRpcRequest,
3506    ) -> McpResult<serde_json::Value> {
3507        if request.method != COMPLETION_COMPLETE {
3508            return Err(McpError::method_not_found(&request.method));
3509        }
3510
3511        let request = CoreRequest::decode(
3512            ProtocolEra::Legacy2024,
3513            COMPLETION_COMPLETE,
3514            request.params.as_ref(),
3515        )
3516        .map_err(|error| McpError::invalid_params(error.to_string()))?;
3517        let CoreRequest::Legacy(LegacyCoreRequest::Completion(params)) = request else {
3518            return Err(McpError::internal_error(
3519                "legacy completion dispatch selected another core request",
3520            ));
3521        };
3522
3523        serde_json::to_value(
3524            self.handle_completion_legacy_in_request(request_ctx, request_cx, params)
3525                .await?,
3526        )
3527        .map_err(McpError::from)
3528    }
3529
3530    /// Handles one exact MCP 2024-11-05 completion request.
3531    pub fn handle_completion_legacy(
3532        &self,
3533        request_ctx: &McpContext,
3534        params: LegacyCompletionParams,
3535    ) -> McpResult<LegacyCompletionResult> {
3536        block_on(self.handle_completion_legacy_in_request(request_ctx, request_ctx.cx(), params))
3537    }
3538
3539    pub(crate) async fn handle_completion_legacy_in_request(
3540        &self,
3541        request_ctx: &McpContext,
3542        request_cx: &Cx,
3543        params: LegacyCompletionParams,
3544    ) -> McpResult<LegacyCompletionResult> {
3545        let dispatch_started_at = request_ctx.cx().now();
3546        if let Some(error) = budget_error(request_ctx) {
3547            return Err(error);
3548        }
3549
3550        let target_handler = match &params.reference {
3551            fastmcp_protocol::LegacyCompletionReference::Prompt { name } => {
3552                self.legacy_prompt_completion_handlers.get(name)
3553            }
3554            fastmcp_protocol::LegacyCompletionReference::Resource { uri } => {
3555                self.legacy_resource_template_completion_handlers.get(uri)
3556            }
3557        };
3558        let handler = target_handler
3559            .or(self.completion_handler.as_ref())
3560            .ok_or_else(|| McpError::method_not_found(COMPLETION_COMPLETE))?;
3561        let handler_ctx =
3562            derive_handler_context(request_ctx, None, None, None, ProtocolEra::Legacy2024);
3563        let handler_timeout =
3564            read_handler_timeout(request_ctx.cx(), "completion_timeout", || handler.timeout())?;
3565        let effective_budget = compose_handler_budget(
3566            request_ctx.cx().budget(),
3567            request_ctx.budget(),
3568            handler_timeout,
3569            dispatch_started_at,
3570        );
3571        let handler_ctx = handler_ctx.with_operation_deadline(effective_budget.deadline);
3572        let outcome = run_handler_in_request(
3573            &handler_ctx,
3574            request_cx,
3575            effective_budget,
3576            "completion",
3577            |child_cx| handler.complete_legacy_async_in_request(&handler_ctx, child_cx, params),
3578        )
3579        .await?;
3580
3581        let completion = match outcome {
3582            Outcome::Ok(completion) => completion,
3583            Outcome::Err(error) => {
3584                return Err(sanitize_handler_error(
3585                    request_ctx.cx(),
3586                    "completion",
3587                    error,
3588                ));
3589            }
3590            Outcome::Cancelled(_) => return Err(McpError::request_cancelled()),
3591            Outcome::Panicked(_payload) => {
3592                return Err(sanitized_handler_panic(request_ctx.cx(), "completion"));
3593            }
3594        };
3595
3596        Ok(LegacyCompletionResult {
3597            completion,
3598            meta: None,
3599        })
3600    }
3601
3602    async fn handle_completion_final_in_request(
3603        &self,
3604        request_ctx: &McpContext,
3605        request_cx: &Cx,
3606        params: FinalCompletionParams,
3607    ) -> McpResult<FinalCompletionResult> {
3608        let dispatch_started_at = request_ctx.cx().now();
3609        if request_cx.is_cancel_requested() {
3610            return Err(McpError::request_cancelled());
3611        }
3612        if let Some(error) = budget_error(request_ctx) {
3613            return Err(error);
3614        }
3615        if !self.has_final_completion_handler() {
3616            return Err(McpError::method_not_found(COMPLETION_COMPLETE));
3617        }
3618
3619        let provider_handler = match &params.reference {
3620            FinalCompletionReference::Prompt { name }
3621            | FinalCompletionReference::PromptWithTitle { name, .. } => {
3622                // Completion is a reference-discovery surface as well as an
3623                // invocation surface. Keep a component disabled for this
3624                // modern connection indistinguishable from an unregistered
3625                // prompt, before consulting its schema or a provider.
3626                if !request_ctx.is_prompt_enabled(name) {
3627                    return Err(McpError::invalid_params(
3628                        "completion prompt reference is not registered",
3629                    ));
3630                }
3631                let prompt = self.final_prompts.get(name).ok_or_else(|| {
3632                    McpError::invalid_params("completion prompt reference is not registered")
3633                })?;
3634                if !prompt
3635                    .definition
3636                    .arguments
3637                    .as_deref()
3638                    .unwrap_or_default()
3639                    .iter()
3640                    .any(|argument| argument.name == params.argument.name)
3641                {
3642                    return Err(McpError::invalid_params(
3643                        "completion argument is not declared by the referenced target",
3644                    ));
3645                }
3646                self.final_prompt_completion_handlers.get(name)
3647            }
3648            FinalCompletionReference::Resource { uri } => {
3649                // Resource-template completion must use the same individual-request
3650                // admission gate as resources/read. Catalog listing is deliberately
3651                // connection-independent, so never fall through to the global
3652                // completion provider for a disabled template.
3653                if !request_ctx.is_resource_enabled(uri) {
3654                    return Err(McpError::invalid_params(
3655                        "completion resource reference is not registered",
3656                    ));
3657                }
3658                let template = self
3659                    .resource_templates
3660                    .get(uri)
3661                    .filter(|entry| entry.final_definition.is_some())
3662                    .ok_or_else(|| {
3663                        McpError::invalid_params("completion resource reference is not registered")
3664                    })?;
3665                let template = fastmcp_protocol::UriTemplate::parse(
3666                    &template.template.uri_template,
3667                )
3668                .map_err(|_| {
3669                    McpError::internal_error(
3670                        "admitted completion resource template is no longer valid",
3671                    )
3672                })?;
3673                if !template.parts().iter().any(|part| {
3674                    matches!(part, UriTemplatePart::Expression(expression)
3675                        if expression
3676                            .variables()
3677                            .iter()
3678                            .any(|variable| variable.name() == params.argument.name))
3679                }) {
3680                    return Err(McpError::invalid_params(
3681                        "completion argument is not declared by the referenced target",
3682                    ));
3683                }
3684                self.final_resource_template_completion_handlers.get(uri)
3685            }
3686        };
3687
3688        let handler = match provider_handler {
3689            Some(handler) => handler,
3690            None if self.default_final_completion_enabled => self
3691                .completion_handler
3692                .as_ref()
3693                .ok_or_else(|| McpError::method_not_found(COMPLETION_COMPLETE))?,
3694            None => {
3695                return Err(McpError::invalid_params(
3696                    "no final completion provider is registered for the referenced target",
3697                ));
3698            }
3699        };
3700        let handler_ctx =
3701            derive_handler_context(request_ctx, None, None, None, ProtocolEra::Modern2026);
3702        let handler_timeout =
3703            read_handler_timeout(request_ctx.cx(), "completion_timeout", || handler.timeout())?;
3704        let effective_budget = compose_handler_budget(
3705            request_ctx.cx().budget(),
3706            request_ctx.budget(),
3707            handler_timeout,
3708            dispatch_started_at,
3709        );
3710        let handler_ctx = handler_ctx.with_operation_deadline(effective_budget.deadline);
3711        let outcome = run_handler_in_request(
3712            &handler_ctx,
3713            request_cx,
3714            effective_budget,
3715            "completion",
3716            |child_cx| handler.complete_final_async_in_request(&handler_ctx, child_cx, params),
3717        )
3718        .await?;
3719
3720        let completion = match outcome {
3721            Outcome::Ok(completion) => {
3722                if completion.values.len() > fastmcp_protocol::MAX_COMPLETION_VALUES {
3723                    return Err(McpError::internal_error(
3724                        "completion handler returned more than 100 values",
3725                    ));
3726                }
3727                completion.validate().map_err(McpError::internal_error)?;
3728                completion
3729            }
3730            Outcome::Err(error) => {
3731                return Err(sanitize_handler_error(
3732                    request_ctx.cx(),
3733                    "completion",
3734                    error,
3735                ));
3736            }
3737            Outcome::Cancelled(_) => return Err(McpError::request_cancelled()),
3738            Outcome::Panicked(_payload) => {
3739                return Err(sanitized_handler_panic(request_ctx.cx(), "completion"));
3740            }
3741        };
3742
3743        Ok(FinalCompletionResult { completion })
3744    }
3745
3746    /// Dispatches a modern request through the transport-neutral final router.
3747    ///
3748    /// This is the modern server-side routing seam. It deliberately has no
3749    /// `Session` argument: final catalog pages and cursors are independent of
3750    /// connection state. A supplied modern connection context still controls
3751    /// individual request admission and binds MRTR retries to its durable
3752    /// partition.
3753    /// Every successful response is re-emitted through the final
3754    /// complete-result contract. State-bearing lifecycle methods and exact
3755    /// 2024-11-05 wire results stay on the legacy adapter rather than
3756    /// acquiring accidental modern semantics.
3757    pub(crate) fn dispatch_stateless(
3758        &self,
3759        request_ctx: &McpContext,
3760        request: &JsonRpcRequest,
3761    ) -> McpResult<serde_json::Value> {
3762        // The connection-oriented server adapter remains synchronous today.
3763        // Keep its ordered compatibility semantics here; modern runtime entry
3764        // points must use `dispatch_stateless_owned` below instead of sharing
3765        // this blocking bridge.
3766        let continuation_cancellation = fastmcp_core::McpRequestCancellation::new();
3767        self.dispatch_stateless_with_continuation_cancellation(
3768            request_ctx,
3769            request,
3770            &continuation_cancellation,
3771        )
3772    }
3773
3774    /// Dispatches a modern request with the exact admitted `params` source.
3775    ///
3776    /// Callers that received an ingress raw-parameter sidecar must use this
3777    /// entry point so final MRTR retries retain ordered response entries and
3778    /// reject duplicate keys before registry admission.
3779    pub(crate) fn dispatch_stateless_with_raw_params(
3780        &self,
3781        request_ctx: &McpContext,
3782        request: &JsonRpcRequest,
3783        raw_params: Option<&str>,
3784    ) -> McpResult<serde_json::Value> {
3785        let continuation_cancellation = fastmcp_core::McpRequestCancellation::new();
3786        self.dispatch_stateless_with_continuation_cancellation_and_raw_params(
3787            request_ctx,
3788            request,
3789            raw_params,
3790            &continuation_cancellation,
3791        )
3792    }
3793
3794    pub(crate) fn dispatch_stateless_with_continuation_cancellation(
3795        &self,
3796        request_ctx: &McpContext,
3797        request: &JsonRpcRequest,
3798        continuation_cancellation: &fastmcp_core::McpRequestCancellation,
3799    ) -> McpResult<serde_json::Value> {
3800        self.dispatch_stateless_with_continuation_cancellation_and_raw_params(
3801            request_ctx,
3802            request,
3803            None,
3804            continuation_cancellation,
3805        )
3806    }
3807
3808    /// Dispatches with connection-owned continuation cancellation and the
3809    /// exact admitted parameter source retained by transport ingress.
3810    pub(crate) fn dispatch_stateless_with_continuation_cancellation_and_raw_params(
3811        &self,
3812        request_ctx: &McpContext,
3813        request: &JsonRpcRequest,
3814        raw_params: Option<&str>,
3815        continuation_cancellation: &fastmcp_core::McpRequestCancellation,
3816    ) -> McpResult<serde_json::Value> {
3817        block_on(self.dispatch_stateless_in_request(
3818            request_ctx,
3819            request_ctx.cx(),
3820            request,
3821            raw_params,
3822            continuation_cancellation,
3823        ))
3824    }
3825
3826    /// Dispatches one modern request in a request-owned structured child task.
3827    ///
3828    /// The caller owns the returned future. It owns exactly one child task,
3829    /// waits for that task to finish, and cancellation of that wait aborts the
3830    /// child through `TaskHandle::join` before control returns. No task is
3831    /// detached: a result is produced only after the handler task has reached a
3832    /// terminal state. The child Cx is propagated to the modern handler hooks
3833    /// so their nested work remains in the same request lifetime.
3834    pub(crate) async fn dispatch_stateless_owned(
3835        self: Arc<Self>,
3836        request_ctx: McpContext,
3837        request: JsonRpcRequest,
3838    ) -> McpResult<serde_json::Value> {
3839        self.dispatch_stateless_owned_with_continuation_cancellation(
3840            request_ctx,
3841            request,
3842            fastmcp_core::McpRequestCancellation::new(),
3843        )
3844        .await
3845    }
3846
3847    /// Dispatches one modern request with the continuation owner selected by
3848    /// its transport connection. The owner is deliberately distinct from the
3849    /// request cancellation: an `input_required` response ends one JSON-RPC
3850    /// request normally, while its retry remains valid until its connection
3851    /// disconnects or the continuation expires.
3852    pub(crate) async fn dispatch_stateless_owned_with_continuation_cancellation(
3853        self: Arc<Self>,
3854        request_ctx: McpContext,
3855        request: JsonRpcRequest,
3856        continuation_cancellation: fastmcp_core::McpRequestCancellation,
3857    ) -> McpResult<serde_json::Value> {
3858        self.dispatch_stateless_owned_with_continuation_cancellation_and_raw_params(
3859            request_ctx,
3860            request,
3861            None,
3862            continuation_cancellation,
3863        )
3864        .await
3865    }
3866
3867    /// Dispatches an owned modern request with its retained raw-parameter
3868    /// sidecar. The sidecar is owned because a request child may outlive the
3869    /// transport frame reader that admitted it.
3870    pub(crate) async fn dispatch_stateless_owned_with_continuation_cancellation_and_raw_params(
3871        self: Arc<Self>,
3872        request_ctx: McpContext,
3873        request: JsonRpcRequest,
3874        raw_params: Option<Arc<str>>,
3875        continuation_cancellation: fastmcp_core::McpRequestCancellation,
3876    ) -> McpResult<serde_json::Value> {
3877        if let Some(error) = budget_error(&request_ctx) {
3878            return Err(error);
3879        }
3880
3881        let join_cx = request_ctx.cx().clone();
3882        let dispatch_ctx = request_ctx.clone();
3883        let spawn_self = Arc::clone(&self);
3884        let spawn_request = request.clone();
3885        let spawn_raw_params = raw_params.clone();
3886        let spawn_continuation_cancellation = continuation_cancellation.clone();
3887        let mut task = match request_ctx.cx().spawn(move |child_cx| async move {
3888            spawn_self
3889                .dispatch_stateless_in_request(
3890                    &dispatch_ctx,
3891                    &child_cx,
3892                    &spawn_request,
3893                    spawn_raw_params.as_deref(),
3894                    &spawn_continuation_cancellation,
3895                )
3896                .await
3897        }) {
3898            Ok(task) => task,
3899            // A context without a spawn gateway (lab/test contexts, plain
3900            // synchronous callers) cannot host the request-owned child; the
3901            // in-request dispatch on the caller's own Cx preserves the same
3902            // cancellation observations without child isolation. Every other
3903            // spawn failure (region closed, quota) stays a scheduling error.
3904            Err(asupersync::runtime::state::SpawnError::RuntimeUnavailable) => {
3905                return self
3906                    .dispatch_stateless_in_request(
3907                        &request_ctx,
3908                        request_ctx.cx(),
3909                        &request,
3910                        raw_params.as_deref(),
3911                        &continuation_cancellation,
3912                    )
3913                    .await;
3914            }
3915            Err(_error) => {
3916                return Err(McpError::internal_error(
3917                    "request-owned modern dispatch could not be scheduled",
3918                ));
3919            }
3920        };
3921
3922        match task.join(&join_cx).await {
3923            Ok(result) => result,
3924            Err(asupersync::runtime::JoinError::Panicked(_payload)) => {
3925                Err(sanitized_handler_panic(&join_cx, "modern_dispatch"))
3926            }
3927            Err(_error) => Err(McpError::request_cancelled()),
3928        }
3929    }
3930
3931    async fn dispatch_stateless_in_request(
3932        &self,
3933        request_ctx: &McpContext,
3934        request_cx: &Cx,
3935        request: &JsonRpcRequest,
3936        raw_params: Option<&str>,
3937        continuation_cancellation: &fastmcp_core::McpRequestCancellation,
3938    ) -> McpResult<serde_json::Value> {
3939        if request_cx.is_cancel_requested() {
3940            return Err(McpError::request_cancelled());
3941        }
3942        if let Some(error) = budget_error(request_ctx) {
3943            return Err(error);
3944        }
3945
3946        let params = request.params.as_ref();
3947        let result = match request.method.as_str() {
3948            // Connection health-check. Session dispatch already answers `{}`.
3949            // Stateless HTTP needs the same check without adding ping to
3950            // FINAL_2026_07_28_METHODS / FinalCoreRequest.
3951            "ping" => serde_json::json!({}),
3952            COMPLETION_COMPLETE => {
3953                let request =
3954                    CoreRequest::decode(ProtocolEra::Modern2026, COMPLETION_COMPLETE, params)
3955                        .map_err(|error| McpError::invalid_params(error.to_string()))?;
3956                let CoreRequest::Final(FinalCoreRequest::Completion(params)) = request else {
3957                    return Err(McpError::internal_error(
3958                        "modern completion dispatch selected another core request",
3959                    ));
3960                };
3961                encode_stateless_handler_result(
3962                    self.handle_completion_final_in_request(request_ctx, request_cx, params)
3963                        .await,
3964                )?
3965            }
3966            "tools/list" => {
3967                let request = CoreRequest::decode(ProtocolEra::Modern2026, "tools/list", params)
3968                    .map_err(|error| McpError::invalid_params(error.to_string()))?;
3969                let CoreRequest::Final(FinalCoreRequest::ToolsList(params)) = request else {
3970                    return Err(McpError::internal_error(
3971                        "modern tools/list dispatch selected another core request",
3972                    ));
3973                };
3974                encode_final_core_result(
3975                    self.handle_final_tools_list(request_ctx, params),
3976                    |result| FinalCoreResult::ToolsList {
3977                        result,
3978                        diagnostic: None,
3979                    },
3980                )?
3981            }
3982            "tools/call" => {
3983                self.admit_final_mrtr_response_map(params)?;
3984                let request = CoreRequest::decode_with_raw_params(
3985                    ProtocolEra::Modern2026,
3986                    "tools/call",
3987                    params,
3988                    raw_params,
3989                )
3990                .map_err(|error| McpError::invalid_params(error.to_string()))?;
3991                let CoreRequest::Final(FinalCoreRequest::ToolsCall(params)) = request else {
3992                    return Err(McpError::internal_error(
3993                        "modern tools/call dispatch selected another core request",
3994                    ));
3995                };
3996                let binding = final_mrtr_binding(
3997                    request_ctx,
3998                    "tools/call",
3999                    params.name.clone(),
4000                    &params.arguments,
4001                )?;
4002                match self.resolve_final_mrtr_retry(
4003                    params.request_state.as_deref(),
4004                    params.input_responses.as_ref(),
4005                    binding.as_ref(),
4006                )? {
4007                    FinalMrtrDispatch::InputRequired(result) => result,
4008                    FinalMrtrDispatch::Fresh => {
4009                        self.dispatch_final_tools_call(
4010                            request_ctx,
4011                            request_cx,
4012                            params,
4013                            binding,
4014                            None,
4015                            continuation_cancellation,
4016                        )
4017                        .await?
4018                    }
4019                    FinalMrtrDispatch::Resume(resume_inputs) => {
4020                        self.dispatch_final_tools_call(
4021                            request_ctx,
4022                            request_cx,
4023                            params,
4024                            binding,
4025                            Some(resume_inputs),
4026                            continuation_cancellation,
4027                        )
4028                        .await?
4029                    }
4030                }
4031            }
4032            "resources/list" => {
4033                let request =
4034                    CoreRequest::decode(ProtocolEra::Modern2026, "resources/list", params)
4035                        .map_err(|error| McpError::invalid_params(error.to_string()))?;
4036                let CoreRequest::Final(FinalCoreRequest::ResourcesList(params)) = &request else {
4037                    return Err(McpError::internal_error(
4038                        "modern resources/list dispatch selected another core request",
4039                    ));
4040                };
4041                encode_final_core_result(
4042                    self.handle_final_resources_list(request_ctx, params.clone()),
4043                    |result| FinalCoreResult::ResourcesList {
4044                        result,
4045                        diagnostic: None,
4046                    },
4047                )?
4048            }
4049            "resources/templates/list" => {
4050                let request = CoreRequest::decode(
4051                    ProtocolEra::Modern2026,
4052                    "resources/templates/list",
4053                    params,
4054                )
4055                .map_err(|error| McpError::invalid_params(error.to_string()))?;
4056                let CoreRequest::Final(FinalCoreRequest::ResourceTemplatesList(params)) = &request
4057                else {
4058                    return Err(McpError::internal_error(
4059                        "modern resources/templates/list dispatch selected another core request",
4060                    ));
4061                };
4062                encode_final_core_result(
4063                    self.handle_final_resource_templates_list(request_ctx, params.clone()),
4064                    |result| FinalCoreResult::ResourceTemplatesList {
4065                        result,
4066                        diagnostic: None,
4067                    },
4068                )?
4069            }
4070            "resources/read" => {
4071                self.admit_final_mrtr_response_map(params)?;
4072                let request = CoreRequest::decode_with_raw_params(
4073                    ProtocolEra::Modern2026,
4074                    "resources/read",
4075                    params,
4076                    raw_params,
4077                )
4078                .map_err(|error| McpError::invalid_params(error.to_string()))?;
4079                let CoreRequest::Final(FinalCoreRequest::ResourcesRead(params)) = &request else {
4080                    return Err(McpError::internal_error(
4081                        "modern resources/read dispatch selected another core request",
4082                    ));
4083                };
4084                let binding = final_mrtr_binding(
4085                    request_ctx,
4086                    "resources/read",
4087                    params.uri.as_str().to_owned(),
4088                    &(),
4089                )?;
4090                match self.resolve_final_mrtr_retry(
4091                    params.request_state.as_deref(),
4092                    params.input_responses.as_ref(),
4093                    binding.as_ref(),
4094                )? {
4095                    FinalMrtrDispatch::InputRequired(result) => result,
4096                    FinalMrtrDispatch::Fresh => {
4097                        self.dispatch_final_resources_read(
4098                            request_ctx,
4099                            request_cx,
4100                            params.clone(),
4101                            binding,
4102                            None,
4103                            continuation_cancellation,
4104                        )
4105                        .await?
4106                    }
4107                    FinalMrtrDispatch::Resume(resume_inputs) => {
4108                        self.dispatch_final_resources_read(
4109                            request_ctx,
4110                            request_cx,
4111                            params.clone(),
4112                            binding,
4113                            Some(resume_inputs),
4114                            continuation_cancellation,
4115                        )
4116                        .await?
4117                    }
4118                }
4119            }
4120            "prompts/list" => {
4121                let request = CoreRequest::decode(ProtocolEra::Modern2026, "prompts/list", params)
4122                    .map_err(|error| McpError::invalid_params(error.to_string()))?;
4123                let CoreRequest::Final(FinalCoreRequest::PromptsList(params)) = &request else {
4124                    return Err(McpError::internal_error(
4125                        "modern prompts/list dispatch selected another core request",
4126                    ));
4127                };
4128                encode_final_core_result(
4129                    self.handle_final_prompts_list(request_ctx, params.clone()),
4130                    |result| FinalCoreResult::PromptsList {
4131                        result,
4132                        diagnostic: None,
4133                    },
4134                )?
4135            }
4136            "prompts/get" => {
4137                self.admit_final_mrtr_response_map(params)?;
4138                let request = CoreRequest::decode_with_raw_params(
4139                    ProtocolEra::Modern2026,
4140                    "prompts/get",
4141                    params,
4142                    raw_params,
4143                )
4144                .map_err(|error| McpError::invalid_params(error.to_string()))?;
4145                let CoreRequest::Final(FinalCoreRequest::PromptsGet(params)) = request else {
4146                    return Err(McpError::internal_error(
4147                        "modern prompts/get dispatch selected another core request",
4148                    ));
4149                };
4150                let binding = final_mrtr_binding(
4151                    request_ctx,
4152                    "prompts/get",
4153                    params.name.clone(),
4154                    &params.arguments,
4155                )?;
4156                match self.resolve_final_mrtr_retry(
4157                    params.request_state.as_deref(),
4158                    params.input_responses.as_ref(),
4159                    binding.as_ref(),
4160                )? {
4161                    FinalMrtrDispatch::InputRequired(result) => result,
4162                    FinalMrtrDispatch::Fresh => {
4163                        self.dispatch_final_prompts_get(
4164                            request_ctx,
4165                            request_cx,
4166                            params,
4167                            binding,
4168                            None,
4169                            continuation_cancellation,
4170                        )
4171                        .await?
4172                    }
4173                    FinalMrtrDispatch::Resume(resume_inputs) => {
4174                        self.dispatch_final_prompts_get(
4175                            request_ctx,
4176                            request_cx,
4177                            params,
4178                            binding,
4179                            Some(resume_inputs),
4180                            continuation_cancellation,
4181                        )
4182                        .await?
4183                    }
4184                }
4185            }
4186            _ => return Err(McpError::method_not_found(&request.method)),
4187        };
4188
4189        if request_cx.is_cancel_requested() {
4190            return Err(McpError::request_cancelled());
4191        }
4192        if let Some(error) = budget_error(request_ctx) {
4193            return Err(error);
4194        }
4195        Ok(result)
4196    }
4197
4198    /// Admits bounded raw retry values before final parameter decoding clones
4199    /// them into method-specific fields or materializes `inputResponses`.
4200    fn admit_final_mrtr_response_map(&self, params: Option<&serde_json::Value>) -> McpResult<()> {
4201        let Some(params) = params else {
4202            return Ok(());
4203        };
4204        admit_mrtr_raw_json_value(params, MAX_MRTR_RAW_PARAMS_BYTES)?;
4205        let Some(input_responses) = params
4206            .as_object()
4207            .and_then(|members| members.get("inputResponses"))
4208        else {
4209            return Ok(());
4210        };
4211        admit_mrtr_raw_json_value(input_responses, MAX_MRTR_RAW_INPUT_RESPONSES_BYTES)?;
4212        let Some(input_responses) = input_responses.as_object() else {
4213            return Ok(());
4214        };
4215        if input_responses.len() > self.mrtr_exchanges.max_inputs_per_round() {
4216            return Err(McpError::invalid_params(
4217                "MRTR inputResponses exceeds the configured bound",
4218            ));
4219        }
4220        Ok(())
4221    }
4222
4223    #[cfg(feature = "tasks")]
4224    fn admit_final_task_tool(&self, metadata: &OpenMetadata) -> McpResult<&FinalTaskRuntime> {
4225        require_final_tasks_capability(metadata)?;
4226        let runtime = self.final_task_runtime.as_ref().ok_or_else(|| {
4227            McpError::internal_error("task-capable tool requires an installed final Tasks runtime")
4228        })?;
4229        runtime.ensure_task_service_ready()?;
4230        Ok(runtime)
4231    }
4232
4233    fn issue_final_mrtr_input_required(
4234        &self,
4235        request_ctx: &McpContext,
4236        continuation_cancellation: fastmcp_core::McpRequestCancellation,
4237        binding: MrtrExchangeBinding,
4238        handler_result: InputRequiredResult,
4239    ) -> McpResult<serde_json::Value> {
4240        // The handler may describe the input it needs, but it never controls
4241        // requestState. Its former state member and open result siblings are
4242        // intentionally not forwarded across this framework boundary.
4243        let input_requests = handler_mrtr_input_requests(request_ctx, &handler_result)?;
4244        let required =
4245            self.mrtr_exchanges
4246                .issue_bound(continuation_cancellation, binding, input_requests)?;
4247        encode_mrtr_input_required_result(required)
4248    }
4249
4250    /// Resolves a final embedded-input retry before its method handler runs.
4251    ///
4252    /// Only a framework-issued state whose immutable operation binding still
4253    /// matches may yield resume inputs. A complete retry passes those typed
4254    /// values into the handler's resume-aware final hook exactly once.
4255    fn resolve_final_mrtr_retry(
4256        &self,
4257        request_state: Option<&str>,
4258        input_responses: Option<&FinalInputResponses>,
4259        binding: Option<&MrtrExchangeBinding>,
4260    ) -> McpResult<FinalMrtrDispatch> {
4261        match (request_state, input_responses) {
4262            (None, None) => Ok(FinalMrtrDispatch::Fresh),
4263            (Some(request_state), None) => {
4264                let binding = binding.ok_or_else(|| {
4265                    McpError::invalid_params("MRTR retries require session state")
4266                })?;
4267                match self
4268                    .mrtr_exchanges
4269                    .accept_state_only_bound(request_state, binding)?
4270                {
4271                    MrtrRetry::Complete(inputs) => Ok(FinalMrtrDispatch::Resume(inputs)),
4272                    MrtrRetry::InputRequired(_) => Err(McpError::internal_error(
4273                        "state-only MRTR retry cannot issue further input requests",
4274                    )),
4275                }
4276            }
4277            (Some(request_state), Some(input_responses)) => {
4278                let binding = binding.ok_or_else(|| {
4279                    McpError::invalid_params("MRTR retries require session state")
4280                })?;
4281                match self.mrtr_exchanges.accept_final_input_responses_bound(
4282                    request_state,
4283                    binding,
4284                    input_responses,
4285                )? {
4286                    MrtrRetry::Complete(inputs) => Ok(FinalMrtrDispatch::Resume(inputs)),
4287                    MrtrRetry::InputRequired(result) => encode_mrtr_input_required_result(result)
4288                        .map(FinalMrtrDispatch::InputRequired),
4289                }
4290            }
4291            _ => Err(McpError::invalid_params(
4292                "final MRTR inputResponses require requestState",
4293            )),
4294        }
4295    }
4296
4297    async fn dispatch_final_tools_call(
4298        &self,
4299        request_ctx: &McpContext,
4300        request_cx: &Cx,
4301        params: FinalCallToolParams,
4302        binding: Option<MrtrExchangeBinding>,
4303        resume_inputs: Option<MrtrCompletedInputs>,
4304        continuation_cancellation: &fastmcp_core::McpRequestCancellation,
4305    ) -> McpResult<serde_json::Value> {
4306        #[cfg(feature = "tasks")]
4307        let request_metadata = params.meta.clone();
4308        let session_state = request_ctx
4309            .session_state()
4310            .cloned()
4311            .unwrap_or_else(SessionState::new);
4312        let outcome = self
4313            .handle_tools_call_final_in_request(
4314                request_ctx,
4315                request_cx,
4316                params,
4317                session_state,
4318                None,
4319                None,
4320                resume_inputs.as_ref(),
4321            )
4322            .await?;
4323        match outcome {
4324            FinalToolOutcome::Complete(result) => encode_final_tools_call_result(Ok(result)),
4325            FinalToolOutcome::InputRequired(result) => {
4326                let binding = binding.ok_or_else(|| {
4327                    McpError::internal_error(
4328                        "MRTR input_required requires session state to bind retries",
4329                    )
4330                })?;
4331                self.issue_final_mrtr_input_required(
4332                    request_ctx,
4333                    continuation_cancellation.clone(),
4334                    binding,
4335                    result,
4336                )
4337            }
4338            #[cfg(feature = "tasks")]
4339            FinalToolOutcome::CreateTask {
4340                work_descriptor,
4341                status_message,
4342            } => {
4343                #[cfg(all(feature = "proxy", feature = "tasks"))]
4344                if let Some(relay) = self.final_task_relay.as_ref() {
4345                    // A relayed task is already durable upstream. Decode its
4346                    // private carrier only after the downstream capability
4347                    // gate, retain the route-bound snapshot for controls, and
4348                    // emit the exact upstream handle without local creation.
4349                    require_final_tasks_capability(&request_metadata)?;
4350                    if let Some(result) = relay.admit_carried_task(&work_descriptor)? {
4351                        return encode_final_task_result(result);
4352                    }
4353                    return Err(McpError::internal_error(
4354                        "a proxy final Tasks relay received a non-relayed CreateTask outcome",
4355                    ));
4356                }
4357                // A handler's declaration means it may return CreateTask; it
4358                // does not turn its Complete or InputRequired outcomes into
4359                // Tasks operations. Admit only the branch that can mutate the
4360                // Tasks store, immediately before that mutation.
4361                let runtime = self.admit_final_task_tool(&request_metadata)?;
4362                encode_final_task_result(
4363                    runtime.create_task_with_work(work_descriptor, status_message)?,
4364                )
4365            }
4366        }
4367    }
4368
4369    async fn dispatch_final_resources_read(
4370        &self,
4371        request_ctx: &McpContext,
4372        request_cx: &Cx,
4373        params: FinalReadResourceParams,
4374        binding: Option<MrtrExchangeBinding>,
4375        resume_inputs: Option<MrtrCompletedInputs>,
4376        continuation_cancellation: &fastmcp_core::McpRequestCancellation,
4377    ) -> McpResult<serde_json::Value> {
4378        let session_state = request_ctx
4379            .session_state()
4380            .cloned()
4381            .unwrap_or_else(SessionState::new);
4382        match self
4383            .handle_resources_read_final_in_request(
4384                request_ctx,
4385                request_cx,
4386                params,
4387                session_state,
4388                None,
4389                None,
4390                resume_inputs.as_ref(),
4391            )
4392            .await?
4393        {
4394            FinalMethodOutcome::Complete(result) => encode_final_resources_read_result(Ok(result)),
4395            FinalMethodOutcome::InputRequired(result) => {
4396                let binding = binding.ok_or_else(|| {
4397                    McpError::internal_error(
4398                        "MRTR input_required requires session state to bind retries",
4399                    )
4400                })?;
4401                self.issue_final_mrtr_input_required(
4402                    request_ctx,
4403                    continuation_cancellation.clone(),
4404                    binding,
4405                    result,
4406                )
4407            }
4408        }
4409    }
4410
4411    async fn dispatch_final_prompts_get(
4412        &self,
4413        request_ctx: &McpContext,
4414        request_cx: &Cx,
4415        params: FinalGetPromptParams,
4416        binding: Option<MrtrExchangeBinding>,
4417        resume_inputs: Option<MrtrCompletedInputs>,
4418        continuation_cancellation: &fastmcp_core::McpRequestCancellation,
4419    ) -> McpResult<serde_json::Value> {
4420        let session_state = request_ctx
4421            .session_state()
4422            .cloned()
4423            .unwrap_or_else(SessionState::new);
4424        match self
4425            .handle_prompts_get_final_in_request(
4426                request_ctx,
4427                request_cx,
4428                params,
4429                session_state,
4430                None,
4431                None,
4432                resume_inputs.as_ref(),
4433            )
4434            .await?
4435        {
4436            FinalMethodOutcome::Complete(result) => encode_final_prompts_get_result(Ok(result)),
4437            FinalMethodOutcome::InputRequired(result) => {
4438                let binding = binding.ok_or_else(|| {
4439                    McpError::internal_error(
4440                        "MRTR input_required requires session state to bind retries",
4441                    )
4442                })?;
4443                self.issue_final_mrtr_input_required(
4444                    request_ctx,
4445                    continuation_cancellation.clone(),
4446                    binding,
4447                    result,
4448                )
4449            }
4450        }
4451    }
4452
4453    /// Handles the tools/list request.
4454    ///
4455    /// If session_state is provided, disabled tools will be filtered out.
4456    /// If include_tags/exclude_tags are provided, tools are filtered by tags.
4457    pub fn handle_tools_list(
4458        &self,
4459        request_ctx: &McpContext,
4460        params: ListToolsParams,
4461        session_state: Option<&SessionState>,
4462    ) -> McpResult<ListToolsResult> {
4463        if let Some(error) = budget_error(request_ctx) {
4464            return Err(error);
4465        }
4466
4467        let tag_filters =
4468            TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
4469        let tag_filters = if params.include_tags.is_some() || params.exclude_tags.is_some() {
4470            Some(&tag_filters)
4471        } else {
4472            None
4473        };
4474        let tools =
4475            crate::catch_extension_unwind(|| self.tools_filtered(session_state, tag_filters))
4476                .map_err(|_payload| sanitized_handler_panic(request_ctx.cx(), "tool_definition"))?;
4477        let Some(page_size) = self.list_page_size else {
4478            return Ok(ListToolsResult {
4479                tools,
4480                next_cursor: None,
4481            });
4482        };
4483        let query = FinalCatalogQuery::from_tag_filters(
4484            params.include_tags.as_deref(),
4485            params.exclude_tags.as_deref(),
4486        );
4487        let (tools, next_cursor) = page_final_catalog(
4488            tools,
4489            params.cursor.as_deref(),
4490            Some(page_size),
4491            FinalCatalogKind::Tools,
4492            self.final_catalog_revision,
4493            &query,
4494        )?;
4495        Ok(ListToolsResult { tools, next_cursor })
4496    }
4497
4498    /// Handles a final tools/list request using only final-admitted entries.
4499    /// Normal registration admits schemas before committing its handler, so a
4500    /// malformed candidate cannot influence a modern catalog page or cursor.
4501    fn handle_final_tools_list(
4502        &self,
4503        request_ctx: &McpContext,
4504        params: FinalListParams,
4505    ) -> McpResult<FinalListToolsResult> {
4506        if let Some(error) = budget_error(request_ctx) {
4507            return Err(error);
4508        }
4509
4510        let query = FinalCatalogQuery::from_final_list_params(&params);
4511        let tag_filters =
4512            TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
4513        let tag_filters = if params.include_tags.is_some() || params.exclude_tags.is_some() {
4514            Some(&tag_filters)
4515        } else {
4516            None
4517        };
4518        let tools = crate::catch_extension_unwind(|| {
4519            self.tool_order
4520                .iter()
4521                .filter_map(|name| self.tools.get(name))
4522                .filter(|entry| entry.final_registration.is_some())
4523                .map(|entry| entry.definition.clone())
4524                .filter(|tool| tag_filters.is_none_or(|filters| filters.matches(&tool.tags)))
4525                .collect::<Vec<_>>()
4526        })
4527        .map_err(|_payload| sanitized_handler_panic(request_ctx.cx(), "tool_definition"))?;
4528
4529        let (tools, next_cursor) = page_final_catalog(
4530            tools,
4531            params.cursor.as_deref(),
4532            self.list_page_size,
4533            FinalCatalogKind::Tools,
4534            self.final_catalog_revision,
4535            &query,
4536        )?;
4537        let result = ListToolsResult { tools, next_cursor };
4538        self.project_final_tools_list(request_ctx, result, self.final_cache_hints.clone())
4539    }
4540
4541    fn project_final_tools_list(
4542        &self,
4543        _request_ctx: &McpContext,
4544        result: ListToolsResult,
4545        cache_hints: FinalCacheHintPolicy,
4546    ) -> McpResult<FinalListToolsResult> {
4547        let tools = result
4548            .tools
4549            .into_iter()
4550            .map(|tool| {
4551                let entry = self.tools.get(&tool.name).ok_or_else(|| {
4552                    McpError::internal_error("listed tool is absent from the router catalog")
4553                })?;
4554                let final_registration = entry.final_registration.as_ref().ok_or_else(|| {
4555                    McpError::internal_error(
4556                        "legacy-only tool reached the final catalog projection",
4557                    )
4558                })?;
4559
4560                Ok(final_registration.final_definition.clone())
4561            })
4562            .collect::<McpResult<Vec<_>>>()?;
4563        Ok(FinalListToolsResult {
4564            tools,
4565            next_cursor: result.next_cursor,
4566            ttl_ms: cache_hints.list_ttl_ms,
4567            cache_scope: cache_hints.scope,
4568        })
4569    }
4570
4571    fn handle_final_resources_list(
4572        &self,
4573        request_ctx: &McpContext,
4574        params: FinalListParams,
4575    ) -> McpResult<FinalListResourcesResult> {
4576        if let Some(error) = budget_error(request_ctx) {
4577            return Err(error);
4578        }
4579        let query = FinalCatalogQuery::from_final_list_params(&params);
4580        let filters = TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
4581        let filters =
4582            (params.include_tags.is_some() || params.exclude_tags.is_some()).then_some(filters);
4583        let resources = self
4584            .resource_order
4585            .iter()
4586            .filter_map(|uri| self.final_resources.get(uri).map(|entry| (uri, entry)))
4587            .filter(|(_, entry)| {
4588                filters
4589                    .as_ref()
4590                    .is_none_or(|filters| filters.matches(&entry.tags))
4591            })
4592            .map(|(_, entry)| {
4593                admit_final_resource_uri(
4594                    entry.uri_use_policy,
4595                    &entry.definition.uri,
4596                    FinalResourceUriUse::CatalogResource,
4597                )?;
4598                Ok(entry.definition.clone())
4599            })
4600            .collect::<McpResult<Vec<_>>>()?;
4601        let (resources, next_cursor) = page_final_catalog(
4602            resources,
4603            params.cursor.as_deref(),
4604            self.list_page_size,
4605            FinalCatalogKind::Resources,
4606            self.final_catalog_revision,
4607            &query,
4608        )?;
4609        Ok(FinalListResourcesResult {
4610            resources,
4611            next_cursor,
4612            ttl_ms: self.final_cache_hints.list_ttl_ms.clone(),
4613            cache_scope: self.final_cache_hints.scope,
4614        })
4615    }
4616
4617    fn handle_final_resource_templates_list(
4618        &self,
4619        request_ctx: &McpContext,
4620        params: FinalListParams,
4621    ) -> McpResult<FinalListResourceTemplatesResult> {
4622        if let Some(error) = budget_error(request_ctx) {
4623            return Err(error);
4624        }
4625        let query = FinalCatalogQuery::from_final_list_params(&params);
4626        let filters = TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
4627        let filters =
4628            (params.include_tags.is_some() || params.exclude_tags.is_some()).then_some(filters);
4629        let resource_templates = self
4630            .resource_template_order
4631            .iter()
4632            .filter_map(|key| self.resource_templates.get(key).map(|entry| (key, entry)))
4633            .filter_map(|(_, entry)| {
4634                entry
4635                    .final_definition
4636                    .as_ref()
4637                    .map(|definition| (definition, &entry.template.tags, entry.uri_use_policy))
4638            })
4639            .filter(|(_, tags, _)| filters.as_ref().is_none_or(|filters| filters.matches(tags)))
4640            .map(|(definition, _, uri_use_policy)| {
4641                admit_final_resource_template_uri(uri_use_policy, &definition.uri_template)?;
4642                Ok(definition.clone())
4643            })
4644            .collect::<McpResult<Vec<_>>>()?;
4645        let (resource_templates, next_cursor) = page_final_catalog(
4646            resource_templates,
4647            params.cursor.as_deref(),
4648            self.list_page_size,
4649            FinalCatalogKind::ResourceTemplates,
4650            self.final_catalog_revision,
4651            &query,
4652        )?;
4653        Ok(FinalListResourceTemplatesResult {
4654            resource_templates,
4655            next_cursor,
4656            ttl_ms: self.final_cache_hints.list_ttl_ms.clone(),
4657            cache_scope: self.final_cache_hints.scope,
4658        })
4659    }
4660
4661    fn handle_final_prompts_list(
4662        &self,
4663        request_ctx: &McpContext,
4664        params: FinalListParams,
4665    ) -> McpResult<FinalListPromptsResult> {
4666        if let Some(error) = budget_error(request_ctx) {
4667            return Err(error);
4668        }
4669        let query = FinalCatalogQuery::from_final_list_params(&params);
4670        let filters = TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
4671        let filters =
4672            (params.include_tags.is_some() || params.exclude_tags.is_some()).then_some(filters);
4673        let prompts = self
4674            .prompt_order
4675            .iter()
4676            .filter_map(|name| self.final_prompts.get(name).map(|entry| (name, entry)))
4677            .filter(|(_, entry)| {
4678                filters
4679                    .as_ref()
4680                    .is_none_or(|filters| filters.matches(&entry.tags))
4681            })
4682            .map(|(_, entry)| entry.definition.clone())
4683            .collect();
4684        let (prompts, next_cursor) = page_final_catalog(
4685            prompts,
4686            params.cursor.as_deref(),
4687            self.list_page_size,
4688            FinalCatalogKind::Prompts,
4689            self.final_catalog_revision,
4690            &query,
4691        )?;
4692        Ok(FinalListPromptsResult {
4693            prompts,
4694            next_cursor,
4695            ttl_ms: self.final_cache_hints.list_ttl_ms.clone(),
4696            cache_scope: self.final_cache_hints.scope,
4697        })
4698    }
4699
4700    /// Handles the tools/call request.
4701    ///
4702    /// # Arguments
4703    ///
4704    /// * `request_ctx` - Request authority for cancellation, identity, auth, and accounting
4705    /// * `params` - The tool call parameters including tool name and arguments
4706    /// * `session_state` - Session state for per-session storage
4707    /// * `notification_sender` - Optional callback for sending progress notifications
4708    /// * `bidirectional_senders` - Optional senders for sampling/elicitation
4709    pub fn handle_tools_call(
4710        &self,
4711        request_ctx: &McpContext,
4712        params: CallToolParams,
4713        session_state: SessionState,
4714        notification_sender: Option<&NotificationSender>,
4715        bidirectional_senders: Option<&BidirectionalSenders>,
4716    ) -> McpResult<CallToolResult> {
4717        block_on(self.handle_tools_call_in_request(
4718            request_ctx,
4719            request_ctx.cx(),
4720            params,
4721            session_state,
4722            notification_sender,
4723            bidirectional_senders,
4724        ))
4725    }
4726
4727    pub(crate) async fn handle_tools_call_in_request(
4728        &self,
4729        request_ctx: &McpContext,
4730        request_cx: &Cx,
4731        params: CallToolParams,
4732        session_state: SessionState,
4733        notification_sender: Option<&NotificationSender>,
4734        bidirectional_senders: Option<&BidirectionalSenders>,
4735    ) -> McpResult<CallToolResult> {
4736        debug!(
4737            target: targets::HANDLER,
4738            "calling modern tool; tool_key={}; arguments_present={}",
4739            safe_log_label(&params.name),
4740            params.arguments.is_some()
4741        );
4742
4743        let dispatch_started_at = request_ctx.cx().now();
4744        if let Some(error) = budget_error(request_ctx) {
4745            return Err(error);
4746        }
4747        if !session_state.is_tool_enabled(&params.name) {
4748            return Err(McpError::new(
4749                McpErrorCode::MethodNotFound,
4750                format!("Tool '{}' is disabled for this session", params.name),
4751            ));
4752        }
4753
4754        let entry = self
4755            .tools
4756            .get(&params.name)
4757            .ok_or_else(|| McpError::method_not_found(&format!("tool: {}", params.name)))?;
4758        if !entry.legacy_enabled {
4759            return Err(McpError::method_not_found(&format!(
4760                "tool: {}",
4761                params.name
4762            )));
4763        }
4764        let handler = &entry.handler;
4765        let arguments = params.arguments.unwrap_or_else(|| serde_json::json!({}));
4766        let validation_result = if self.strict_input_validation {
4767            validate_strict(&entry.definition.input_schema, &arguments)
4768        } else {
4769            validate(&entry.definition.input_schema, &arguments)
4770        };
4771        if let Err(validation_errors) = validation_result {
4772            let error_messages: Vec<String> = validation_errors
4773                .iter()
4774                .map(|error| format!("{}: {}", error.path, error.message))
4775                .collect();
4776            return Err(McpError::invalid_params(format!(
4777                "Input validation failed: {}",
4778                error_messages.join("; ")
4779            )));
4780        }
4781
4782        let progress_marker = params
4783            .meta
4784            .as_ref()
4785            .and_then(|meta| meta.progress_marker.clone());
4786        let ctx = derive_handler_context(
4787            request_ctx,
4788            progress_marker,
4789            notification_sender,
4790            bidirectional_senders,
4791            ProtocolEra::Legacy2024,
4792        );
4793        let handler_timeout =
4794            read_handler_timeout(request_ctx.cx(), "tool_timeout", || handler.timeout())?;
4795        let effective_budget = compose_handler_budget(
4796            request_ctx.cx().budget(),
4797            request_ctx.budget(),
4798            handler_timeout,
4799            dispatch_started_at,
4800        );
4801        let ctx = ctx.with_operation_deadline(effective_budget.deadline);
4802        let outcome =
4803            run_handler_in_request(&ctx, request_cx, effective_budget, "tool", |child_cx| {
4804                handler.call_async_in_request(&ctx, child_cx, arguments)
4805            })
4806            .await?;
4807
4808        match outcome {
4809            Outcome::Ok(content) => Ok(CallToolResult {
4810                content: legacy_contents_from_handler(content)?,
4811                is_error: false,
4812                meta: None,
4813                additional: BTreeMap::new(),
4814            }),
4815            Outcome::Err(error) => {
4816                let error = sanitize_handler_error(request_ctx.cx(), "tool", error);
4817                if is_framework_terminal_tool_error(error.code) {
4818                    return Err(error);
4819                }
4820                Ok(CallToolResult {
4821                    content: vec![LegacyContent::Text {
4822                        text: error.message,
4823                        annotations: None,
4824                        additional: BTreeMap::new(),
4825                    }],
4826                    is_error: true,
4827                    meta: None,
4828                    additional: BTreeMap::new(),
4829                })
4830            }
4831            Outcome::Cancelled(_) => Err(McpError::request_cancelled()),
4832            Outcome::Panicked(_payload) => Err(sanitized_handler_panic(request_ctx.cx(), "tool")),
4833        }
4834    }
4835
4836    /// Handles one final MCP 2026-07-28 `tools/call` request.
4837    ///
4838    /// Legacy dispatch remains on [`Self::handle_tools_call`], including its
4839    /// exact `CallToolResult` behavior. Final dispatch calls the final handler
4840    /// hook directly and encodes the returned complete result with the typed
4841    /// core result codec.
4842    async fn handle_tools_call_final_in_request(
4843        &self,
4844        request_ctx: &McpContext,
4845        request_cx: &Cx,
4846        params: FinalCallToolParams,
4847        session_state: SessionState,
4848        notification_sender: Option<&NotificationSender>,
4849        bidirectional_senders: Option<&BidirectionalSenders>,
4850        resume_inputs: Option<&MrtrCompletedInputs>,
4851    ) -> McpResult<FinalToolOutcome> {
4852        debug!(
4853            target: targets::HANDLER,
4854            "calling final tool; tool_key={}; arguments_present={}",
4855            safe_log_label(&params.name),
4856            !params.arguments.is_absent()
4857        );
4858
4859        let dispatch_started_at = request_ctx.cx().now();
4860        if request_cx.is_cancel_requested() {
4861            return Err(McpError::request_cancelled());
4862        }
4863        if let Some(error) = budget_error(request_ctx) {
4864            return Err(error);
4865        }
4866        let progress_marker = final_progress_marker(&params.meta)?;
4867        // Modern connection state is attached to `request_ctx`. Stateless
4868        // callers still supply the explicit `session_state`, so both views
4869        // must admit the component before a final handler (or an MRTR state)
4870        // can be reached.
4871        if !session_state.is_tool_enabled(&params.name)
4872            || !request_ctx.is_tool_enabled(&params.name)
4873        {
4874            return Err(McpError::new(
4875                McpErrorCode::MethodNotFound,
4876                format!("Tool '{}' is disabled for this session", params.name),
4877            ));
4878        }
4879
4880        let entry = self
4881            .tools
4882            .get(&params.name)
4883            .ok_or_else(|| McpError::invalid_params(format!("Unknown tool: {}", params.name)))?;
4884        let final_registration = entry
4885            .final_registration
4886            .as_ref()
4887            .ok_or_else(|| McpError::invalid_params(format!("Unknown tool: {}", params.name)))?;
4888        let handler = &entry.handler;
4889        if handler.declares_final_mrtr() && request_ctx.session_cache_partition().is_none() {
4890            return Err(McpError::invalid_params(
4891                MRTR_REQUIRES_BOUND_MODERN_CONNECTION,
4892            ));
4893        }
4894        let input_schema = final_registration.schemas.input.as_ref();
4895        let output_schema = final_registration.schemas.output.as_ref();
4896        // FinalArguments deserialization rejects an explicit null before this
4897        // point, so `arguments` is here always Absent or a typed value.
4898        #[cfg(feature = "tasks")]
4899        let declares_final_tasks = final_registration.declares_final_tasks;
4900        let arguments = params
4901            .arguments
4902            .into_value()
4903            .unwrap_or_else(|| serde_json::json!({}));
4904        let input_validation_failed = match input_schema {
4905            Some(schema) => {
4906                let validation = if self.strict_input_validation {
4907                    validate_strict(schema.schema(), &arguments)
4908                } else {
4909                    schema.validate(&arguments)
4910                };
4911                validation.is_err()
4912            }
4913            None if self.strict_input_validation => {
4914                // Upstream/proxy tools skip local schema admission so a valid
4915                // non-object catalog schema can be retained without inventing
4916                // an error payload. Gateway strict mode still refuses
4917                // additionalProperties against that catalog input schema.
4918                validate_strict(
4919                    &final_registration.final_definition.input_schema,
4920                    &arguments,
4921                )
4922                .is_err()
4923            }
4924            None => false,
4925        };
4926        if input_validation_failed {
4927            let mut result = crate::handler::promote_legacy_tool_content(vec![Content::text(
4928                "Tool arguments do not match the declared input schema.",
4929            )])?;
4930            result.payload.is_error = true;
4931            result.payload.structured_content = final_registration
4932                .schemas
4933                .errors
4934                .as_ref()
4935                .map(|errors| errors.input_validation.clone());
4936            return Ok(FinalToolOutcome::Complete(result));
4937        }
4938
4939        // A route-bound proxy asks its selected upstream to create a task
4940        // during this handler call. Unlike a local handler's deferred
4941        // `CreateTask`, that side effect cannot be rolled back after the
4942        // proxy returns. Require the exact downstream Tasks declaration
4943        // before invoking any task-capable proxy handler.
4944        #[cfg(all(feature = "proxy", feature = "tasks"))]
4945        if declares_final_tasks && self.final_task_relay.is_some() {
4946            require_final_tasks_capability(&params.meta)?;
4947        }
4948
4949        let ctx = derive_handler_context(
4950            request_ctx,
4951            progress_marker,
4952            notification_sender,
4953            bidirectional_senders,
4954            ProtocolEra::Modern2026,
4955        );
4956        let handler_timeout =
4957            read_handler_timeout(request_ctx.cx(), "tool_timeout", || handler.timeout())?;
4958        let effective_budget = compose_handler_budget(
4959            request_ctx.cx().budget(),
4960            request_ctx.budget(),
4961            handler_timeout,
4962            dispatch_started_at,
4963        );
4964        let ctx = ctx.with_operation_deadline(effective_budget.deadline);
4965        let outcome =
4966            run_handler_in_request(&ctx, request_cx, effective_budget, "tool", |child_cx| {
4967                // MRTR-aware handlers receive every call through the resuming
4968                // hook: None marks the initial invocation, Some the admitted
4969                // retry. The default resuming hook forwards to the plain
4970                // final hook, so MRTR-unaware handlers are unaffected.
4971                handler.call_final_outcome_async_resuming_in_request(
4972                    &ctx,
4973                    child_cx,
4974                    arguments,
4975                    resume_inputs,
4976                )
4977            })
4978            .await?;
4979
4980        match outcome {
4981            Outcome::Ok(result) => {
4982                match &result {
4983                    FinalToolOutcome::Complete(result) => {
4984                        if let Some(output_schema) = output_schema {
4985                            let structured_content = result
4986                                .payload
4987                                .structured_content
4988                                .as_ref()
4989                                .ok_or_else(|| {
4990                                    McpError::internal_error(
4991                                        "tool output is missing structuredContent required by the declared output schema",
4992                                    )
4993                                })?;
4994                            if output_schema.validate(structured_content).is_err() {
4995                                return Err(McpError::internal_error(
4996                                    "tool output does not match the declared output schema",
4997                                ));
4998                            }
4999                        }
5000                    }
5001                    #[cfg(feature = "tasks")]
5002                    FinalToolOutcome::CreateTask { .. } if !declares_final_tasks => {
5003                        return Err(McpError::invalid_request(
5004                            "tool returned CreateTask without declaring final Tasks capability",
5005                        ));
5006                    }
5007                    FinalToolOutcome::InputRequired(_) => {}
5008                    #[cfg(feature = "tasks")]
5009                    FinalToolOutcome::CreateTask { .. } => {}
5010                }
5011                Ok(result)
5012            }
5013            Outcome::Err(error) => {
5014                let error = sanitize_handler_error(request_ctx.cx(), "tool", error);
5015                if is_framework_terminal_tool_error(error.code) {
5016                    return Err(error);
5017                }
5018                let mut result =
5019                    crate::handler::promote_legacy_tool_content(vec![Content::Text {
5020                        text: error.message,
5021                    }])?;
5022                result.payload.is_error = true;
5023                result.payload.structured_content = final_registration
5024                    .schemas
5025                    .errors
5026                    .as_ref()
5027                    .map(|errors| errors.handler.clone());
5028                Ok(FinalToolOutcome::Complete(result))
5029            }
5030            Outcome::Cancelled(_) => Err(McpError::request_cancelled()),
5031            Outcome::Panicked(_payload) => Err(sanitized_handler_panic(request_ctx.cx(), "tool")),
5032        }
5033    }
5034
5035    /// Handles the resources/list request.
5036    ///
5037    /// If session_state is provided, disabled resources will be filtered out.
5038    /// If include_tags/exclude_tags are provided, resources are filtered by tags.
5039    pub fn handle_resources_list(
5040        &self,
5041        request_ctx: &McpContext,
5042        params: ListResourcesParams,
5043        session_state: Option<&SessionState>,
5044    ) -> McpResult<ListResourcesResult> {
5045        if let Some(error) = budget_error(request_ctx) {
5046            return Err(error);
5047        }
5048
5049        let tag_filters =
5050            TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
5051        let tag_filters = if params.include_tags.is_some() || params.exclude_tags.is_some() {
5052            Some(&tag_filters)
5053        } else {
5054            None
5055        };
5056        let resources = crate::catch_extension_unwind(|| {
5057            self.resources_filtered(session_state, tag_filters)
5058        })
5059        .map_err(|_payload| sanitized_handler_panic(request_ctx.cx(), "resource_definition"))?;
5060        let Some(page_size) = self.list_page_size else {
5061            return Ok(ListResourcesResult {
5062                resources,
5063                next_cursor: None,
5064            });
5065        };
5066        let query = FinalCatalogQuery::from_tag_filters(
5067            params.include_tags.as_deref(),
5068            params.exclude_tags.as_deref(),
5069        );
5070        let (resources, next_cursor) = page_final_catalog(
5071            resources,
5072            params.cursor.as_deref(),
5073            Some(page_size),
5074            FinalCatalogKind::Resources,
5075            self.final_catalog_revision,
5076            &query,
5077        )?;
5078        Ok(ListResourcesResult {
5079            resources,
5080            next_cursor,
5081        })
5082    }
5083
5084    /// Handles the resources/templates/list request.
5085    ///
5086    /// If session_state is provided, disabled resource templates will be filtered out.
5087    /// If include_tags/exclude_tags are provided, templates are filtered by tags.
5088    pub fn handle_resource_templates_list(
5089        &self,
5090        request_ctx: &McpContext,
5091        params: ListResourceTemplatesParams,
5092        session_state: Option<&SessionState>,
5093    ) -> McpResult<ListResourceTemplatesResult> {
5094        if let Some(error) = budget_error(request_ctx) {
5095            return Err(error);
5096        }
5097
5098        let tag_filters =
5099            TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
5100        let tag_filters = if params.include_tags.is_some() || params.exclude_tags.is_some() {
5101            Some(&tag_filters)
5102        } else {
5103            None
5104        };
5105        let templates = self.resource_templates_filtered(session_state, tag_filters);
5106        let Some(page_size) = self.list_page_size else {
5107            return Ok(ListResourceTemplatesResult {
5108                resource_templates: templates,
5109                next_cursor: None,
5110            });
5111        };
5112        let query = FinalCatalogQuery::from_tag_filters(
5113            params.include_tags.as_deref(),
5114            params.exclude_tags.as_deref(),
5115        );
5116        let (resource_templates, next_cursor) = page_final_catalog(
5117            templates,
5118            params.cursor.as_deref(),
5119            Some(page_size),
5120            FinalCatalogKind::ResourceTemplates,
5121            self.final_catalog_revision,
5122            &query,
5123        )?;
5124        Ok(ListResourceTemplatesResult {
5125            resource_templates,
5126            next_cursor,
5127        })
5128    }
5129
5130    /// Handles the resources/read request.
5131    ///
5132    /// # Arguments
5133    ///
5134    /// * `request_ctx` - Request authority for cancellation, identity, auth, and accounting
5135    /// * `params` - The resource read parameters including URI
5136    /// * `session_state` - Session state for per-session storage
5137    /// * `notification_sender` - Optional callback for sending progress notifications
5138    /// * `bidirectional_senders` - Optional senders for sampling/elicitation
5139    pub fn handle_resources_read(
5140        &self,
5141        request_ctx: &McpContext,
5142        params: &ReadResourceParams,
5143        session_state: SessionState,
5144        notification_sender: Option<&NotificationSender>,
5145        bidirectional_senders: Option<&BidirectionalSenders>,
5146    ) -> McpResult<ReadResourceResult> {
5147        block_on(self.handle_resources_read_in_request(
5148            request_ctx,
5149            request_ctx.cx(),
5150            params,
5151            session_state,
5152            notification_sender,
5153            bidirectional_senders,
5154        ))
5155    }
5156
5157    pub(crate) async fn handle_resources_read_in_request(
5158        &self,
5159        request_ctx: &McpContext,
5160        request_cx: &Cx,
5161        params: &ReadResourceParams,
5162        session_state: SessionState,
5163        notification_sender: Option<&NotificationSender>,
5164        bidirectional_senders: Option<&BidirectionalSenders>,
5165    ) -> McpResult<ReadResourceResult> {
5166        debug!(
5167            target: targets::HANDLER,
5168            "reading modern resource; resource_key={}",
5169            safe_log_label(&params.uri)
5170        );
5171
5172        let dispatch_started_at = request_ctx.cx().now();
5173        if let Some(error) = budget_error(request_ctx) {
5174            return Err(error);
5175        }
5176        if !session_state.is_resource_enabled(&params.uri) {
5177            return Err(McpError::new(
5178                McpErrorCode::ResourceNotFound,
5179                format!("Resource '{}' is disabled for this session", params.uri),
5180            ));
5181        }
5182
5183        let resolved = self
5184            .resolve_resource_for_era(&params.uri, Some(ProtocolEra::Legacy2024))
5185            .ok_or_else(|| McpError::resource_not_found(&params.uri))?;
5186        if !resolved.legacy_enabled {
5187            return Err(McpError::resource_not_found(&params.uri));
5188        }
5189        let progress_marker = params
5190            .meta
5191            .as_ref()
5192            .and_then(|meta| meta.progress_marker.clone());
5193        let ctx = derive_handler_context(
5194            request_ctx,
5195            progress_marker,
5196            notification_sender,
5197            bidirectional_senders,
5198            ProtocolEra::Legacy2024,
5199        );
5200        let handler_timeout = read_handler_timeout(request_ctx.cx(), "resource_timeout", || {
5201            resolved.handler.timeout()
5202        })?;
5203        let effective_budget = compose_handler_budget(
5204            request_ctx.cx().budget(),
5205            request_ctx.budget(),
5206            handler_timeout,
5207            dispatch_started_at,
5208        );
5209        let ctx = ctx.with_operation_deadline(effective_budget.deadline);
5210        let outcome =
5211            run_handler_in_request(&ctx, request_cx, effective_budget, "resource", |child_cx| {
5212                resolved.handler.read_async_with_uri_in_request(
5213                    &ctx,
5214                    child_cx,
5215                    &params.uri,
5216                    &resolved.params,
5217                )
5218            })
5219            .await?;
5220
5221        let contents = match outcome {
5222            Outcome::Ok(contents) => contents,
5223            Outcome::Err(error) => {
5224                return Err(sanitize_handler_error(request_ctx.cx(), "resource", error));
5225            }
5226            Outcome::Cancelled(_) => return Err(McpError::request_cancelled()),
5227            Outcome::Panicked(_payload) => {
5228                return Err(sanitized_handler_panic(request_ctx.cx(), "resource"));
5229            }
5230        };
5231
5232        Ok(ReadResourceResult {
5233            contents: legacy_resource_contents_from_handler(contents)?,
5234            meta: None,
5235            additional: BTreeMap::new(),
5236        })
5237    }
5238
5239    /// Handles one final MCP 2026-07-28 `resources/read` request.
5240    ///
5241    /// Legacy dispatch remains on [`Self::handle_resources_read`], including
5242    /// its exact `ReadResourceResult` shape. Final dispatch calls the final
5243    /// handler hook directly so embedded resource metadata, open fields, and
5244    /// cache hints are not projected through the legacy resource surface.
5245    async fn handle_resources_read_final_in_request(
5246        &self,
5247        request_ctx: &McpContext,
5248        request_cx: &Cx,
5249        params: FinalReadResourceParams,
5250        session_state: SessionState,
5251        notification_sender: Option<&NotificationSender>,
5252        bidirectional_senders: Option<&BidirectionalSenders>,
5253        resume_inputs: Option<&MrtrCompletedInputs>,
5254    ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
5255        let progress_marker = final_progress_marker(&params.meta)?;
5256        let uri = params.uri.as_str();
5257        debug!(
5258            target: targets::HANDLER,
5259            "reading final resource; resource_key={}",
5260            safe_log_label(uri)
5261        );
5262
5263        let dispatch_started_at = request_ctx.cx().now();
5264        if request_cx.is_cancel_requested() {
5265            return Err(McpError::request_cancelled());
5266        }
5267        if let Some(error) = budget_error(request_ctx) {
5268            return Err(error);
5269        }
5270        // See the corresponding final tools/call check: a modern connection's
5271        // durable component state lives on the request context.
5272        if !session_state.is_resource_enabled(uri) || !request_ctx.is_resource_enabled(uri) {
5273            return Err(McpError::new(
5274                McpErrorCode::ResourceNotFound,
5275                format!("Resource '{uri}' is disabled for this session"),
5276            ));
5277        }
5278
5279        let resolved = self
5280            .resolve_resource_for_era(uri, Some(ProtocolEra::Modern2026))
5281            .ok_or_else(|| {
5282                McpError::with_data(
5283                    McpErrorCode::InvalidParams,
5284                    "Resource not found",
5285                    serde_json::json!({"uri": uri}),
5286                )
5287            })?;
5288        if !resolved.final_enabled {
5289            return Err(McpError::invalid_params(
5290                "resource is registered only for exact MCP 2024-11-05 dispatch",
5291            ));
5292        }
5293        if resolved.handler.declares_final_mrtr() && request_ctx.session_cache_partition().is_none()
5294        {
5295            return Err(McpError::invalid_params(
5296                MRTR_REQUIRES_BOUND_MODERN_CONNECTION,
5297            ));
5298        }
5299        admit_final_resource_uri(
5300            resolved.uri_use_policy,
5301            &params.uri,
5302            FinalResourceUriUse::ResourceReadTarget,
5303        )?;
5304        let cache_hint_provenance = crate::catch_extension_unwind(|| {
5305            resolved.handler.final_resource_read_cache_hint_provenance()
5306        })
5307        .map_err(|_payload| {
5308            McpError::internal_error("resource cache-hint provenance hook panicked during dispatch")
5309        })?;
5310        let ctx = derive_handler_context(
5311            request_ctx,
5312            progress_marker,
5313            notification_sender,
5314            bidirectional_senders,
5315            ProtocolEra::Modern2026,
5316        );
5317        let handler_timeout = read_handler_timeout(request_ctx.cx(), "resource_timeout", || {
5318            resolved.handler.timeout()
5319        })?;
5320        let effective_budget = compose_handler_budget(
5321            request_ctx.cx().budget(),
5322            request_ctx.budget(),
5323            handler_timeout,
5324            dispatch_started_at,
5325        );
5326        let ctx = ctx.with_operation_deadline(effective_budget.deadline);
5327        let outcome =
5328            run_handler_in_request(&ctx, request_cx, effective_budget, "resource", |child_cx| {
5329                if let Some(resume_inputs) = resume_inputs {
5330                    resolved
5331                        .handler
5332                        .read_final_outcome_async_with_uri_resuming_in_request(
5333                            &ctx,
5334                            child_cx,
5335                            uri,
5336                            &resolved.params,
5337                            Some(resume_inputs),
5338                        )
5339                } else {
5340                    resolved
5341                        .handler
5342                        .read_final_outcome_async_with_uri_in_request(
5343                            &ctx,
5344                            child_cx,
5345                            uri,
5346                            &resolved.params,
5347                        )
5348                }
5349            })
5350            .await?;
5351
5352        match outcome {
5353            Outcome::Ok(mut result) => {
5354                admit_final_resource_read_outcome(resolved.uri_use_policy, &result)?;
5355                // Provenance, not equality with a wire value, determines
5356                // whether router policy owns these hints. An explicit final
5357                // handler may intentionally choose the same values as the
5358                // legacy bridge and must still retain them unchanged.
5359                if let FinalMethodOutcome::Complete(complete) = &mut result
5360                    && cache_hint_provenance == FinalResourceReadCacheHintProvenance::RouterPolicy
5361                {
5362                    complete.payload.ttl_ms = self.final_cache_hints.resource_read_ttl_ms.clone();
5363                    complete.payload.cache_scope = self.final_cache_hints.scope;
5364                }
5365                Ok(result)
5366            }
5367            Outcome::Err(error) => Err(sanitize_handler_error(request_ctx.cx(), "resource", error)),
5368            Outcome::Cancelled(_) => Err(McpError::request_cancelled()),
5369            Outcome::Panicked(_payload) => {
5370                Err(sanitized_handler_panic(request_ctx.cx(), "resource"))
5371            }
5372        }
5373    }
5374
5375    /// Handles the prompts/list request.
5376    ///
5377    /// If session_state is provided, disabled prompts will be filtered out.
5378    /// If include_tags/exclude_tags are provided, prompts are filtered by tags.
5379    pub fn handle_prompts_list(
5380        &self,
5381        request_ctx: &McpContext,
5382        params: ListPromptsParams,
5383        session_state: Option<&SessionState>,
5384    ) -> McpResult<ListPromptsResult> {
5385        if let Some(error) = budget_error(request_ctx) {
5386            return Err(error);
5387        }
5388
5389        let tag_filters =
5390            TagFilters::new(params.include_tags.as_ref(), params.exclude_tags.as_ref());
5391        let tag_filters = if params.include_tags.is_some() || params.exclude_tags.is_some() {
5392            Some(&tag_filters)
5393        } else {
5394            None
5395        };
5396        let prompts =
5397            crate::catch_extension_unwind(|| self.prompts_filtered(session_state, tag_filters))
5398                .map_err(|_payload| {
5399                    sanitized_handler_panic(request_ctx.cx(), "prompt_definition")
5400                })?;
5401        let Some(page_size) = self.list_page_size else {
5402            return Ok(ListPromptsResult {
5403                prompts,
5404                next_cursor: None,
5405            });
5406        };
5407        let query = FinalCatalogQuery::from_tag_filters(
5408            params.include_tags.as_deref(),
5409            params.exclude_tags.as_deref(),
5410        );
5411        let (prompts, next_cursor) = page_final_catalog(
5412            prompts,
5413            params.cursor.as_deref(),
5414            Some(page_size),
5415            FinalCatalogKind::Prompts,
5416            self.final_catalog_revision,
5417            &query,
5418        )?;
5419        Ok(ListPromptsResult {
5420            prompts,
5421            next_cursor,
5422        })
5423    }
5424
5425    /// Handles the prompts/get request.
5426    ///
5427    /// # Arguments
5428    ///
5429    /// * `request_ctx` - Request authority for cancellation, identity, auth, and accounting
5430    /// * `params` - The prompt get parameters including name and arguments
5431    /// * `session_state` - Session state for per-session storage
5432    /// * `notification_sender` - Optional callback for sending progress notifications
5433    /// * `bidirectional_senders` - Optional senders for sampling/elicitation
5434    pub fn handle_prompts_get(
5435        &self,
5436        request_ctx: &McpContext,
5437        params: GetPromptParams,
5438        session_state: SessionState,
5439        notification_sender: Option<&NotificationSender>,
5440        bidirectional_senders: Option<&BidirectionalSenders>,
5441    ) -> McpResult<GetPromptResult> {
5442        block_on(self.handle_prompts_get_in_request(
5443            request_ctx,
5444            request_ctx.cx(),
5445            params,
5446            session_state,
5447            notification_sender,
5448            bidirectional_senders,
5449        ))
5450    }
5451
5452    pub(crate) async fn handle_prompts_get_in_request(
5453        &self,
5454        request_ctx: &McpContext,
5455        request_cx: &Cx,
5456        params: GetPromptParams,
5457        session_state: SessionState,
5458        notification_sender: Option<&NotificationSender>,
5459        bidirectional_senders: Option<&BidirectionalSenders>,
5460    ) -> McpResult<GetPromptResult> {
5461        debug!(
5462            target: targets::HANDLER,
5463            "getting modern prompt; prompt_key={}; arguments_present={}",
5464            safe_log_label(&params.name),
5465            params.arguments.is_some()
5466        );
5467
5468        let dispatch_started_at = request_ctx.cx().now();
5469        if let Some(error) = budget_error(request_ctx) {
5470            return Err(error);
5471        }
5472        if !session_state.is_prompt_enabled(&params.name) {
5473            return Err(McpError::new(
5474                McpErrorCode::PromptNotFound,
5475                format!("Prompt '{}' is disabled for this session", params.name),
5476            ));
5477        }
5478
5479        let handler = self.prompts.get(&params.name).ok_or_else(|| {
5480            McpError::new(
5481                McpErrorCode::PromptNotFound,
5482                format!("Prompt not found: {}", params.name),
5483            )
5484        })?;
5485        if self.final_only_prompts.contains(&params.name) {
5486            return Err(McpError::new(
5487                McpErrorCode::PromptNotFound,
5488                format!("Prompt not found: {}", params.name),
5489            ));
5490        }
5491        let description = crate::catch_extension_unwind(|| handler.definition().description)
5492            .map_err(|_payload| sanitized_handler_panic(request_ctx.cx(), "prompt_definition"))?;
5493        let progress_marker = params
5494            .meta
5495            .as_ref()
5496            .and_then(|meta| meta.progress_marker.clone());
5497        let ctx = derive_handler_context(
5498            request_ctx,
5499            progress_marker,
5500            notification_sender,
5501            bidirectional_senders,
5502            ProtocolEra::Legacy2024,
5503        );
5504        let handler_timeout =
5505            read_handler_timeout(request_ctx.cx(), "prompt_timeout", || handler.timeout())?;
5506        let effective_budget = compose_handler_budget(
5507            request_ctx.cx().budget(),
5508            request_ctx.budget(),
5509            handler_timeout,
5510            dispatch_started_at,
5511        );
5512        let ctx = ctx.with_operation_deadline(effective_budget.deadline);
5513        let arguments = params.arguments.unwrap_or_default();
5514        let outcome =
5515            run_handler_in_request(&ctx, request_cx, effective_budget, "prompt", |child_cx| {
5516                handler.get_async_in_request(&ctx, child_cx, arguments)
5517            })
5518            .await?;
5519
5520        let messages = match outcome {
5521            Outcome::Ok(messages) => messages,
5522            Outcome::Err(error) => {
5523                return Err(sanitize_handler_error(request_ctx.cx(), "prompt", error));
5524            }
5525            Outcome::Cancelled(_) => return Err(McpError::request_cancelled()),
5526            Outcome::Panicked(_payload) => {
5527                return Err(sanitized_handler_panic(request_ctx.cx(), "prompt"));
5528            }
5529        };
5530
5531        Ok(GetPromptResult {
5532            description,
5533            messages: legacy_prompt_messages_from_handler(messages)?,
5534            meta: None,
5535            additional: BTreeMap::new(),
5536        })
5537    }
5538
5539    /// Handles one final MCP 2026-07-28 `prompts/get` request.
5540    ///
5541    /// Legacy dispatch remains on [`Self::handle_prompts_get`], including its
5542    /// exact `GetPromptResult` projection. Final dispatch calls the final
5543    /// handler hook directly so handler-authored final content and complete
5544    /// result metadata never pass through the legacy prompt surface.
5545    async fn handle_prompts_get_final_in_request(
5546        &self,
5547        request_ctx: &McpContext,
5548        request_cx: &Cx,
5549        params: FinalGetPromptParams,
5550        session_state: SessionState,
5551        notification_sender: Option<&NotificationSender>,
5552        bidirectional_senders: Option<&BidirectionalSenders>,
5553        resume_inputs: Option<&MrtrCompletedInputs>,
5554    ) -> McpResult<FinalMethodOutcome<FinalGetPromptResult>> {
5555        debug!(
5556            target: targets::HANDLER,
5557            "getting final prompt; prompt_key={}; arguments_present={}",
5558            safe_log_label(&params.name),
5559            !params.arguments.is_absent()
5560        );
5561
5562        let dispatch_started_at = request_ctx.cx().now();
5563        if request_cx.is_cancel_requested() {
5564            return Err(McpError::request_cancelled());
5565        }
5566        if let Some(error) = budget_error(request_ctx) {
5567            return Err(error);
5568        }
5569        let progress_marker = final_progress_marker(&params.meta)?;
5570        // See the corresponding final tools/call check: a modern connection's
5571        // durable component state lives on the request context.
5572        if !session_state.is_prompt_enabled(&params.name)
5573            || !request_ctx.is_prompt_enabled(&params.name)
5574        {
5575            return Err(McpError::new(
5576                McpErrorCode::PromptNotFound,
5577                format!("Prompt '{}' is disabled for this session", params.name),
5578            ));
5579        }
5580
5581        let handler = self
5582            .prompts
5583            .get(&params.name)
5584            .ok_or_else(|| McpError::invalid_params(format!("Unknown prompt: {}", params.name)))?;
5585        let final_registration = self.final_prompts.get(&params.name).ok_or_else(|| {
5586            McpError::invalid_params("prompt is registered only for exact MCP 2024-11-05 dispatch")
5587        })?;
5588        if handler.declares_final_mrtr() && request_ctx.session_cache_partition().is_none() {
5589            return Err(McpError::invalid_params(
5590                MRTR_REQUIRES_BOUND_MODERN_CONNECTION,
5591            ));
5592        }
5593        // FinalArguments deserialization rejects an explicit null before this
5594        // point, so `arguments` is here always Absent or a typed value.
5595        let arguments = params.arguments.into_value().unwrap_or_default();
5596        if final_registration
5597            .definition
5598            .arguments
5599            .as_deref()
5600            .unwrap_or_default()
5601            .iter()
5602            .any(|argument| {
5603                argument.required == Some(true) && !arguments.contains_key(&argument.name)
5604            })
5605        {
5606            return Err(McpError::invalid_params("Missing required prompt argument"));
5607        }
5608        if arguments.keys().any(|name| {
5609            !final_registration
5610                .definition
5611                .arguments
5612                .as_deref()
5613                .unwrap_or_default()
5614                .iter()
5615                .any(|argument| &argument.name == name)
5616        }) {
5617            return Err(McpError::invalid_params("Unknown prompt argument"));
5618        }
5619        let ctx = derive_handler_context(
5620            request_ctx,
5621            progress_marker,
5622            notification_sender,
5623            bidirectional_senders,
5624            ProtocolEra::Modern2026,
5625        );
5626        let handler_timeout =
5627            read_handler_timeout(request_ctx.cx(), "prompt_timeout", || handler.timeout())?;
5628        let effective_budget = compose_handler_budget(
5629            request_ctx.cx().budget(),
5630            request_ctx.budget(),
5631            handler_timeout,
5632            dispatch_started_at,
5633        );
5634        let ctx = ctx.with_operation_deadline(effective_budget.deadline);
5635        let arguments = arguments.into_iter().collect();
5636        let outcome =
5637            run_handler_in_request(&ctx, request_cx, effective_budget, "prompt", |child_cx| {
5638                if let Some(resume_inputs) = resume_inputs {
5639                    handler.get_final_outcome_async_resuming_in_request(
5640                        &ctx,
5641                        child_cx,
5642                        arguments,
5643                        Some(resume_inputs),
5644                    )
5645                } else {
5646                    handler.get_final_outcome_async_in_request(&ctx, child_cx, arguments)
5647                }
5648            })
5649            .await?;
5650
5651        match outcome {
5652            Outcome::Ok(result) => {
5653                admit_final_prompt_outcome(final_registration.uri_use_policy, &result)?;
5654                Ok(result)
5655            }
5656            Outcome::Err(error) => Err(sanitize_handler_error(request_ctx.cx(), "prompt", error)),
5657            Outcome::Cancelled(_) => Err(McpError::request_cancelled()),
5658            Outcome::Panicked(_payload) => Err(sanitized_handler_panic(request_ctx.cx(), "prompt")),
5659        }
5660    }
5661}
5662
5663impl Default for Router {
5664    fn default() -> Self {
5665        Self::new()
5666    }
5667}
5668
5669// ============================================================================
5670// Mount/Composition Support
5671// ============================================================================
5672
5673/// Result of a mount operation.
5674#[derive(Debug, Default)]
5675pub struct MountResult {
5676    /// Number of tools mounted.
5677    pub tools: usize,
5678    /// Number of resources mounted.
5679    pub resources: usize,
5680    /// Number of resource templates mounted.
5681    pub resource_templates: usize,
5682    /// Number of prompts mounted.
5683    pub prompts: usize,
5684    /// Any warnings generated during mounting (e.g., name conflicts).
5685    pub warnings: Vec<String>,
5686    /// Errors that caused the mount operation to be rejected.
5687    ///
5688    /// A rejected mount does not mutate the destination router.
5689    pub errors: Vec<String>,
5690}
5691
5692impl MountResult {
5693    /// Returns true if any components were mounted.
5694    #[must_use]
5695    pub fn has_components(&self) -> bool {
5696        self.tools > 0 || self.resources > 0 || self.resource_templates > 0 || self.prompts > 0
5697    }
5698
5699    /// Returns true if mounting was not rejected.
5700    #[must_use]
5701    pub fn is_success(&self) -> bool {
5702        self.errors.is_empty()
5703    }
5704
5705    fn merge(&mut self, other: Self) {
5706        self.tools += other.tools;
5707        self.resources += other.resources;
5708        self.resource_templates += other.resource_templates;
5709        self.prompts += other.prompts;
5710        self.warnings.extend(other.warnings);
5711        self.errors.extend(other.errors);
5712    }
5713}
5714
5715#[derive(Clone, Copy)]
5716enum MountSelection {
5717    All,
5718    Tools,
5719    Resources,
5720    Prompts,
5721}
5722
5723impl MountSelection {
5724    const fn includes_tools(self) -> bool {
5725        matches!(self, Self::All | Self::Tools)
5726    }
5727
5728    const fn includes_resources(self) -> bool {
5729        matches!(self, Self::All | Self::Resources)
5730    }
5731
5732    const fn includes_prompts(self) -> bool {
5733        matches!(self, Self::All | Self::Prompts)
5734    }
5735}
5736
5737impl Router {
5738    /// Applies a prefix to a name or URI.
5739    fn apply_prefix(name: &str, prefix: Option<&str>) -> String {
5740        match prefix {
5741            Some(p) if !p.is_empty() => format!("{}/{}", p, name),
5742            _ => name.to_string(),
5743        }
5744    }
5745
5746    /// Returns whether mounting preserves component keys exactly.
5747    ///
5748    /// `Some("")` is deliberately equivalent to no prefix: [`Self::apply_prefix`]
5749    /// leaves every key byte-for-byte unchanged in both cases. Final route
5750    /// projection and collision admission must use this rule rather than
5751    /// distinguishing the two `Option` representations.
5752    fn prefix_preserves_keys(prefix: Option<&str>) -> bool {
5753        prefix.is_none_or(str::is_empty)
5754    }
5755
5756    /// Validates a prefix string.
5757    ///
5758    /// Prefixes must be alphanumeric plus underscores and hyphens,
5759    /// and cannot contain slashes.
5760    fn validate_prefix(prefix: &str) -> Result<(), String> {
5761        if prefix.is_empty() {
5762            return Ok(());
5763        }
5764        if prefix.contains('/') {
5765            return Err("Invalid mount prefix: slashes are not permitted".to_string());
5766        }
5767        // Allow alphanumeric, underscore, hyphen
5768        for ch in prefix.chars() {
5769            if !ch.is_alphanumeric() && ch != '_' && ch != '-' {
5770                return Err(
5771                    "Invalid mount prefix: invalid characters are not permitted".to_string()
5772                );
5773            }
5774        }
5775        Ok(())
5776    }
5777
5778    fn mount_preflight(
5779        &self,
5780        other: &Self,
5781        prefix: Option<&str>,
5782        behavior: crate::DuplicateBehavior,
5783        selection: MountSelection,
5784    ) -> MountResult {
5785        let mut result = MountResult::default();
5786
5787        if let Some(prefix) = prefix {
5788            if let Err(error) = Self::validate_prefix(prefix) {
5789                // Keep the warning for callers of the original API while also
5790                // recording the rejection as a real failure.
5791                result.warnings.push(error.clone());
5792                result.errors.push(error);
5793                return result;
5794            }
5795        }
5796
5797        if behavior == crate::DuplicateBehavior::Error {
5798            let mut conflicts = Vec::new();
5799            if selection.includes_tools() {
5800                for name in other.tools.keys() {
5801                    let mounted_name = Self::apply_prefix(name, prefix);
5802                    if self.tools.contains_key(&mounted_name) {
5803                        conflicts.push(("Tool", mounted_name));
5804                    }
5805                }
5806            }
5807            if selection.includes_resources() {
5808                for uri in other.resources.keys() {
5809                    let mounted_uri = Self::apply_prefix(uri, prefix);
5810                    if self.resources.contains_key(&mounted_uri) {
5811                        conflicts.push(("Resource", mounted_uri));
5812                    }
5813                }
5814                for uri_template in other.resource_templates.keys() {
5815                    let mounted_uri_template = Self::apply_prefix(uri_template, prefix);
5816                    if self.resource_templates.contains_key(&mounted_uri_template) {
5817                        conflicts.push(("Resource template", mounted_uri_template));
5818                    }
5819                }
5820            }
5821            if selection.includes_prompts() {
5822                for name in other.prompts.keys() {
5823                    let mounted_name = Self::apply_prefix(name, prefix);
5824                    if self.prompts.contains_key(&mounted_name) {
5825                        conflicts.push(("Prompt", mounted_name));
5826                    }
5827                }
5828            }
5829
5830            conflicts.sort_by(|a, b| a.0.cmp(b.0).then_with(|| a.1.cmp(&b.1)));
5831            result
5832                .errors
5833                .extend(conflicts.into_iter().map(|(kind, key)| {
5834                    format!(
5835                        "Mount rejected because {kind} already exists; component_key={}",
5836                        safe_log_label(&key)
5837                    )
5838                }));
5839        }
5840
5841        self.preflight_final_resource_route_collisions(
5842            other,
5843            prefix,
5844            behavior,
5845            selection,
5846            &mut result,
5847        );
5848        self.preflight_mcp_apps_mount_bindings(other, prefix, behavior, selection, &mut result);
5849        result
5850    }
5851
5852    /// Projects the final resource routes that a mount would leave in the
5853    /// destination, then refuses ambiguous exact/template or template/template
5854    /// languages before consuming any source handlers. A nonempty prefix
5855    /// produces legacy-only resources, so it cannot introduce a final route
5856    /// collision.
5857    fn preflight_final_resource_route_collisions(
5858        &self,
5859        other: &Self,
5860        prefix: Option<&str>,
5861        behavior: crate::DuplicateBehavior,
5862        selection: MountSelection,
5863        result: &mut MountResult,
5864    ) {
5865        if !selection.includes_resources() {
5866            return;
5867        }
5868
5869        let mut final_resources: HashSet<String> = self.final_resources.keys().cloned().collect();
5870        let mut final_templates: HashMap<String, ReversibleResourceTemplate> = self
5871            .resource_templates
5872            .iter()
5873            .filter_map(|(uri_template, entry)| {
5874                entry
5875                    .final_definition
5876                    .as_ref()
5877                    .zip(entry.matcher.as_ref())
5878                    .map(|(_, matcher)| (uri_template.clone(), matcher.clone()))
5879            })
5880            .collect();
5881
5882        for uri in other.resources.keys() {
5883            let mounted_uri = Self::apply_prefix(uri, prefix);
5884            let replaces_destination = !self.resources.contains_key(&mounted_uri)
5885                || behavior == crate::DuplicateBehavior::Replace;
5886            if !replaces_destination {
5887                continue;
5888            }
5889            if Self::prefix_preserves_keys(prefix) && other.final_resources.contains_key(uri) {
5890                final_resources.insert(mounted_uri);
5891            } else {
5892                final_resources.remove(&mounted_uri);
5893            }
5894        }
5895
5896        for (uri_template, entry) in &other.resource_templates {
5897            let mounted_uri_template = Self::apply_prefix(uri_template, prefix);
5898            let replaces_destination = !self.resource_templates.contains_key(&mounted_uri_template)
5899                || behavior == crate::DuplicateBehavior::Replace;
5900            if !replaces_destination {
5901                continue;
5902            }
5903            if Self::prefix_preserves_keys(prefix)
5904                && let Some(matcher) = entry
5905                    .final_definition
5906                    .as_ref()
5907                    .zip(entry.matcher.as_ref())
5908                    .map(|(_, matcher)| matcher.clone())
5909            {
5910                final_templates.insert(mounted_uri_template, matcher);
5911            } else {
5912                final_templates.remove(&mounted_uri_template);
5913            }
5914        }
5915
5916        let mut template_routes: Vec<_> = final_templates.iter().collect();
5917        template_routes.sort_unstable_by_key(|(uri, _)| *uri);
5918        let mut exact_routes: Vec<_> = final_resources.iter().collect();
5919        exact_routes.sort_unstable();
5920        let mut collisions = Vec::new();
5921
5922        for (template_uri, matcher) in &template_routes {
5923            for exact_uri in &exact_routes {
5924                match matcher.match_uri(exact_uri) {
5925                    Ok(Some(_)) => collisions.push(format!(
5926                        "Mount rejected because a final resource template collides with an exact final resource; template_key={}; resource_key={}",
5927                        safe_log_label(template_uri),
5928                        safe_log_label(exact_uri)
5929                    )),
5930                    Ok(None) => {}
5931                    Err(_) => collisions.push(format!(
5932                        "Mount rejected because a final resource template lost match admission; template_key={}",
5933                        safe_log_label(template_uri)
5934                    )),
5935                }
5936            }
5937        }
5938
5939        for (index, (left_uri, left)) in template_routes.iter().enumerate() {
5940            for (right_uri, right) in template_routes.iter().skip(index + 1) {
5941                match reversible_templates_may_overlap(left, right) {
5942                    Ok(true) => collisions.push(format!(
5943                        "Mount rejected because final resource template languages collide; left_template_key={}; right_template_key={}",
5944                        safe_log_label(left_uri),
5945                        safe_log_label(right_uri)
5946                    )),
5947                    Ok(false) => {}
5948                    Err(_) => collisions.push(format!(
5949                        "Mount rejected because a final resource template lost match admission; template_key={}",
5950                        safe_log_label(left_uri)
5951                    )),
5952                }
5953            }
5954        }
5955
5956        collisions.sort_unstable();
5957        collisions.dedup();
5958        result.errors.extend(collisions);
5959    }
5960
5961    fn preflight_mcp_apps_mount_bindings(
5962        &self,
5963        other: &Self,
5964        prefix: Option<&str>,
5965        behavior: crate::DuplicateBehavior,
5966        selection: MountSelection,
5967        result: &mut MountResult,
5968    ) {
5969        let mut final_resources: HashMap<String, FinalResource> = self
5970            .final_resources
5971            .iter()
5972            .map(|(uri, registration)| (uri.clone(), registration.definition.clone()))
5973            .collect();
5974        let mut final_tools: HashMap<String, FinalTool> = self
5975            .tools
5976            .iter()
5977            .filter_map(|(name, registration)| {
5978                registration
5979                    .final_registration
5980                    .as_ref()
5981                    .map(|final_registration| {
5982                        (name.clone(), final_registration.final_definition.clone())
5983                    })
5984            })
5985            .collect();
5986
5987        if selection.includes_resources() {
5988            for uri in other.resources.keys() {
5989                let mounted_uri = Self::apply_prefix(uri, prefix);
5990                let replacing = !self.resources.contains_key(&mounted_uri)
5991                    || matches!(behavior, crate::DuplicateBehavior::Replace);
5992                if !replacing {
5993                    continue;
5994                }
5995                let source_final = other.final_resources.get(uri);
5996                if !Self::prefix_preserves_keys(prefix)
5997                    && source_final.is_some_and(|registration| {
5998                        Self::mcp_apps_ui_resource_requires_special_admission(
5999                            &registration.definition,
6000                        )
6001                        .unwrap_or(true)
6002                    })
6003                {
6004                    result.errors.push(format!(
6005                        "Mount rejected because a final-only MCP Apps UI resource cannot be prefixed; resource_key={}",
6006                        safe_log_label(uri)
6007                    ));
6008                    continue;
6009                }
6010                if Self::prefix_preserves_keys(prefix) {
6011                    if let Some(registration) = source_final {
6012                        final_resources.insert(mounted_uri, registration.definition.clone());
6013                    } else {
6014                        final_resources.remove(&mounted_uri);
6015                    }
6016                } else {
6017                    final_resources.remove(&mounted_uri);
6018                }
6019            }
6020        }
6021
6022        if selection.includes_tools() {
6023            for (name, registration) in &other.tools {
6024                let mounted_name = Self::apply_prefix(name, prefix);
6025                let replacing = !self.tools.contains_key(&mounted_name)
6026                    || matches!(behavior, crate::DuplicateBehavior::Replace);
6027                if !replacing {
6028                    continue;
6029                }
6030                if let Some(final_registration) = registration.final_registration.as_ref() {
6031                    let mut definition = final_registration.final_definition.clone();
6032                    definition.name.clone_from(&mounted_name);
6033                    final_tools.insert(mounted_name, definition);
6034                } else {
6035                    final_tools.remove(&mounted_name);
6036                }
6037            }
6038        }
6039
6040        for (name, tool) in final_tools {
6041            let binding = match tool.mcp_apps_resource_binding() {
6042                Ok(binding) => binding,
6043                Err(error) => {
6044                    result.errors.push(format!(
6045                        "Mount rejected because tool has invalid MCP Apps metadata; tool_key={}; error={error}",
6046                        safe_log_label(&name)
6047                    ));
6048                    continue;
6049                }
6050            };
6051            let Some(binding) = binding else {
6052                continue;
6053            };
6054            let Some(resource) = final_resources.get(binding.resource_uri.as_str()) else {
6055                result.errors.push(format!(
6056                    "Mount rejected because an MCP Apps tool binding has no final HTML resource; tool_key={}; resource_uri={}",
6057                    safe_log_label(&name),
6058                    binding.resource_uri.as_str()
6059                ));
6060                continue;
6061            };
6062            if let Err(error) = binding.validate_resource(resource) {
6063                result.errors.push(format!(
6064                    "Mount rejected because an MCP Apps tool binding is invalid; tool_key={}; error={error}",
6065                    safe_log_label(&name)
6066                ));
6067            }
6068        }
6069    }
6070
6071    fn should_mount_duplicate(
6072        behavior: crate::DuplicateBehavior,
6073        kind: &'static str,
6074        key: &str,
6075        result: &mut MountResult,
6076    ) -> bool {
6077        match behavior {
6078            crate::DuplicateBehavior::Error => {
6079                result.errors.push(format!(
6080                    "Mount rejected because {kind} already exists; component_key={}",
6081                    safe_log_label(key)
6082                ));
6083                false
6084            }
6085            crate::DuplicateBehavior::Warn => {
6086                result.warnings.push(format!(
6087                    "{kind} already exists, keeping original; component_key={}",
6088                    safe_log_label(key)
6089                ));
6090                false
6091            }
6092            crate::DuplicateBehavior::Replace => {
6093                result.warnings.push(format!(
6094                    "{kind} already exists, replacing original; component_key={}",
6095                    safe_log_label(key)
6096                ));
6097                true
6098            }
6099            crate::DuplicateBehavior::Ignore => false,
6100        }
6101    }
6102
6103    /// Mounts all handlers from another router with an optional prefix.
6104    ///
6105    /// This consumes the source router and moves its handlers into this router.
6106    /// Names/URIs are prefixed with `prefix/` if a prefix is provided.
6107    ///
6108    /// # Example
6109    ///
6110    /// ```ignore
6111    /// let mut main_router = Router::new();
6112    /// let db_router = Router::new();
6113    /// // ... add handlers to db_router ...
6114    ///
6115    /// main_router.mount(db_router, Some("db"));
6116    /// // Tool "query" becomes "db/query"
6117    /// ```
6118    pub fn mount(&mut self, other: Router, prefix: Option<&str>) -> MountResult {
6119        self.mount_with_behavior(other, prefix, crate::DuplicateBehavior::Replace)
6120    }
6121
6122    /// Mounts all handlers using the specified duplicate behavior.
6123    ///
6124    /// Prefix validation happens before any destination mutation. With
6125    /// [`crate::DuplicateBehavior::Error`], every selected component is
6126    /// preflighted and any conflict rejects the entire mount atomically.
6127    pub fn mount_with_behavior(
6128        &mut self,
6129        other: Router,
6130        prefix: Option<&str>,
6131        behavior: crate::DuplicateBehavior,
6132    ) -> MountResult {
6133        let preflight = self.mount_preflight(&other, prefix, behavior, MountSelection::All);
6134        if !preflight.is_success() {
6135            return preflight;
6136        }
6137
6138        let mut result = preflight;
6139
6140        let Router {
6141            tools,
6142            tool_order,
6143            resources,
6144            final_only_resources,
6145            final_resources,
6146            resource_order,
6147            prompts,
6148            final_only_prompts,
6149            final_prompts,
6150            prompt_order,
6151            resource_templates,
6152            resource_template_order,
6153            ..
6154        } = other;
6155
6156        // Mount tools
6157        result.merge(self.mount_tools_from(tools, tool_order, prefix, behavior));
6158
6159        // Mount resources
6160        result.merge(self.mount_resources_from(
6161            resources,
6162            final_only_resources,
6163            final_resources,
6164            resource_order,
6165            prefix,
6166            behavior,
6167        ));
6168
6169        // Mount resource templates
6170        result.merge(self.mount_resource_templates_from(
6171            resource_templates,
6172            resource_template_order,
6173            prefix,
6174            behavior,
6175        ));
6176
6177        // Mount prompts
6178        result.merge(self.mount_prompts_from(
6179            prompts,
6180            final_only_prompts,
6181            final_prompts,
6182            prompt_order,
6183            prefix,
6184            behavior,
6185        ));
6186
6187        // Log mount result
6188        if result.has_components() {
6189            debug!(
6190                target: targets::HANDLER,
6191                "mounted {} tools, {} resources, {} templates, {} prompts; prefix_present={}; prefix_key={}",
6192                result.tools,
6193                result.resources,
6194                result.resource_templates,
6195                result.prompts,
6196                prefix.is_some(),
6197                safe_log_label(prefix.unwrap_or_default())
6198            );
6199            self.advance_final_catalog_revision();
6200        }
6201
6202        result
6203    }
6204
6205    /// Mounts tools and prompts with an optional name prefix, and keeps
6206    /// resource and template keys exact.
6207    ///
6208    /// A nonempty `{prefix}/{uri}` key is not an absolute final URI. Callers
6209    /// that need a modern resource catalog after namespacing tools/prompts
6210    /// must preserve the child's resource URIs instead of prefixing them.
6211    pub fn mount_namespaced_with_behavior(
6212        &mut self,
6213        other: Router,
6214        prefix: Option<&str>,
6215        behavior: crate::DuplicateBehavior,
6216    ) -> MountResult {
6217        let mut preflight = self.mount_preflight(&other, prefix, behavior, MountSelection::Tools);
6218        preflight.merge(self.mount_preflight(&other, prefix, behavior, MountSelection::Prompts));
6219        preflight.merge(self.mount_preflight(&other, None, behavior, MountSelection::Resources));
6220        if !preflight.is_success() {
6221            return preflight;
6222        }
6223
6224        let mut result = preflight;
6225        let Router {
6226            tools,
6227            tool_order,
6228            resources,
6229            final_only_resources,
6230            final_resources,
6231            resource_order,
6232            prompts,
6233            final_only_prompts,
6234            final_prompts,
6235            prompt_order,
6236            resource_templates,
6237            resource_template_order,
6238            ..
6239        } = other;
6240
6241        result.merge(self.mount_tools_from(tools, tool_order, prefix, behavior));
6242        result.merge(self.mount_resources_from(
6243            resources,
6244            final_only_resources,
6245            final_resources,
6246            resource_order,
6247            None,
6248            behavior,
6249        ));
6250        result.merge(self.mount_resource_templates_from(
6251            resource_templates,
6252            resource_template_order,
6253            None,
6254            behavior,
6255        ));
6256        result.merge(self.mount_prompts_from(
6257            prompts,
6258            final_only_prompts,
6259            final_prompts,
6260            prompt_order,
6261            prefix,
6262            behavior,
6263        ));
6264
6265        if result.has_components() {
6266            debug!(
6267                target: targets::HANDLER,
6268                "mounted namespaced {} tools, {} resources, {} templates, {} prompts; prefix_present={}; prefix_key={}",
6269                result.tools,
6270                result.resources,
6271                result.resource_templates,
6272                result.prompts,
6273                prefix.is_some(),
6274                safe_log_label(prefix.unwrap_or_default())
6275            );
6276            self.advance_final_catalog_revision();
6277        }
6278
6279        result
6280    }
6281
6282    /// Mounts only tools from a router.
6283    pub fn mount_tools(&mut self, other: Router, prefix: Option<&str>) -> MountResult {
6284        self.mount_tools_with_behavior(other, prefix, crate::DuplicateBehavior::Replace)
6285    }
6286
6287    /// Mounts only tools using the specified duplicate behavior.
6288    pub fn mount_tools_with_behavior(
6289        &mut self,
6290        other: Router,
6291        prefix: Option<&str>,
6292        behavior: crate::DuplicateBehavior,
6293    ) -> MountResult {
6294        let preflight = self.mount_preflight(&other, prefix, behavior, MountSelection::Tools);
6295        if !preflight.is_success() {
6296            return preflight;
6297        }
6298        let result = self.mount_tools_from(other.tools, other.tool_order, prefix, behavior);
6299        if result.has_components() {
6300            self.advance_final_catalog_revision();
6301        }
6302        result
6303    }
6304
6305    /// Internal: mount tools from a HashMap.
6306    fn mount_tools_from(
6307        &mut self,
6308        mut tools: HashMap<String, AdmittedToolRegistration>,
6309        tool_order: Vec<String>,
6310        prefix: Option<&str>,
6311        behavior: crate::DuplicateBehavior,
6312    ) -> MountResult {
6313        let mut result = MountResult::default();
6314
6315        for name in tool_order {
6316            let Some(entry) = tools.remove(&name) else {
6317                continue;
6318            };
6319            let mounted_name = Self::apply_prefix(&name, prefix);
6320            trace!(
6321                target: targets::HANDLER,
6322                "mounting tool; source_key={}; mounted_key={}",
6323                safe_log_label(&name),
6324                safe_log_label(&mounted_name)
6325            );
6326
6327            // Check for conflicts
6328            let existed = self.tools.contains_key(&mounted_name);
6329            if existed
6330                && !Self::should_mount_duplicate(behavior, "Tool", &mounted_name, &mut result)
6331            {
6332                continue;
6333            }
6334
6335            // Rewrite the immutable definition snapshot and wrap only the
6336            // dispatch target. Admitted schemas and final metadata move as one
6337            // entry and are never recomputed during mounting.
6338            let mounted = entry.with_mounted_name(mounted_name.clone());
6339            let needs_order_push = !existed && !self.tool_order.iter().any(|n| n == &mounted_name);
6340            self.tools.insert(mounted_name.clone(), mounted);
6341            if needs_order_push {
6342                self.tool_order.push(mounted_name);
6343            }
6344            result.tools += 1;
6345        }
6346
6347        if !tools.is_empty() {
6348            // Defensive: older Routers or unusual construction could leave items untracked by
6349            // tool_order. Mount them deterministically to avoid HashMap iteration order leaks.
6350            let mut remaining: Vec<(String, AdmittedToolRegistration)> =
6351                tools.into_iter().collect();
6352            remaining.sort_by(|a, b| a.0.cmp(&b.0));
6353            for (name, entry) in remaining {
6354                let mounted_name = Self::apply_prefix(&name, prefix);
6355
6356                let existed = self.tools.contains_key(&mounted_name);
6357                if existed
6358                    && !Self::should_mount_duplicate(behavior, "Tool", &mounted_name, &mut result)
6359                {
6360                    continue;
6361                }
6362
6363                let mounted = entry.with_mounted_name(mounted_name.clone());
6364                self.tools.insert(mounted_name.clone(), mounted);
6365                if !existed && !self.tool_order.iter().any(|n| n == &mounted_name) {
6366                    self.tool_order.push(mounted_name);
6367                }
6368                result.tools += 1;
6369            }
6370        }
6371
6372        result
6373    }
6374
6375    /// Mounts only resources from a router.
6376    pub fn mount_resources(&mut self, other: Router, prefix: Option<&str>) -> MountResult {
6377        self.mount_resources_with_behavior(other, prefix, crate::DuplicateBehavior::Replace)
6378    }
6379
6380    /// Mounts resources and resource templates using the specified duplicate
6381    /// behavior.
6382    pub fn mount_resources_with_behavior(
6383        &mut self,
6384        other: Router,
6385        prefix: Option<&str>,
6386        behavior: crate::DuplicateBehavior,
6387    ) -> MountResult {
6388        let preflight = self.mount_preflight(&other, prefix, behavior, MountSelection::Resources);
6389        if !preflight.is_success() {
6390            return preflight;
6391        }
6392
6393        let Router {
6394            resources,
6395            final_only_resources,
6396            final_resources,
6397            resource_order,
6398            resource_templates,
6399            resource_template_order,
6400            ..
6401        } = other;
6402        let mut result = preflight;
6403        result.merge(self.mount_resources_from(
6404            resources,
6405            final_only_resources,
6406            final_resources,
6407            resource_order,
6408            prefix,
6409            behavior,
6410        ));
6411        let template_result = self.mount_resource_templates_from(
6412            resource_templates,
6413            resource_template_order,
6414            prefix,
6415            behavior,
6416        );
6417        result.merge(template_result);
6418        if result.has_components() {
6419            self.advance_final_catalog_revision();
6420        }
6421        result
6422    }
6423
6424    /// Internal: mount resources from a HashMap.
6425    fn mount_resources_from(
6426        &mut self,
6427        mut resources: HashMap<String, BoxedResourceHandler>,
6428        final_only_resources: HashSet<String>,
6429        mut final_resources: HashMap<String, AdmittedFinalResourceRegistration>,
6430        resource_order: Vec<String>,
6431        prefix: Option<&str>,
6432        behavior: crate::DuplicateBehavior,
6433    ) -> MountResult {
6434        use crate::handler::MountedResourceHandler;
6435
6436        let mut result = MountResult::default();
6437
6438        for uri in resource_order {
6439            let Some(handler) = resources.remove(&uri) else {
6440                continue;
6441            };
6442            let mounted_uri = Self::apply_prefix(&uri, prefix);
6443            trace!(
6444                target: targets::HANDLER,
6445                "mounting resource; source_key={}; mounted_key={}",
6446                safe_log_label(&uri),
6447                safe_log_label(&mounted_uri)
6448            );
6449
6450            // Check for conflicts
6451            let existed = self.resources.contains_key(&mounted_uri);
6452            if existed
6453                && !Self::should_mount_duplicate(behavior, "Resource", &mounted_uri, &mut result)
6454            {
6455                continue;
6456            }
6457
6458            // Wrap with mounted URI and insert
6459            let mounted = MountedResourceHandler::new(handler, uri.clone(), mounted_uri.clone());
6460            let needs_order_push =
6461                !existed && !self.resource_order.iter().any(|u| u == &mounted_uri);
6462            self.resources
6463                .insert(mounted_uri.clone(), Box::new(mounted));
6464            if Self::prefix_preserves_keys(prefix) {
6465                if let Some(final_registration) = final_resources.remove(&uri) {
6466                    self.final_resources
6467                        .insert(mounted_uri.clone(), final_registration);
6468                } else {
6469                    self.final_resources.remove(&mounted_uri);
6470                }
6471            } else {
6472                // Nonempty-prefixed resource URIs are intentionally
6473                // legacy-only: the mounting namespace is not an absolute
6474                // final URI.
6475                self.final_resources.remove(&mounted_uri);
6476            }
6477            if Self::prefix_preserves_keys(prefix) && final_only_resources.contains(&uri) {
6478                self.final_only_resources.insert(mounted_uri.clone());
6479            } else {
6480                self.final_only_resources.remove(&mounted_uri);
6481            }
6482            if needs_order_push {
6483                self.resource_order.push(mounted_uri);
6484            }
6485            result.resources += 1;
6486        }
6487
6488        if !resources.is_empty() {
6489            let mut remaining: Vec<(String, BoxedResourceHandler)> =
6490                resources.into_iter().collect();
6491            remaining.sort_by(|a, b| a.0.cmp(&b.0));
6492            for (uri, handler) in remaining {
6493                let mounted_uri = Self::apply_prefix(&uri, prefix);
6494
6495                let existed = self.resources.contains_key(&mounted_uri);
6496                if existed
6497                    && !Self::should_mount_duplicate(
6498                        behavior,
6499                        "Resource",
6500                        &mounted_uri,
6501                        &mut result,
6502                    )
6503                {
6504                    continue;
6505                }
6506
6507                let mounted =
6508                    MountedResourceHandler::new(handler, uri.clone(), mounted_uri.clone());
6509                self.resources
6510                    .insert(mounted_uri.clone(), Box::new(mounted));
6511                if Self::prefix_preserves_keys(prefix) {
6512                    if let Some(final_registration) = final_resources.remove(&uri) {
6513                        self.final_resources
6514                            .insert(mounted_uri.clone(), final_registration);
6515                    } else {
6516                        self.final_resources.remove(&mounted_uri);
6517                    }
6518                } else {
6519                    self.final_resources.remove(&mounted_uri);
6520                }
6521                if Self::prefix_preserves_keys(prefix) && final_only_resources.contains(&uri) {
6522                    self.final_only_resources.insert(mounted_uri.clone());
6523                } else {
6524                    self.final_only_resources.remove(&mounted_uri);
6525                }
6526                if !existed && !self.resource_order.iter().any(|u| u == &mounted_uri) {
6527                    self.resource_order.push(mounted_uri);
6528                }
6529                result.resources += 1;
6530            }
6531        }
6532
6533        result
6534    }
6535
6536    /// Internal: mount resource templates from a HashMap.
6537    fn mount_resource_templates_from(
6538        &mut self,
6539        mut templates: HashMap<String, ResourceTemplateEntry>,
6540        resource_template_order: Vec<String>,
6541        prefix: Option<&str>,
6542        behavior: crate::DuplicateBehavior,
6543    ) -> MountResult {
6544        use crate::handler::MountedResourceHandler;
6545
6546        let mut result = MountResult::default();
6547
6548        for uri_template in resource_template_order {
6549            let Some(entry) = templates.remove(&uri_template) else {
6550                continue;
6551            };
6552            let mounted_uri_template = Self::apply_prefix(&uri_template, prefix);
6553            trace!(
6554                target: targets::HANDLER,
6555                "mounting resource template; source_key={}; mounted_key={}",
6556                safe_log_label(&uri_template),
6557                safe_log_label(&mounted_uri_template)
6558            );
6559
6560            // Check for conflicts
6561            let existed = self.resource_templates.contains_key(&mounted_uri_template);
6562            if existed
6563                && !Self::should_mount_duplicate(
6564                    behavior,
6565                    "Resource template",
6566                    &mounted_uri_template,
6567                    &mut result,
6568                )
6569            {
6570                continue;
6571            }
6572
6573            // Create new template with mounted URI
6574            let mut mounted_template = entry.template.clone();
6575            mounted_template.uri_template = mounted_uri_template.clone();
6576
6577            // Wrap handler if present
6578            let mounted_handler = entry.handler.map(|h| {
6579                let wrapped: BoxedResourceHandler =
6580                    Box::new(MountedResourceHandler::with_template(
6581                        h,
6582                        uri_template.clone(),
6583                        mounted_uri_template.clone(),
6584                        mounted_template.clone(),
6585                    ));
6586                wrapped
6587            });
6588
6589            // Create new entry with mounted template.
6590            let legacy_matcher = if entry.legacy_enabled {
6591                match admit_legacy_resource_template(&mounted_uri_template) {
6592                    Ok(matcher) => Some(matcher),
6593                    Err(error) => {
6594                        result.errors.push(format!(
6595                            "Mount rejected exact-2024 resource template; template_key={}; code={:?}",
6596                            safe_log_label(&mounted_uri_template),
6597                            error.code
6598                        ));
6599                        continue;
6600                    }
6601                }
6602            } else {
6603                None
6604            };
6605            let final_enabled =
6606                Self::prefix_preserves_keys(prefix) && entry.final_definition.is_some();
6607            let (matcher, specificity) = if final_enabled {
6608                match admit_resource_template(&mounted_uri_template) {
6609                    Ok((matcher, specificity)) => (Some(matcher), specificity),
6610                    Err(error) => {
6611                        result.errors.push(format!(
6612                            "Mount rejected resource template; template_key={}; code={:?}",
6613                            safe_log_label(&mounted_uri_template),
6614                            error.code
6615                        ));
6616                        continue;
6617                    }
6618                }
6619            } else {
6620                let specificity = legacy_matcher.as_ref().map_or(
6621                    entry.specificity,
6622                    LegacyResourceTemplateMatcher::specificity,
6623                );
6624                (None, specificity)
6625            };
6626            let mounted_entry = ResourceTemplateEntry {
6627                matcher,
6628                legacy_matcher,
6629                specificity,
6630                template: mounted_template,
6631                handler: mounted_handler,
6632                // A nonempty mount prefix produces a relative legacy
6633                // namespace (for example, `peer/mcp://...`). It cannot
6634                // preserve the exact final absolute-URI contract, so mirror
6635                // static-resource mounting and expose the mounted route to
6636                // legacy only.
6637                final_definition: if Self::prefix_preserves_keys(prefix) {
6638                    entry.final_definition.map(|mut definition| {
6639                        definition.uri_template = mounted_uri_template.clone();
6640                        definition
6641                    })
6642                } else {
6643                    None
6644                },
6645                uri_use_policy: entry.uri_use_policy,
6646                legacy_enabled: entry.legacy_enabled,
6647            };
6648
6649            let needs_order_push = !existed
6650                && !self
6651                    .resource_template_order
6652                    .iter()
6653                    .any(|t| t == &mounted_uri_template);
6654            self.resource_templates
6655                .insert(mounted_uri_template.clone(), mounted_entry);
6656            if needs_order_push {
6657                self.resource_template_order.push(mounted_uri_template);
6658            }
6659            result.resource_templates += 1;
6660        }
6661
6662        if !templates.is_empty() {
6663            let mut remaining: Vec<(String, ResourceTemplateEntry)> =
6664                templates.into_iter().collect();
6665            remaining.sort_by(|a, b| a.0.cmp(&b.0));
6666            for (uri_template, entry) in remaining {
6667                let mounted_uri_template = Self::apply_prefix(&uri_template, prefix);
6668
6669                let existed = self.resource_templates.contains_key(&mounted_uri_template);
6670                if existed
6671                    && !Self::should_mount_duplicate(
6672                        behavior,
6673                        "Resource template",
6674                        &mounted_uri_template,
6675                        &mut result,
6676                    )
6677                {
6678                    continue;
6679                }
6680
6681                let mut mounted_template = entry.template.clone();
6682                mounted_template.uri_template = mounted_uri_template.clone();
6683
6684                let mounted_handler = entry.handler.map(|h| {
6685                    let wrapped: BoxedResourceHandler =
6686                        Box::new(MountedResourceHandler::with_template(
6687                            h,
6688                            uri_template,
6689                            mounted_uri_template.clone(),
6690                            mounted_template.clone(),
6691                        ));
6692                    wrapped
6693                });
6694
6695                let legacy_matcher = if entry.legacy_enabled {
6696                    match admit_legacy_resource_template(&mounted_uri_template) {
6697                        Ok(matcher) => Some(matcher),
6698                        Err(error) => {
6699                            result.errors.push(format!(
6700                                "Mount rejected exact-2024 resource template; template_key={}; code={:?}",
6701                                safe_log_label(&mounted_uri_template),
6702                                error.code
6703                            ));
6704                            continue;
6705                        }
6706                    }
6707                } else {
6708                    None
6709                };
6710                let final_enabled =
6711                    Self::prefix_preserves_keys(prefix) && entry.final_definition.is_some();
6712                let (matcher, specificity) = if final_enabled {
6713                    match admit_resource_template(&mounted_uri_template) {
6714                        Ok((matcher, specificity)) => (Some(matcher), specificity),
6715                        Err(error) => {
6716                            result.errors.push(format!(
6717                                "Mount rejected resource template; template_key={}; code={:?}",
6718                                safe_log_label(&mounted_uri_template),
6719                                error.code
6720                            ));
6721                            continue;
6722                        }
6723                    }
6724                } else {
6725                    let specificity = legacy_matcher.as_ref().map_or(
6726                        entry.specificity,
6727                        LegacyResourceTemplateMatcher::specificity,
6728                    );
6729                    (None, specificity)
6730                };
6731                let mounted_entry = ResourceTemplateEntry {
6732                    matcher,
6733                    legacy_matcher,
6734                    specificity,
6735                    template: mounted_template,
6736                    handler: mounted_handler,
6737                    // See the ordered-template path above: nonempty-prefixed
6738                    // template routes are legacy-only because their URI
6739                    // namespace is no longer absolute for exact final
6740                    // resource contents.
6741                    final_definition: if Self::prefix_preserves_keys(prefix) {
6742                        entry.final_definition.map(|mut definition| {
6743                            definition.uri_template = mounted_uri_template.clone();
6744                            definition
6745                        })
6746                    } else {
6747                        None
6748                    },
6749                    uri_use_policy: entry.uri_use_policy,
6750                    legacy_enabled: entry.legacy_enabled,
6751                };
6752
6753                self.resource_templates
6754                    .insert(mounted_uri_template.clone(), mounted_entry);
6755                if !existed
6756                    && !self
6757                        .resource_template_order
6758                        .iter()
6759                        .any(|t| t == &mounted_uri_template)
6760                {
6761                    self.resource_template_order
6762                        .push(mounted_uri_template.clone());
6763                }
6764                result.resource_templates += 1;
6765            }
6766        }
6767
6768        // Rebuild sorted keys if we added templates
6769        if result.resource_templates > 0 {
6770            self.rebuild_sorted_template_keys();
6771        }
6772
6773        result
6774    }
6775
6776    /// Mounts only prompts from a router.
6777    pub fn mount_prompts(&mut self, other: Router, prefix: Option<&str>) -> MountResult {
6778        self.mount_prompts_with_behavior(other, prefix, crate::DuplicateBehavior::Replace)
6779    }
6780
6781    /// Mounts only prompts using the specified duplicate behavior.
6782    pub fn mount_prompts_with_behavior(
6783        &mut self,
6784        other: Router,
6785        prefix: Option<&str>,
6786        behavior: crate::DuplicateBehavior,
6787    ) -> MountResult {
6788        let preflight = self.mount_preflight(&other, prefix, behavior, MountSelection::Prompts);
6789        if !preflight.is_success() {
6790            return preflight;
6791        }
6792        let result = self.mount_prompts_from(
6793            other.prompts,
6794            other.final_only_prompts,
6795            other.final_prompts,
6796            other.prompt_order,
6797            prefix,
6798            behavior,
6799        );
6800        if result.has_components() {
6801            self.advance_final_catalog_revision();
6802        }
6803        result
6804    }
6805
6806    /// Internal: mount prompts from a HashMap.
6807    fn mount_prompts_from(
6808        &mut self,
6809        mut prompts: HashMap<String, BoxedPromptHandler>,
6810        final_only_prompts: HashSet<String>,
6811        mut final_prompts: HashMap<String, AdmittedFinalPromptRegistration>,
6812        prompt_order: Vec<String>,
6813        prefix: Option<&str>,
6814        behavior: crate::DuplicateBehavior,
6815    ) -> MountResult {
6816        use crate::handler::MountedPromptHandler;
6817
6818        let mut result = MountResult::default();
6819
6820        for name in prompt_order {
6821            let Some(handler) = prompts.remove(&name) else {
6822                continue;
6823            };
6824            let mounted_name = Self::apply_prefix(&name, prefix);
6825            trace!(
6826                target: targets::HANDLER,
6827                "mounting prompt; source_key={}; mounted_key={}",
6828                safe_log_label(&name),
6829                safe_log_label(&mounted_name)
6830            );
6831
6832            // Check for conflicts
6833            let existed = self.prompts.contains_key(&mounted_name);
6834            if existed
6835                && !Self::should_mount_duplicate(behavior, "Prompt", &mounted_name, &mut result)
6836            {
6837                continue;
6838            }
6839
6840            // Wrap with mounted name and insert
6841            let mounted = MountedPromptHandler::new(handler, mounted_name.clone());
6842            let needs_order_push =
6843                !existed && !self.prompt_order.iter().any(|n| n == &mounted_name);
6844            self.prompts.insert(mounted_name.clone(), Box::new(mounted));
6845            match final_prompts.remove(&name) {
6846                Some(mut final_registration) => {
6847                    final_registration.definition.name.clone_from(&mounted_name);
6848                    self.final_prompts
6849                        .insert(mounted_name.clone(), final_registration);
6850                }
6851                None => {
6852                    self.final_prompts.remove(&mounted_name);
6853                }
6854            }
6855            if final_only_prompts.contains(&name) {
6856                self.final_only_prompts.insert(mounted_name.clone());
6857            } else {
6858                self.final_only_prompts.remove(&mounted_name);
6859            }
6860            if needs_order_push {
6861                self.prompt_order.push(mounted_name);
6862            }
6863            result.prompts += 1;
6864        }
6865
6866        if !prompts.is_empty() {
6867            let mut remaining: Vec<(String, BoxedPromptHandler)> = prompts.into_iter().collect();
6868            remaining.sort_by(|a, b| a.0.cmp(&b.0));
6869            for (name, handler) in remaining {
6870                let mounted_name = Self::apply_prefix(&name, prefix);
6871
6872                let existed = self.prompts.contains_key(&mounted_name);
6873                if existed
6874                    && !Self::should_mount_duplicate(behavior, "Prompt", &mounted_name, &mut result)
6875                {
6876                    continue;
6877                }
6878
6879                let mounted = MountedPromptHandler::new(handler, mounted_name.clone());
6880                self.prompts.insert(mounted_name.clone(), Box::new(mounted));
6881                match final_prompts.remove(&name) {
6882                    Some(mut final_registration) => {
6883                        final_registration.definition.name.clone_from(&mounted_name);
6884                        self.final_prompts
6885                            .insert(mounted_name.clone(), final_registration);
6886                    }
6887                    None => {
6888                        self.final_prompts.remove(&mounted_name);
6889                    }
6890                }
6891                if final_only_prompts.contains(&name) {
6892                    self.final_only_prompts.insert(mounted_name.clone());
6893                } else {
6894                    self.final_only_prompts.remove(&mounted_name);
6895                }
6896                if !existed && !self.prompt_order.iter().any(|n| n == &mounted_name) {
6897                    self.prompt_order.push(mounted_name);
6898                }
6899                result.prompts += 1;
6900            }
6901        }
6902
6903        result
6904    }
6905
6906    /// Consumes the router and returns its internal handlers.
6907    ///
6908    /// This is used internally for mounting operations.
6909    #[must_use]
6910    #[allow(dead_code)]
6911    pub(crate) fn into_parts(
6912        self,
6913    ) -> (
6914        HashMap<String, BoxedToolHandler>,
6915        HashMap<String, BoxedResourceHandler>,
6916        HashMap<String, ResourceTemplateEntry>,
6917        HashMap<String, BoxedPromptHandler>,
6918    ) {
6919        let tools = self
6920            .tools
6921            .into_iter()
6922            .map(|(name, entry)| (name, entry.handler))
6923            .collect();
6924        (tools, self.resources, self.resource_templates, self.prompts)
6925    }
6926}
6927
6928struct ResolvedResource<'a> {
6929    handler: &'a BoxedResourceHandler,
6930    params: UriParams,
6931    final_enabled: bool,
6932    legacy_enabled: bool,
6933    uri_use_policy: ResourceUriUsePolicy,
6934}
6935
6936impl ResolvedResource<'_> {
6937    const fn is_enabled_in(&self, era: ProtocolEra) -> bool {
6938        match era {
6939            ProtocolEra::Legacy2024 => self.legacy_enabled,
6940            ProtocolEra::Modern2026 => self.final_enabled,
6941        }
6942    }
6943}
6944
6945/// Entry for a resource template with its matcher and optional handler.
6946pub(crate) struct ResourceTemplateEntry {
6947    pub(crate) matcher: Option<ReversibleResourceTemplate>,
6948    legacy_matcher: Option<LegacyResourceTemplateMatcher>,
6949    specificity: (usize, usize, usize),
6950    pub(crate) template: ResourceTemplate,
6951    pub(crate) handler: Option<BoxedResourceHandler>,
6952    final_definition: Option<FinalResourceTemplate>,
6953    uri_use_policy: ResourceUriUsePolicy,
6954    legacy_enabled: bool,
6955}
6956
6957/// The frozen, exact-2024 template matcher. Legacy registrations deliberately
6958/// do not inherit new RFC 6570 operators: their parameter surface is limited
6959/// to `{name}` and `{+name}`, with the historical non-empty, percent-decoded
6960/// capture rules retained for existing handlers.
6961#[derive(Debug, Clone)]
6962struct LegacyResourceTemplateMatcher {
6963    segments: Vec<LegacyResourceTemplateSegment>,
6964}
6965
6966#[derive(Debug, Clone)]
6967enum LegacyResourceTemplateSegment {
6968    Literal(String),
6969    Parameter(String),
6970}
6971
6972impl LegacyResourceTemplateMatcher {
6973    fn parse(pattern: &str) -> Result<Self, ()> {
6974        let mut segments = Vec::new();
6975        let mut literal = String::new();
6976        let mut chars = pattern.chars().peekable();
6977        let mut names = HashSet::new();
6978
6979        while let Some(character) = chars.next() {
6980            match character {
6981                '{' if matches!(chars.peek(), Some('{')) => {
6982                    let _ = chars.next();
6983                    literal.push('{');
6984                }
6985                '{' => {
6986                    if !literal.is_empty() {
6987                        segments.push(LegacyResourceTemplateSegment::Literal(std::mem::take(
6988                            &mut literal,
6989                        )));
6990                    }
6991                    let mut expression = String::new();
6992                    let mut closed = false;
6993                    for next in chars.by_ref() {
6994                        if next == '}' {
6995                            closed = true;
6996                            break;
6997                        }
6998                        expression.push(next);
6999                    }
7000                    if !closed {
7001                        return Err(());
7002                    }
7003
7004                    let name = expression.strip_prefix('+').unwrap_or(&expression);
7005                    if name.is_empty()
7006                        || name.starts_with('+')
7007                        || matches!(
7008                            expression.chars().next(),
7009                            Some('#' | '.' | '/' | ';' | '?' | '&')
7010                        )
7011                        || name
7012                            .chars()
7013                            .any(|character| matches!(character, '*' | ':' | ','))
7014                        || !names.insert(name.to_owned())
7015                    {
7016                        return Err(());
7017                    }
7018                    segments.push(LegacyResourceTemplateSegment::Parameter(name.to_owned()));
7019                }
7020                '}' if matches!(chars.peek(), Some('}')) => {
7021                    let _ = chars.next();
7022                    literal.push('}');
7023                }
7024                '}' => return Err(()),
7025                character => literal.push(character),
7026            }
7027        }
7028
7029        if !literal.is_empty() {
7030            segments.push(LegacyResourceTemplateSegment::Literal(literal));
7031        }
7032        Ok(Self { segments })
7033    }
7034
7035    fn specificity(&self) -> (usize, usize, usize) {
7036        let mut literal_bytes = 0usize;
7037        let mut literal_parts = 0usize;
7038        for segment in &self.segments {
7039            if let LegacyResourceTemplateSegment::Literal(literal) = segment {
7040                literal_bytes = literal_bytes.saturating_add(literal.len());
7041                literal_parts = literal_parts.saturating_add(1);
7042            }
7043        }
7044        (literal_bytes, literal_parts, self.segments.len())
7045    }
7046
7047    fn matches(&self, uri: &str) -> Option<UriParams> {
7048        let mut params = UriParams::new();
7049        let mut remainder = uri;
7050        let mut segments = self.segments.iter().peekable();
7051
7052        while let Some(segment) = segments.next() {
7053            match segment {
7054                LegacyResourceTemplateSegment::Literal(literal) => {
7055                    remainder = remainder.strip_prefix(literal)?;
7056                }
7057                LegacyResourceTemplateSegment::Parameter(name) => {
7058                    let next_literal = segments.peek().and_then(|next| match next {
7059                        LegacyResourceTemplateSegment::Literal(literal) => Some(literal.as_str()),
7060                        LegacyResourceTemplateSegment::Parameter(_) => None,
7061                    });
7062                    if next_literal.is_none() && segments.peek().is_some() {
7063                        return None;
7064                    }
7065
7066                    let value = if let Some(literal) = next_literal {
7067                        let index = remainder.find(literal)?;
7068                        let value = &remainder[..index];
7069                        remainder = &remainder[index..];
7070                        value
7071                    } else {
7072                        if remainder.is_empty() {
7073                            return None;
7074                        }
7075                        let parameter_count = self
7076                            .segments
7077                            .iter()
7078                            .filter(|segment| {
7079                                matches!(segment, LegacyResourceTemplateSegment::Parameter(_))
7080                            })
7081                            .count();
7082                        let end = if parameter_count == 1 {
7083                            remainder.len()
7084                        } else {
7085                            remainder.find('/').unwrap_or(remainder.len())
7086                        };
7087                        let value = &remainder[..end];
7088                        remainder = &remainder[end..];
7089                        value
7090                    };
7091                    if value.is_empty() {
7092                        return None;
7093                    }
7094                    params.insert(name.clone(), legacy_percent_decode(value)?);
7095                }
7096            }
7097        }
7098
7099        remainder.is_empty().then_some(params)
7100    }
7101}
7102
7103fn admit_legacy_resource_template(source: &str) -> McpResult<LegacyResourceTemplateMatcher> {
7104    LegacyResourceTemplateMatcher::parse(source).map_err(|()| {
7105        McpError::invalid_params(
7106            "exact-2024 resource templates only admit unmodified {name} and {+name} parameters",
7107        )
7108    })
7109}
7110
7111fn legacy_percent_decode(input: &str) -> Option<String> {
7112    if !input.as_bytes().contains(&b'%') {
7113        return Some(input.to_owned());
7114    }
7115    let bytes = input.as_bytes();
7116    let mut output = Vec::with_capacity(bytes.len());
7117    let mut index = 0usize;
7118    while index < bytes.len() {
7119        match bytes[index] {
7120            b'%' if index + 2 < bytes.len() => {
7121                let high = legacy_hex_value(bytes[index + 1])?;
7122                let low = legacy_hex_value(bytes[index + 2])?;
7123                output.push((high << 4) | low);
7124                index += 3;
7125            }
7126            b'%' => return None,
7127            byte => {
7128                output.push(byte);
7129                index += 1;
7130            }
7131        }
7132    }
7133    String::from_utf8(output).ok()
7134}
7135
7136const fn legacy_hex_value(byte: u8) -> Option<u8> {
7137    match byte {
7138        b'0'..=b'9' => Some(byte - b'0'),
7139        b'a'..=b'f' => Some(byte - b'a' + 10),
7140        b'A'..=b'F' => Some(byte - b'A' + 10),
7141        _ => None,
7142    }
7143}
7144
7145/// Parses a resource template with the canonical RFC 6570 implementation and
7146/// admits only templates the protocol can reverse-match exactly for routing.
7147fn admit_resource_template(
7148    source: &str,
7149) -> McpResult<(ReversibleResourceTemplate, (usize, usize, usize))> {
7150    let template = fastmcp_protocol::UriTemplate::parse(source)
7151        .map_err(|_| McpError::invalid_params("resource template is not valid RFC 6570"))?;
7152    let specificity = resource_template_specificity(&template);
7153    let matcher = template
7154        .compile_reversible()
7155        .map_err(|_| McpError::invalid_params("resource template cannot be matched reversibly"))?;
7156    Ok((matcher, specificity))
7157}
7158
7159fn resource_template_specificity(
7160    template: &fastmcp_protocol::UriTemplate,
7161) -> (usize, usize, usize) {
7162    let mut literal_bytes = 0usize;
7163    let mut literal_parts = 0usize;
7164    for part in template.parts() {
7165        if let fastmcp_protocol::UriTemplatePart::Literal(literal) = part {
7166            literal_bytes = literal_bytes.saturating_add(literal.len());
7167            literal_parts = literal_parts.saturating_add(1);
7168        }
7169    }
7170    (literal_bytes, literal_parts, template.parts().len())
7171}
7172
7173/// Returns whether two reversible template languages might intersect.
7174///
7175/// A literal prefix is emitted before any expression and therefore provides a
7176/// byte-exact proof of disjointness when the prefixes disagree. When the
7177/// prefixes are compatible, reject conservatively: an RFC 6570 capture can be
7178/// omitted or can absorb following literals, so accepting without a full
7179/// language-intersection proof would reintroduce dispatch-order authority.
7180fn reversible_templates_may_overlap(
7181    left: &ReversibleResourceTemplate,
7182    right: &ReversibleResourceTemplate,
7183) -> McpResult<bool> {
7184    let left_prefix = reversible_template_leading_literal_prefix(left)?;
7185    let right_prefix = reversible_template_leading_literal_prefix(right)?;
7186    Ok(left_prefix.starts_with(&right_prefix) || right_prefix.starts_with(&left_prefix))
7187}
7188
7189fn reversible_template_leading_literal_prefix(
7190    matcher: &ReversibleResourceTemplate,
7191) -> McpResult<String> {
7192    let mut literal = String::new();
7193    for part in matcher.template().parts() {
7194        let UriTemplatePart::Literal(part) = part else {
7195            break;
7196        };
7197        literal.push_str(part);
7198    }
7199    fastmcp_protocol::UriTemplate::parse(&literal)
7200        .and_then(|template| template.expand(&fastmcp_protocol::TemplateValues::new()))
7201        .map_err(|_| McpError::internal_error("admitted resource template lost literal prefix"))
7202}
7203
7204// ============================================================================
7205// Resource Reader Implementation
7206// ============================================================================
7207
7208use fastmcp_core::{
7209    MAX_RESOURCE_READ_DEPTH, ResourceContentItem, ResourceReadResult, ResourceReader,
7210};
7211use std::pin::Pin;
7212
7213/// A wrapper that implements `ResourceReader` for a shared `Router`.
7214///
7215/// This allows handlers to read resources from within tool/resource/prompt
7216/// handlers, enabling cross-component access.
7217#[derive(Clone)]
7218enum RouterAccess {
7219    Shared(Arc<Router>),
7220    RequestScoped(Weak<Router>),
7221}
7222
7223impl RouterAccess {
7224    fn upgrade(&self) -> McpResult<Arc<Router>> {
7225        match self {
7226            Self::Shared(router) => Ok(Arc::clone(router)),
7227            Self::RequestScoped(router) => router.upgrade().ok_or_else(|| {
7228                McpError::new(
7229                    McpErrorCode::RequestCancelled,
7230                    "Request router is no longer available",
7231                )
7232            }),
7233        }
7234    }
7235}
7236
7237pub(crate) struct RouterResourceReader {
7238    /// Access to the router without extending a server request's lifetime.
7239    router: RouterAccess,
7240    /// Session state for handlers.
7241    session_state: SessionState,
7242}
7243
7244impl RouterResourceReader {
7245    /// Creates a new resource reader with the given router and session state.
7246    #[must_use]
7247    pub(crate) fn new(router: Arc<Router>, session_state: SessionState) -> Self {
7248        Self {
7249            router: RouterAccess::Shared(router),
7250            session_state,
7251        }
7252    }
7253
7254    pub(crate) fn request_scoped(router: Weak<Router>, session_state: SessionState) -> Self {
7255        Self {
7256            router: RouterAccess::RequestScoped(router),
7257            session_state,
7258        }
7259    }
7260
7261    fn from_access(router: RouterAccess, session_state: SessionState) -> Self {
7262        Self {
7263            router,
7264            session_state,
7265        }
7266    }
7267}
7268
7269impl ResourceReader for RouterResourceReader {
7270    fn read_resource<'a>(
7271        &'a self,
7272        parent_ctx: &'a McpContext,
7273        uri: &'a str,
7274        depth: u32,
7275    ) -> Pin<
7276        Box<
7277            dyn std::future::Future<Output = fastmcp_core::McpResult<ResourceReadResult>>
7278                + Send
7279                + 'a,
7280        >,
7281    > {
7282        // Check recursion depth
7283        if depth > MAX_RESOURCE_READ_DEPTH {
7284            return Box::pin(async move {
7285                Err(McpError::new(
7286                    McpErrorCode::InternalError,
7287                    format!(
7288                        "Maximum resource read depth ({}) exceeded",
7289                        MAX_RESOURCE_READ_DEPTH
7290                    ),
7291                ))
7292            });
7293        }
7294
7295        // Clone what we need for the async block
7296        let parent_ctx = parent_ctx.clone();
7297        let uri = uri.to_string();
7298        let router_access = self.router.clone();
7299        let session_state = self.session_state.clone();
7300
7301        Box::pin(async move {
7302            debug!(
7303                target: targets::HANDLER,
7304                "cross-component resource read; resource_key={}; depth={}; request={}",
7305                safe_log_label(&uri),
7306                depth,
7307                parent_ctx.request_id()
7308            );
7309            let router = router_access.upgrade()?;
7310            let operation_started_at = parent_ctx.cx().now();
7311            if let Some(error) = budget_error(&parent_ctx) {
7312                return Err(error);
7313            }
7314            if !session_state.is_resource_enabled(&uri) {
7315                return Err(McpError::new(
7316                    McpErrorCode::ResourceNotFound,
7317                    format!("Resource '{}' is disabled for this session", uri),
7318                ));
7319            }
7320
7321            // Resolve the resource
7322            let resolved = router.resolve_resource(&uri).ok_or_else(|| {
7323                McpError::new(
7324                    McpErrorCode::ResourceNotFound,
7325                    format!("Resource not found: {}", uri),
7326                )
7327            })?;
7328            let handler_timeout =
7329                read_handler_timeout(parent_ctx.cx(), "resource_timeout", || {
7330                    resolved.handler.timeout()
7331                })?;
7332            let effective_budget = compose_handler_budget(
7333                parent_ctx.cx().budget(),
7334                parent_ctx.budget(),
7335                handler_timeout,
7336                operation_started_at,
7337            );
7338
7339            // Derive the child from the parent request authority, preserving
7340            // auth, mask state, budget accounting, and request identity.
7341            let child_ctx = with_nested_component_access(
7342                parent_ctx
7343                    .clone()
7344                    .with_operation_deadline(effective_budget.deadline),
7345                router_access.clone(),
7346                session_state.clone(),
7347                depth,
7348            );
7349
7350            // Read the resource on the request-owned future. Nested
7351            // `block_on` would replace the ambient Cx and can stall the
7352            // already-running HTTP/stdio dispatcher.
7353            let outcome = run_handler_in_request(
7354                &child_ctx,
7355                parent_ctx.cx(),
7356                effective_budget,
7357                "resource",
7358                |request_cx| {
7359                    resolved.handler.read_async_with_uri_in_request(
7360                        &child_ctx,
7361                        request_cx,
7362                        &uri,
7363                        &resolved.params,
7364                    )
7365                },
7366            )
7367            .await?;
7368
7369            // Convert outcome to result
7370            let contents = match outcome {
7371                Outcome::Ok(contents) => contents,
7372                Outcome::Err(error) => {
7373                    return Err(sanitize_handler_error(parent_ctx.cx(), "resource", error));
7374                }
7375                Outcome::Cancelled(_) => return Err(McpError::request_cancelled()),
7376                Outcome::Panicked(_payload) => {
7377                    return Err(sanitized_handler_panic(parent_ctx.cx(), "resource"));
7378                }
7379            };
7380
7381            // Convert protocol ResourceContent to core ResourceContentItem
7382            let items: Vec<ResourceContentItem> = contents
7383                .into_iter()
7384                .map(|c| ResourceContentItem {
7385                    uri: c.uri,
7386                    mime_type: c.mime_type,
7387                    text: c.text,
7388                    blob: c.blob,
7389                })
7390                .collect();
7391
7392            Ok(ResourceReadResult::new(items))
7393        })
7394    }
7395}
7396
7397// ============================================================================
7398// Tool Caller Implementation
7399// ============================================================================
7400
7401use fastmcp_core::{MAX_TOOL_CALL_DEPTH, ToolCallResult, ToolCaller, ToolContentItem};
7402
7403/// A wrapper that implements `ToolCaller` for a shared `Router`.
7404///
7405/// This allows handlers to call other tools from within tool/resource/prompt
7406/// handlers, enabling cross-component access.
7407pub(crate) struct RouterToolCaller {
7408    /// Access to the router without extending a server request's lifetime.
7409    router: RouterAccess,
7410    /// Session state for handlers.
7411    session_state: SessionState,
7412}
7413
7414impl RouterToolCaller {
7415    /// Creates a new tool caller with the given router and session state.
7416    #[must_use]
7417    pub(crate) fn new(router: Arc<Router>, session_state: SessionState) -> Self {
7418        Self {
7419            router: RouterAccess::Shared(router),
7420            session_state,
7421        }
7422    }
7423
7424    pub(crate) fn request_scoped(router: Weak<Router>, session_state: SessionState) -> Self {
7425        Self {
7426            router: RouterAccess::RequestScoped(router),
7427            session_state,
7428        }
7429    }
7430
7431    fn from_access(router: RouterAccess, session_state: SessionState) -> Self {
7432        Self {
7433            router,
7434            session_state,
7435        }
7436    }
7437}
7438
7439impl ToolCaller for RouterToolCaller {
7440    fn call_tool<'a>(
7441        &'a self,
7442        parent_ctx: &'a McpContext,
7443        name: &'a str,
7444        args: serde_json::Value,
7445        depth: u32,
7446    ) -> Pin<
7447        Box<dyn std::future::Future<Output = fastmcp_core::McpResult<ToolCallResult>> + Send + 'a>,
7448    > {
7449        // Check recursion depth
7450        if depth > MAX_TOOL_CALL_DEPTH {
7451            return Box::pin(async move {
7452                Err(McpError::new(
7453                    McpErrorCode::InternalError,
7454                    format!("Maximum tool call depth ({}) exceeded", MAX_TOOL_CALL_DEPTH),
7455                ))
7456            });
7457        }
7458
7459        // Clone what we need for the async block
7460        let parent_ctx = parent_ctx.clone();
7461        let name = name.to_string();
7462        let router_access = self.router.clone();
7463        let session_state = self.session_state.clone();
7464
7465        Box::pin(async move {
7466            debug!(
7467                target: targets::HANDLER,
7468                "cross-component tool call; tool_key={}; depth={}; request={}",
7469                safe_log_label(&name),
7470                depth,
7471                parent_ctx.request_id()
7472            );
7473            let router = router_access.upgrade()?;
7474            let operation_started_at = parent_ctx.cx().now();
7475            if let Some(error) = budget_error(&parent_ctx) {
7476                return Err(error);
7477            }
7478            if !session_state.is_tool_enabled(&name) {
7479                return Err(McpError::new(
7480                    McpErrorCode::MethodNotFound,
7481                    format!("Tool '{}' is disabled for this session", name),
7482                ));
7483            }
7484
7485            // Find the tool handler
7486            let entry = router
7487                .tools
7488                .get(&name)
7489                .ok_or_else(|| McpError::method_not_found(&format!("tool: {}", name)))?;
7490            if !entry.legacy_enabled {
7491                return Err(McpError::method_not_found(&format!("tool: {}", name)));
7492            }
7493            let handler = &entry.handler;
7494
7495            // Validate arguments against the tool's input schema
7496            // Use strict or lenient validation based on router configuration
7497            let validation_result = if router.strict_input_validation {
7498                validate_strict(&entry.definition.input_schema, &args)
7499            } else {
7500                validate(&entry.definition.input_schema, &args)
7501            };
7502
7503            if let Err(validation_errors) = validation_result {
7504                let error_messages: Vec<String> = validation_errors
7505                    .iter()
7506                    .map(|e| format!("{}: {}", e.path, e.message))
7507                    .collect();
7508                return Err(McpError::invalid_params(format!(
7509                    "Input validation failed: {}",
7510                    error_messages.join("; ")
7511                )));
7512            }
7513            let handler_timeout =
7514                read_handler_timeout(parent_ctx.cx(), "tool_timeout", || handler.timeout())?;
7515            let effective_budget = compose_handler_budget(
7516                parent_ctx.cx().budget(),
7517                parent_ctx.budget(),
7518                handler_timeout,
7519                operation_started_at,
7520            );
7521
7522            // Derive the child from the parent request authority, preserving
7523            // auth, mask state, budget accounting, and request identity.
7524            let child_ctx = with_nested_component_access(
7525                parent_ctx
7526                    .clone()
7527                    .with_operation_deadline(effective_budget.deadline),
7528                router_access.clone(),
7529                session_state.clone(),
7530                depth,
7531            );
7532
7533            // Call the tool on the request-owned future. Nested `block_on`
7534            // would replace the ambient Cx and can stall the already-running
7535            // HTTP/stdio dispatcher.
7536            let outcome = run_handler_in_request(
7537                &child_ctx,
7538                parent_ctx.cx(),
7539                effective_budget,
7540                "tool",
7541                |request_cx| handler.call_async_in_request(&child_ctx, request_cx, args),
7542            )
7543            .await?;
7544
7545            // Convert outcome to result
7546            match outcome {
7547                Outcome::Ok(content) => {
7548                    // Convert protocol Content to core ToolContentItem
7549                    let items: Vec<ToolContentItem> = content
7550                        .into_iter()
7551                        .map(|c| match c {
7552                            Content::Text { text } => ToolContentItem::Text { text },
7553                            Content::Image { data, mime_type } => {
7554                                ToolContentItem::Image { data, mime_type }
7555                            }
7556                            Content::Audio { data, mime_type } => {
7557                                ToolContentItem::Audio { data, mime_type }
7558                            }
7559                            Content::Resource { resource } => ToolContentItem::Resource {
7560                                uri: resource.uri,
7561                                mime_type: resource.mime_type,
7562                                text: resource.text,
7563                                blob: resource.blob,
7564                            },
7565                        })
7566                        .collect();
7567
7568                    Ok(ToolCallResult::success(items))
7569                }
7570                Outcome::Err(e) => {
7571                    let e = sanitize_handler_error(parent_ctx.cx(), "tool", e);
7572                    if is_framework_terminal_tool_error(e.code) {
7573                        return Err(e);
7574                    }
7575                    // Tool errors become error results, not failures
7576                    Ok(ToolCallResult::error(e.message))
7577                }
7578                Outcome::Cancelled(_) => Err(McpError::request_cancelled()),
7579                Outcome::Panicked(_payload) => {
7580                    Err(sanitized_handler_panic(parent_ctx.cx(), "tool"))
7581                }
7582            }
7583        })
7584    }
7585}
7586
7587fn with_nested_component_access(
7588    ctx: McpContext,
7589    router: RouterAccess,
7590    state: SessionState,
7591    depth: u32,
7592) -> McpContext {
7593    ctx.with_resource_read_depth(depth)
7594        .with_tool_call_depth(depth)
7595        .with_prompt_get_depth(depth)
7596        .with_tool_caller(Arc::new(RouterToolCaller::from_access(
7597            router.clone(),
7598            state.clone(),
7599        )))
7600        .with_resource_reader(Arc::new(RouterResourceReader::from_access(
7601            router.clone(),
7602            state.clone(),
7603        )))
7604        .with_prompt_caller(Arc::new(RouterPromptCaller::from_access(router, state)))
7605}
7606
7607// ============================================================================
7608// Prompt Caller Implementation
7609// ============================================================================
7610
7611pub(crate) struct RouterPromptCaller {
7612    router: RouterAccess,
7613    session_state: SessionState,
7614}
7615
7616impl RouterPromptCaller {
7617    #[must_use]
7618    pub(crate) fn new(router: Arc<Router>, session_state: SessionState) -> Self {
7619        Self {
7620            router: RouterAccess::Shared(router),
7621            session_state,
7622        }
7623    }
7624
7625    fn from_access(router: RouterAccess, session_state: SessionState) -> Self {
7626        Self {
7627            router,
7628            session_state,
7629        }
7630    }
7631}
7632
7633impl PromptCaller for RouterPromptCaller {
7634    fn get_prompt<'a>(
7635        &'a self,
7636        parent_ctx: &'a McpContext,
7637        name: &'a str,
7638        arguments: HashMap<String, String>,
7639        depth: u32,
7640    ) -> Pin<
7641        Box<dyn std::future::Future<Output = fastmcp_core::McpResult<PromptGetResult>> + Send + 'a>,
7642    > {
7643        if depth > fastmcp_core::MAX_PROMPT_GET_DEPTH {
7644            return Box::pin(async move {
7645                Err(McpError::new(
7646                    McpErrorCode::InternalError,
7647                    format!(
7648                        "Maximum prompt get depth ({}) exceeded",
7649                        fastmcp_core::MAX_PROMPT_GET_DEPTH
7650                    ),
7651                ))
7652            });
7653        }
7654
7655        let parent_ctx = parent_ctx.clone();
7656        let name = name.to_string();
7657        let router_access = self.router.clone();
7658        let session_state = self.session_state.clone();
7659
7660        Box::pin(async move {
7661            debug!(
7662                target: targets::HANDLER,
7663                "cross-component prompt get; prompt_key={}; depth={}; request={}",
7664                safe_log_label(&name),
7665                depth,
7666                parent_ctx.request_id()
7667            );
7668            let router = router_access.upgrade()?;
7669            let operation_started_at = parent_ctx.cx().now();
7670            if let Some(error) = budget_error(&parent_ctx) {
7671                return Err(error);
7672            }
7673            if !session_state.is_prompt_enabled(&name) {
7674                return Err(McpError::new(
7675                    McpErrorCode::PromptNotFound,
7676                    format!("Prompt '{name}' is disabled for this session"),
7677                ));
7678            }
7679            let handler = router.prompts.get(&name).ok_or_else(|| {
7680                McpError::new(
7681                    McpErrorCode::PromptNotFound,
7682                    format!("Prompt not found: {name}"),
7683                )
7684            })?;
7685            if router.final_only_prompts.contains(&name) {
7686                return Err(McpError::new(
7687                    McpErrorCode::PromptNotFound,
7688                    format!("Prompt not found: {name}"),
7689                ));
7690            }
7691            let description = crate::catch_extension_unwind(|| handler.definition().description)
7692                .map_err(|_payload| {
7693                    sanitized_handler_panic(parent_ctx.cx(), "prompt_definition")
7694                })?;
7695            let handler_timeout =
7696                read_handler_timeout(parent_ctx.cx(), "prompt_timeout", || handler.timeout())?;
7697            let effective_budget = compose_handler_budget(
7698                parent_ctx.cx().budget(),
7699                parent_ctx.budget(),
7700                handler_timeout,
7701                operation_started_at,
7702            );
7703            let child_ctx = with_nested_component_access(
7704                parent_ctx
7705                    .clone()
7706                    .with_operation_deadline(effective_budget.deadline),
7707                router_access,
7708                session_state,
7709                depth,
7710            );
7711            let outcome = run_handler_in_request(
7712                &child_ctx,
7713                parent_ctx.cx(),
7714                effective_budget,
7715                "prompt",
7716                |request_cx| handler.get_async_in_request(&child_ctx, request_cx, arguments),
7717            )
7718            .await?;
7719            let messages = match outcome {
7720                Outcome::Ok(messages) => messages,
7721                Outcome::Err(error) => {
7722                    return Err(sanitize_handler_error(parent_ctx.cx(), "prompt", error));
7723                }
7724                Outcome::Cancelled(_) => return Err(McpError::request_cancelled()),
7725                Outcome::Panicked(_payload) => {
7726                    return Err(sanitized_handler_panic(parent_ctx.cx(), "prompt"));
7727                }
7728            };
7729            Ok(PromptGetResult {
7730                description,
7731                messages: messages
7732                    .into_iter()
7733                    .map(|message| PromptMessageItem {
7734                        role: match message.role {
7735                            Role::User => PromptMessageRole::User,
7736                            Role::Assistant => PromptMessageRole::Assistant,
7737                        },
7738                        text: match message.content {
7739                            Content::Text { text } => Some(text),
7740                            _ => None,
7741                        },
7742                    })
7743                    .collect(),
7744            })
7745        })
7746    }
7747}
7748
7749#[cfg(test)]
7750mod safe_log_label_tests {
7751    use super::{LOG_LABEL_HASH_INPUT_LIMIT, safe_log_label};
7752
7753    #[test]
7754    fn safe_label_is_deterministic_non_verbatim_metadata() {
7755        let canary = "router-log-canary-secret";
7756        let first = format!("{}", safe_log_label(canary));
7757        let second = format!("{:?}", safe_log_label(canary));
7758
7759        assert_eq!(first, second);
7760        assert!(first.contains(&format!("bytes={}", canary.len())));
7761        assert!(first.contains("sha256_prefix="));
7762        assert!(!first.contains(canary));
7763        assert_ne!(first, format!("{}", safe_log_label("different-label")));
7764    }
7765
7766    #[test]
7767    fn safe_label_hashing_is_bounded_for_oversized_input() {
7768        let oversized = "x".repeat(LOG_LABEL_HASH_INPUT_LIMIT + 37);
7769        let rendered = format!("{}", safe_log_label(&oversized));
7770
7771        assert!(rendered.contains(&format!("bytes={}", oversized.len())));
7772        assert!(rendered.contains(&format!("hashed_prefix_bytes={LOG_LABEL_HASH_INPUT_LIMIT}")));
7773        assert!(!rendered.contains(&oversized));
7774    }
7775
7776    #[test]
7777    fn source_has_no_verbatim_argument_or_label_log_formats() {
7778        let source = include_str!("router.rs");
7779        let forbidden = [
7780            concat!("Tool ", "arguments:"),
7781            concat!("Prompt ", "arguments:"),
7782            concat!("Calling ", "tool: {}"),
7783            concat!("Reading ", "resource: {}"),
7784            concat!("Getting ", "prompt: {}"),
7785            concat!("Cross-component tool ", "call: {}"),
7786            concat!("Cross-component resource ", "read: {}"),
7787            concat!("Invalid URI template ", "'{}'"),
7788        ];
7789
7790        for format in forbidden {
7791            assert!(!source.contains(format), "raw log format remains: {format}");
7792        }
7793    }
7794}
7795
7796#[cfg(test)]
7797mod cursor_tests {
7798    use super::{
7799        FinalCatalogKind, FinalCatalogQuery, decode_cursor_offset,
7800        decode_final_catalog_cursor_offset, encode_cursor_offset, encode_final_catalog_cursor,
7801    };
7802
7803    #[test]
7804    fn roundtrip_zero() {
7805        let encoded = encode_cursor_offset(0);
7806        let decoded = decode_cursor_offset(Some(&encoded)).unwrap();
7807        assert_eq!(decoded, 0);
7808    }
7809
7810    #[test]
7811    fn roundtrip_large_offset() {
7812        let encoded = encode_cursor_offset(12345);
7813        let decoded = decode_cursor_offset(Some(&encoded)).unwrap();
7814        assert_eq!(decoded, 12345);
7815    }
7816
7817    #[test]
7818    fn none_cursor_returns_zero() {
7819        assert_eq!(decode_cursor_offset(None).unwrap(), 0);
7820    }
7821
7822    #[test]
7823    fn invalid_base64_returns_error() {
7824        let err = decode_cursor_offset(Some("not-valid-base64!!!")).unwrap_err();
7825        assert!(err.message.contains("base64"));
7826    }
7827
7828    #[test]
7829    fn valid_base64_but_not_json_returns_error() {
7830        let encoded =
7831            base64::Engine::encode(&base64::engine::general_purpose::STANDARD, b"not json");
7832        let err = decode_cursor_offset(Some(&encoded)).unwrap_err();
7833        assert!(err.message.contains("JSON"));
7834    }
7835
7836    #[test]
7837    fn valid_json_but_no_offset_returns_error() {
7838        let payload = serde_json::json!({"other": 1});
7839        let bytes = serde_json::to_vec(&payload).unwrap();
7840        let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
7841        let err = decode_cursor_offset(Some(&encoded)).unwrap_err();
7842        assert!(err.message.contains("offset"));
7843    }
7844
7845    #[test]
7846    fn final_catalog_cursor_binds_the_catalog_revision_and_query() {
7847        let include_tags = vec!["Visible".to_owned(), "visible".to_owned()];
7848        let exclude_tags = vec!["excluded".to_owned()];
7849        let query = FinalCatalogQuery::from_tag_filters(Some(&include_tags), Some(&exclude_tags));
7850        let cursor = encode_final_catalog_cursor(FinalCatalogKind::Resources, 41, &query, 7);
7851        let equivalent_include_tags = vec!["visible".to_owned()];
7852        let equivalent_query = FinalCatalogQuery::from_tag_filters(
7853            Some(&equivalent_include_tags),
7854            Some(&exclude_tags),
7855        );
7856        assert_eq!(
7857            decode_final_catalog_cursor_offset(
7858                Some(&cursor),
7859                FinalCatalogKind::Resources,
7860                41,
7861                &equivalent_query,
7862                8,
7863            )
7864            .expect("a final cursor accepts a semantically equivalent canonical query"),
7865            7
7866        );
7867        let stale = decode_final_catalog_cursor_offset(
7868            Some(&cursor),
7869            FinalCatalogKind::Resources,
7870            42,
7871            &query,
7872            8,
7873        )
7874        .expect_err("changing only the catalog revision rejects a stale continuation");
7875        assert!(stale.message.contains("stale catalog revision"));
7876
7877        let wrong_kind = decode_final_catalog_cursor_offset(
7878            Some(&cursor),
7879            FinalCatalogKind::Prompts,
7880            41,
7881            &query,
7882            8,
7883        )
7884        .expect_err("changing only the list method rejects a cross-catalog continuation");
7885        assert!(wrong_kind.message.contains("another list method"));
7886
7887        let other_include_tags = vec!["other".to_owned()];
7888        let other_query =
7889            FinalCatalogQuery::from_tag_filters(Some(&other_include_tags), Some(&exclude_tags));
7890        let wrong_query = decode_final_catalog_cursor_offset(
7891            Some(&cursor),
7892            FinalCatalogKind::Resources,
7893            41,
7894            &other_query,
7895            8,
7896        )
7897        .expect_err("changing only the request filters rejects the continuation");
7898        assert!(wrong_query.message.contains("query filters"));
7899
7900        let out_of_range = encode_final_catalog_cursor(FinalCatalogKind::Resources, 41, &query, 8);
7901        let range_error = decode_final_catalog_cursor_offset(
7902            Some(&out_of_range),
7903            FinalCatalogKind::Resources,
7904            41,
7905            &query,
7906            8,
7907        )
7908        .expect_err("an offset at the end of a catalog is never a router-minted continuation");
7909        assert!(
7910            range_error
7911                .message
7912                .contains("outside the requested catalog page")
7913        );
7914    }
7915}
7916
7917#[cfg(test)]
7918mod tag_filter_tests {
7919    use super::TagFilters;
7920
7921    #[test]
7922    fn no_filters_matches_anything() {
7923        let f = TagFilters::default();
7924        assert!(f.matches(&[]));
7925        assert!(f.matches(&["a".to_string()]));
7926    }
7927
7928    #[test]
7929    fn include_filter_requires_all_tags() {
7930        let include = vec!["a".to_string(), "b".to_string()];
7931        let f = TagFilters::new(Some(&include), None);
7932        assert!(f.matches(&["a".to_string(), "b".to_string(), "c".to_string()]));
7933        assert!(!f.matches(&["a".to_string()])); // missing "b"
7934    }
7935
7936    #[test]
7937    fn exclude_filter_rejects_any_tag() {
7938        let exclude = vec!["x".to_string()];
7939        let f = TagFilters::new(None, Some(&exclude));
7940        assert!(f.matches(&["a".to_string(), "b".to_string()]));
7941        assert!(!f.matches(&["a".to_string(), "x".to_string()]));
7942    }
7943
7944    #[test]
7945    fn include_and_exclude_combined() {
7946        let include = vec!["a".to_string()];
7947        let exclude = vec!["b".to_string()];
7948        let f = TagFilters::new(Some(&include), Some(&exclude));
7949        assert!(f.matches(&["a".to_string()]));
7950        assert!(!f.matches(&["a".to_string(), "b".to_string()])); // excluded
7951        assert!(!f.matches(&["c".to_string()])); // missing "a"
7952    }
7953
7954    #[test]
7955    fn case_insensitive_matching() {
7956        let include = vec!["Alpha".to_string()];
7957        let f = TagFilters::new(Some(&include), None);
7958        assert!(f.matches(&["alpha".to_string()]));
7959        assert!(f.matches(&["ALPHA".to_string()]));
7960    }
7961
7962    #[test]
7963    fn empty_include_array_passes_all() {
7964        let include: Vec<String> = vec![];
7965        let f = TagFilters::new(Some(&include), None);
7966        assert!(f.matches(&[]));
7967        assert!(f.matches(&["anything".to_string()]));
7968    }
7969
7970    #[test]
7971    fn tag_filters_debug() {
7972        let f = TagFilters::default();
7973        let debug = format!("{:?}", f);
7974        assert!(debug.contains("TagFilters"));
7975    }
7976}
7977
7978#[cfg(test)]
7979mod router_tests {
7980    use super::*;
7981    use crate::bidirectional::MrtrInputResponse;
7982    use crate::handler::{
7983        CompletionHandler, DEFAULT_FINAL_RESOURCE_TTL_MS, FinalElicitationContextExt,
7984        FinalToolSchemaAuthority, PromptHandler, ResourceHandler, ToolHandler,
7985        UpstreamFinalToolSchemaRegistration,
7986    };
7987    use crate::http_admission::{HttpAdmissionLimits, HttpEndpointConfig, admit_modern_post};
7988    #[cfg(feature = "tasks")]
7989    use crate::tasks::{
7990        ApplicationTaskSupervisor, FinalTaskSupervisorFuture, FinalTaskSupervisorHandoff,
7991        FinalTaskWorkDescriptor,
7992    };
7993    #[cfg(feature = "tasks")]
7994    use crate::{FinalTaskRuntimeConfig, FinalTaskStore, InMemoryFinalTaskStore};
7995    use asupersync::channel::oneshot;
7996    use asupersync::runtime::{RuntimeBuilder, RuntimeHandle};
7997    use asupersync::types::CancelKind;
7998    use fastmcp_core::{ClientCapabilityInfo, McpContext, McpResult, SessionState};
7999    use fastmcp_protocol::common_types::{
8000        Annotations, ContentBlock, EmbeddedResourceContents, OpenMetadata, RawIcon,
8001    };
8002    use fastmcp_protocol::{
8003        CompleteResult, CompletionValues, Content, CoreResultDiscriminatorPolicy, DecodedResult,
8004        FinalCallToolResult, FinalCompletionParams, FinalGetPromptResult, FinalPromptMessage,
8005        LegacyCompletionParams, LegacyResourceContent, Prompt, PromptArgument, PromptMessage,
8006        Resource, ResourceContent, ResourceTemplate, ResultPeerEra, Tool, decode_peer_result,
8007    };
8008    use std::collections::BTreeMap;
8009    use std::fmt;
8010    #[cfg(feature = "tasks")]
8011    use std::future::Future;
8012    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
8013    use std::sync::{Arc, Mutex};
8014    use std::task::Poll;
8015
8016    fn request_context(
8017        cx: &Cx,
8018        request_id: u64,
8019        budget: Budget,
8020        state: &SessionState,
8021    ) -> McpContext {
8022        McpContext::with_state(cx.clone(), request_id, state.clone()).with_budget_ceiling(budget)
8023    }
8024
8025    async fn yield_once() {
8026        let mut yielded = false;
8027        std::future::poll_fn(|task_cx| {
8028            if std::mem::replace(&mut yielded, true) {
8029                Poll::Ready(())
8030            } else {
8031                task_cx.waker().wake_by_ref();
8032                Poll::Pending
8033            }
8034        })
8035        .await;
8036    }
8037
8038    #[test]
8039    fn legacy_result_adapters_emit_exact_2024_defaults() {
8040        let tool = CallToolResult {
8041            content: legacy_contents_from_handler(vec![
8042                Content::Text {
8043                    text: "ready".to_owned(),
8044                },
8045                Content::Image {
8046                    data: "aGVsbG8=".to_owned(),
8047                    mime_type: "image/png".to_owned(),
8048                },
8049                Content::Resource {
8050                    resource: ResourceContent {
8051                        uri: "file:///tool.txt".to_owned(),
8052                        mime_type: Some("text/plain".to_owned()),
8053                        text: Some("tool resource".to_owned()),
8054                        blob: None,
8055                    },
8056                },
8057            ])
8058            .expect("handler content is representable by the exact legacy union"),
8059            is_error: false,
8060            meta: None,
8061            additional: BTreeMap::new(),
8062        };
8063        assert_eq!(
8064            serde_json::to_value(tool).expect("legacy tool result serializes"),
8065            serde_json::json!({
8066                "content": [
8067                    {"type": "text", "text": "ready"},
8068                    {"type": "image", "data": "aGVsbG8=", "mimeType": "image/png"},
8069                    {
8070                        "type": "resource",
8071                        "resource": {
8072                            "uri": "file:///tool.txt",
8073                            "mimeType": "text/plain",
8074                            "text": "tool resource"
8075                        }
8076                    }
8077                ]
8078            })
8079        );
8080
8081        let resource = ReadResourceResult {
8082            contents: legacy_resource_contents_from_handler(vec![ResourceContent {
8083                uri: "file:///report.bin".to_owned(),
8084                mime_type: Some("application/octet-stream".to_owned()),
8085                text: None,
8086                blob: Some("AAEC".to_owned()),
8087            }])
8088            .expect("handler resource is representable by the exact legacy union"),
8089            meta: None,
8090            additional: BTreeMap::new(),
8091        };
8092        assert_eq!(
8093            serde_json::to_value(resource).expect("legacy resource result serializes"),
8094            serde_json::json!({
8095                "contents": [{
8096                    "uri": "file:///report.bin",
8097                    "mimeType": "application/octet-stream",
8098                    "blob": "AAEC"
8099                }]
8100            })
8101        );
8102
8103        let prompt = GetPromptResult {
8104            description: Some("ask a question".to_owned()),
8105            messages: legacy_prompt_messages_from_handler(vec![PromptMessage {
8106                role: fastmcp_protocol::Role::User,
8107                content: Content::Text {
8108                    text: "summarize".to_owned(),
8109                },
8110            }])
8111            .expect("handler prompt message is representable by the exact legacy union"),
8112            meta: None,
8113            additional: BTreeMap::new(),
8114        };
8115        assert_eq!(
8116            serde_json::to_value(prompt).expect("legacy prompt result serializes"),
8117            serde_json::json!({
8118                "description": "ask a question",
8119                "messages": [{
8120                    "role": "user",
8121                    "content": {"type": "text", "text": "summarize"}
8122                }]
8123            })
8124        );
8125    }
8126
8127    #[test]
8128    fn legacy_resource_adapter_preserves_open_members_when_promoted() {
8129        let resource = LegacyResourceContent::Text {
8130            uri: "file:///open.txt".to_owned(),
8131            text: "preserve me".to_owned(),
8132            mime_type: Some("text/plain".to_owned()),
8133            additional: BTreeMap::from([
8134                (
8135                    "_meta".to_owned(),
8136                    serde_json::json!({"legacy": "uninterpreted"}),
8137                ),
8138                (
8139                    "com.example/legacy".to_owned(),
8140                    serde_json::json!({"retained": true}),
8141                ),
8142            ]),
8143        };
8144
8145        let promoted = promote_legacy_resource_content(resource)
8146            .expect("schema-valid legacy resource is promotable");
8147        assert_eq!(
8148            serde_json::to_value(promoted).expect("promoted resource serializes"),
8149            serde_json::json!({
8150                "uri": "file:///open.txt",
8151                "mimeType": "text/plain",
8152                "text": "preserve me",
8153                "_meta": {"legacy": "uninterpreted"},
8154                "com.example/legacy": {"retained": true}
8155            })
8156        );
8157    }
8158
8159    #[test]
8160    fn legacy_content_adapter_rejects_only_audio_without_mutating_baseline() {
8161        let baseline = vec![Content::Image {
8162            data: "aGVsbG8=".to_owned(),
8163            mime_type: "image/png".to_owned(),
8164        }];
8165        let baseline_wire = serde_json::to_value(
8166            legacy_contents_from_handler(baseline.clone())
8167                .expect("baseline content is representable by the legacy adapter"),
8168        )
8169        .expect("baseline legacy content serializes");
8170        let planted = vec![Content::Audio {
8171            data: "aGVsbG8=".to_owned(),
8172            mime_type: "image/png".to_owned(),
8173        }];
8174
8175        assert!(
8176            legacy_contents_from_handler(planted).is_err(),
8177            "changing only the content discriminator to audio rejects the legacy adapter"
8178        );
8179        assert_eq!(
8180            serde_json::to_value(
8181                legacy_contents_from_handler(baseline)
8182                    .expect("baseline remains representable after rejection"),
8183            )
8184            .expect("baseline legacy content remains serializable"),
8185            baseline_wire,
8186            "rejected legacy conversion cannot mutate the accepted baseline"
8187        );
8188    }
8189
8190    fn spawn_owned_modern_request(
8191        runtime: &RuntimeHandle,
8192        router: Arc<Router>,
8193        request_context_id: u64,
8194        wire_id: &'static str,
8195        label: &'static str,
8196        control_sender: Option<oneshot::Sender<Cx>>,
8197    ) -> oneshot::Receiver<McpResult<serde_json::Value>> {
8198        let (response_sender, response_receiver) = oneshot::channel();
8199        runtime
8200            .try_spawn_with_cx(move |request_cx| {
8201                if let Some(control_sender) = control_sender {
8202                    control_sender
8203                        .send_blocking(request_cx.clone())
8204                        .expect("the cancellation controller remains available");
8205                }
8206                // Modern tools/call computes an MRTR exchange binding, which
8207                // requires a session cache partition on the request context.
8208                let request_ctx =
8209                    McpContext::with_state(request_cx, request_context_id, SessionState::new());
8210                let request = JsonRpcRequest::new(
8211                    "tools/call",
8212                    Some(serde_json::json!({
8213                        "_meta": {
8214                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8215                            "io.modelcontextprotocol/clientCapabilities": {},
8216                        },
8217                        "name": "concurrent-modern-tool",
8218                        "arguments": {"request": label},
8219                    })),
8220                    wire_id,
8221                );
8222                async move {
8223                    let result = router.dispatch_stateless_owned(request_ctx, request).await;
8224                    response_sender
8225                        .send_blocking(result)
8226                        .expect("the modern dispatch observer remains available");
8227                }
8228            })
8229            .expect("the runtime admits the request owner");
8230        response_receiver
8231    }
8232
8233    // ── Stub handlers ──────────────────────────────────────────────────
8234
8235    struct NamedTool {
8236        name: String,
8237        tags: Vec<String>,
8238    }
8239
8240    impl NamedTool {
8241        fn new(name: &str) -> Self {
8242            Self {
8243                name: name.to_string(),
8244                tags: vec![],
8245            }
8246        }
8247        fn with_tags(name: &str, tags: Vec<String>) -> Self {
8248            Self {
8249                name: name.to_string(),
8250                tags,
8251            }
8252        }
8253    }
8254
8255    impl ToolHandler for NamedTool {
8256        fn definition(&self) -> Tool {
8257            Tool {
8258                name: self.name.clone(),
8259                description: Some(format!("Tool {}", self.name)),
8260                input_schema: serde_json::json!({"type": "object"}),
8261                output_schema: None,
8262                icon: None,
8263                version: None,
8264                tags: self.tags.clone(),
8265                annotations: None,
8266            }
8267        }
8268        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
8269            Ok(vec![Content::text(format!("called {}", self.name))])
8270        }
8271    }
8272
8273    struct RouterProgressTool;
8274
8275    impl ToolHandler for RouterProgressTool {
8276        fn definition(&self) -> Tool {
8277            Tool {
8278                name: "router-progress-tool".to_owned(),
8279                description: None,
8280                input_schema: serde_json::json!({
8281                    "type": "object",
8282                    "required": ["total"],
8283                    "properties": {"total": {"type": "number"}},
8284                    "additionalProperties": false,
8285                }),
8286                output_schema: None,
8287                icon: None,
8288                version: None,
8289                tags: Vec::new(),
8290                annotations: None,
8291            }
8292        }
8293
8294        fn call(&self, ctx: &McpContext, args: serde_json::Value) -> McpResult<Vec<Content>> {
8295            let total = args
8296                .get("total")
8297                .and_then(serde_json::Value::as_f64)
8298                .ok_or_else(|| McpError::invalid_params("router progress total is required"))?;
8299            ctx.report_progress_with_total(12_000.0, total, Some("router-final"));
8300            Ok(vec![Content::text("progress emitted")])
8301        }
8302    }
8303
8304    struct DuplicateInvariantTool {
8305        label: &'static str,
8306        schema_property: &'static str,
8307        legacy_calls: Arc<AtomicUsize>,
8308        final_calls: Arc<AtomicUsize>,
8309    }
8310
8311    impl ToolHandler for DuplicateInvariantTool {
8312        fn definition(&self) -> Tool {
8313            Tool {
8314                name: "duplicate-invariant-tool".to_owned(),
8315                description: Some(self.label.to_owned()),
8316                input_schema: serde_json::json!({
8317                    "type": "object",
8318                    "properties": {
8319                        (self.schema_property): {"type": "boolean"}
8320                    },
8321                    "additionalProperties": false
8322                }),
8323                output_schema: None,
8324                icon: None,
8325                version: None,
8326                tags: vec![self.label.to_owned()],
8327                annotations: None,
8328            }
8329        }
8330
8331        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
8332            self.legacy_calls.fetch_add(1, Ordering::SeqCst);
8333            Ok(vec![Content::text(self.label)])
8334        }
8335
8336        fn call_final_outcome(
8337            &self,
8338            _ctx: &McpContext,
8339            _args: serde_json::Value,
8340        ) -> McpResult<FinalToolOutcome> {
8341            self.final_calls.fetch_add(1, Ordering::SeqCst);
8342            Ok(FinalToolOutcome::Complete(final_tool_complete_result(
8343                FinalCallToolResult {
8344                    content: vec![ContentBlock::text(self.label)],
8345                    is_error: false,
8346                    structured_content: None,
8347                },
8348            )))
8349        }
8350    }
8351
8352    /// A normal-registration candidate whose final output-schema field is
8353    /// invalid. It is used to prove admission rejects without catalog change.
8354    struct InvalidFinalSchemaNamedTool {
8355        name: String,
8356        tags: Vec<String>,
8357    }
8358
8359    impl InvalidFinalSchemaNamedTool {
8360        fn with_tags(name: &str, tags: Vec<String>) -> Self {
8361            Self {
8362                name: name.to_owned(),
8363                tags,
8364            }
8365        }
8366    }
8367
8368    impl ToolHandler for InvalidFinalSchemaNamedTool {
8369        fn definition(&self) -> Tool {
8370            Tool {
8371                name: self.name.clone(),
8372                description: Some(format!("Invalid-schema tool {}", self.name)),
8373                input_schema: serde_json::json!({"type": "object"}),
8374                // The final field itself must be a JSON object. This scalar
8375                // schema document is deliberately invalid for local normal
8376                // registration.
8377                output_schema: Some(serde_json::json!(false)),
8378                icon: None,
8379                version: None,
8380                tags: self.tags.clone(),
8381                annotations: None,
8382            }
8383        }
8384
8385        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
8386            Ok(vec![Content::text(format!("called {}", self.name))])
8387        }
8388    }
8389
8390    fn final_tools_list_request(
8391        cursor: Option<&str>,
8392        include_tags: Option<Vec<&str>>,
8393        exclude_tags: Option<Vec<&str>>,
8394        id: i64,
8395    ) -> JsonRpcRequest {
8396        let mut params = serde_json::Map::new();
8397        params.insert(
8398            "_meta".to_owned(),
8399            serde_json::json!({
8400                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8401                "io.modelcontextprotocol/clientCapabilities": {},
8402            }),
8403        );
8404        if let Some(cursor) = cursor {
8405            params.insert("cursor".to_owned(), serde_json::json!(cursor));
8406        }
8407        if let Some(include_tags) = include_tags {
8408            params.insert("includeTags".to_owned(), serde_json::json!(include_tags));
8409        }
8410        if let Some(exclude_tags) = exclude_tags {
8411            params.insert("excludeTags".to_owned(), serde_json::json!(exclude_tags));
8412        }
8413        JsonRpcRequest::new("tools/list", Some(serde_json::Value::Object(params)), id)
8414    }
8415
8416    fn mounted_tool_router() -> Router {
8417        let mut source = Router::new();
8418        source
8419            .add_tool(NamedTool::with_tags("first", vec!["visible".to_owned()]))
8420            .expect("tool registration succeeds");
8421        source
8422            .add_tool(NamedTool::with_tags("second", vec!["visible".to_owned()]))
8423            .expect("tool registration succeeds");
8424        source
8425            .add_tool(NamedTool::with_tags(
8426                "excluded",
8427                vec!["visible".to_owned(), "excluded".to_owned()],
8428            ))
8429            .expect("tool registration succeeds");
8430        source
8431            .add_tool(NamedTool::with_tags("other", vec!["other".to_owned()]))
8432            .expect("tool registration succeeds");
8433
8434        let mut router = Router::new();
8435        let mounted = router.mount_tools(source, Some("peer"));
8436        assert_eq!(mounted.tools, 4);
8437        router
8438    }
8439
8440    static MACRO_DUAL_ERA_TOOL_CALLS: AtomicUsize = AtomicUsize::new(0);
8441    /// Serializes the tests that reset and assert the shared call counter;
8442    /// concurrent resets interleave and turn the absolute counts flaky.
8443    static MACRO_DUAL_ERA_TOOL_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
8444
8445    fn final_tool_complete_result(
8446        payload: FinalCallToolResult,
8447    ) -> CompleteResult<FinalCallToolResult> {
8448        let params = serde_json::json!({
8449            "_meta": {
8450                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8451                "io.modelcontextprotocol/clientCapabilities": {},
8452            },
8453            "name": "macro_dual_era_tool",
8454        });
8455        let request = CoreRequest::decode(ProtocolEra::Modern2026, "tools/call", Some(&params))
8456            .expect("test final tools/call request");
8457        let mut wire = serde_json::to_value(payload).expect("final tool payload serializes");
8458        wire.as_object_mut()
8459            .expect("final tool payload is an object")
8460            .insert("resultType".to_owned(), serde_json::json!("complete"));
8461        let encoded = serde_json::to_string(&wire).expect("final tool wire serializes");
8462        let CoreResult::Final(FinalCoreResult::ToolsCall { result, .. }) = request
8463            .decode_result(&encoded)
8464            .expect("typed final tools/call result")
8465        else {
8466            panic!("typed final tools/call result is selected");
8467        };
8468        result
8469    }
8470
8471    #[fastmcp_derive::tool]
8472    fn macro_dual_era_tool() -> CompleteResult<FinalCallToolResult> {
8473        MACRO_DUAL_ERA_TOOL_CALLS.fetch_add(1, Ordering::SeqCst);
8474        final_tool_complete_result(FinalCallToolResult {
8475            content: vec![ContentBlock::text("macro final tool result")],
8476            is_error: false,
8477            structured_content: Some(serde_json::json!({"weather":"clear"})),
8478        })
8479    }
8480
8481    struct SchemaBoundaryTool {
8482        final_calls: Arc<AtomicUsize>,
8483        legacy_calls: Arc<AtomicUsize>,
8484        output_matches_schema: bool,
8485        output_is_error: bool,
8486        output_has_unevaluated_property: bool,
8487        invalid_final_input_schema: bool,
8488        missing_final_input_object_type: bool,
8489        invalid_final_output_schema: bool,
8490    }
8491
8492    impl ToolHandler for SchemaBoundaryTool {
8493        fn definition(&self) -> Tool {
8494            Tool {
8495                name: "schema-boundary-tool".to_owned(),
8496                description: None,
8497                input_schema: if self.invalid_final_input_schema {
8498                    serde_json::json!(42)
8499                } else if self.missing_final_input_object_type {
8500                    serde_json::json!({"properties": {"value": {"type": "string"}}})
8501                } else {
8502                    serde_json::json!({
8503                        "$schema": "https://json-schema.org/draft/2020-12/schema",
8504                        "type": "object",
8505                        "required": ["value"],
8506                        "properties": {"value": {"type": "string"}},
8507                        "unevaluatedProperties": false,
8508                    })
8509                },
8510                output_schema: Some(if self.invalid_final_output_schema {
8511                    serde_json::json!(42)
8512                } else {
8513                    serde_json::json!({
8514                        "$schema": "https://json-schema.org/draft/2020-12/schema",
8515                        "type": "object",
8516                        "properties": {
8517                            "accepted": {"type": "boolean"},
8518                            "error": {
8519                                "enum": ["input-validation", "handler"]
8520                            }
8521                        },
8522                        "anyOf": [
8523                            {"required": ["accepted"]},
8524                            {"required": ["error"]}
8525                        ],
8526                        "unevaluatedProperties": false,
8527                    })
8528                }),
8529                icon: None,
8530                version: None,
8531                tags: Vec::new(),
8532                annotations: None,
8533            }
8534        }
8535
8536        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
8537            self.legacy_calls.fetch_add(1, Ordering::SeqCst);
8538            Ok(vec![Content::text("legacy schema-boundary result")])
8539        }
8540
8541        fn final_tool_error_structured_content(
8542            &self,
8543            kind: ToolErrorKind,
8544        ) -> Option<serde_json::Value> {
8545            Some(match kind {
8546                ToolErrorKind::InputValidation => {
8547                    serde_json::json!({"error": "input-validation"})
8548                }
8549                ToolErrorKind::Handler => serde_json::json!({"error": "handler"}),
8550            })
8551        }
8552
8553        fn call_final_outcome(
8554            &self,
8555            _ctx: &McpContext,
8556            _args: serde_json::Value,
8557        ) -> McpResult<FinalToolOutcome> {
8558            self.final_calls.fetch_add(1, Ordering::SeqCst);
8559            let structured_content = if !self.output_matches_schema {
8560                serde_json::json!({"accepted": "not-a-boolean"})
8561            } else if self.output_has_unevaluated_property {
8562                serde_json::json!({"accepted": true, "unexpected": true})
8563            } else {
8564                serde_json::json!({"accepted": true})
8565            };
8566            Ok(FinalToolOutcome::Complete(final_tool_complete_result(
8567                FinalCallToolResult {
8568                    content: vec![ContentBlock::text("final schema-boundary result")],
8569                    is_error: self.output_is_error,
8570                    structured_content: Some(structured_content),
8571                },
8572            )))
8573        }
8574    }
8575
8576    struct UpstreamScalarSchemaTool {
8577        registered_proxy: bool,
8578    }
8579
8580    impl ToolHandler for UpstreamScalarSchemaTool {
8581        fn definition(&self) -> Tool {
8582            Tool {
8583                name: "upstream-scalar-schema-tool".to_owned(),
8584                description: None,
8585                input_schema: serde_json::json!({"type": "object"}),
8586                output_schema: Some(serde_json::json!(false)),
8587                icon: None,
8588                version: None,
8589                tags: Vec::new(),
8590                annotations: None,
8591            }
8592        }
8593
8594        fn final_tool_schema_authority(&self) -> FinalToolSchemaAuthority {
8595            // This public, forgeable label must not bypass local validation.
8596            FinalToolSchemaAuthority::Upstream
8597        }
8598
8599        fn upstream_final_tool_schema_registration(
8600            &self,
8601        ) -> Option<UpstreamFinalToolSchemaRegistration> {
8602            self.registered_proxy
8603                .then(UpstreamFinalToolSchemaRegistration::exact_proxy)
8604        }
8605
8606        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
8607            Ok(vec![Content::text("legacy")])
8608        }
8609
8610        fn call_final_outcome(
8611            &self,
8612            _ctx: &McpContext,
8613            _args: serde_json::Value,
8614        ) -> McpResult<FinalToolOutcome> {
8615            Ok(FinalToolOutcome::Complete(final_tool_complete_result(
8616                FinalCallToolResult {
8617                    content: vec![ContentBlock::text("upstream")],
8618                    is_error: false,
8619                    structured_content: Some(serde_json::json!({"upstream": true})),
8620                },
8621            )))
8622        }
8623    }
8624
8625    /// A dual-era tool whose admitted output schema and handlers deliberately
8626    /// differ across registrations. It proves a successful replacement commits
8627    /// both the handler and final schema together.
8628    struct AdmittedSchemaReplacementTool {
8629        legacy_calls: Arc<AtomicUsize>,
8630        final_calls: Arc<AtomicUsize>,
8631        legacy_label: &'static str,
8632        output_schema: serde_json::Value,
8633        structured_content: Option<serde_json::Value>,
8634    }
8635
8636    impl ToolHandler for AdmittedSchemaReplacementTool {
8637        fn definition(&self) -> Tool {
8638            Tool {
8639                name: "admitted-schema-replacement-tool".to_owned(),
8640                description: Some(self.legacy_label.to_owned()),
8641                input_schema: serde_json::json!({
8642                    "$schema": "https://json-schema.org/draft/2020-12/schema",
8643                    "type": "object",
8644                    "unevaluatedProperties": false,
8645                }),
8646                output_schema: Some(self.output_schema.clone()),
8647                icon: None,
8648                version: None,
8649                tags: Vec::new(),
8650                annotations: None,
8651            }
8652        }
8653
8654        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
8655            self.legacy_calls.fetch_add(1, Ordering::SeqCst);
8656            Ok(vec![Content::text(self.legacy_label)])
8657        }
8658
8659        fn final_tool_error_structured_content(
8660            &self,
8661            kind: ToolErrorKind,
8662        ) -> Option<serde_json::Value> {
8663            match self
8664                .output_schema
8665                .get("type")
8666                .and_then(serde_json::Value::as_str)
8667            {
8668                Some("string") => Some(serde_json::json!(match kind {
8669                    ToolErrorKind::InputValidation => "input-validation-error",
8670                    ToolErrorKind::Handler => "handler-error",
8671                })),
8672                Some("boolean") => Some(serde_json::json!(matches!(kind, ToolErrorKind::Handler))),
8673                Some("null") => Some(serde_json::Value::Null),
8674                Some("object") => Some(serde_json::json!({
8675                    "error": match kind {
8676                        ToolErrorKind::InputValidation => "input-validation",
8677                        ToolErrorKind::Handler => "handler",
8678                    }
8679                })),
8680                _ => None,
8681            }
8682        }
8683
8684        fn call_final_outcome(
8685            &self,
8686            _ctx: &McpContext,
8687            _args: serde_json::Value,
8688        ) -> McpResult<FinalToolOutcome> {
8689            self.final_calls.fetch_add(1, Ordering::SeqCst);
8690            Ok(FinalToolOutcome::Complete(final_tool_complete_result(
8691                FinalCallToolResult {
8692                    content: vec![ContentBlock::text(self.legacy_label)],
8693                    is_error: false,
8694                    structured_content: self.structured_content.clone(),
8695                },
8696            )))
8697        }
8698    }
8699
8700    #[derive(Clone, Copy)]
8701    enum ErrorMapperMode {
8702        Complete,
8703        MissingHandler,
8704        InvalidHandler,
8705        OversizedHandler,
8706    }
8707
8708    struct ErrorMappedTool {
8709        mode: ErrorMapperMode,
8710        calls: Arc<AtomicUsize>,
8711    }
8712
8713    impl ToolHandler for ErrorMappedTool {
8714        fn definition(&self) -> Tool {
8715            Tool {
8716                name: "error-mapped-tool".to_owned(),
8717                description: None,
8718                input_schema: serde_json::json!({
8719                    "type": "object",
8720                    "required": ["value"],
8721                    "properties": {"value": {"type": "string"}},
8722                    "additionalProperties": false
8723                }),
8724                output_schema: Some(serde_json::json!({
8725                    "type": "object",
8726                    "required": ["error"],
8727                    "properties": {"error": {"type": "string"}},
8728                    "additionalProperties": false
8729                })),
8730                icon: None,
8731                version: None,
8732                tags: Vec::new(),
8733                annotations: None,
8734            }
8735        }
8736
8737        fn final_tool_error_structured_content(
8738            &self,
8739            kind: ToolErrorKind,
8740        ) -> Option<serde_json::Value> {
8741            match (self.mode, kind) {
8742                (ErrorMapperMode::MissingHandler, ToolErrorKind::Handler) => None,
8743                (ErrorMapperMode::InvalidHandler, ToolErrorKind::Handler) => {
8744                    Some(serde_json::json!({"error": 7}))
8745                }
8746                (ErrorMapperMode::OversizedHandler, ToolErrorKind::Handler) => Some(
8747                    serde_json::json!({"error": "x".repeat(MAX_FINAL_TOOL_ERROR_STRUCTURED_CONTENT_BYTES)}),
8748                ),
8749                (_, ToolErrorKind::InputValidation) => {
8750                    Some(serde_json::json!({"error": "input-validation"}))
8751                }
8752                (_, ToolErrorKind::Handler) => Some(serde_json::json!({"error": "handler"})),
8753            }
8754        }
8755
8756        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
8757            self.calls.fetch_add(1, Ordering::SeqCst);
8758            Err(McpError::new(
8759                McpErrorCode::ToolExecutionError,
8760                "mapped handler failure",
8761            ))
8762        }
8763    }
8764
8765    fn final_tools_call_request(
8766        name: &str,
8767        arguments: serde_json::Value,
8768        id: i64,
8769    ) -> JsonRpcRequest {
8770        JsonRpcRequest::new(
8771            "tools/call",
8772            Some(serde_json::json!({
8773                "_meta": {
8774                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8775                    "io.modelcontextprotocol/clientCapabilities": {},
8776                },
8777                "name": name,
8778                "arguments": arguments,
8779            })),
8780            id,
8781        )
8782    }
8783
8784    fn admit_http_wire(
8785        method: &str,
8786        target: &str,
8787        body: &[u8],
8788    ) -> (JsonRpcRequest, Option<Arc<str>>) {
8789        let endpoint = HttpEndpointConfig::new(
8790            "/mcp",
8791            HttpAdmissionLimits::new(16, 8_192, 65_536).expect("nonzero HTTP limits"),
8792        )
8793        .expect("HTTP endpoint config");
8794        let headers = vec![
8795            ("Content-Type".to_owned(), "application/json".to_owned()),
8796            ("Accept".to_owned(), "application/json".to_owned()),
8797            ("MCP-Protocol-Version".to_owned(), "2026-07-28".to_owned()),
8798            ("Mcp-Method".to_owned(), method.to_owned()),
8799            ("Mcp-Name".to_owned(), target.to_owned()),
8800        ];
8801        admit_modern_post(&endpoint, "POST", "/mcp", &headers, body)
8802            .expect("wire request admits through HTTP")
8803            .into_request_and_raw_params()
8804    }
8805
8806    #[test]
8807    fn final_router_progress_admits_one_smaller_total_without_replacing_an_outer_runtime() {
8808        let mut router = Router::new();
8809        router
8810            .add_tool(RouterProgressTool)
8811            .expect("router progress tool registers for both eras");
8812        let cx = Cx::for_testing();
8813        let state = SessionState::new();
8814        let request_ctx = request_context(&cx, 177, Budget::INFINITE, &state);
8815        let sent = Arc::new(Mutex::new(Vec::new()));
8816        let sent_clone = Arc::clone(&sent);
8817        let notification_sender: NotificationSender = Arc::new(move |notification| {
8818            sent_clone
8819                .lock()
8820                .expect("notification collection is not poisoned")
8821                .push(notification);
8822        });
8823        let baseline = serde_json::json!({
8824            "_meta": {
8825                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8826                "io.modelcontextprotocol/clientCapabilities": {},
8827                "progressToken": "router-final-progress",
8828            },
8829            "name": "router-progress-tool",
8830            "arguments": {"total": 12000.0},
8831        });
8832        let mut planted = baseline.clone();
8833        planted["arguments"]["total"] = serde_json::json!(11999.0);
8834        assert_eq!(
8835            baseline["_meta"], planted["_meta"],
8836            "the progress total is the sole planted request dimension"
8837        );
8838        assert_eq!(baseline["name"], planted["name"]);
8839
8840        let baseline: FinalCallToolParams =
8841            serde_json::from_value(baseline).expect("baseline final request is valid");
8842        let outcome = block_on(router.handle_tools_call_final_in_request(
8843            &request_ctx,
8844            request_ctx.cx(),
8845            baseline,
8846            state.clone(),
8847            Some(&notification_sender),
8848            None,
8849            None,
8850        ))
8851        .expect("ordinary final router dispatch completes");
8852        assert!(matches!(outcome, FinalToolOutcome::Complete(_)));
8853        let notification = sent
8854            .lock()
8855            .expect("notification collection is not poisoned")[0]
8856            .clone();
8857        let wire = serde_json::to_string(
8858            notification
8859                .params
8860                .as_ref()
8861                .expect("ordinary final progress has parameters"),
8862        )
8863        .expect("ordinary final progress parameters serialize");
8864        assert!(wire.contains("\"progress\":12000"));
8865        assert!(wire.contains("\"total\":12000"));
8866
8867        let planted: FinalCallToolParams =
8868            serde_json::from_value(planted).expect("one-variable planted request is valid");
8869        let outcome = block_on(router.handle_tools_call_final_in_request(
8870            &request_ctx,
8871            request_ctx.cx(),
8872            planted,
8873            state,
8874            Some(&notification_sender),
8875            None,
8876            None,
8877        ))
8878        .expect("the handler result remains valid when only total is smaller");
8879        assert!(matches!(outcome, FinalToolOutcome::Complete(_)));
8880        assert_eq!(
8881            sent.lock()
8882                .expect("notification collection is not poisoned")
8883                .len(),
8884            2,
8885            "a smaller total is not a final-progress violation"
8886        );
8887        let wire = serde_json::to_string(
8888            sent.lock()
8889                .expect("notification collection is not poisoned")[1]
8890                .params
8891                .as_ref()
8892                .expect("second final progress has parameters"),
8893        )
8894        .expect("second final progress parameters serialize");
8895        assert!(wire.contains("\"progress\":12000"));
8896        assert!(wire.contains("\"total\":11999"));
8897    }
8898
8899    #[test]
8900    fn final_router_preserves_an_outer_final_progress_runtime() {
8901        let mut router = Router::new();
8902        router
8903            .add_tool(RouterProgressTool)
8904            .expect("router progress tool registers for both eras");
8905        let cx = Cx::for_testing();
8906        let state = SessionState::new();
8907        let sent = Arc::new(Mutex::new(Vec::new()));
8908        let sent_clone = Arc::clone(&sent);
8909        let outer_runtime = Arc::new(crate::handler::FinalProgressRuntime::new(
8910            ProgressMarker::from("outer-owned-marker"),
8911            move |notification| {
8912                sent_clone
8913                    .lock()
8914                    .expect("notification collection is not poisoned")
8915                    .push(notification);
8916            },
8917        ));
8918        let request_ctx =
8919            McpContext::with_progress(cx, 178, Arc::clone(&outer_runtime).into_reporter());
8920        let params: FinalCallToolParams = serde_json::from_value(serde_json::json!({
8921            "_meta": {
8922                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8923                "io.modelcontextprotocol/clientCapabilities": {},
8924                "progressToken": "handler-supplied-marker",
8925            },
8926            "name": "router-progress-tool",
8927            "arguments": {"total": 11999.0},
8928        }))
8929        .expect("final tool parameters are valid");
8930        let notification_sender: NotificationSender =
8931            Arc::new(|_| panic!("router must not replace an installed final-progress runtime"));
8932
8933        let outcome = block_on(router.handle_tools_call_final_in_request(
8934            &request_ctx,
8935            request_ctx.cx(),
8936            params,
8937            state,
8938            Some(&notification_sender),
8939            None,
8940            None,
8941        ))
8942        .expect("outer final progress runtime remains usable");
8943        assert!(matches!(outcome, FinalToolOutcome::Complete(_)));
8944        assert!(outer_runtime.flush_pending());
8945
8946        let sent = sent
8947            .lock()
8948            .expect("notification collection is not poisoned");
8949        assert_eq!(sent.len(), 1);
8950        let wire = serde_json::to_string(
8951            sent[0]
8952                .params
8953                .as_ref()
8954                .expect("outer runtime notification has parameters"),
8955        )
8956        .expect("outer runtime parameters serialize");
8957        assert!(wire.contains("\"progressToken\":\"outer-owned-marker\""));
8958        assert!(wire.contains("\"progress\":12000"));
8959        assert!(wire.contains("\"total\":11999"));
8960    }
8961
8962    #[cfg(feature = "tasks")]
8963    struct TaskCapableRouterTool {
8964        final_calls: Arc<AtomicUsize>,
8965    }
8966
8967    #[cfg(feature = "tasks")]
8968    impl ToolHandler for TaskCapableRouterTool {
8969        fn definition(&self) -> Tool {
8970            Tool {
8971                name: "task-capable-router-tool".to_owned(),
8972                description: None,
8973                input_schema: serde_json::json!({
8974                    "$schema": "https://json-schema.org/draft/2020-12/schema",
8975                    "type": "object",
8976                    "unevaluatedProperties": false,
8977                }),
8978                output_schema: None,
8979                icon: None,
8980                version: None,
8981                tags: Vec::new(),
8982                annotations: None,
8983            }
8984        }
8985
8986        fn declares_final_tasks(&self) -> bool {
8987            true
8988        }
8989
8990        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
8991            Ok(vec![Content::text("legacy task-capable router result")])
8992        }
8993
8994        fn call_final_outcome(
8995            &self,
8996            _ctx: &McpContext,
8997            _args: serde_json::Value,
8998        ) -> McpResult<FinalToolOutcome> {
8999            self.final_calls.fetch_add(1, Ordering::SeqCst);
9000            Ok(FinalToolOutcome::CreateTask {
9001                work_descriptor: FinalTaskWorkDescriptor::new(serde_json::json!({
9002                    "operation": "task-capable-router-tool"
9003                }))?,
9004                status_message: Some("router task created".to_owned()),
9005            })
9006        }
9007    }
9008
9009    /// A single declared task-capable handler whose result branch is selected
9010    /// only by the `createTask` argument. It proves registration is not itself
9011    /// a Tasks operation.
9012    #[cfg(feature = "tasks")]
9013    struct ConditionalTaskCapableRouterTool {
9014        final_calls: Arc<AtomicUsize>,
9015    }
9016
9017    #[cfg(feature = "tasks")]
9018    impl ToolHandler for ConditionalTaskCapableRouterTool {
9019        fn definition(&self) -> Tool {
9020            Tool {
9021                name: "conditional-task-capable-router-tool".to_owned(),
9022                description: None,
9023                input_schema: serde_json::json!({
9024                    "$schema": "https://json-schema.org/draft/2020-12/schema",
9025                    "type": "object",
9026                    "properties": {"createTask": {"type": "boolean"}},
9027                    "required": ["createTask"],
9028                    "unevaluatedProperties": false,
9029                }),
9030                output_schema: None,
9031                icon: None,
9032                version: None,
9033                tags: Vec::new(),
9034                annotations: None,
9035            }
9036        }
9037
9038        fn declares_final_tasks(&self) -> bool {
9039            true
9040        }
9041
9042        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
9043            Ok(vec![Content::text(
9044                "legacy conditional task-capable router result",
9045            )])
9046        }
9047
9048        fn call_final_outcome(
9049            &self,
9050            _ctx: &McpContext,
9051            args: serde_json::Value,
9052        ) -> McpResult<FinalToolOutcome> {
9053            self.final_calls.fetch_add(1, Ordering::SeqCst);
9054            if args
9055                .get("createTask")
9056                .and_then(serde_json::Value::as_bool)
9057                .unwrap_or(false)
9058            {
9059                return Ok(FinalToolOutcome::CreateTask {
9060                    work_descriptor: FinalTaskWorkDescriptor::new(serde_json::json!({
9061                        "operation": "conditional-task-capable-router-tool"
9062                    }))?,
9063                    status_message: None,
9064                });
9065            }
9066            Ok(FinalToolOutcome::Complete(final_tool_complete_result(
9067                FinalCallToolResult {
9068                    content: vec![ContentBlock::text("ordinary final result")],
9069                    is_error: false,
9070                    structured_content: None,
9071                },
9072            )))
9073        }
9074    }
9075
9076    /// Simulates a handler that overrides the request-owned hook and bypasses
9077    /// the trait's ordinary declaration guard. The router must still prevent
9078    /// its undeclared task outcome from reaching task creation.
9079    #[cfg(feature = "tasks")]
9080    struct UndeclaredTaskOutcomeRouterTool {
9081        final_calls: Arc<AtomicUsize>,
9082    }
9083
9084    #[cfg(feature = "tasks")]
9085    impl ToolHandler for UndeclaredTaskOutcomeRouterTool {
9086        fn definition(&self) -> Tool {
9087            Tool {
9088                name: "undeclared-task-outcome-router-tool".to_owned(),
9089                description: None,
9090                input_schema: serde_json::json!({
9091                    "$schema": "https://json-schema.org/draft/2020-12/schema",
9092                    "type": "object",
9093                    "unevaluatedProperties": false,
9094                }),
9095                output_schema: None,
9096                icon: None,
9097                version: None,
9098                tags: Vec::new(),
9099                annotations: None,
9100            }
9101        }
9102
9103        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
9104            Ok(vec![Content::text("legacy undeclared-task outcome")])
9105        }
9106
9107        fn call_final_outcome_async_in_request<'a>(
9108            &'a self,
9109            _ctx: &'a McpContext,
9110            _request_cx: &'a Cx,
9111            _args: serde_json::Value,
9112        ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
9113            self.final_calls.fetch_add(1, Ordering::SeqCst);
9114            let work_descriptor = FinalTaskWorkDescriptor::new(serde_json::json!({
9115                "operation": "undeclared-task-outcome-router-tool"
9116            }));
9117            Box::pin(async move {
9118                match work_descriptor {
9119                    Ok(work_descriptor) => Outcome::Ok(FinalToolOutcome::CreateTask {
9120                        work_descriptor,
9121                        status_message: None,
9122                    }),
9123                    Err(error) => Outcome::Err(error),
9124                }
9125            })
9126        }
9127    }
9128
9129    #[cfg(feature = "tasks")]
9130    struct NoopFinalTaskSupervisor;
9131
9132    #[cfg(feature = "tasks")]
9133    impl ApplicationTaskSupervisor for NoopFinalTaskSupervisor {
9134        fn resume<'a>(
9135            &'a self,
9136            _cx: &'a Cx,
9137            _handoff: FinalTaskSupervisorHandoff,
9138        ) -> FinalTaskSupervisorFuture<'a> {
9139            Box::pin(async { Ok(()) })
9140        }
9141    }
9142
9143    #[cfg(feature = "tasks")]
9144    fn task_runtime_for_router(store: Arc<InMemoryFinalTaskStore>) -> FinalTaskRuntime {
9145        let store: Arc<dyn FinalTaskStore> = store;
9146        FinalTaskRuntime::new(
9147            store,
9148            FinalTaskRuntimeConfig::new(60_000, Some(5_000))
9149                .expect("a finite final Task policy is valid"),
9150            Arc::new(|_notification| {}),
9151        )
9152    }
9153
9154    #[cfg(feature = "tasks")]
9155    fn final_task_capable_tool_request(id: i64) -> JsonRpcRequest {
9156        JsonRpcRequest::new(
9157            "tools/call",
9158            Some(serde_json::json!({
9159                "_meta": {
9160                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
9161                    "io.modelcontextprotocol/clientCapabilities": {
9162                        "extensions": {
9163                            "io.modelcontextprotocol/tasks": {}
9164                        }
9165                    },
9166                },
9167                "name": "task-capable-router-tool",
9168                "arguments": {},
9169            })),
9170            id,
9171        )
9172    }
9173
9174    struct FinalCatalogTool {
9175        metadata: OpenMetadata,
9176        icons: Vec<RawIcon>,
9177    }
9178
9179    impl ToolHandler for FinalCatalogTool {
9180        fn definition(&self) -> Tool {
9181            Tool {
9182                name: "final-catalog-tool".to_owned(),
9183                description: Some("final catalog description".to_owned()),
9184                input_schema: serde_json::json!({"type": "object"}),
9185                output_schema: Some(serde_json::json!({"type": "object"})),
9186                icon: Some(fastmcp_protocol::Icon::new("https://legacy.test/icon.png")),
9187                version: Some("legacy-version".to_owned()),
9188                tags: vec!["legacy-tag".to_owned()],
9189                annotations: None,
9190            }
9191        }
9192
9193        fn final_title(&self) -> Option<&str> {
9194            Some("Final Catalog Tool")
9195        }
9196
9197        fn final_icons(&self) -> Option<&[RawIcon]> {
9198            Some(&self.icons)
9199        }
9200
9201        fn final_metadata(&self) -> Option<&OpenMetadata> {
9202            Some(&self.metadata)
9203        }
9204
9205        fn final_definition(&self) -> Option<FinalTool> {
9206            Some(FinalTool {
9207                name: "final-catalog-tool".to_owned(),
9208                title: Some("Exact Final Catalog Tool".to_owned()),
9209                description: Some("exact final catalog description".to_owned()),
9210                input_schema: serde_json::json!({"type": "object"}),
9211                output_schema: Some(serde_json::json!({"type": "object"})),
9212                annotations: Some(FinalToolAnnotations {
9213                    title: Some("Exact annotation title".to_owned()),
9214                    destructive: Some(false),
9215                    idempotent: Some(true),
9216                    read_only: Some(true),
9217                    open_world_hint: Some(false),
9218                }),
9219                icons: Some(self.icons.clone()),
9220                meta: Some(self.metadata.clone()),
9221            })
9222        }
9223
9224        fn final_tool_error_structured_content(
9225            &self,
9226            kind: ToolErrorKind,
9227        ) -> Option<serde_json::Value> {
9228            Some(serde_json::json!({
9229                "error": match kind {
9230                    ToolErrorKind::InputValidation => "input-validation",
9231                    ToolErrorKind::Handler => "handler",
9232                }
9233            }))
9234        }
9235
9236        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
9237            Ok(vec![Content::text("legacy final catalog result")])
9238        }
9239    }
9240
9241    struct FinalCatalogResource {
9242        metadata: OpenMetadata,
9243        icons: Vec<RawIcon>,
9244        annotations: Annotations,
9245    }
9246
9247    impl ResourceHandler for FinalCatalogResource {
9248        fn definition(&self) -> Resource {
9249            Resource {
9250                uri: "file:///final-catalog-resource".to_owned(),
9251                name: "final-catalog-resource".to_owned(),
9252                description: Some("final resource description".to_owned()),
9253                mime_type: Some("text/plain".to_owned()),
9254                icon: None,
9255                version: None,
9256                tags: Vec::new(),
9257            }
9258        }
9259
9260        fn final_title(&self) -> Option<&str> {
9261            Some("Final Catalog Resource")
9262        }
9263
9264        fn final_icons(&self) -> Option<&[RawIcon]> {
9265            Some(&self.icons)
9266        }
9267
9268        fn final_annotations(&self) -> Option<&Annotations> {
9269            Some(&self.annotations)
9270        }
9271
9272        fn final_metadata(&self) -> Option<&OpenMetadata> {
9273            Some(&self.metadata)
9274        }
9275
9276        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
9277            Ok(Vec::new())
9278        }
9279    }
9280
9281    struct FinalCatalogResourceTemplate {
9282        metadata: OpenMetadata,
9283        icons: Vec<RawIcon>,
9284        annotations: Annotations,
9285    }
9286
9287    impl ResourceHandler for FinalCatalogResourceTemplate {
9288        fn definition(&self) -> Resource {
9289            Resource {
9290                uri: "template://placeholder".to_owned(),
9291                name: "final-catalog-template".to_owned(),
9292                description: None,
9293                mime_type: None,
9294                icon: None,
9295                version: None,
9296                tags: Vec::new(),
9297            }
9298        }
9299
9300        fn template(&self) -> Option<ResourceTemplate> {
9301            Some(ResourceTemplate {
9302                uri_template: "template://{id}".to_owned(),
9303                name: "final-catalog-template".to_owned(),
9304                description: Some("final template description".to_owned()),
9305                mime_type: Some("application/json".to_owned()),
9306                icon: None,
9307                version: None,
9308                tags: Vec::new(),
9309            })
9310        }
9311
9312        fn final_template_title(&self) -> Option<&str> {
9313            Some("Final Catalog Template")
9314        }
9315
9316        fn final_template_icons(&self) -> Option<&[RawIcon]> {
9317            Some(&self.icons)
9318        }
9319
9320        fn final_template_annotations(&self) -> Option<&Annotations> {
9321            Some(&self.annotations)
9322        }
9323
9324        fn final_template_metadata(&self) -> Option<&OpenMetadata> {
9325            Some(&self.metadata)
9326        }
9327
9328        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
9329            Ok(Vec::new())
9330        }
9331    }
9332
9333    struct FinalCatalogPrompt {
9334        metadata: OpenMetadata,
9335        icons: Vec<RawIcon>,
9336    }
9337
9338    impl PromptHandler for FinalCatalogPrompt {
9339        fn definition(&self) -> Prompt {
9340            Prompt {
9341                name: "final-catalog-prompt".to_owned(),
9342                description: Some("final prompt description".to_owned()),
9343                arguments: vec![PromptArgument {
9344                    name: "optional-argument".to_owned(),
9345                    description: Some("must remain explicitly false".to_owned()),
9346                    required: false,
9347                }],
9348                icon: None,
9349                version: None,
9350                tags: Vec::new(),
9351            }
9352        }
9353
9354        fn final_title(&self) -> Option<&str> {
9355            Some("Final Catalog Prompt")
9356        }
9357
9358        fn final_icons(&self) -> Option<&[RawIcon]> {
9359            Some(&self.icons)
9360        }
9361
9362        fn final_metadata(&self) -> Option<&OpenMetadata> {
9363            Some(&self.metadata)
9364        }
9365
9366        fn get(
9367            &self,
9368            _ctx: &McpContext,
9369            _args: std::collections::HashMap<String, String>,
9370        ) -> McpResult<Vec<PromptMessage>> {
9371            Ok(Vec::new())
9372        }
9373    }
9374
9375    struct DirectFinalPrompt {
9376        final_calls: Arc<AtomicUsize>,
9377    }
9378
9379    impl PromptHandler for DirectFinalPrompt {
9380        fn definition(&self) -> Prompt {
9381            Prompt {
9382                name: "direct-final-prompt".to_owned(),
9383                description: Some("legacy prompt definition".to_owned()),
9384                arguments: vec![],
9385                icon: None,
9386                version: None,
9387                tags: Vec::new(),
9388            }
9389        }
9390
9391        fn get(
9392            &self,
9393            _ctx: &McpContext,
9394            _args: std::collections::HashMap<String, String>,
9395        ) -> McpResult<Vec<PromptMessage>> {
9396            Err(McpError::internal_error(
9397                "legacy prompt projection must not service a final request",
9398            ))
9399        }
9400
9401        fn get_final(
9402            &self,
9403            _ctx: &McpContext,
9404            _args: std::collections::HashMap<String, String>,
9405        ) -> McpResult<CompleteResult<FinalGetPromptResult>> {
9406            self.final_calls.fetch_add(1, Ordering::SeqCst);
9407            let content_meta = OpenMetadata::try_from_entries([(
9408                "com.example/direct-prompt".to_owned(),
9409                serde_json::json!({"source": "final-handler"}),
9410            )])
9411            .expect("direct prompt content metadata is valid");
9412            Ok(CompleteResult::new(
9413                FinalGetPromptResult {
9414                    description: Some("direct final prompt description".to_owned()),
9415                    messages: vec![FinalPromptMessage {
9416                        role: fastmcp_protocol::Role::Assistant,
9417                        content: ContentBlock::Audio {
9418                            data: "aGVsbG8=".to_owned(),
9419                            mime_type: "audio/mpeg".to_owned(),
9420                            annotations: None,
9421                            meta: Some(content_meta),
9422                            additional: BTreeMap::from([(
9423                                "com.example/direct-field".to_owned(),
9424                                serde_json::json!(true),
9425                            )]),
9426                        },
9427                    }],
9428                },
9429                empty_final_result_meta()?,
9430            ))
9431        }
9432    }
9433
9434    struct PromptArgumentBoundary {
9435        final_calls: Arc<AtomicUsize>,
9436        legacy_calls: Arc<AtomicUsize>,
9437    }
9438
9439    impl PromptHandler for PromptArgumentBoundary {
9440        fn definition(&self) -> Prompt {
9441            Prompt {
9442                name: "prompt-argument-boundary".to_owned(),
9443                description: None,
9444                arguments: vec![PromptArgument {
9445                    name: "topic".to_owned(),
9446                    description: Some("required modern prompt topic".to_owned()),
9447                    required: true,
9448                }],
9449                icon: None,
9450                version: None,
9451                tags: Vec::new(),
9452            }
9453        }
9454
9455        fn get(
9456            &self,
9457            _ctx: &McpContext,
9458            _args: std::collections::HashMap<String, String>,
9459        ) -> McpResult<Vec<PromptMessage>> {
9460            self.legacy_calls.fetch_add(1, Ordering::SeqCst);
9461            Ok(vec![PromptMessage {
9462                role: fastmcp_protocol::Role::Assistant,
9463                content: Content::text("legacy prompt-argument-boundary result"),
9464            }])
9465        }
9466
9467        fn get_final(
9468            &self,
9469            _ctx: &McpContext,
9470            _args: std::collections::HashMap<String, String>,
9471        ) -> McpResult<CompleteResult<FinalGetPromptResult>> {
9472            self.final_calls.fetch_add(1, Ordering::SeqCst);
9473            Ok(CompleteResult::new(
9474                FinalGetPromptResult {
9475                    description: None,
9476                    messages: vec![FinalPromptMessage {
9477                        role: fastmcp_protocol::Role::Assistant,
9478                        content: ContentBlock::text("final prompt-argument-boundary result"),
9479                    }],
9480                },
9481                empty_final_result_meta()?,
9482            ))
9483        }
9484    }
9485
9486    /// Deliberately changes its legacy definition after registration. Modern
9487    /// prompt validation must use the admission snapshot instead.
9488    struct MutablePromptDefinition {
9489        expose_admitted_argument: Arc<AtomicBool>,
9490        final_calls: Arc<AtomicUsize>,
9491    }
9492
9493    impl PromptHandler for MutablePromptDefinition {
9494        fn definition(&self) -> Prompt {
9495            let argument = if self.expose_admitted_argument.load(Ordering::SeqCst) {
9496                PromptArgument {
9497                    name: "topic".to_owned(),
9498                    description: Some("admitted required argument".to_owned()),
9499                    required: true,
9500                }
9501            } else {
9502                PromptArgument {
9503                    name: "mutated".to_owned(),
9504                    description: Some("must not affect final validation".to_owned()),
9505                    required: false,
9506                }
9507            };
9508            Prompt {
9509                name: "mutable-prompt-definition".to_owned(),
9510                description: None,
9511                arguments: vec![argument],
9512                icon: None,
9513                version: None,
9514                tags: Vec::new(),
9515            }
9516        }
9517
9518        fn get(
9519            &self,
9520            _ctx: &McpContext,
9521            _args: std::collections::HashMap<String, String>,
9522        ) -> McpResult<Vec<PromptMessage>> {
9523            Ok(Vec::new())
9524        }
9525
9526        fn get_final(
9527            &self,
9528            _ctx: &McpContext,
9529            _args: std::collections::HashMap<String, String>,
9530        ) -> McpResult<CompleteResult<FinalGetPromptResult>> {
9531            self.final_calls.fetch_add(1, Ordering::SeqCst);
9532            Ok(CompleteResult::new(
9533                FinalGetPromptResult {
9534                    description: None,
9535                    messages: Vec::new(),
9536                },
9537                empty_final_result_meta()?,
9538            ))
9539        }
9540    }
9541
9542    fn input_required_result(forged_request_state: &str) -> InputRequiredResult {
9543        let encoded = serde_json::json!({
9544            "resultType": "input_required",
9545            "inputRequests": {"roots": {"method": "roots/list"}},
9546            "requestState": forged_request_state,
9547        })
9548        .to_string();
9549        let (decoded, diagnostic) = decode_peer_result(
9550            &encoded,
9551            ResultPeerEra::Modern,
9552            &CoreResultDiscriminatorPolicy,
9553        )
9554        .expect("test input-required result decodes");
9555        assert!(diagnostic.is_none());
9556        let DecodedResult::InputRequired(result) = decoded else {
9557            panic!("test result is input_required");
9558        };
9559        result
9560    }
9561
9562    fn sampling_input_required_result(forged_request_state: &str) -> InputRequiredResult {
9563        let encoded = serde_json::json!({
9564            "resultType": "input_required",
9565            "inputRequests": {
9566                "sample": {
9567                    "method": "sampling/createMessage",
9568                    "params": {
9569                        "messages": [{
9570                            "role": "assistant",
9571                            "content": {
9572                                "type": "tool_use",
9573                                "id": "weather-1",
9574                                "name": "weather",
9575                                "input": {"city": "Boston"},
9576                            },
9577                        }],
9578                        "maxTokens": 16,
9579                        "tools": [{"name": "weather", "inputSchema": {"type": "object"}}],
9580                        "toolChoice": {"mode": "required"},
9581                    },
9582                },
9583            },
9584            "requestState": forged_request_state,
9585        })
9586        .to_string();
9587        let (decoded, diagnostic) = decode_peer_result(
9588            &encoded,
9589            ResultPeerEra::Modern,
9590            &CoreResultDiscriminatorPolicy,
9591        )
9592        .expect("final sampling input-required result decodes");
9593        assert!(diagnostic.is_none());
9594        let DecodedResult::InputRequired(result) = decoded else {
9595            panic!("test result is sampling input_required");
9596        };
9597        result
9598    }
9599
9600    fn state_only_input_required_result(forged_request_state: &str) -> InputRequiredResult {
9601        let encoded = serde_json::json!({
9602            "resultType": "input_required",
9603            "requestState": forged_request_state,
9604        })
9605        .to_string();
9606        let (decoded, diagnostic) = decode_peer_result(
9607            &encoded,
9608            ResultPeerEra::Modern,
9609            &CoreResultDiscriminatorPolicy,
9610        )
9611        .expect("test state-only input-required result decodes");
9612        assert!(diagnostic.is_none());
9613        let DecodedResult::InputRequired(result) = decoded else {
9614            panic!("test result is state-only input_required");
9615        };
9616        assert!(result.input_requests().is_none());
9617        result
9618    }
9619
9620    fn router_roots_response_wire() -> serde_json::Value {
9621        serde_json::to_value(
9622            MrtrInputResponse::roots(fastmcp_protocol::ListRootsResult::empty())
9623                .expect("roots response serializes"),
9624        )
9625        .expect("roots response converts to a wire value")
9626    }
9627
9628    struct InputRequiredTool {
9629        legacy_calls: Arc<AtomicUsize>,
9630        final_calls: Arc<AtomicUsize>,
9631    }
9632
9633    struct StateOnlyInputRequiredTool {
9634        initial_calls: Arc<AtomicUsize>,
9635        resumed_calls: Arc<AtomicUsize>,
9636    }
9637
9638    impl ToolHandler for StateOnlyInputRequiredTool {
9639        fn definition(&self) -> Tool {
9640            Tool {
9641                name: "state-only-input-required-tool".to_owned(),
9642                description: None,
9643                input_schema: serde_json::json!({"type": "object"}),
9644                output_schema: None,
9645                icon: None,
9646                version: None,
9647                tags: Vec::new(),
9648                annotations: None,
9649            }
9650        }
9651
9652        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
9653            Ok(vec![Content::text(
9654                "legacy state-only input-required result",
9655            )])
9656        }
9657
9658        fn declares_final_mrtr(&self) -> bool {
9659            true
9660        }
9661
9662        fn call_final_outcome(
9663            &self,
9664            _ctx: &McpContext,
9665            _args: serde_json::Value,
9666        ) -> McpResult<FinalToolOutcome> {
9667            self.initial_calls.fetch_add(1, Ordering::SeqCst);
9668            Ok(FinalToolOutcome::InputRequired(
9669                state_only_input_required_result("handler-forged-state"),
9670            ))
9671        }
9672
9673        fn call_final_outcome_async_resuming_in_request<'a>(
9674            &'a self,
9675            ctx: &'a McpContext,
9676            _request_cx: &'a Cx,
9677            arguments: serde_json::Value,
9678            resume_inputs: Option<&'a MrtrCompletedInputs>,
9679        ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
9680            Box::pin(async move {
9681                // None marks the initial invocation under the unified
9682                // resuming hook; the retry must carry admitted inputs.
9683                let Some(resume_inputs) = resume_inputs else {
9684                    return match self.call_final_outcome(ctx, arguments) {
9685                        Ok(result) => Outcome::Ok(result),
9686                        Err(error) => Outcome::Err(error),
9687                    };
9688                };
9689                if !resume_inputs.responses().is_empty() {
9690                    return Outcome::Err(McpError::internal_error(
9691                        "state-only MRTR resume unexpectedly carried inputs",
9692                    ));
9693                }
9694                self.resumed_calls.fetch_add(1, Ordering::SeqCst);
9695                Outcome::Ok(FinalToolOutcome::Complete(final_tool_complete_result(
9696                    FinalCallToolResult {
9697                        content: vec![ContentBlock::text("state-only resumed")],
9698                        is_error: false,
9699                        structured_content: None,
9700                    },
9701                )))
9702            })
9703        }
9704    }
9705
9706    impl ToolHandler for InputRequiredTool {
9707        fn definition(&self) -> Tool {
9708            Tool {
9709                name: "input-required-tool".to_owned(),
9710                description: None,
9711                input_schema: serde_json::json!({"type": "object"}),
9712                output_schema: None,
9713                icon: None,
9714                version: None,
9715                tags: Vec::new(),
9716                annotations: None,
9717            }
9718        }
9719
9720        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
9721            self.legacy_calls.fetch_add(1, Ordering::SeqCst);
9722            Ok(vec![Content::text("legacy tool result")])
9723        }
9724
9725        fn declares_final_mrtr(&self) -> bool {
9726            true
9727        }
9728
9729        fn call_final_outcome(
9730            &self,
9731            _ctx: &McpContext,
9732            _args: serde_json::Value,
9733        ) -> McpResult<FinalToolOutcome> {
9734            self.final_calls.fetch_add(1, Ordering::SeqCst);
9735            Ok(FinalToolOutcome::InputRequired(input_required_result(
9736                "tool-retry-state",
9737            )))
9738        }
9739
9740        fn call_final_outcome_async_resuming_in_request<'a>(
9741            &'a self,
9742            ctx: &'a McpContext,
9743            _request_cx: &'a Cx,
9744            arguments: serde_json::Value,
9745            resume_inputs: Option<&'a MrtrCompletedInputs>,
9746        ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
9747            Box::pin(async move {
9748                // None marks the initial invocation under the unified
9749                // resuming hook; the retry must carry admitted inputs.
9750                let Some(resume_inputs) = resume_inputs else {
9751                    return match self.call_final_outcome(ctx, arguments) {
9752                        Ok(result) => Outcome::Ok(result),
9753                        Err(error) => Outcome::Err(error),
9754                    };
9755                };
9756                match resume_inputs.roots("roots") {
9757                    Ok(Some(_)) => match self.call_final_outcome(ctx, arguments) {
9758                        Ok(result) => Outcome::Ok(result),
9759                        Err(error) => Outcome::Err(error),
9760                    },
9761                    Ok(None) => Outcome::Err(McpError::internal_error("MRTR roots input was lost")),
9762                    Err(error) => Outcome::Err(error),
9763                }
9764            })
9765        }
9766    }
9767
9768    struct OneShotMrtrTool {
9769        calls: Arc<AtomicUsize>,
9770    }
9771
9772    struct ContextElicitationTool {
9773        initial_calls: Arc<AtomicUsize>,
9774        resumed_calls: Arc<AtomicUsize>,
9775    }
9776
9777    struct ContextSamplingTool {
9778        calls: Arc<AtomicUsize>,
9779    }
9780
9781    impl ToolHandler for ContextElicitationTool {
9782        fn definition(&self) -> Tool {
9783            Tool {
9784                name: "context-elicitation-tool".to_owned(),
9785                description: None,
9786                input_schema: serde_json::json!({"type": "object"}),
9787                output_schema: None,
9788                icon: None,
9789                version: None,
9790                tags: Vec::new(),
9791                annotations: None,
9792            }
9793        }
9794
9795        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
9796            Ok(vec![Content::text("legacy result")])
9797        }
9798
9799        fn declares_final_mrtr(&self) -> bool {
9800            true
9801        }
9802
9803        fn call_final_outcome(
9804            &self,
9805            ctx: &McpContext,
9806            _args: serde_json::Value,
9807        ) -> McpResult<FinalToolOutcome> {
9808            self.initial_calls.fetch_add(1, Ordering::SeqCst);
9809            let elicitation = ctx.final_elicitation_form(
9810                "approval",
9811                "Approve this operation",
9812                serde_json::json!({
9813                    "type": "object",
9814                    "properties": {"approved": {"type": "boolean"}},
9815                    "required": ["approved"],
9816                }),
9817            )?;
9818            Ok(FinalToolOutcome::InputRequired(
9819                elicitation.into_input_required()?,
9820            ))
9821        }
9822
9823        fn call_final_outcome_async_resuming_in_request<'a>(
9824            &'a self,
9825            ctx: &'a McpContext,
9826            _request_cx: &'a Cx,
9827            arguments: serde_json::Value,
9828            resume_inputs: Option<&'a MrtrCompletedInputs>,
9829        ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
9830            Box::pin(async move {
9831                let Some(resume_inputs) = resume_inputs else {
9832                    return match self.call_final_outcome(ctx, arguments) {
9833                        Ok(result) => Outcome::Ok(result),
9834                        Err(error) => Outcome::Err(error),
9835                    };
9836                };
9837                let response = match resume_inputs.elicitation("approval") {
9838                    Ok(Some(response)) => response,
9839                    Ok(None) => {
9840                        return Outcome::Err(McpError::internal_error(
9841                            "MRTR elicitation response was lost",
9842                        ));
9843                    }
9844                    Err(error) => return Outcome::Err(error),
9845                };
9846                if response.action != fastmcp_protocol::ElicitAction::Accept
9847                    || response
9848                        .content
9849                        .as_ref()
9850                        .and_then(|content| content.get("approved"))
9851                        != Some(&fastmcp_protocol::ElicitContentValue::Bool(true))
9852                {
9853                    return Outcome::Err(McpError::invalid_params(
9854                        "MRTR elicitation response did not approve the operation",
9855                    ));
9856                }
9857                self.resumed_calls.fetch_add(1, Ordering::SeqCst);
9858                Outcome::Ok(FinalToolOutcome::Complete(final_tool_complete_result(
9859                    FinalCallToolResult {
9860                        content: vec![ContentBlock::text("approved")],
9861                        is_error: false,
9862                        structured_content: None,
9863                    },
9864                )))
9865            })
9866        }
9867    }
9868
9869    impl ToolHandler for ContextSamplingTool {
9870        fn definition(&self) -> Tool {
9871            Tool {
9872                name: "context-sampling-tool".to_owned(),
9873                description: None,
9874                input_schema: serde_json::json!({"type": "object"}),
9875                output_schema: None,
9876                icon: None,
9877                version: None,
9878                tags: Vec::new(),
9879                annotations: None,
9880            }
9881        }
9882
9883        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
9884            Ok(vec![Content::text("legacy result")])
9885        }
9886
9887        fn declares_final_mrtr(&self) -> bool {
9888            true
9889        }
9890
9891        fn call_final_outcome(
9892            &self,
9893            _ctx: &McpContext,
9894            _args: serde_json::Value,
9895        ) -> McpResult<FinalToolOutcome> {
9896            self.calls.fetch_add(1, Ordering::SeqCst);
9897            Ok(FinalToolOutcome::InputRequired(
9898                sampling_input_required_result("handler-forged-state"),
9899            ))
9900        }
9901    }
9902
9903    impl ToolHandler for OneShotMrtrTool {
9904        fn definition(&self) -> Tool {
9905            Tool {
9906                name: "one-shot-mrtr-tool".to_owned(),
9907                description: None,
9908                input_schema: serde_json::json!({"type": "object"}),
9909                output_schema: None,
9910                icon: None,
9911                version: None,
9912                tags: Vec::new(),
9913                annotations: None,
9914            }
9915        }
9916
9917        fn declares_final_mrtr(&self) -> bool {
9918            true
9919        }
9920
9921        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
9922            Ok(vec![Content::text("one-shot legacy result")])
9923        }
9924
9925        fn call_final_outcome(
9926            &self,
9927            _ctx: &McpContext,
9928            _args: serde_json::Value,
9929        ) -> McpResult<FinalToolOutcome> {
9930            self.calls.fetch_add(1, Ordering::SeqCst);
9931            Ok(FinalToolOutcome::InputRequired(input_required_result(
9932                "one-shot-handler-state",
9933            )))
9934        }
9935
9936        fn call_final_outcome_async_resuming_in_request<'a>(
9937            &'a self,
9938            ctx: &'a McpContext,
9939            _request_cx: &'a Cx,
9940            arguments: serde_json::Value,
9941            resume_inputs: Option<&'a MrtrCompletedInputs>,
9942        ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
9943            Box::pin(async move {
9944                let Some(resume_inputs) = resume_inputs else {
9945                    return match self.call_final_outcome(ctx, arguments) {
9946                        Ok(result) => Outcome::Ok(result),
9947                        Err(error) => Outcome::Err(error),
9948                    };
9949                };
9950                match resume_inputs.roots("roots") {
9951                    Ok(Some(_)) => {
9952                        self.calls.fetch_add(1, Ordering::SeqCst);
9953                        Outcome::Ok(FinalToolOutcome::Complete(final_tool_complete_result(
9954                            FinalCallToolResult {
9955                                content: vec![ContentBlock::text("one-shot resumed")],
9956                                is_error: false,
9957                                structured_content: None,
9958                            },
9959                        )))
9960                    }
9961                    Ok(None) => Outcome::Err(McpError::internal_error("MRTR roots input was lost")),
9962                    Err(error) => Outcome::Err(error),
9963                }
9964            })
9965        }
9966    }
9967
9968    #[cfg(feature = "tasks")]
9969    struct TaskCapableInputRequiredTool {
9970        final_calls: Arc<AtomicUsize>,
9971    }
9972
9973    #[cfg(feature = "tasks")]
9974    impl ToolHandler for TaskCapableInputRequiredTool {
9975        fn definition(&self) -> Tool {
9976            Tool {
9977                name: "task-capable-input-required-tool".to_owned(),
9978                description: None,
9979                input_schema: serde_json::json!({"type": "object"}),
9980                output_schema: None,
9981                icon: None,
9982                version: None,
9983                tags: Vec::new(),
9984                annotations: None,
9985            }
9986        }
9987
9988        fn declares_final_tasks(&self) -> bool {
9989            true
9990        }
9991
9992        fn declares_final_mrtr(&self) -> bool {
9993            true
9994        }
9995
9996        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
9997            Ok(vec![Content::text(
9998                "legacy task-capable input-required result",
9999            )])
10000        }
10001
10002        fn call_final_outcome(
10003            &self,
10004            _ctx: &McpContext,
10005            _args: serde_json::Value,
10006        ) -> McpResult<FinalToolOutcome> {
10007            self.final_calls.fetch_add(1, Ordering::SeqCst);
10008            Ok(FinalToolOutcome::InputRequired(input_required_result(
10009                "task-capable-tool-retry-state",
10010            )))
10011        }
10012    }
10013
10014    struct InputRequiredResource {
10015        legacy_calls: Arc<AtomicUsize>,
10016        final_calls: Arc<AtomicUsize>,
10017    }
10018
10019    impl ResourceHandler for InputRequiredResource {
10020        fn definition(&self) -> Resource {
10021            Resource {
10022                uri: "file:///input-required-resource".to_owned(),
10023                name: "input-required-resource".to_owned(),
10024                description: None,
10025                mime_type: Some("text/plain".to_owned()),
10026                icon: None,
10027                version: None,
10028                tags: Vec::new(),
10029            }
10030        }
10031
10032        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10033            self.legacy_calls.fetch_add(1, Ordering::SeqCst);
10034            Ok(vec![ResourceContent {
10035                uri: "file:///input-required-resource".to_owned(),
10036                mime_type: Some("text/plain".to_owned()),
10037                text: Some("legacy resource result".to_owned()),
10038                blob: None,
10039            }])
10040        }
10041
10042        fn declares_final_mrtr(&self) -> bool {
10043            true
10044        }
10045
10046        fn read_final_outcome(
10047            &self,
10048            _ctx: &McpContext,
10049        ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
10050            self.final_calls.fetch_add(1, Ordering::SeqCst);
10051            Ok(FinalMethodOutcome::InputRequired(input_required_result(
10052                "resource-retry-state",
10053            )))
10054        }
10055
10056        fn read_final_outcome_async_with_uri_resuming_in_request<'a>(
10057            &'a self,
10058            ctx: &'a McpContext,
10059            _request_cx: &'a Cx,
10060            _uri: &'a str,
10061            _params: &'a UriParams,
10062            resume_inputs: Option<&'a MrtrCompletedInputs>,
10063        ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
10064            Box::pin(async move {
10065                let Some(resume_inputs) = resume_inputs else {
10066                    return Outcome::Err(McpError::internal_error("MRTR resume inputs were lost"));
10067                };
10068                match resume_inputs.roots("roots") {
10069                    Ok(Some(_)) => match self.read_final_outcome(ctx) {
10070                        Ok(result) => Outcome::Ok(result),
10071                        Err(error) => Outcome::Err(error),
10072                    },
10073                    Ok(None) => Outcome::Err(McpError::internal_error("MRTR roots input was lost")),
10074                    Err(error) => Outcome::Err(error),
10075                }
10076            })
10077        }
10078    }
10079
10080    struct InputRequiredPrompt {
10081        legacy_calls: Arc<AtomicUsize>,
10082        final_calls: Arc<AtomicUsize>,
10083    }
10084
10085    impl PromptHandler for InputRequiredPrompt {
10086        fn definition(&self) -> Prompt {
10087            Prompt {
10088                name: "input-required-prompt".to_owned(),
10089                description: None,
10090                arguments: Vec::new(),
10091                icon: None,
10092                version: None,
10093                tags: Vec::new(),
10094            }
10095        }
10096
10097        fn get(
10098            &self,
10099            _ctx: &McpContext,
10100            _args: std::collections::HashMap<String, String>,
10101        ) -> McpResult<Vec<PromptMessage>> {
10102            self.legacy_calls.fetch_add(1, Ordering::SeqCst);
10103            Ok(Vec::new())
10104        }
10105
10106        fn declares_final_mrtr(&self) -> bool {
10107            true
10108        }
10109
10110        fn get_final_outcome(
10111            &self,
10112            _ctx: &McpContext,
10113            _args: std::collections::HashMap<String, String>,
10114        ) -> McpResult<FinalMethodOutcome<FinalGetPromptResult>> {
10115            self.final_calls.fetch_add(1, Ordering::SeqCst);
10116            Ok(FinalMethodOutcome::InputRequired(input_required_result(
10117                "prompt-retry-state",
10118            )))
10119        }
10120
10121        fn get_final_outcome_async_resuming_in_request<'a>(
10122            &'a self,
10123            ctx: &'a McpContext,
10124            _request_cx: &'a Cx,
10125            arguments: std::collections::HashMap<String, String>,
10126            resume_inputs: Option<&'a MrtrCompletedInputs>,
10127        ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalGetPromptResult>>> {
10128            Box::pin(async move {
10129                let Some(resume_inputs) = resume_inputs else {
10130                    return Outcome::Err(McpError::internal_error("MRTR resume inputs were lost"));
10131                };
10132                match resume_inputs.roots("roots") {
10133                    Ok(Some(_)) => match self.get_final_outcome(ctx, arguments) {
10134                        Ok(result) => Outcome::Ok(result),
10135                        Err(error) => Outcome::Err(error),
10136                    },
10137                    Ok(None) => Outcome::Err(McpError::internal_error("MRTR roots input was lost")),
10138                    Err(error) => Outcome::Err(error),
10139                }
10140            })
10141        }
10142    }
10143
10144    fn direct_final_prompt_request(id: i64) -> JsonRpcRequest {
10145        JsonRpcRequest::new(
10146            "prompts/get",
10147            Some(serde_json::json!({
10148                "_meta": {
10149                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
10150                    "io.modelcontextprotocol/clientCapabilities": {},
10151                },
10152                "name": "direct-final-prompt",
10153            })),
10154            id,
10155        )
10156    }
10157
10158    fn final_prompt_get_request(
10159        name: &str,
10160        arguments: serde_json::Value,
10161        id: i64,
10162    ) -> JsonRpcRequest {
10163        JsonRpcRequest::new(
10164            "prompts/get",
10165            Some(serde_json::json!({
10166                "_meta": {
10167                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
10168                    "io.modelcontextprotocol/clientCapabilities": {},
10169                },
10170                "name": name,
10171                "arguments": arguments,
10172            })),
10173            id,
10174        )
10175    }
10176
10177    struct DirectFinalResource {
10178        legacy_calls: Arc<AtomicUsize>,
10179        final_calls: Arc<AtomicUsize>,
10180    }
10181
10182    impl ResourceHandler for DirectFinalResource {
10183        fn definition(&self) -> Resource {
10184            Resource {
10185                uri: "file:///direct-final-resource".to_owned(),
10186                name: "direct-final-resource".to_owned(),
10187                description: Some("legacy resource definition".to_owned()),
10188                mime_type: Some("text/plain".to_owned()),
10189                icon: None,
10190                version: None,
10191                tags: Vec::new(),
10192            }
10193        }
10194
10195        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10196            self.legacy_calls.fetch_add(1, Ordering::SeqCst);
10197            Ok(vec![ResourceContent {
10198                uri: "file:///direct-final-resource".to_owned(),
10199                mime_type: Some("text/plain".to_owned()),
10200                text: Some("legacy resource result".to_owned()),
10201                blob: None,
10202            }])
10203        }
10204
10205        fn read_final(
10206            &self,
10207            _ctx: &McpContext,
10208        ) -> McpResult<CompleteResult<FinalReadResourceResult>> {
10209            self.final_calls.fetch_add(1, Ordering::SeqCst);
10210            let content_meta = OpenMetadata::try_from_entries([(
10211                "com.example/direct-resource".to_owned(),
10212                serde_json::json!({"source": "final-handler"}),
10213            )])
10214            .expect("direct resource content metadata is valid");
10215            Ok(CompleteResult::new(
10216                FinalReadResourceResult {
10217                    contents: vec![EmbeddedResourceContents::Text {
10218                        uri: AbsoluteUri::parse("file:///direct-final-resource")
10219                            .expect("routed direct resource URI is valid"),
10220                        text: "direct final resource result".to_owned(),
10221                        mime_type: Some("text/markdown".to_owned()),
10222                        meta: Some(content_meta),
10223                        additional: BTreeMap::from([(
10224                            "com.example/direct-field".to_owned(),
10225                            serde_json::json!(true),
10226                        )]),
10227                    }],
10228                    ttl_ms: CacheTtl::milliseconds(321),
10229                    cache_scope: CacheScope::Public,
10230                },
10231                empty_final_result_meta()?,
10232            ))
10233        }
10234
10235        fn final_resource_read_cache_hint_provenance(
10236            &self,
10237        ) -> FinalResourceReadCacheHintProvenance {
10238            FinalResourceReadCacheHintProvenance::Explicit
10239        }
10240    }
10241
10242    struct HttpsCatalogResource {
10243        client_direct_https: bool,
10244    }
10245
10246    impl ResourceHandler for HttpsCatalogResource {
10247        fn definition(&self) -> Resource {
10248            Resource {
10249                uri: "https://client.example.test/catalog.txt".to_owned(),
10250                name: "client-direct-catalog".to_owned(),
10251                description: None,
10252                mime_type: Some("text/plain".to_owned()),
10253                icon: None,
10254                version: None,
10255                tags: Vec::new(),
10256            }
10257        }
10258
10259        fn final_client_direct_https(&self) -> bool {
10260            self.client_direct_https
10261        }
10262
10263        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10264            Ok(Vec::new())
10265        }
10266    }
10267
10268    struct ClientDirectHttpsPrompt;
10269
10270    impl PromptHandler for ClientDirectHttpsPrompt {
10271        fn definition(&self) -> Prompt {
10272            Prompt {
10273                name: "client-direct-https-prompt".to_owned(),
10274                description: None,
10275                arguments: Vec::new(),
10276                icon: None,
10277                version: None,
10278                tags: Vec::new(),
10279            }
10280        }
10281
10282        fn final_client_direct_https(&self) -> bool {
10283            true
10284        }
10285
10286        fn get(
10287            &self,
10288            _ctx: &McpContext,
10289            _args: std::collections::HashMap<String, String>,
10290        ) -> McpResult<Vec<PromptMessage>> {
10291            Ok(Vec::new())
10292        }
10293
10294        fn get_final(
10295            &self,
10296            _ctx: &McpContext,
10297            _args: std::collections::HashMap<String, String>,
10298        ) -> McpResult<CompleteResult<FinalGetPromptResult>> {
10299            Ok(CompleteResult::new(
10300                FinalGetPromptResult {
10301                    description: None,
10302                    messages: vec![FinalPromptMessage {
10303                        role: fastmcp_protocol::Role::Assistant,
10304                        content: ContentBlock::ResourceLink {
10305                            icons: None,
10306                            name: "client-direct-link".to_owned(),
10307                            title: None,
10308                            uri: AbsoluteUri::parse("https://client.example.test/prompt-link")
10309                                .expect("test HTTPS URI is valid"),
10310                            description: None,
10311                            mime_type: Some("text/plain".to_owned()),
10312                            annotations: None,
10313                            size: None,
10314                            meta: None,
10315                            additional: BTreeMap::new(),
10316                        },
10317                    }],
10318                },
10319                empty_final_result_meta()?,
10320            ))
10321        }
10322    }
10323
10324    struct MrtrHttpsEmbeddedResource {
10325        initial_calls: Arc<AtomicUsize>,
10326        resumed_calls: Arc<AtomicUsize>,
10327    }
10328
10329    impl ResourceHandler for MrtrHttpsEmbeddedResource {
10330        fn definition(&self) -> Resource {
10331            Resource {
10332                uri: "mcp://uri-policy/mrtr".to_owned(),
10333                name: "uri-policy-mrtr".to_owned(),
10334                description: None,
10335                mime_type: Some("text/plain".to_owned()),
10336                icon: None,
10337                version: None,
10338                tags: Vec::new(),
10339            }
10340        }
10341
10342        fn final_client_direct_https(&self) -> bool {
10343            true
10344        }
10345
10346        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10347            Ok(Vec::new())
10348        }
10349
10350        fn read_final_outcome(
10351            &self,
10352            _ctx: &McpContext,
10353        ) -> McpResult<FinalMethodOutcome<FinalReadResourceResult>> {
10354            self.initial_calls.fetch_add(1, Ordering::SeqCst);
10355            Ok(FinalMethodOutcome::InputRequired(input_required_result(
10356                "uri-policy-mrtr-state",
10357            )))
10358        }
10359
10360        fn read_final_outcome_async_with_uri_resuming_in_request<'a>(
10361            &'a self,
10362            _ctx: &'a McpContext,
10363            _request_cx: &'a Cx,
10364            _uri: &'a str,
10365            _params: &'a UriParams,
10366            resume_inputs: Option<&'a MrtrCompletedInputs>,
10367        ) -> BoxFuture<'a, McpOutcome<FinalMethodOutcome<FinalReadResourceResult>>> {
10368            Box::pin(async move {
10369                let Some(resume_inputs) = resume_inputs else {
10370                    return Outcome::Err(McpError::internal_error("MRTR resume inputs were lost"));
10371                };
10372                if !matches!(resume_inputs.roots("roots"), Ok(Some(_))) {
10373                    return Outcome::Err(McpError::internal_error(
10374                        "MRTR roots input was not preserved",
10375                    ));
10376                }
10377                self.resumed_calls.fetch_add(1, Ordering::SeqCst);
10378                Outcome::Ok(FinalMethodOutcome::Complete(CompleteResult::new(
10379                    FinalReadResourceResult {
10380                        contents: vec![EmbeddedResourceContents::Text {
10381                            uri: AbsoluteUri::parse("https://client.example.test/mrtr-content")
10382                                .expect("test HTTPS URI is valid"),
10383                            text: "must not be emitted as embedded content".to_owned(),
10384                            mime_type: Some("text/plain".to_owned()),
10385                            meta: None,
10386                            additional: BTreeMap::new(),
10387                        }],
10388                        ttl_ms: CacheTtl::milliseconds(1),
10389                        cache_scope: CacheScope::Private,
10390                    },
10391                    empty_final_result_meta().expect("empty final metadata is valid"),
10392                )))
10393            })
10394        }
10395    }
10396
10397    struct SentinelHintResource {
10398        provenance: FinalResourceReadCacheHintProvenance,
10399    }
10400
10401    impl ResourceHandler for SentinelHintResource {
10402        fn definition(&self) -> Resource {
10403            Resource {
10404                uri: "file:///sentinel-hint-resource".to_owned(),
10405                name: "sentinel-hint-resource".to_owned(),
10406                description: None,
10407                mime_type: Some("text/plain".to_owned()),
10408                icon: None,
10409                version: None,
10410                tags: Vec::new(),
10411            }
10412        }
10413
10414        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10415            Ok(Vec::new())
10416        }
10417
10418        fn read_final(
10419            &self,
10420            _ctx: &McpContext,
10421        ) -> McpResult<CompleteResult<FinalReadResourceResult>> {
10422            Ok(CompleteResult::new(
10423                FinalReadResourceResult {
10424                    contents: Vec::new(),
10425                    // This is intentionally the former sentinel value. Only
10426                    // the explicit provenance may preserve it.
10427                    ttl_ms: CacheTtl::milliseconds(DEFAULT_FINAL_RESOURCE_TTL_MS),
10428                    cache_scope: CacheScope::Private,
10429                },
10430                empty_final_result_meta()?,
10431            ))
10432        }
10433
10434        fn final_resource_read_cache_hint_provenance(
10435            &self,
10436        ) -> FinalResourceReadCacheHintProvenance {
10437            self.provenance
10438        }
10439    }
10440
10441    fn direct_final_resource_request(id: i64) -> JsonRpcRequest {
10442        JsonRpcRequest::new(
10443            "resources/read",
10444            Some(serde_json::json!({
10445                "_meta": {
10446                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
10447                    "io.modelcontextprotocol/clientCapabilities": {},
10448                },
10449                "uri": "file:///direct-final-resource",
10450            })),
10451            id,
10452        )
10453    }
10454
10455    struct EchoCompletion;
10456
10457    impl CompletionHandler for EchoCompletion {
10458        fn complete_legacy(
10459            &self,
10460            _ctx: &McpContext,
10461            params: LegacyCompletionParams,
10462        ) -> McpResult<CompletionValues> {
10463            Ok(CompletionValues {
10464                values: vec![format!("{}ging", params.argument.value)],
10465                total: Some(1),
10466                has_more: Some(false),
10467            })
10468        }
10469
10470        fn complete_final(
10471            &self,
10472            _ctx: &McpContext,
10473            params: FinalCompletionParams,
10474        ) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
10475            Ok(fastmcp_protocol::FinalCompletionValues {
10476                values: vec![format!("{}ging", params.argument.value)],
10477                total: Some(fastmcp_protocol::JsonInteger::from(1_i64)),
10478                has_more: Some(false),
10479            })
10480        }
10481    }
10482
10483    struct CountingCompletion {
10484        final_calls: Arc<AtomicUsize>,
10485    }
10486
10487    impl CompletionHandler for CountingCompletion {
10488        fn complete_legacy(
10489            &self,
10490            _ctx: &McpContext,
10491            _params: LegacyCompletionParams,
10492        ) -> McpResult<CompletionValues> {
10493            Ok(CompletionValues {
10494                values: vec!["legacy".to_owned()],
10495                total: Some(1),
10496                has_more: Some(false),
10497            })
10498        }
10499
10500        fn complete_final(
10501            &self,
10502            _ctx: &McpContext,
10503            params: FinalCompletionParams,
10504        ) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
10505            self.final_calls.fetch_add(1, Ordering::SeqCst);
10506            Ok(fastmcp_protocol::FinalCompletionValues {
10507                values: vec![format!("{}ging", params.argument.value)],
10508                total: Some(fastmcp_protocol::JsonInteger::from(1_i64)),
10509                has_more: Some(false),
10510            })
10511        }
10512    }
10513
10514    struct ProviderCompletion {
10515        value: &'static str,
10516        final_calls: Arc<AtomicUsize>,
10517    }
10518
10519    impl CompletionHandler for ProviderCompletion {
10520        fn complete_legacy(
10521            &self,
10522            _ctx: &McpContext,
10523            _params: LegacyCompletionParams,
10524        ) -> McpResult<CompletionValues> {
10525            Ok(CompletionValues {
10526                values: vec![self.value.to_owned()],
10527                total: Some(1),
10528                has_more: Some(false),
10529            })
10530        }
10531
10532        fn complete_final(
10533            &self,
10534            _ctx: &McpContext,
10535            _params: FinalCompletionParams,
10536        ) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
10537            self.final_calls.fetch_add(1, Ordering::SeqCst);
10538            Ok(fastmcp_protocol::FinalCompletionValues {
10539                values: vec![self.value.to_owned()],
10540                total: Some(fastmcp_protocol::JsonInteger::from(1_i64)),
10541                has_more: Some(false),
10542            })
10543        }
10544    }
10545
10546    struct ReversibleLevelFourTemplateResource {
10547        read_calls: Arc<AtomicUsize>,
10548    }
10549
10550    impl ResourceHandler for ReversibleLevelFourTemplateResource {
10551        fn definition(&self) -> Resource {
10552            Resource {
10553                uri: "mcp://resource/template".to_owned(),
10554                name: "level-four-template".to_owned(),
10555                description: None,
10556                mime_type: Some("text/plain".to_owned()),
10557                icon: None,
10558                version: None,
10559                tags: Vec::new(),
10560            }
10561        }
10562
10563        fn template(&self) -> Option<ResourceTemplate> {
10564            Some(marked_template(
10565                "mcp://resource{/collection*}/manifest{?revision*}",
10566                "level-four-template",
10567            ))
10568        }
10569
10570        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10571            unreachable!("templated reads receive their URI parameters")
10572        }
10573
10574        fn read_with_uri(
10575            &self,
10576            _ctx: &McpContext,
10577            uri: &str,
10578            params: &UriParams,
10579        ) -> McpResult<Vec<ResourceContent>> {
10580            self.read_calls.fetch_add(1, Ordering::SeqCst);
10581            Ok(vec![ResourceContent {
10582                uri: uri.to_owned(),
10583                mime_type: Some("text/plain".to_owned()),
10584                text: Some(format!(
10585                    "{}:{}",
10586                    params.get("collection").expect("collection is captured"),
10587                    params.get("revision").expect("revision is captured")
10588                )),
10589                blob: None,
10590            }])
10591        }
10592    }
10593
10594    struct LegacyTemplateResource {
10595        read_calls: Arc<AtomicUsize>,
10596    }
10597
10598    impl ResourceHandler for LegacyTemplateResource {
10599        fn definition(&self) -> Resource {
10600            Resource {
10601                uri: "mcp://resource/legacy-template".to_owned(),
10602                name: "legacy-template".to_owned(),
10603                description: None,
10604                mime_type: Some("text/plain".to_owned()),
10605                icon: None,
10606                version: None,
10607                tags: Vec::new(),
10608            }
10609        }
10610
10611        fn template(&self) -> Option<ResourceTemplate> {
10612            Some(marked_template(
10613                "mcp://resource/{collection}/manifest?revision={revision}",
10614                "legacy-template",
10615            ))
10616        }
10617
10618        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10619            unreachable!("templated reads receive their URI parameters")
10620        }
10621
10622        fn read_with_uri(
10623            &self,
10624            _ctx: &McpContext,
10625            uri: &str,
10626            params: &UriParams,
10627        ) -> McpResult<Vec<ResourceContent>> {
10628            self.read_calls.fetch_add(1, Ordering::SeqCst);
10629            Ok(vec![ResourceContent {
10630                uri: uri.to_owned(),
10631                mime_type: Some("text/plain".to_owned()),
10632                text: Some(format!(
10633                    "{}:{}",
10634                    params.get("collection").expect("collection is captured"),
10635                    params.get("revision").expect("revision is captured")
10636                )),
10637                blob: None,
10638            }])
10639        }
10640    }
10641
10642    struct CompletionValueBoundary {
10643        final_calls: Arc<AtomicUsize>,
10644    }
10645
10646    impl CompletionHandler for CompletionValueBoundary {
10647        fn complete_legacy(
10648            &self,
10649            _ctx: &McpContext,
10650            _params: LegacyCompletionParams,
10651        ) -> McpResult<CompletionValues> {
10652            Ok(CompletionValues {
10653                values: Vec::new(),
10654                total: None,
10655                has_more: None,
10656            })
10657        }
10658
10659        fn complete_final(
10660            &self,
10661            _ctx: &McpContext,
10662            params: FinalCompletionParams,
10663        ) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
10664            self.final_calls.fetch_add(1, Ordering::SeqCst);
10665            let value_count = if params.argument.value == "one-over" {
10666                fastmcp_protocol::MAX_COMPLETION_VALUES + 1
10667            } else {
10668                fastmcp_protocol::MAX_COMPLETION_VALUES
10669            };
10670            Ok(fastmcp_protocol::FinalCompletionValues {
10671                values: (0..value_count)
10672                    .map(|index| format!("completion-{index}"))
10673                    .collect(),
10674                total: (params.argument.value == "negative-total")
10675                    .then(|| fastmcp_protocol::JsonInteger::from(-1_i64)),
10676                has_more: None,
10677            })
10678        }
10679    }
10680
10681    struct ConcurrentModernTool {
10682        started: Arc<AtomicUsize>,
10683        completed: Arc<Mutex<Vec<String>>>,
10684    }
10685
10686    impl ConcurrentModernTool {
10687        fn new(started: Arc<AtomicUsize>, completed: Arc<Mutex<Vec<String>>>) -> Self {
10688            Self { started, completed }
10689        }
10690    }
10691
10692    impl ToolHandler for ConcurrentModernTool {
10693        fn definition(&self) -> Tool {
10694            Tool {
10695                name: "concurrent-modern-tool".to_string(),
10696                description: Some("deterministic modern dispatch probe".to_string()),
10697                input_schema: serde_json::json!({
10698                    "type": "object",
10699                    "required": ["request"],
10700                    "properties": {"request": {"type": "string"}},
10701                }),
10702                output_schema: None,
10703                icon: None,
10704                version: None,
10705                tags: vec![],
10706                annotations: None,
10707            }
10708        }
10709
10710        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
10711            Err(McpError::internal_error(
10712                "concurrent modern dispatch requires the async request hook",
10713            ))
10714        }
10715
10716        // The modern stateless in-request path consults the disjoint final
10717        // outcome hook, not `call_async_in_request`; override the hook the
10718        // router actually dispatches through.
10719        fn call_final_outcome_async_in_request<'a>(
10720            &'a self,
10721            ctx: &'a McpContext,
10722            request_cx: &'a Cx,
10723            arguments: serde_json::Value,
10724        ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
10725            Box::pin(async move {
10726                let label = arguments
10727                    .get("request")
10728                    .and_then(serde_json::Value::as_str)
10729                    .unwrap_or("missing")
10730                    .to_string();
10731                self.started.fetch_add(1, Ordering::SeqCst);
10732
10733                while self.started.load(Ordering::SeqCst) < 2 {
10734                    if ctx.checkpoint().is_err() || request_cx.is_cancel_requested() {
10735                        return Outcome::Cancelled(asupersync::CancelReason::user(
10736                            "request cancelled before concurrent admission",
10737                        ));
10738                    }
10739                    yield_once().await;
10740                }
10741
10742                if label == "cancelled" {
10743                    loop {
10744                        if ctx.checkpoint().is_err() || request_cx.is_cancel_requested() {
10745                            return Outcome::Cancelled(asupersync::CancelReason::user(
10746                                "request cancellation observed by child Cx",
10747                            ));
10748                        }
10749                        yield_once().await;
10750                    }
10751                }
10752
10753                if ctx.checkpoint().is_err() || request_cx.is_cancel_requested() {
10754                    return Outcome::Cancelled(asupersync::CancelReason::user(
10755                        "request cancelled before completion",
10756                    ));
10757                }
10758                self.completed
10759                    .lock()
10760                    .expect("completion probe lock is not poisoned")
10761                    .push(label.clone());
10762                match crate::handler::promote_legacy_tool_content(vec![Content::text(label)]) {
10763                    Ok(result) => Outcome::Ok(FinalToolOutcome::Complete(result)),
10764                    Err(error) => Outcome::Err(error),
10765                }
10766            })
10767        }
10768    }
10769
10770    struct ErrorTool {
10771        name: &'static str,
10772        code: McpErrorCode,
10773    }
10774
10775    impl ToolHandler for ErrorTool {
10776        fn definition(&self) -> Tool {
10777            Tool {
10778                name: self.name.to_string(),
10779                description: None,
10780                input_schema: serde_json::json!({"type": "object"}),
10781                output_schema: None,
10782                icon: None,
10783                version: None,
10784                tags: vec![],
10785                annotations: None,
10786            }
10787        }
10788
10789        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
10790            Err(McpError::new(self.code, "nested tool error"))
10791        }
10792    }
10793
10794    struct AlternatingTool {
10795        calls: Arc<AtomicU64>,
10796    }
10797
10798    impl ToolHandler for AlternatingTool {
10799        fn definition(&self) -> Tool {
10800            Tool {
10801                name: "alternating_tool".to_string(),
10802                description: None,
10803                input_schema: serde_json::json!({"type": "object"}),
10804                output_schema: None,
10805                icon: None,
10806                version: None,
10807                tags: vec![],
10808                annotations: None,
10809            }
10810        }
10811
10812        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
10813            Err(McpError::internal_error("async alternating tool only"))
10814        }
10815
10816        fn call_async<'a>(
10817            &'a self,
10818            ctx: &'a McpContext,
10819            _arguments: serde_json::Value,
10820        ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
10821            Box::pin(async move {
10822                self.calls.fetch_add(1, Ordering::Relaxed);
10823                match ctx.read_resource("loop://resource").await {
10824                    Ok(_) => Outcome::Ok(vec![Content::text("unexpected completion")]),
10825                    Err(error) => Outcome::Err(error),
10826                }
10827            })
10828        }
10829    }
10830
10831    struct NamedResource {
10832        uri: String,
10833        tags: Vec<String>,
10834    }
10835
10836    impl NamedResource {
10837        fn new(uri: &str) -> Self {
10838            Self {
10839                uri: uri.to_string(),
10840                tags: vec![],
10841            }
10842        }
10843        fn with_tags(uri: &str, tags: Vec<String>) -> Self {
10844            Self {
10845                uri: uri.to_string(),
10846                tags,
10847            }
10848        }
10849    }
10850
10851    impl ResourceHandler for NamedResource {
10852        fn definition(&self) -> Resource {
10853            Resource {
10854                uri: self.uri.clone(),
10855                name: self.uri.clone(),
10856                description: None,
10857                mime_type: Some("text/plain".to_string()),
10858                icon: None,
10859                version: None,
10860                tags: self.tags.clone(),
10861            }
10862        }
10863        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10864            Ok(vec![ResourceContent {
10865                uri: self.uri.clone(),
10866                mime_type: Some("text/plain".to_string()),
10867                text: Some("content".to_string()),
10868                blob: None,
10869            }])
10870        }
10871    }
10872
10873    struct AlternatingResource {
10874        calls: Arc<AtomicU64>,
10875    }
10876
10877    impl ResourceHandler for AlternatingResource {
10878        fn definition(&self) -> Resource {
10879            Resource {
10880                uri: "loop://resource".to_string(),
10881                name: "alternating-resource".to_string(),
10882                description: None,
10883                mime_type: Some("text/plain".to_string()),
10884                icon: None,
10885                version: None,
10886                tags: vec![],
10887            }
10888        }
10889
10890        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10891            Err(McpError::internal_error("async alternating resource only"))
10892        }
10893
10894        fn read_async<'a>(
10895            &'a self,
10896            ctx: &'a McpContext,
10897        ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
10898            Box::pin(async move {
10899                self.calls.fetch_add(1, Ordering::Relaxed);
10900                match ctx
10901                    .call_tool("alternating_tool", serde_json::json!({}))
10902                    .await
10903                {
10904                    Ok(_) => Outcome::Ok(vec![ResourceContent {
10905                        uri: "loop://resource".to_string(),
10906                        mime_type: Some("text/plain".to_string()),
10907                        text: Some("unexpected completion".to_string()),
10908                        blob: None,
10909                    }]),
10910                    Err(error) => Outcome::Err(error),
10911                }
10912            })
10913        }
10914    }
10915
10916    struct CostLedgerTool {
10917        remaining_after_parent_debit: Arc<AtomicU64>,
10918        remaining_after_nested_read: Arc<AtomicU64>,
10919    }
10920
10921    impl ToolHandler for CostLedgerTool {
10922        fn definition(&self) -> Tool {
10923            Tool {
10924                name: "cost_ledger_tool".to_string(),
10925                description: None,
10926                input_schema: serde_json::json!({"type": "object"}),
10927                output_schema: None,
10928                icon: None,
10929                version: None,
10930                tags: vec![],
10931                annotations: None,
10932            }
10933        }
10934
10935        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
10936            Err(McpError::internal_error("async cost-ledger tool only"))
10937        }
10938
10939        fn call_async<'a>(
10940            &'a self,
10941            ctx: &'a McpContext,
10942            _arguments: serde_json::Value,
10943        ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
10944            Box::pin(async move {
10945                if ctx.consume_cost(1).is_err() {
10946                    return Outcome::Err(McpError::request_cancelled());
10947                }
10948                self.remaining_after_parent_debit.store(
10949                    ctx.budget()
10950                        .cost_quota
10951                        .expect("test request has finite cost quota"),
10952                    Ordering::Relaxed,
10953                );
10954
10955                if let Err(error) = ctx.read_resource("cost://nested").await {
10956                    return Outcome::Err(error);
10957                }
10958                self.remaining_after_nested_read.store(
10959                    ctx.budget()
10960                        .cost_quota
10961                        .expect("test request has finite cost quota"),
10962                    Ordering::Relaxed,
10963                );
10964                Outcome::Ok(vec![Content::text("shared ledger")])
10965            })
10966        }
10967    }
10968
10969    struct CostLedgerResource {
10970        remaining_after_nested_debit: Arc<AtomicU64>,
10971    }
10972
10973    impl ResourceHandler for CostLedgerResource {
10974        fn definition(&self) -> Resource {
10975            Resource {
10976                uri: "cost://nested".to_string(),
10977                name: "cost-ledger-resource".to_string(),
10978                description: None,
10979                mime_type: Some("text/plain".to_string()),
10980                icon: None,
10981                version: None,
10982                tags: vec![],
10983            }
10984        }
10985
10986        fn read(&self, ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
10987            ctx.consume_cost(1)
10988                .map_err(|_| McpError::request_cancelled())?;
10989            self.remaining_after_nested_debit.store(
10990                ctx.budget()
10991                    .cost_quota
10992                    .expect("test request has finite cost quota"),
10993                Ordering::Relaxed,
10994            );
10995            Ok(vec![ResourceContent {
10996                uri: "cost://nested".to_string(),
10997                mime_type: Some("text/plain".to_string()),
10998                text: Some("nested debit".to_string()),
10999                blob: None,
11000            }])
11001        }
11002    }
11003
11004    struct NamedPrompt {
11005        name: String,
11006        tags: Vec<String>,
11007    }
11008
11009    impl NamedPrompt {
11010        fn new(name: &str) -> Self {
11011            Self {
11012                name: name.to_string(),
11013                tags: vec![],
11014            }
11015        }
11016        fn with_tags(name: &str, tags: Vec<String>) -> Self {
11017            Self {
11018                name: name.to_string(),
11019                tags,
11020            }
11021        }
11022    }
11023
11024    impl PromptHandler for NamedPrompt {
11025        fn definition(&self) -> Prompt {
11026            Prompt {
11027                name: self.name.clone(),
11028                description: Some(format!("Prompt {}", self.name)),
11029                arguments: vec![],
11030                icon: None,
11031                version: None,
11032                tags: self.tags.clone(),
11033            }
11034        }
11035        fn get(
11036            &self,
11037            _ctx: &McpContext,
11038            _args: std::collections::HashMap<String, String>,
11039        ) -> McpResult<Vec<PromptMessage>> {
11040            Ok(vec![])
11041        }
11042    }
11043
11044    fn marked_template(uri_template: &str, marker: &str) -> ResourceTemplate {
11045        ResourceTemplate {
11046            uri_template: uri_template.to_string(),
11047            name: marker.to_string(),
11048            description: Some(marker.to_string()),
11049            mime_type: None,
11050            icon: None,
11051            version: None,
11052            tags: vec![marker.to_string()],
11053        }
11054    }
11055
11056    fn marked_router(marker: &str) -> Router {
11057        let mut router = Router::new();
11058        router
11059            .add_tool(NamedTool::with_tags(
11060                "duplicate_tool",
11061                vec![marker.to_string()],
11062            ))
11063            .expect("tool registration succeeds");
11064        router.add_resource(NamedResource::with_tags(
11065            "duplicate://resource",
11066            vec![marker.to_string()],
11067        ));
11068        router.add_resource_template(marked_template("duplicate://{item}", marker));
11069        router.add_prompt(NamedPrompt::with_tags(
11070            "duplicate_prompt",
11071            vec![marker.to_string()],
11072        ));
11073        router
11074    }
11075
11076    fn assert_router_marker(router: &Router, marker: &str) {
11077        assert_eq!(
11078            router
11079                .get_tool("duplicate_tool")
11080                .expect("tool exists")
11081                .definition()
11082                .tags,
11083            vec![marker.to_string()]
11084        );
11085        assert_eq!(
11086            router
11087                .get_resource("duplicate://resource")
11088                .expect("resource exists")
11089                .definition()
11090                .tags,
11091            vec![marker.to_string()]
11092        );
11093        assert_eq!(
11094            router
11095                .get_resource_template("duplicate://{item}")
11096                .expect("resource template exists")
11097                .tags,
11098            vec![marker.to_string()]
11099        );
11100        assert_eq!(
11101            router
11102                .get_prompt("duplicate_prompt")
11103                .expect("prompt exists")
11104                .definition()
11105                .tags,
11106            vec![marker.to_string()]
11107        );
11108    }
11109
11110    struct BudgetProbeTool {
11111        timeout: Option<Duration>,
11112        delay: Duration,
11113        observed_deadline: Arc<Mutex<Option<Time>>>,
11114        timeout_read: Arc<std::sync::atomic::AtomicBool>,
11115    }
11116
11117    impl ToolHandler for BudgetProbeTool {
11118        fn definition(&self) -> Tool {
11119            Tool {
11120                name: "budget_probe".to_string(),
11121                description: None,
11122                input_schema: serde_json::json!({"type": "object"}),
11123                output_schema: None,
11124                icon: None,
11125                version: None,
11126                tags: vec![],
11127                annotations: None,
11128            }
11129        }
11130
11131        fn timeout(&self) -> Option<Duration> {
11132            self.timeout_read.store(true, Ordering::Relaxed);
11133            self.timeout
11134        }
11135
11136        fn call(&self, ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
11137            eprintln!(
11138                "WM-BUDGET call entered deadline={:?}",
11139                ctx.budget().deadline
11140            );
11141            *self
11142                .observed_deadline
11143                .lock()
11144                .unwrap_or_else(std::sync::PoisonError::into_inner) = ctx.budget().deadline;
11145            if !self.delay.is_zero() {
11146                std::thread::sleep(self.delay);
11147            }
11148            Ok(vec![Content::text("completed")])
11149        }
11150    }
11151
11152    struct SlowDefinitionTool {
11153        definition_reads: Arc<AtomicU64>,
11154        called: Arc<std::sync::atomic::AtomicBool>,
11155    }
11156
11157    impl ToolHandler for SlowDefinitionTool {
11158        fn definition(&self) -> Tool {
11159            if self.definition_reads.fetch_add(1, Ordering::Relaxed) > 0 {
11160                eprintln!(
11161                    "WM-PROBE requery backtrace:\n{}",
11162                    std::backtrace::Backtrace::force_capture()
11163                );
11164                std::thread::sleep(Duration::from_millis(15));
11165            }
11166            Tool {
11167                name: "slow_definition".to_string(),
11168                description: None,
11169                input_schema: serde_json::json!({"type": "object"}),
11170                output_schema: None,
11171                icon: None,
11172                version: None,
11173                tags: vec![],
11174                annotations: None,
11175            }
11176        }
11177
11178        fn timeout(&self) -> Option<Duration> {
11179            Some(Duration::from_millis(1))
11180        }
11181
11182        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
11183            self.called.store(true, Ordering::Relaxed);
11184            Ok(vec![Content::text("must not run")])
11185        }
11186    }
11187
11188    const PANIC_CANARY: &str = "Bearer peer-secret\n\u{001b}[31mred\u{001b}[0m\u{001b}]8;;https://invalid\u{0007}link\u{202e}";
11189
11190    struct PanickingDisplay(String);
11191
11192    impl fmt::Display for PanickingDisplay {
11193        fn fmt(&self, _formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
11194            panic!("display-must-not-run: {}", self.0);
11195        }
11196    }
11197
11198    struct UnwindingPanicTool {
11199        payload: String,
11200        non_string: bool,
11201    }
11202
11203    impl ToolHandler for UnwindingPanicTool {
11204        fn definition(&self) -> Tool {
11205            Tool {
11206                name: "panic_tool".to_string(),
11207                description: None,
11208                input_schema: serde_json::json!({"type": "object"}),
11209                output_schema: None,
11210                icon: None,
11211                version: None,
11212                tags: vec![],
11213                annotations: None,
11214            }
11215        }
11216
11217        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
11218            unreachable!("the async override is the handler boundary under test")
11219        }
11220
11221        fn call_async<'a>(
11222            &'a self,
11223            _ctx: &'a McpContext,
11224            _arguments: serde_json::Value,
11225        ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
11226            Box::pin(async move {
11227                if self.non_string {
11228                    std::panic::panic_any(PanickingDisplay(self.payload.clone()));
11229                }
11230                panic!("{}", self.payload);
11231            })
11232        }
11233    }
11234
11235    struct OutcomePanicTool(String);
11236
11237    impl ToolHandler for OutcomePanicTool {
11238        fn definition(&self) -> Tool {
11239            Tool {
11240                name: "outcome_panic_tool".to_string(),
11241                description: None,
11242                input_schema: serde_json::json!({"type": "object"}),
11243                output_schema: None,
11244                icon: None,
11245                version: None,
11246                tags: vec![],
11247                annotations: None,
11248            }
11249        }
11250
11251        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
11252            unreachable!("the async override is the handler boundary under test")
11253        }
11254
11255        fn call_async<'a>(
11256            &'a self,
11257            _ctx: &'a McpContext,
11258            _arguments: serde_json::Value,
11259        ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
11260            Box::pin(async move {
11261                Outcome::Panicked(asupersync::types::PanicPayload::new(self.0.clone()))
11262            })
11263        }
11264    }
11265
11266    struct OpaqueInternalTool;
11267
11268    impl ToolHandler for OpaqueInternalTool {
11269        fn definition(&self) -> Tool {
11270            Tool {
11271                name: "opaque_internal_tool".to_string(),
11272                description: None,
11273                input_schema: serde_json::json!({"type": "object"}),
11274                output_schema: None,
11275                icon: None,
11276                version: None,
11277                tags: vec![],
11278                annotations: None,
11279            }
11280        }
11281
11282        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
11283            Err(McpError::with_data(
11284                McpErrorCode::InternalError,
11285                PANIC_CANARY,
11286                serde_json::json!({"secret": PANIC_CANARY}),
11287            ))
11288        }
11289    }
11290
11291    struct OpaqueInternalResource;
11292
11293    impl ResourceHandler for OpaqueInternalResource {
11294        fn definition(&self) -> Resource {
11295            Resource {
11296                uri: "opaque://internal".to_string(),
11297                name: "opaque-internal-resource".to_string(),
11298                description: None,
11299                mime_type: None,
11300                icon: None,
11301                version: None,
11302                tags: vec![],
11303            }
11304        }
11305
11306        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
11307            Err(McpError::with_data(
11308                McpErrorCode::InternalError,
11309                PANIC_CANARY,
11310                serde_json::json!({"secret": PANIC_CANARY}),
11311            ))
11312        }
11313    }
11314
11315    struct OpaqueInternalPrompt;
11316
11317    impl PromptHandler for OpaqueInternalPrompt {
11318        fn definition(&self) -> Prompt {
11319            Prompt {
11320                name: "opaque_internal_prompt".to_string(),
11321                description: None,
11322                arguments: vec![],
11323                icon: None,
11324                version: None,
11325                tags: vec![],
11326            }
11327        }
11328
11329        fn get(
11330            &self,
11331            _ctx: &McpContext,
11332            _args: std::collections::HashMap<String, String>,
11333        ) -> McpResult<Vec<PromptMessage>> {
11334            Err(McpError::with_data(
11335                McpErrorCode::InternalError,
11336                PANIC_CANARY,
11337                serde_json::json!({"secret": PANIC_CANARY}),
11338            ))
11339        }
11340    }
11341
11342    struct PanicResource;
11343
11344    impl ResourceHandler for PanicResource {
11345        fn definition(&self) -> Resource {
11346            Resource {
11347                uri: "panic://resource".to_string(),
11348                name: "panic-resource".to_string(),
11349                description: None,
11350                mime_type: None,
11351                icon: None,
11352                version: None,
11353                tags: vec![],
11354            }
11355        }
11356
11357        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
11358            panic!("{PANIC_CANARY}")
11359        }
11360    }
11361
11362    struct PanicPrompt;
11363
11364    impl PromptHandler for PanicPrompt {
11365        fn definition(&self) -> Prompt {
11366            Prompt {
11367                name: "panic_prompt".to_string(),
11368                description: None,
11369                arguments: vec![],
11370                icon: None,
11371                version: None,
11372                tags: vec![],
11373            }
11374        }
11375
11376        fn get(
11377            &self,
11378            _ctx: &McpContext,
11379            _args: std::collections::HashMap<String, String>,
11380        ) -> McpResult<Vec<PromptMessage>> {
11381            panic!("{PANIC_CANARY}")
11382        }
11383    }
11384
11385    struct DefinitionPanicTool(std::sync::atomic::AtomicBool);
11386
11387    impl ToolHandler for DefinitionPanicTool {
11388        fn definition(&self) -> Tool {
11389            if self.0.swap(true, Ordering::Relaxed) {
11390                panic!("{PANIC_CANARY}");
11391            }
11392            Tool {
11393                name: "definition_panic_tool".to_string(),
11394                description: None,
11395                input_schema: serde_json::json!({"type": "object"}),
11396                output_schema: None,
11397                icon: None,
11398                version: None,
11399                tags: vec![],
11400                annotations: None,
11401            }
11402        }
11403
11404        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
11405            Ok(vec![])
11406        }
11407    }
11408
11409    struct DefinitionPanicResource(std::sync::atomic::AtomicBool);
11410
11411    impl ResourceHandler for DefinitionPanicResource {
11412        fn definition(&self) -> Resource {
11413            if self.0.swap(true, Ordering::Relaxed) {
11414                panic!("{PANIC_CANARY}");
11415            }
11416            Resource {
11417                uri: "panic://definition".to_string(),
11418                name: "definition-panic-resource".to_string(),
11419                description: None,
11420                mime_type: None,
11421                icon: None,
11422                version: None,
11423                tags: vec![],
11424            }
11425        }
11426
11427        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
11428            Ok(vec![])
11429        }
11430    }
11431
11432    struct TemplatePanicResource(std::sync::atomic::AtomicBool);
11433
11434    impl ResourceHandler for TemplatePanicResource {
11435        fn definition(&self) -> Resource {
11436            Resource {
11437                uri: "panic-template://placeholder".to_string(),
11438                name: "template-panic-resource".to_string(),
11439                description: None,
11440                mime_type: None,
11441                icon: None,
11442                version: None,
11443                tags: vec![],
11444            }
11445        }
11446
11447        fn template(&self) -> Option<ResourceTemplate> {
11448            if self.0.swap(true, Ordering::Relaxed) {
11449                panic!("{PANIC_CANARY}");
11450            }
11451            Some(ResourceTemplate {
11452                uri_template: "panic-template://{id}".to_string(),
11453                name: "template-panic-resource".to_string(),
11454                description: None,
11455                mime_type: None,
11456                icon: None,
11457                version: None,
11458                tags: vec![],
11459            })
11460        }
11461
11462        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
11463            unreachable!("templated reads use read_with_uri")
11464        }
11465
11466        fn read_with_uri(
11467            &self,
11468            _ctx: &McpContext,
11469            uri: &str,
11470            _params: &UriParams,
11471        ) -> McpResult<Vec<ResourceContent>> {
11472            Ok(vec![ResourceContent {
11473                uri: uri.to_string(),
11474                mime_type: None,
11475                text: Some("template-content".to_string()),
11476                blob: None,
11477            }])
11478        }
11479    }
11480
11481    struct DefinitionPanicPrompt(std::sync::atomic::AtomicBool);
11482
11483    impl PromptHandler for DefinitionPanicPrompt {
11484        fn definition(&self) -> Prompt {
11485            if self.0.swap(true, Ordering::Relaxed) {
11486                panic!("{PANIC_CANARY}");
11487            }
11488            Prompt {
11489                name: "definition_panic_prompt".to_string(),
11490                description: None,
11491                arguments: vec![],
11492                icon: None,
11493                version: None,
11494                tags: vec![],
11495            }
11496        }
11497
11498        fn get(
11499            &self,
11500            _ctx: &McpContext,
11501            _args: std::collections::HashMap<String, String>,
11502        ) -> McpResult<Vec<PromptMessage>> {
11503            Ok(vec![])
11504        }
11505    }
11506
11507    // ── Router::new ────────────────────────────────────────────────────
11508
11509    #[test]
11510    fn new_router_is_empty() {
11511        let r = Router::new();
11512        assert_eq!(r.tools_count(), 0);
11513        assert_eq!(r.resources_count(), 0);
11514        assert_eq!(r.resource_templates_count(), 0);
11515        assert_eq!(r.prompts_count(), 0);
11516        assert!(r.tools().is_empty());
11517        assert!(r.resources().is_empty());
11518        assert!(r.resource_templates().is_empty());
11519        assert!(r.prompts().is_empty());
11520    }
11521
11522    #[test]
11523    fn default_router_is_empty() {
11524        let r = Router::default();
11525        assert_eq!(r.tools_count(), 0);
11526    }
11527
11528    // ── add_tool / get_tool ────────────────────────────────────────────
11529
11530    #[test]
11531    fn add_and_get_tool() {
11532        let mut r = Router::new();
11533        r.add_tool(NamedTool::new("my_tool"))
11534            .expect("tool registration succeeds");
11535        assert_eq!(r.tools_count(), 1);
11536        assert!(r.get_tool("my_tool").is_some());
11537        assert!(r.get_tool("other").is_none());
11538    }
11539
11540    #[test]
11541    fn add_tool_replace_on_duplicate() {
11542        let mut r = Router::new();
11543        r.add_tool(NamedTool::new("t"))
11544            .expect("initial registration succeeds");
11545        r.add_tool(NamedTool::new("t"))
11546            .expect("valid replacement succeeds");
11547        assert_eq!(r.tools_count(), 1);
11548        // Order preserved (only one entry)
11549        assert_eq!(r.tools().len(), 1);
11550    }
11551
11552    #[test]
11553    fn tools_returns_definitions_in_order() {
11554        let mut r = Router::new();
11555        r.add_tool(NamedTool::new("b"))
11556            .expect("tool registration succeeds");
11557        r.add_tool(NamedTool::new("a"))
11558            .expect("tool registration succeeds");
11559        let names: Vec<_> = r.tools().iter().map(|t| t.name.clone()).collect();
11560        assert_eq!(names, vec!["b", "a"]); // insertion order
11561    }
11562
11563    #[test]
11564    fn explicit_legacy_tool_is_callable_only_on_exact_2024_routes() {
11565        let mut router = Router::new();
11566        router
11567            .add_legacy_tool(InvalidFinalSchemaNamedTool::with_tags(
11568                "legacy-only-tool",
11569                vec!["legacy".to_owned()],
11570            ))
11571            .expect("explicit legacy registration does not claim final admission");
11572        assert_eq!(
11573            router
11574                .tools()
11575                .into_iter()
11576                .map(|tool| tool.name)
11577                .collect::<Vec<_>>(),
11578            vec!["legacy-only-tool"]
11579        );
11580        assert!(
11581            !router
11582                .server_discovery_behavior_registry()
11583                .contains(ServerBehavior::ToolsList),
11584            "a legacy-only catalog cannot produce a modern discovery claim"
11585        );
11586
11587        let cx = Cx::for_testing();
11588        let state = SessionState::new();
11589        let request_ctx = request_context(&cx, 78, Budget::INFINITE, &state);
11590        let legacy = router
11591            .handle_tools_call(
11592                &request_ctx,
11593                CallToolParams {
11594                    name: "legacy-only-tool".to_owned(),
11595                    arguments: Some(serde_json::json!({})),
11596                    meta: None,
11597                },
11598                state,
11599                None,
11600                None,
11601            )
11602            .expect("exact 2024 dispatch reaches the explicit legacy handler");
11603        assert_eq!(
11604            serde_json::to_value(legacy).expect("legacy result serializes")["content"][0]["text"],
11605            "called legacy-only-tool"
11606        );
11607
11608        let modern_catalog = router
11609            .dispatch_stateless(
11610                &request_ctx,
11611                &final_tools_list_request(None, None, None, 79_i64),
11612            )
11613            .expect("modern tools/list remains a valid empty catalog");
11614        assert_eq!(modern_catalog["tools"], serde_json::json!([]));
11615        let modern_error = router
11616            .dispatch_stateless(
11617                &request_ctx,
11618                &final_tools_call_request("legacy-only-tool", serde_json::json!({}), 80_i64),
11619            )
11620            .expect_err("modern dispatch cannot resolve an explicit legacy-only tool");
11621        assert_eq!(modern_error.code, McpErrorCode::InvalidParams);
11622        assert_eq!(router.tools_count(), 1);
11623    }
11624
11625    #[test]
11626    fn explicit_legacy_resource_and_prompt_are_inert_on_final_routes() {
11627        let mut router = Router::new();
11628        router.add_legacy_resource(NamedResource::new("file:///legacy-only"));
11629        router.add_legacy_resource_template(marked_template("file:///{name}", "legacy-only"));
11630        router.add_legacy_prompt(NamedPrompt::new("legacy-only-prompt"));
11631        let cx = Cx::for_testing();
11632        let state = SessionState::new();
11633        let request_ctx = request_context(&cx, 181, Budget::INFINITE, &state);
11634
11635        let legacy_resource = router
11636            .handle_resources_read(
11637                &request_ctx,
11638                &ReadResourceParams {
11639                    uri: "file:///legacy-only".to_owned(),
11640                    meta: None,
11641                },
11642                state.clone(),
11643                None,
11644                None,
11645            )
11646            .expect("the exact legacy resource route retains its explicit registration");
11647        assert_eq!(legacy_resource.contents.len(), 1);
11648        let legacy_prompt = router
11649            .handle_prompts_get(
11650                &request_ctx,
11651                GetPromptParams {
11652                    name: "legacy-only-prompt".to_owned(),
11653                    arguments: None,
11654                    meta: None,
11655                },
11656                state,
11657                None,
11658                None,
11659            )
11660            .expect("the exact legacy prompt route retains its explicit registration");
11661        assert!(legacy_prompt.messages.is_empty());
11662
11663        let modern_resources = router
11664            .dispatch_stateless(
11665                &request_ctx,
11666                &JsonRpcRequest::new(
11667                    "resources/list",
11668                    Some(serde_json::json!({
11669                        "_meta": {
11670                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
11671                            "io.modelcontextprotocol/clientCapabilities": {},
11672                        },
11673                    })),
11674                    182_i64,
11675                ),
11676            )
11677            .expect("final discovery remains valid with only legacy resources");
11678        assert_eq!(modern_resources["resources"], serde_json::json!([]));
11679        let modern_templates = router
11680            .dispatch_stateless(
11681                &request_ctx,
11682                &JsonRpcRequest::new(
11683                    "resources/templates/list",
11684                    Some(serde_json::json!({
11685                        "_meta": {
11686                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
11687                            "io.modelcontextprotocol/clientCapabilities": {},
11688                        },
11689                    })),
11690                    183_i64,
11691                ),
11692            )
11693            .expect("final template discovery remains valid with only legacy templates");
11694        assert_eq!(modern_templates["resourceTemplates"], serde_json::json!([]));
11695
11696        let modern_prompt = router
11697            .dispatch_stateless(
11698                &request_ctx,
11699                &JsonRpcRequest::new(
11700                    "prompts/get",
11701                    Some(serde_json::json!({
11702                        "_meta": {
11703                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
11704                            "io.modelcontextprotocol/clientCapabilities": {},
11705                        },
11706                        "name": "legacy-only-prompt",
11707                    })),
11708                    184_i64,
11709                ),
11710            )
11711            .expect_err("changing only the dispatch era refuses the legacy-only prompt");
11712        assert_eq!(modern_prompt.code, McpErrorCode::InvalidParams);
11713        assert!(
11714            !router
11715                .server_discovery_behavior_registry()
11716                .contains(ServerBehavior::ResourcesList)
11717        );
11718        assert!(
11719            !router
11720                .server_discovery_behavior_registry()
11721                .contains(ServerBehavior::PromptsList)
11722        );
11723    }
11724
11725    // ── add_tool_with_behavior ─────────────────────────────────────────
11726
11727    #[test]
11728    fn duplicate_error_warn_and_ignore_preserve_handler_schema_and_order() {
11729        for behavior in [
11730            crate::DuplicateBehavior::Error,
11731            crate::DuplicateBehavior::Warn,
11732            crate::DuplicateBehavior::Ignore,
11733        ] {
11734            let original_legacy_calls = Arc::new(AtomicUsize::new(0));
11735            let original_final_calls = Arc::new(AtomicUsize::new(0));
11736            let candidate_legacy_calls = Arc::new(AtomicUsize::new(0));
11737            let candidate_final_calls = Arc::new(AtomicUsize::new(0));
11738            let mut router = Router::new();
11739            router
11740                .add_tool(NamedTool::new("before"))
11741                .expect("baseline tool admission succeeds");
11742            router
11743                .add_tool(DuplicateInvariantTool {
11744                    label: "original",
11745                    schema_property: "original_property",
11746                    legacy_calls: Arc::clone(&original_legacy_calls),
11747                    final_calls: Arc::clone(&original_final_calls),
11748                })
11749                .expect("original admission succeeds");
11750            router
11751                .add_tool(NamedTool::new("after"))
11752                .expect("trailing tool admission succeeds");
11753
11754            let cx = Cx::for_testing();
11755            let state = SessionState::new();
11756            let request_ctx = request_context(&cx, 81, Budget::INFINITE, &state);
11757            let legacy_before =
11758                serde_json::to_value(router.tools()).expect("legacy catalog serializes");
11759            let modern_before = router
11760                .dispatch_stateless(
11761                    &request_ctx,
11762                    &final_tools_list_request(None, None, None, 81_i64),
11763                )
11764                .expect("modern catalog is available");
11765
11766            let registration = router.add_tool_with_behavior(
11767                DuplicateInvariantTool {
11768                    label: "candidate",
11769                    schema_property: "candidate_property",
11770                    legacy_calls: Arc::clone(&candidate_legacy_calls),
11771                    final_calls: Arc::clone(&candidate_final_calls),
11772                },
11773                behavior,
11774            );
11775            if behavior == crate::DuplicateBehavior::Error {
11776                let error = registration.expect_err("Error rejects the duplicate");
11777                assert!(error.message.contains("already exists"));
11778            } else {
11779                registration.expect("Warn and Ignore retain the original");
11780            }
11781
11782            assert_eq!(
11783                serde_json::to_value(router.tools()).expect("legacy catalog serializes"),
11784                legacy_before
11785            );
11786            assert_eq!(
11787                router
11788                    .dispatch_stateless(
11789                        &request_ctx,
11790                        &final_tools_list_request(None, None, None, 82_i64),
11791                    )
11792                    .expect("modern catalog remains available"),
11793                modern_before
11794            );
11795            assert_eq!(
11796                router
11797                    .tools()
11798                    .into_iter()
11799                    .map(|tool| tool.name)
11800                    .collect::<Vec<_>>(),
11801                vec!["before", "duplicate-invariant-tool", "after"]
11802            );
11803
11804            let legacy = router
11805                .handle_tools_call(
11806                    &request_ctx,
11807                    CallToolParams {
11808                        name: "duplicate-invariant-tool".to_owned(),
11809                        arguments: Some(serde_json::json!({})),
11810                        meta: None,
11811                    },
11812                    state.clone(),
11813                    None,
11814                    None,
11815                )
11816                .expect("legacy dispatch retains the original");
11817            assert_eq!(
11818                serde_json::to_value(legacy).expect("legacy result serializes")["content"][0]["text"],
11819                "original"
11820            );
11821            let modern = router
11822                .dispatch_stateless(
11823                    &request_ctx,
11824                    &final_tools_call_request(
11825                        "duplicate-invariant-tool",
11826                        serde_json::json!({}),
11827                        83_i64,
11828                    ),
11829                )
11830                .expect("modern dispatch retains the original");
11831            assert_eq!(modern["content"][0]["text"], "original");
11832            assert_eq!(original_legacy_calls.load(Ordering::SeqCst), 1);
11833            assert_eq!(original_final_calls.load(Ordering::SeqCst), 1);
11834            assert_eq!(candidate_legacy_calls.load(Ordering::SeqCst), 0);
11835            assert_eq!(candidate_final_calls.load(Ordering::SeqCst), 0);
11836        }
11837    }
11838
11839    #[test]
11840    fn duplicate_replace_atomically_updates_handler_schema_and_retains_order() {
11841        let replacement_legacy_calls = Arc::new(AtomicUsize::new(0));
11842        let replacement_final_calls = Arc::new(AtomicUsize::new(0));
11843        let mut router = Router::new();
11844        router
11845            .add_tool(NamedTool::new("before"))
11846            .expect("baseline admission succeeds");
11847        router
11848            .add_tool(DuplicateInvariantTool {
11849                label: "original",
11850                schema_property: "original_property",
11851                legacy_calls: Arc::new(AtomicUsize::new(0)),
11852                final_calls: Arc::new(AtomicUsize::new(0)),
11853            })
11854            .expect("original admission succeeds");
11855        router
11856            .add_tool(NamedTool::new("after"))
11857            .expect("trailing admission succeeds");
11858        router
11859            .add_tool_with_behavior(
11860                DuplicateInvariantTool {
11861                    label: "replacement",
11862                    schema_property: "replacement_property",
11863                    legacy_calls: Arc::clone(&replacement_legacy_calls),
11864                    final_calls: Arc::clone(&replacement_final_calls),
11865                },
11866                crate::DuplicateBehavior::Replace,
11867            )
11868            .expect("Replace admits before committing the candidate");
11869
11870        assert_eq!(
11871            router
11872                .tools()
11873                .into_iter()
11874                .map(|tool| tool.name)
11875                .collect::<Vec<_>>(),
11876            vec!["before", "duplicate-invariant-tool", "after"]
11877        );
11878        let cx = Cx::for_testing();
11879        let state = SessionState::new();
11880        let request_ctx = request_context(&cx, 84, Budget::INFINITE, &state);
11881        let modern_catalog = router
11882            .dispatch_stateless(
11883                &request_ctx,
11884                &final_tools_list_request(None, None, None, 84_i64),
11885            )
11886            .expect("modern replacement catalog is available");
11887        assert_eq!(
11888            modern_catalog["tools"][1]["inputSchema"]["properties"],
11889            serde_json::json!({"replacement_property": {"type": "boolean"}})
11890        );
11891        assert_eq!(
11892            router.tools()[1].description.as_deref(),
11893            Some("replacement")
11894        );
11895
11896        let legacy = router
11897            .handle_tools_call(
11898                &request_ctx,
11899                CallToolParams {
11900                    name: "duplicate-invariant-tool".to_owned(),
11901                    arguments: Some(serde_json::json!({})),
11902                    meta: None,
11903                },
11904                state,
11905                None,
11906                None,
11907            )
11908            .expect("legacy dispatch uses replacement");
11909        assert_eq!(
11910            serde_json::to_value(legacy).expect("legacy result serializes")["content"][0]["text"],
11911            "replacement"
11912        );
11913        let modern = router
11914            .dispatch_stateless(
11915                &request_ctx,
11916                &final_tools_call_request(
11917                    "duplicate-invariant-tool",
11918                    serde_json::json!({}),
11919                    85_i64,
11920                ),
11921            )
11922            .expect("modern dispatch uses replacement");
11923        assert_eq!(modern["content"][0]["text"], "replacement");
11924        assert_eq!(replacement_legacy_calls.load(Ordering::SeqCst), 1);
11925        assert_eq!(replacement_final_calls.load(Ordering::SeqCst), 1);
11926    }
11927
11928    #[test]
11929    fn add_tool_behavior_new_tool_ok() {
11930        let mut r = Router::new();
11931        r.add_tool_with_behavior(NamedTool::new("t"), crate::DuplicateBehavior::Error)
11932            .unwrap();
11933        assert_eq!(r.tools_count(), 1);
11934    }
11935
11936    // ── add_resource / get_resource ────────────────────────────────────
11937
11938    #[test]
11939    fn add_and_get_resource() {
11940        let mut r = Router::new();
11941        r.add_resource(NamedResource::new("file:///a.txt"));
11942        assert_eq!(r.resources_count(), 1);
11943        assert!(r.get_resource("file:///a.txt").is_some());
11944        assert!(r.get_resource("file:///b.txt").is_none());
11945    }
11946
11947    #[test]
11948    fn resources_returns_definitions_in_order() {
11949        let mut r = Router::new();
11950        r.add_resource(NamedResource::new("file:///b"));
11951        r.add_resource(NamedResource::new("file:///a"));
11952        let uris: Vec<_> = r.resources().iter().map(|res| res.uri.clone()).collect();
11953        assert_eq!(uris, vec!["file:///b", "file:///a"]);
11954    }
11955
11956    // ── add_resource_with_behavior ─────────────────────────────────────
11957
11958    #[test]
11959    fn add_resource_behavior_error_on_duplicate() {
11960        let mut r = Router::new();
11961        r.add_resource(NamedResource::new("file:///a"));
11962        let err = r
11963            .add_resource_with_behavior(
11964                NamedResource::new("file:///a"),
11965                crate::DuplicateBehavior::Error,
11966            )
11967            .unwrap_err();
11968        assert!(err.message.contains("already exists"));
11969    }
11970
11971    #[test]
11972    fn add_resource_behavior_ignore() {
11973        let mut r = Router::new();
11974        r.add_resource(NamedResource::new("file:///a"));
11975        r.add_resource_with_behavior(
11976            NamedResource::new("file:///a"),
11977            crate::DuplicateBehavior::Ignore,
11978        )
11979        .unwrap();
11980        assert_eq!(r.resources_count(), 1);
11981    }
11982
11983    // ── add_prompt / get_prompt ────────────────────────────────────────
11984
11985    #[test]
11986    fn add_and_get_prompt() {
11987        let mut r = Router::new();
11988        r.add_prompt(NamedPrompt::new("greet"));
11989        assert_eq!(r.prompts_count(), 1);
11990        assert!(r.get_prompt("greet").is_some());
11991        assert!(r.get_prompt("other").is_none());
11992    }
11993
11994    #[test]
11995    fn prompts_returns_definitions_in_order() {
11996        let mut r = Router::new();
11997        r.add_prompt(NamedPrompt::new("z"));
11998        r.add_prompt(NamedPrompt::new("a"));
11999        let names: Vec<_> = r.prompts().iter().map(|p| p.name.clone()).collect();
12000        assert_eq!(names, vec!["z", "a"]);
12001    }
12002
12003    // ── add_prompt_with_behavior ───────────────────────────────────────
12004
12005    #[test]
12006    fn add_prompt_behavior_error_on_duplicate() {
12007        let mut r = Router::new();
12008        r.add_prompt(NamedPrompt::new("p"));
12009        let err = r
12010            .add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Error)
12011            .unwrap_err();
12012        assert!(err.message.contains("already exists"));
12013    }
12014
12015    #[test]
12016    fn add_prompt_behavior_warn_keeps_original() {
12017        let mut r = Router::new();
12018        r.add_prompt(NamedPrompt::new("p"));
12019        r.add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Warn)
12020            .unwrap();
12021        assert_eq!(r.prompts_count(), 1);
12022    }
12023
12024    #[test]
12025    fn duplicate_registration_errors_do_not_echo_peer_identifiers() {
12026        let canary = "raw-peer-identifier-canary";
12027
12028        let mut tools = Router::new();
12029        tools
12030            .add_tool(NamedTool::new(canary))
12031            .expect("tool registration succeeds");
12032        let tool_error = tools
12033            .add_tool_with_behavior(NamedTool::new(canary), crate::DuplicateBehavior::Error)
12034            .unwrap_err();
12035
12036        // Resource identities must be scheme-valid to survive final-catalog
12037        // projection; the canary stays the peer-controlled URI component.
12038        let canary_uri = format!("resource://{canary}");
12039        let mut resources = Router::new();
12040        resources.add_resource(NamedResource::new(&canary_uri));
12041        let resource_error = resources
12042            .add_resource_with_behavior(
12043                NamedResource::new(&canary_uri),
12044                crate::DuplicateBehavior::Error,
12045            )
12046            .unwrap_err();
12047
12048        let mut templates = Router::new();
12049        templates.add_resource_template(marked_template(&canary_uri, "original"));
12050        let template_error = templates
12051            .add_resource_template_with_behavior(
12052                marked_template(&canary_uri, "incoming"),
12053                crate::DuplicateBehavior::Error,
12054            )
12055            .unwrap_err();
12056
12057        let mut prompts = Router::new();
12058        prompts.add_prompt(NamedPrompt::new(canary));
12059        let prompt_error = prompts
12060            .add_prompt_with_behavior(NamedPrompt::new(canary), crate::DuplicateBehavior::Error)
12061            .unwrap_err();
12062
12063        for error in [tool_error, resource_error, template_error, prompt_error] {
12064            assert!(error.message.contains("already exists"));
12065            assert!(!error.message.contains(canary));
12066        }
12067    }
12068
12069    // ── add_resource_template ──────────────────────────────────────────
12070
12071    #[test]
12072    fn add_resource_template_and_list() {
12073        let mut r = Router::new();
12074        let tmpl = ResourceTemplate {
12075            uri_template: "db://{table}".to_string(),
12076            name: "db".to_string(),
12077            description: None,
12078            mime_type: None,
12079            icon: None,
12080            version: None,
12081            tags: vec![],
12082        };
12083        r.add_resource_template(tmpl);
12084        assert_eq!(r.resource_templates_count(), 1);
12085        assert!(r.get_resource_template("db://{table}").is_some());
12086        assert!(r.get_resource_template("db://{other}").is_none());
12087    }
12088
12089    #[test]
12090    fn add_resource_template_replaces_existing() {
12091        let mut r = Router::new();
12092        let tmpl1 = ResourceTemplate {
12093            uri_template: "db://{table}".to_string(),
12094            name: "db1".to_string(),
12095            description: None,
12096            mime_type: None,
12097            icon: None,
12098            version: None,
12099            tags: vec![],
12100        };
12101        let tmpl2 = ResourceTemplate {
12102            uri_template: "db://{table}".to_string(),
12103            name: "db2".to_string(),
12104            description: None,
12105            mime_type: None,
12106            icon: None,
12107            version: None,
12108            tags: vec![],
12109        };
12110        r.add_resource_template(tmpl1);
12111        r.add_resource_template(tmpl2);
12112        assert_eq!(r.resource_templates_count(), 1);
12113        let tmpl = r.get_resource_template("db://{table}").unwrap();
12114        assert_eq!(tmpl.name, "db2");
12115    }
12116
12117    #[test]
12118    fn add_resource_template_with_behavior_preserves_or_replaces_identity() {
12119        for behavior in [
12120            crate::DuplicateBehavior::Warn,
12121            crate::DuplicateBehavior::Ignore,
12122            crate::DuplicateBehavior::Error,
12123        ] {
12124            let mut router = Router::new();
12125            router.add_resource_template(marked_template("peer://{secret}", "original"));
12126            let result = router.add_resource_template_with_behavior(
12127                marked_template("peer://{secret}", "incoming"),
12128                behavior,
12129            );
12130
12131            if behavior == crate::DuplicateBehavior::Error {
12132                let error = result.expect_err("Error policy rejects the duplicate");
12133                assert!(error.message.contains("already exists"));
12134                assert!(!error.message.contains("peer://{secret}"));
12135            } else {
12136                result.expect("Warn and Ignore keep the original");
12137            }
12138            assert_eq!(router.resource_templates_count(), 1);
12139            assert_eq!(
12140                router
12141                    .get_resource_template("peer://{secret}")
12142                    .expect("original template remains")
12143                    .name,
12144                "original"
12145            );
12146        }
12147
12148        let mut router = Router::new();
12149        router.add_resource_template(marked_template("peer://{secret}", "original"));
12150        router
12151            .add_resource_template_with_behavior(
12152                marked_template("peer://{secret}", "incoming"),
12153                crate::DuplicateBehavior::Replace,
12154            )
12155            .expect("Replace accepts the duplicate");
12156        assert_eq!(router.resource_templates_count(), 1);
12157        assert_eq!(
12158            router
12159                .get_resource_template("peer://{secret}")
12160                .expect("replacement template exists")
12161                .name,
12162            "incoming"
12163        );
12164    }
12165
12166    // ── resource_exists / resolve_resource ──────────────────────────────
12167
12168    #[test]
12169    fn resource_exists_for_static_resource() {
12170        let mut r = Router::new();
12171        r.add_resource(NamedResource::new("file:///a.txt"));
12172        assert!(r.resource_exists("file:///a.txt"));
12173        assert!(!r.resource_exists("file:///b.txt"));
12174    }
12175
12176    // ── strict_input_validation ────────────────────────────────────────
12177
12178    #[test]
12179    fn strict_input_validation_default_off() {
12180        let r = Router::new();
12181        assert!(!r.strict_input_validation());
12182    }
12183
12184    #[test]
12185    fn set_strict_input_validation() {
12186        let mut r = Router::new();
12187        r.set_strict_input_validation(true);
12188        assert!(r.strict_input_validation());
12189        r.set_strict_input_validation(false);
12190        assert!(!r.strict_input_validation());
12191    }
12192
12193    // ── set_list_page_size ─────────────────────────────────────────────
12194
12195    #[test]
12196    fn set_list_page_size_zero_treated_as_none() {
12197        let mut r = Router::new();
12198        r.set_list_page_size(Some(0));
12199        // Zero page size is filtered to None
12200        assert!(r.list_page_size.is_none());
12201    }
12202
12203    #[test]
12204    fn set_list_page_size_positive() {
12205        let mut r = Router::new();
12206        r.set_list_page_size(Some(10));
12207        assert_eq!(r.list_page_size, Some(10));
12208    }
12209
12210    #[test]
12211    fn set_list_page_size_none() {
12212        let mut r = Router::new();
12213        r.set_list_page_size(Some(10));
12214        r.set_list_page_size(None);
12215        assert!(r.list_page_size.is_none());
12216    }
12217
12218    // ── tools_filtered ─────────────────────────────────────────────────
12219
12220    #[test]
12221    fn tools_filtered_no_filters_returns_all() {
12222        let mut r = Router::new();
12223        r.add_tool(NamedTool::new("a"))
12224            .expect("tool registration succeeds");
12225        r.add_tool(NamedTool::new("b"))
12226            .expect("tool registration succeeds");
12227        let tools = r.tools_filtered(None, None);
12228        assert_eq!(tools.len(), 2);
12229    }
12230
12231    #[test]
12232    fn tools_filtered_by_session_state_disables() {
12233        let mut r = Router::new();
12234        r.add_tool(NamedTool::new("a"))
12235            .expect("tool registration succeeds");
12236        r.add_tool(NamedTool::new("b"))
12237            .expect("tool registration succeeds");
12238        let state = SessionState::new();
12239        let disabled: std::collections::HashSet<String> = ["a".to_string()].into_iter().collect();
12240        state.set("fastmcp.disabled_tools", &disabled);
12241        let tools = r.tools_filtered(Some(&state), None);
12242        assert_eq!(tools.len(), 1);
12243        assert_eq!(tools[0].name, "b");
12244    }
12245
12246    #[test]
12247    fn tools_filtered_by_tags() {
12248        let mut r = Router::new();
12249        r.add_tool(NamedTool::with_tags("a", vec!["db".to_string()]))
12250            .expect("tool registration succeeds");
12251        r.add_tool(NamedTool::with_tags("b", vec!["web".to_string()]))
12252            .expect("tool registration succeeds");
12253        let include = vec!["db".to_string()];
12254        let filters = TagFilters::new(Some(&include), None);
12255        let tools = r.tools_filtered(None, Some(&filters));
12256        assert_eq!(tools.len(), 1);
12257        assert_eq!(tools[0].name, "a");
12258    }
12259
12260    // ── resources_filtered ─────────────────────────────────────────────
12261
12262    #[test]
12263    fn resources_filtered_by_session_state() {
12264        let mut r = Router::new();
12265        r.add_resource(NamedResource::new("file:///a"));
12266        r.add_resource(NamedResource::new("file:///b"));
12267        let state = SessionState::new();
12268        let disabled: std::collections::HashSet<String> =
12269            ["file:///a".to_string()].into_iter().collect();
12270        state.set("fastmcp.disabled_resources", &disabled);
12271        let res = r.resources_filtered(Some(&state), None);
12272        assert_eq!(res.len(), 1);
12273        assert_eq!(res[0].uri, "file:///b");
12274    }
12275
12276    // ── prompts_filtered ───────────────────────────────────────────────
12277
12278    #[test]
12279    fn prompts_filtered_by_session_state() {
12280        let mut r = Router::new();
12281        r.add_prompt(NamedPrompt::new("a"));
12282        r.add_prompt(NamedPrompt::new("b"));
12283        let state = SessionState::new();
12284        let disabled: std::collections::HashSet<String> = ["a".to_string()].into_iter().collect();
12285        state.set("fastmcp.disabled_prompts", &disabled);
12286        let prompts = r.prompts_filtered(Some(&state), None);
12287        assert_eq!(prompts.len(), 1);
12288        assert_eq!(prompts[0].name, "b");
12289    }
12290
12291    #[test]
12292    fn prompts_filtered_by_tags() {
12293        let mut r = Router::new();
12294        r.add_prompt(NamedPrompt::with_tags("a", vec!["internal".to_string()]));
12295        r.add_prompt(NamedPrompt::with_tags("b", vec!["public".to_string()]));
12296        let exclude = vec!["internal".to_string()];
12297        let filters = TagFilters::new(None, Some(&exclude));
12298        let prompts = r.prompts_filtered(None, Some(&filters));
12299        assert_eq!(prompts.len(), 1);
12300        assert_eq!(prompts[0].name, "b");
12301    }
12302
12303    // ── resource_templates_filtered ────────────────────────────────────
12304
12305    #[test]
12306    fn resource_templates_filtered_by_session_state() {
12307        let mut r = Router::new();
12308        r.add_resource_template(ResourceTemplate {
12309            uri_template: "db://{table}".to_string(),
12310            name: "db".to_string(),
12311            description: None,
12312            mime_type: None,
12313            icon: None,
12314            version: None,
12315            tags: vec!["admin".to_string()],
12316        });
12317        r.add_resource_template(ResourceTemplate {
12318            uri_template: "cache://{key}".to_string(),
12319            name: "cache".to_string(),
12320            description: None,
12321            mime_type: None,
12322            icon: None,
12323            version: None,
12324            tags: vec![],
12325        });
12326        let state = SessionState::new();
12327        let disabled: std::collections::HashSet<String> =
12328            ["db://{table}".to_string()].into_iter().collect();
12329        state.set("fastmcp.disabled_resources", &disabled);
12330        let tmpls = r.resource_templates_filtered(Some(&state), None);
12331        assert_eq!(tmpls.len(), 1);
12332        assert_eq!(tmpls[0].name, "cache");
12333    }
12334
12335    // ── apply_prefix / validate_prefix ─────────────────────────────────
12336
12337    #[test]
12338    fn apply_prefix_with_prefix() {
12339        assert_eq!(Router::apply_prefix("tool", Some("ns")), "ns/tool");
12340    }
12341
12342    #[test]
12343    fn apply_prefix_no_prefix() {
12344        assert_eq!(Router::apply_prefix("tool", None), "tool");
12345    }
12346
12347    #[test]
12348    fn apply_prefix_empty_prefix() {
12349        assert_eq!(Router::apply_prefix("tool", Some("")), "tool");
12350    }
12351
12352    #[test]
12353    fn validate_prefix_valid() {
12354        assert!(Router::validate_prefix("my-prefix_1").is_ok());
12355    }
12356
12357    #[test]
12358    fn validate_prefix_empty_is_ok() {
12359        assert!(Router::validate_prefix("").is_ok());
12360    }
12361
12362    #[test]
12363    fn validate_prefix_rejects_slashes() {
12364        let err = Router::validate_prefix("a/b").unwrap_err();
12365        assert!(err.contains("slashes"));
12366    }
12367
12368    #[test]
12369    fn validate_prefix_rejects_special_chars() {
12370        let err = Router::validate_prefix("a@b").unwrap_err();
12371        assert!(err.contains("invalid character"));
12372    }
12373
12374    // ── MountResult ────────────────────────────────────────────────────
12375
12376    #[test]
12377    fn mount_result_default_has_no_components() {
12378        let r = MountResult::default();
12379        assert!(!r.has_components());
12380        assert!(r.is_success());
12381    }
12382
12383    #[test]
12384    fn mount_result_with_tools_has_components() {
12385        let mut r = MountResult::default();
12386        r.tools = 1;
12387        assert!(r.has_components());
12388    }
12389
12390    #[test]
12391    fn mount_result_debug() {
12392        let r = MountResult::default();
12393        let debug = format!("{:?}", r);
12394        assert!(debug.contains("MountResult"));
12395    }
12396
12397    // ── mount ──────────────────────────────────────────────────────────
12398
12399    #[test]
12400    fn mount_tools_with_prefix() {
12401        let mut main = Router::new();
12402        let mut sub = Router::new();
12403        sub.add_tool(NamedTool::new("query"))
12404            .expect("tool registration succeeds");
12405        let result = main.mount(sub, Some("db"));
12406        assert_eq!(result.tools, 1);
12407        assert!(main.get_tool("db/query").is_some());
12408        assert!(main.get_tool("query").is_none());
12409    }
12410
12411    #[test]
12412    fn mount_without_prefix() {
12413        let mut main = Router::new();
12414        let mut sub = Router::new();
12415        sub.add_tool(NamedTool::new("query"))
12416            .expect("tool registration succeeds");
12417        let result = main.mount(sub, None);
12418        assert_eq!(result.tools, 1);
12419        assert!(main.get_tool("query").is_some());
12420    }
12421
12422    #[test]
12423    fn mount_resources_with_prefix() {
12424        let mut main = Router::new();
12425        let mut sub = Router::new();
12426        sub.add_resource(NamedResource::new("file:///a"));
12427        let result = main.mount(sub, Some("ns"));
12428        assert_eq!(result.resources, 1);
12429        assert!(main.get_resource("ns/file:///a").is_some());
12430
12431        let cx = Cx::for_testing();
12432        let state = SessionState::new();
12433        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
12434        let read = main
12435            .handle_resources_read(
12436                &request_ctx,
12437                &ReadResourceParams {
12438                    uri: "ns/file:///a".to_string(),
12439                    meta: None,
12440                },
12441                state,
12442                None,
12443                None,
12444            )
12445            .expect("mounted resource is readable through its public URI");
12446        let [
12447            LegacyResourceContent::Text {
12448                uri,
12449                text,
12450                additional,
12451                ..
12452            },
12453        ] = read.contents.as_slice()
12454        else {
12455            panic!("mounted resource must retain the exact legacy text shape");
12456        };
12457        assert_eq!(uri, "ns/file:///a");
12458        assert_eq!(text, "content");
12459        assert!(additional.is_empty());
12460    }
12461
12462    #[test]
12463    fn mount_namespaced_prefixes_tools_and_keeps_resource_uris() {
12464        let mut main = Router::new();
12465        let mut sub = Router::new();
12466        sub.add_tool(NamedTool::new("query"));
12467        sub.add_resource(NamedResource::new("file:///a"));
12468        let result =
12469            main.mount_namespaced_with_behavior(sub, Some("ns"), crate::DuplicateBehavior::Replace);
12470        assert!(result.is_success());
12471        assert_eq!(result.tools, 1);
12472        assert_eq!(result.resources, 1);
12473        assert!(main.get_tool("ns/query").is_some());
12474        assert!(main.get_tool("query").is_none());
12475        assert!(main.get_resource("file:///a").is_some());
12476        assert!(main.get_resource("ns/file:///a").is_none());
12477        assert!(main.final_resources.contains_key("file:///a"));
12478    }
12479
12480    #[test]
12481    fn mounting_resource_does_not_requery_one_shot_definition() {
12482        let mut source = Router::new();
12483        source.add_resource(DefinitionPanicResource(std::sync::atomic::AtomicBool::new(
12484            false,
12485        )));
12486        let mut destination = Router::new();
12487
12488        let result = destination.mount_resources(source, Some("ns"));
12489
12490        assert!(result.is_success());
12491        assert_eq!(result.resources, 1);
12492        assert!(destination.get_resource("ns/panic://definition").is_some());
12493    }
12494
12495    #[test]
12496    fn mounting_resource_template_does_not_requery_one_shot_template() {
12497        let mut source = Router::new();
12498        source.add_resource(TemplatePanicResource(std::sync::atomic::AtomicBool::new(
12499            false,
12500        )));
12501        let mut destination = Router::new();
12502
12503        let result = destination.mount_resources(source, Some("ns"));
12504
12505        assert!(result.is_success());
12506        assert_eq!(result.resource_templates, 1);
12507        assert!(
12508            destination
12509                .get_resource_template("ns/panic-template://{id}")
12510                .is_some()
12511        );
12512    }
12513
12514    #[test]
12515    fn prefixed_resource_template_mount_is_legacy_only_after_final_source_dispatch() {
12516        struct FinalTemplateResource;
12517
12518        impl ResourceHandler for FinalTemplateResource {
12519            fn definition(&self) -> Resource {
12520                Resource {
12521                    uri: "mcp://mounted/template".to_owned(),
12522                    name: "mounted-template".to_owned(),
12523                    description: None,
12524                    mime_type: Some("text/plain".to_owned()),
12525                    icon: None,
12526                    version: None,
12527                    tags: Vec::new(),
12528                }
12529            }
12530
12531            fn template(&self) -> Option<ResourceTemplate> {
12532                Some(marked_template("mcp://mounted/{id}", "mounted-template"))
12533            }
12534
12535            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
12536                unreachable!("templated reads receive their URI parameters")
12537            }
12538
12539            fn read_with_uri(
12540                &self,
12541                _ctx: &McpContext,
12542                uri: &str,
12543                _params: &UriParams,
12544            ) -> McpResult<Vec<ResourceContent>> {
12545                Ok(vec![ResourceContent {
12546                    uri: uri.to_owned(),
12547                    mime_type: Some("text/plain".to_owned()),
12548                    text: Some("source-template".to_owned()),
12549                    blob: None,
12550                }])
12551            }
12552        }
12553
12554        let cx = Cx::for_testing();
12555        let state = SessionState::new();
12556        let request_ctx = request_context(&cx, 191, Budget::INFINITE, &state);
12557        let final_metadata = serde_json::json!({
12558            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
12559            "io.modelcontextprotocol/clientCapabilities": {},
12560        });
12561        let mut source = Router::new();
12562        source.add_resource(FinalTemplateResource);
12563
12564        let source_final_read = source
12565            .dispatch_stateless(
12566                &request_ctx,
12567                &JsonRpcRequest::new(
12568                    "resources/read",
12569                    Some(serde_json::json!({
12570                        "_meta": final_metadata.clone(),
12571                        "uri": "mcp://mounted/item",
12572                    })),
12573                    191_i64,
12574                ),
12575            )
12576            .expect("the unprefixed final template dispatches");
12577        assert_eq!(source_final_read["contents"][0]["text"], "source-template");
12578
12579        let mut destination = Router::new();
12580        let mounted = destination.mount_resources(source, Some("peer"));
12581        assert!(mounted.is_success());
12582        assert_eq!(mounted.resource_templates, 1);
12583
12584        let legacy_templates = destination
12585            .handle_resource_templates_list(
12586                &request_ctx,
12587                ListResourceTemplatesParams::default(),
12588                None,
12589            )
12590            .expect("the prefixed template remains in legacy discovery");
12591        assert_eq!(
12592            legacy_templates.resource_templates[0].uri_template,
12593            "peer/mcp://mounted/{id}"
12594        );
12595        let legacy_read = destination
12596            .handle_resources_read(
12597                &request_ctx,
12598                &ReadResourceParams {
12599                    uri: "peer/mcp://mounted/item".to_owned(),
12600                    meta: None,
12601                },
12602                state.clone(),
12603                None,
12604                None,
12605            )
12606            .expect("the prefixed template remains readable on the legacy surface");
12607        assert_eq!(
12608            serde_json::to_value(legacy_read).expect("legacy result serializes")["contents"][0]["text"],
12609            "source-template"
12610        );
12611
12612        let final_templates = destination
12613            .dispatch_stateless(
12614                &request_ctx,
12615                &JsonRpcRequest::new(
12616                    "resources/templates/list",
12617                    Some(serde_json::json!({"_meta": final_metadata.clone()})),
12618                    192_i64,
12619                ),
12620            )
12621            .expect("prefixed legacy-only templates do not break final discovery");
12622        assert_eq!(final_templates["resourceTemplates"], serde_json::json!([]));
12623
12624        let final_error = destination
12625            .dispatch_stateless(
12626                &request_ctx,
12627                &JsonRpcRequest::new(
12628                    "resources/read",
12629                    Some(serde_json::json!({
12630                        "_meta": final_metadata,
12631                        "uri": "peer/mcp://mounted/item",
12632                    })),
12633                    193_i64,
12634                ),
12635            )
12636            .expect_err("the relative mounted namespace is not exposed to final dispatch");
12637        assert_eq!(final_error.code, McpErrorCode::InvalidParams);
12638    }
12639
12640    #[test]
12641    fn nested_resource_mounts_translate_both_namespace_layers() {
12642        let mut leaf = Router::new();
12643        leaf.add_resource(NamedResource::new("file:///a"));
12644
12645        let mut middle = Router::new();
12646        assert!(middle.mount(leaf, Some("ns")).is_success());
12647        let mut outer = Router::new();
12648        assert!(outer.mount(middle, Some("ns")).is_success());
12649
12650        let cx = Cx::for_testing();
12651        let state = SessionState::new();
12652        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
12653        let read = outer
12654            .handle_resources_read(
12655                &request_ctx,
12656                &ReadResourceParams {
12657                    uri: "ns/ns/file:///a".to_string(),
12658                    meta: None,
12659                },
12660                state,
12661                None,
12662                None,
12663            )
12664            .expect("nested mounted resource is readable");
12665        let [
12666            LegacyResourceContent::Text {
12667                uri,
12668                text,
12669                additional,
12670                ..
12671            },
12672        ] = read.contents.as_slice()
12673        else {
12674            panic!("nested mounted resource must retain the exact legacy text shape");
12675        };
12676        assert_eq!(uri, "ns/ns/file:///a");
12677        assert_eq!(text, "content");
12678        assert!(additional.is_empty());
12679    }
12680
12681    #[test]
12682    fn nested_resource_template_mounts_translate_every_namespace_layer() {
12683        struct TemplatedResource;
12684
12685        impl ResourceHandler for TemplatedResource {
12686            fn definition(&self) -> Resource {
12687                Resource {
12688                    uri: "db://placeholder".to_string(),
12689                    name: "database".to_string(),
12690                    description: None,
12691                    mime_type: Some("text/plain".to_string()),
12692                    icon: None,
12693                    version: None,
12694                    tags: vec![],
12695                }
12696            }
12697
12698            fn template(&self) -> Option<ResourceTemplate> {
12699                Some(ResourceTemplate {
12700                    uri_template: "db://{table}".to_string(),
12701                    name: "database".to_string(),
12702                    description: None,
12703                    mime_type: Some("text/plain".to_string()),
12704                    icon: None,
12705                    version: None,
12706                    tags: vec![],
12707                })
12708            }
12709
12710            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
12711                unreachable!("templated reads use read_with_uri")
12712            }
12713
12714            fn read_with_uri(
12715                &self,
12716                _ctx: &McpContext,
12717                uri: &str,
12718                params: &UriParams,
12719            ) -> McpResult<Vec<ResourceContent>> {
12720                Ok(vec![ResourceContent {
12721                    uri: uri.to_string(),
12722                    mime_type: Some("text/plain".to_string()),
12723                    text: params.get("table").cloned(),
12724                    blob: None,
12725                }])
12726            }
12727        }
12728
12729        let mut source = Router::new();
12730        source.add_resource(TemplatedResource);
12731        let mut middle = Router::new();
12732        assert!(middle.mount_resources(source, Some("peer")).is_success());
12733        let mut mounted = Router::new();
12734        assert!(mounted.mount_resources(middle, Some("peer")).is_success());
12735
12736        let cx = Cx::for_testing();
12737        let state = SessionState::new();
12738        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
12739        let read = mounted
12740            .handle_resources_read(
12741                &request_ctx,
12742                &ReadResourceParams {
12743                    uri: "peer/peer/db://users".to_string(),
12744                    meta: None,
12745                },
12746                state,
12747                None,
12748                None,
12749            )
12750            .expect("mounted template is readable through its public URI");
12751        let [
12752            LegacyResourceContent::Text {
12753                uri,
12754                text,
12755                additional,
12756                ..
12757            },
12758        ] = read.contents.as_slice()
12759        else {
12760            panic!("mounted template must retain the exact legacy text shape");
12761        };
12762        assert_eq!(uri, "peer/peer/db://users");
12763        assert_eq!(text, "users");
12764        assert!(additional.is_empty());
12765    }
12766
12767    #[test]
12768    fn handle_resources_read_resolves_true_async_mounted_template_and_translates_uri() {
12769        struct AsyncTemplatedResource {
12770            observed: Arc<Mutex<Option<(String, String)>>>,
12771        }
12772
12773        impl ResourceHandler for AsyncTemplatedResource {
12774            fn definition(&self) -> Resource {
12775                Resource {
12776                    uri: "async-db://placeholder".to_string(),
12777                    name: "async-database".to_string(),
12778                    description: None,
12779                    mime_type: Some("text/plain".to_string()),
12780                    icon: None,
12781                    version: None,
12782                    tags: vec![],
12783                }
12784            }
12785
12786            fn template(&self) -> Option<ResourceTemplate> {
12787                Some(ResourceTemplate {
12788                    uri_template: "async-db://{table}".to_string(),
12789                    name: "async-database".to_string(),
12790                    description: None,
12791                    mime_type: Some("text/plain".to_string()),
12792                    icon: None,
12793                    version: None,
12794                    tags: vec![],
12795                })
12796            }
12797
12798            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
12799                unreachable!("router must dispatch templated resources through the async override")
12800            }
12801
12802            fn read_with_uri(
12803                &self,
12804                _ctx: &McpContext,
12805                _uri: &str,
12806                _params: &UriParams,
12807            ) -> McpResult<Vec<ResourceContent>> {
12808                unreachable!("router must not fall back to the synchronous templated read")
12809            }
12810
12811            fn read_async_with_uri<'a>(
12812                &'a self,
12813                _ctx: &'a McpContext,
12814                uri: &'a str,
12815                params: &'a UriParams,
12816            ) -> BoxFuture<'a, McpOutcome<Vec<ResourceContent>>> {
12817                Box::pin(async move {
12818                    let mut first_poll = true;
12819                    std::future::poll_fn(move |waker| {
12820                        if std::mem::take(&mut first_poll) {
12821                            waker.waker().wake_by_ref();
12822                            std::task::Poll::Pending
12823                        } else {
12824                            std::task::Poll::Ready(())
12825                        }
12826                    })
12827                    .await;
12828                    let Some(table) = params.get("table").cloned() else {
12829                        return Outcome::Err(McpError::invalid_params(
12830                            "mounted template did not resolve its table parameter",
12831                        ));
12832                    };
12833                    *self.observed.lock().expect("observation mutex poisoned") =
12834                        Some((uri.to_string(), table.clone()));
12835                    Outcome::Ok(vec![ResourceContent {
12836                        uri: uri.to_string(),
12837                        mime_type: Some("text/plain".to_string()),
12838                        text: Some(format!("async table {table}")),
12839                        blob: None,
12840                    }])
12841                })
12842            }
12843        }
12844
12845        let observed = Arc::new(Mutex::new(None));
12846        let mut source = Router::new();
12847        source.add_resource(AsyncTemplatedResource {
12848            observed: Arc::clone(&observed),
12849        });
12850        let mut mounted = Router::new();
12851        let mount_result = mounted.mount_resources(source, Some("peer"));
12852        assert!(mount_result.is_success());
12853        assert_eq!(mount_result.resource_templates, 1);
12854
12855        let cx = Cx::for_testing();
12856        let state = SessionState::new();
12857        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
12858        let read = mounted
12859            .handle_resources_read(
12860                &request_ctx,
12861                &ReadResourceParams {
12862                    uri: "peer/async-db://users".to_string(),
12863                    meta: None,
12864                },
12865                state,
12866                None,
12867                None,
12868            )
12869            .expect("true-async mounted template is readable through its public URI");
12870
12871        assert_eq!(read.contents.len(), 1);
12872        let [
12873            LegacyResourceContent::Text {
12874                uri,
12875                text,
12876                additional,
12877                ..
12878            },
12879        ] = read.contents.as_slice()
12880        else {
12881            panic!("async mounted template must retain the exact legacy text shape");
12882        };
12883        assert_eq!(uri, "peer/async-db://users");
12884        assert_eq!(text, "async table users");
12885        assert!(additional.is_empty());
12886        assert_eq!(
12887            *observed.lock().expect("observation mutex poisoned"),
12888            Some(("async-db://users".to_string(), "users".to_string()))
12889        );
12890    }
12891
12892    #[test]
12893    fn mount_prompts_with_prefix() {
12894        let mut main = Router::new();
12895        let mut sub = Router::new();
12896        sub.add_prompt(NamedPrompt::new("greet"));
12897        let result = main.mount(sub, Some("ns"));
12898        assert_eq!(result.prompts, 1);
12899        assert!(main.get_prompt("ns/greet").is_some());
12900    }
12901
12902    #[test]
12903    fn mount_warns_on_conflict() {
12904        let mut main = Router::new();
12905        main.add_tool(NamedTool::new("t"))
12906            .expect("tool registration succeeds");
12907        let mut sub = Router::new();
12908        sub.add_tool(NamedTool::new("t"))
12909            .expect("tool registration succeeds");
12910        let result = main.mount(sub, None);
12911        assert_eq!(result.tools, 1);
12912        assert!(!result.warnings.is_empty());
12913        assert!(result.warnings[0].contains("already exists"));
12914    }
12915
12916    #[test]
12917    fn mount_rejects_invalid_prefix_without_mutating() {
12918        let mut main = Router::new();
12919        main.add_tool(NamedTool::new("original"))
12920            .expect("tool registration succeeds");
12921        let mut sub = Router::new();
12922        sub.add_tool(NamedTool::new("incoming"))
12923            .expect("tool registration succeeds");
12924        let result = main.mount(sub, Some("bad/prefix"));
12925        assert!(!result.is_success());
12926        assert_eq!(result.tools, 0);
12927        assert!(!result.warnings.is_empty());
12928        assert!(result.warnings[0].contains("slashes"));
12929        assert!(!result.errors[0].contains("bad/prefix"));
12930        assert_eq!(main.tools_count(), 1);
12931        assert!(main.get_tool("original").is_some());
12932        assert!(main.get_tool("bad/prefix/incoming").is_none());
12933    }
12934
12935    #[test]
12936    fn mount_with_behavior_honors_policy_for_every_component_kind() {
12937        for behavior in [
12938            crate::DuplicateBehavior::Warn,
12939            crate::DuplicateBehavior::Ignore,
12940            crate::DuplicateBehavior::Replace,
12941            crate::DuplicateBehavior::Error,
12942        ] {
12943            let mut main = marked_router("original");
12944            let mut sub = marked_router("incoming");
12945            if behavior == crate::DuplicateBehavior::Error {
12946                sub.add_tool(NamedTool::new("unique_tool"))
12947                    .expect("tool registration succeeds");
12948            }
12949
12950            let result = main.mount_with_behavior(sub, None, behavior);
12951            let replaced = behavior == crate::DuplicateBehavior::Replace;
12952            let rejected = behavior == crate::DuplicateBehavior::Error;
12953            let expected_mounted = usize::from(replaced);
12954
12955            assert_eq!(result.is_success(), !rejected);
12956            assert_eq!(result.tools, expected_mounted);
12957            assert_eq!(result.resources, expected_mounted);
12958            assert_eq!(result.resource_templates, expected_mounted);
12959            assert_eq!(result.prompts, expected_mounted);
12960            assert_eq!(result.errors.len(), if rejected { 4 } else { 0 });
12961            assert_eq!(
12962                result.warnings.len(),
12963                if matches!(
12964                    behavior,
12965                    crate::DuplicateBehavior::Warn | crate::DuplicateBehavior::Replace
12966                ) {
12967                    4
12968                } else {
12969                    0
12970                }
12971            );
12972            assert_router_marker(&main, if replaced { "incoming" } else { "original" });
12973            assert!(main.get_tool("unique_tool").is_none());
12974
12975            for message in result.warnings.iter().chain(&result.errors) {
12976                assert!(!message.contains("duplicate_tool"));
12977                assert!(!message.contains("duplicate://resource"));
12978                assert!(!message.contains("duplicate://{item}"));
12979                assert!(!message.contains("duplicate_prompt"));
12980            }
12981        }
12982    }
12983
12984    #[test]
12985    fn behavior_aware_partial_mounts_preflight_error_atomically() {
12986        let mut tools = Router::new();
12987        tools
12988            .add_tool(NamedTool::with_tags("same", vec!["original".to_string()]))
12989            .expect("tool registration succeeds");
12990        let mut tool_source = Router::new();
12991        tool_source
12992            .add_tool(NamedTool::with_tags("same", vec!["incoming".to_string()]))
12993            .expect("tool registration succeeds");
12994        tool_source
12995            .add_tool(NamedTool::new("unique"))
12996            .expect("tool registration succeeds");
12997        let tool_result =
12998            tools.mount_tools_with_behavior(tool_source, None, crate::DuplicateBehavior::Error);
12999        assert!(!tool_result.is_success());
13000        assert_eq!(tool_result.tools, 0);
13001        assert_eq!(tools.tools_count(), 1);
13002        assert_eq!(
13003            tools.get_tool("same").unwrap().definition().tags,
13004            vec!["original".to_string()]
13005        );
13006        assert!(tools.get_tool("unique").is_none());
13007
13008        let mut resources = Router::new();
13009        resources.add_resource(NamedResource::with_tags(
13010            "same://resource",
13011            vec!["original".to_string()],
13012        ));
13013        let mut resource_source = Router::new();
13014        resource_source.add_resource(NamedResource::with_tags(
13015            "same://resource",
13016            vec!["incoming".to_string()],
13017        ));
13018        resource_source.add_resource_template(marked_template("unique://{item}", "incoming"));
13019        let resource_result = resources.mount_resources_with_behavior(
13020            resource_source,
13021            None,
13022            crate::DuplicateBehavior::Error,
13023        );
13024        assert!(!resource_result.is_success());
13025        assert_eq!(resource_result.resources, 0);
13026        assert_eq!(resource_result.resource_templates, 0);
13027        assert_eq!(resources.resources_count(), 1);
13028        assert_eq!(resources.resource_templates_count(), 0);
13029        assert_eq!(
13030            resources
13031                .get_resource("same://resource")
13032                .unwrap()
13033                .definition()
13034                .tags,
13035            vec!["original".to_string()]
13036        );
13037
13038        let mut prompts = Router::new();
13039        prompts.add_prompt(NamedPrompt::with_tags("same", vec!["original".to_string()]));
13040        let mut prompt_source = Router::new();
13041        prompt_source.add_prompt(NamedPrompt::with_tags("same", vec!["incoming".to_string()]));
13042        prompt_source.add_prompt(NamedPrompt::new("unique"));
13043        let prompt_result = prompts.mount_prompts_with_behavior(
13044            prompt_source,
13045            None,
13046            crate::DuplicateBehavior::Error,
13047        );
13048        assert!(!prompt_result.is_success());
13049        assert_eq!(prompt_result.prompts, 0);
13050        assert_eq!(prompts.prompts_count(), 1);
13051        assert_eq!(
13052            prompts.get_prompt("same").unwrap().definition().tags,
13053            vec!["original".to_string()]
13054        );
13055        assert!(prompts.get_prompt("unique").is_none());
13056    }
13057
13058    #[test]
13059    fn full_error_mount_is_atomic_across_component_kinds() {
13060        let mut main = Router::new();
13061        main.add_tool(NamedTool::with_tags(
13062            "conflict",
13063            vec!["original".to_string()],
13064        ))
13065        .expect("tool registration succeeds");
13066
13067        let mut sub = Router::new();
13068        sub.add_tool(NamedTool::with_tags(
13069            "conflict",
13070            vec!["incoming".to_string()],
13071        ))
13072        .expect("tool registration succeeds");
13073        sub.add_resource(NamedResource::new("unique://resource"));
13074        sub.add_resource_template(marked_template("unique://{item}", "incoming"));
13075        sub.add_prompt(NamedPrompt::new("unique_prompt"));
13076
13077        let result = main.mount_with_behavior(sub, None, crate::DuplicateBehavior::Error);
13078        assert!(!result.is_success());
13079        assert_eq!(result.errors.len(), 1);
13080        assert!(!result.has_components());
13081        assert_eq!(main.tools_count(), 1);
13082        assert_eq!(main.resources_count(), 0);
13083        assert_eq!(main.resource_templates_count(), 0);
13084        assert_eq!(main.prompts_count(), 0);
13085        assert_eq!(
13086            main.get_tool("conflict").unwrap().definition().tags,
13087            vec!["original".to_string()]
13088        );
13089    }
13090
13091    #[test]
13092    fn invalid_prefix_rejects_every_partial_mount_without_mutation() {
13093        let mut tools = Router::new();
13094        let mut tool_source = Router::new();
13095        tool_source
13096            .add_tool(NamedTool::new("tool"))
13097            .expect("tool registration succeeds");
13098        let tool_result = tools.mount_tools_with_behavior(
13099            tool_source,
13100            Some("peer/secret"),
13101            crate::DuplicateBehavior::Replace,
13102        );
13103        assert!(!tool_result.is_success());
13104        assert_eq!(tools.tools_count(), 0);
13105
13106        let mut resources = Router::new();
13107        let mut resource_source = Router::new();
13108        resource_source.add_resource(NamedResource::new("resource://value"));
13109        resource_source.add_resource_template(marked_template("template://{value}", "incoming"));
13110        let resource_result = resources.mount_resources_with_behavior(
13111            resource_source,
13112            Some("peer/secret"),
13113            crate::DuplicateBehavior::Replace,
13114        );
13115        assert!(!resource_result.is_success());
13116        assert_eq!(resources.resources_count(), 0);
13117        assert_eq!(resources.resource_templates_count(), 0);
13118
13119        let mut prompts = Router::new();
13120        let mut prompt_source = Router::new();
13121        prompt_source.add_prompt(NamedPrompt::new("prompt"));
13122        let prompt_result = prompts.mount_prompts_with_behavior(
13123            prompt_source,
13124            Some("peer/secret"),
13125            crate::DuplicateBehavior::Replace,
13126        );
13127        assert!(!prompt_result.is_success());
13128        assert_eq!(prompts.prompts_count(), 0);
13129
13130        for result in [tool_result, resource_result, prompt_result] {
13131            assert_eq!(result.errors.len(), 1);
13132            assert!(!result.errors[0].contains("peer/secret"));
13133            assert!(!result.has_components());
13134        }
13135    }
13136
13137    // ── mount_tools / mount_resources / mount_prompts ──────────────────
13138
13139    #[test]
13140    fn mount_tools_only() {
13141        let mut main = Router::new();
13142        let mut sub = Router::new();
13143        sub.add_tool(NamedTool::new("t1"))
13144            .expect("tool registration succeeds");
13145        sub.add_prompt(NamedPrompt::new("p1"));
13146        let result = main.mount_tools(sub, Some("ns"));
13147        assert_eq!(result.tools, 1);
13148        assert!(main.get_tool("ns/t1").is_some());
13149        assert_eq!(main.prompts_count(), 0); // prompts not mounted
13150    }
13151
13152    #[test]
13153    fn mount_prompts_only() {
13154        let mut main = Router::new();
13155        let mut sub = Router::new();
13156        sub.add_tool(NamedTool::new("t1"))
13157            .expect("tool registration succeeds");
13158        sub.add_prompt(NamedPrompt::new("p1"));
13159        let result = main.mount_prompts(sub, Some("ns"));
13160        assert_eq!(result.prompts, 1);
13161        assert!(main.get_prompt("ns/p1").is_some());
13162        assert_eq!(main.tools_count(), 0); // tools not mounted
13163    }
13164
13165    // ── handle_tools_list pagination ───────────────────────────────────
13166
13167    #[test]
13168    fn handle_tools_list_no_pagination() {
13169        let mut r = Router::new();
13170        r.add_tool(NamedTool::new("a"))
13171            .expect("tool registration succeeds");
13172        r.add_tool(NamedTool::new("b"))
13173            .expect("tool registration succeeds");
13174        let cx = Cx::for_testing();
13175        let params = ListToolsParams {
13176            cursor: None,
13177            include_tags: None,
13178            exclude_tags: None,
13179        };
13180        let request_ctx = McpContext::new(cx, 1);
13181        let result = r.handle_tools_list(&request_ctx, params, None).unwrap();
13182        assert_eq!(result.tools.len(), 2);
13183        assert!(result.next_cursor.is_none());
13184    }
13185
13186    #[test]
13187    fn handle_tools_list_with_pagination() {
13188        let mut r = Router::new();
13189        r.set_list_page_size(Some(1));
13190        r.add_tool(NamedTool::new("a"))
13191            .expect("tool registration succeeds");
13192        r.add_tool(NamedTool::new("b"))
13193            .expect("tool registration succeeds");
13194        let cx = Cx::for_testing();
13195        let request_ctx = McpContext::new(cx, 1);
13196
13197        // First page
13198        let params = ListToolsParams {
13199            cursor: None,
13200            include_tags: None,
13201            exclude_tags: None,
13202        };
13203        let result = r.handle_tools_list(&request_ctx, params, None).unwrap();
13204        assert_eq!(result.tools.len(), 1);
13205        assert_eq!(result.tools[0].name, "a");
13206        assert!(result.next_cursor.is_some());
13207
13208        // Second page
13209        let params = ListToolsParams {
13210            cursor: result.next_cursor,
13211            include_tags: None,
13212            exclude_tags: None,
13213        };
13214        let result = r.handle_tools_list(&request_ctx, params, None).unwrap();
13215        assert_eq!(result.tools.len(), 1);
13216        assert_eq!(result.tools[0].name, "b");
13217        assert!(result.next_cursor.is_none());
13218    }
13219
13220    #[test]
13221    fn handle_tools_list_with_tag_filter() {
13222        let mut r = Router::new();
13223        r.add_tool(NamedTool::with_tags("a", vec!["db".to_string()]))
13224            .expect("tool registration succeeds");
13225        r.add_tool(NamedTool::with_tags("b", vec!["web".to_string()]))
13226            .expect("tool registration succeeds");
13227        let cx = Cx::for_testing();
13228        let params = ListToolsParams {
13229            cursor: None,
13230            include_tags: Some(vec!["db".to_string()]),
13231            exclude_tags: None,
13232        };
13233        let request_ctx = McpContext::new(cx, 1);
13234        let result = r.handle_tools_list(&request_ctx, params, None).unwrap();
13235        assert_eq!(result.tools.len(), 1);
13236        assert_eq!(result.tools[0].name, "a");
13237    }
13238
13239    // ── handle_resources_list pagination ───────────────────────────────
13240
13241    #[test]
13242    fn handle_resources_list_no_pagination() {
13243        let mut r = Router::new();
13244        r.add_resource(NamedResource::new("file:///a"));
13245        let cx = Cx::for_testing();
13246        let params = ListResourcesParams {
13247            cursor: None,
13248            include_tags: None,
13249            exclude_tags: None,
13250        };
13251        let request_ctx = McpContext::new(cx, 1);
13252        let result = r.handle_resources_list(&request_ctx, params, None).unwrap();
13253        assert_eq!(result.resources.len(), 1);
13254        assert!(result.next_cursor.is_none());
13255    }
13256
13257    #[test]
13258    fn handle_resources_list_with_pagination() {
13259        let mut r = Router::new();
13260        r.set_list_page_size(Some(1));
13261        r.add_resource(NamedResource::new("file:///a"));
13262        r.add_resource(NamedResource::new("file:///b"));
13263        let cx = Cx::for_testing();
13264        let params = ListResourcesParams {
13265            cursor: None,
13266            include_tags: None,
13267            exclude_tags: None,
13268        };
13269        let request_ctx = McpContext::new(cx, 1);
13270        let result = r.handle_resources_list(&request_ctx, params, None).unwrap();
13271        assert_eq!(result.resources.len(), 1);
13272        assert!(result.next_cursor.is_some());
13273    }
13274
13275    // ── handle_prompts_list pagination ─────────────────────────────────
13276
13277    #[test]
13278    fn handle_prompts_list_no_pagination() {
13279        let mut r = Router::new();
13280        r.add_prompt(NamedPrompt::new("greet"));
13281        let cx = Cx::for_testing();
13282        let params = ListPromptsParams {
13283            cursor: None,
13284            include_tags: None,
13285            exclude_tags: None,
13286        };
13287        let request_ctx = McpContext::new(cx, 1);
13288        let result = r.handle_prompts_list(&request_ctx, params, None).unwrap();
13289        assert_eq!(result.prompts.len(), 1);
13290        assert!(result.next_cursor.is_none());
13291    }
13292
13293    // ── handle_resource_templates_list ──────────────────────────────────
13294
13295    #[test]
13296    fn handle_resource_templates_list_no_pagination() {
13297        let mut r = Router::new();
13298        r.add_resource_template(ResourceTemplate {
13299            uri_template: "db://{table}".to_string(),
13300            name: "db".to_string(),
13301            description: None,
13302            mime_type: None,
13303            icon: None,
13304            version: None,
13305            tags: vec![],
13306        });
13307        let cx = Cx::for_testing();
13308        let params = ListResourceTemplatesParams {
13309            cursor: None,
13310            include_tags: None,
13311            exclude_tags: None,
13312        };
13313        let request_ctx = McpContext::new(cx, 1);
13314        let result = r
13315            .handle_resource_templates_list(&request_ctx, params, None)
13316            .unwrap();
13317        assert_eq!(result.resource_templates.len(), 1);
13318        assert!(result.next_cursor.is_none());
13319    }
13320
13321    // ── handle_initialize ──────────────────────────────────────────────
13322
13323    #[test]
13324    fn handle_initialize_returns_protocol_version() {
13325        let r = Router::new();
13326        let cx = Cx::for_testing();
13327        let mut session = Session::new(
13328            fastmcp_protocol::ServerInfo {
13329                name: "test".to_string(),
13330                version: "1.0".to_string(),
13331            },
13332            fastmcp_protocol::ServerCapabilities::default(),
13333        );
13334        let params = InitializeParams {
13335            protocol_version: PROTOCOL_VERSION.to_string(),
13336            capabilities: fastmcp_protocol::ClientCapabilities::default(),
13337            client_info: fastmcp_protocol::ClientInfo {
13338                name: "test-client".to_string(),
13339                version: "1.0".to_string(),
13340            },
13341        };
13342        let request_ctx = McpContext::new(cx, 1);
13343        let result = r
13344            .handle_initialize(
13345                &request_ctx,
13346                &mut session,
13347                params,
13348                Some("test instructions"),
13349            )
13350            .unwrap();
13351        assert_eq!(result.protocol_version, PROTOCOL_VERSION);
13352        assert_eq!(result.server_info.name, "test");
13353        assert_eq!(result.instructions.as_deref(), Some("test instructions"));
13354    }
13355
13356    #[test]
13357    fn handle_initialize_no_instructions() {
13358        let r = Router::new();
13359        let cx = Cx::for_testing();
13360        let mut session = Session::new(
13361            fastmcp_protocol::ServerInfo {
13362                name: "srv".to_string(),
13363                version: "0.1".to_string(),
13364            },
13365            fastmcp_protocol::ServerCapabilities::default(),
13366        );
13367        let params = InitializeParams {
13368            protocol_version: PROTOCOL_VERSION.to_string(),
13369            capabilities: fastmcp_protocol::ClientCapabilities::default(),
13370            client_info: fastmcp_protocol::ClientInfo {
13371                name: "c".to_string(),
13372                version: "0.1".to_string(),
13373            },
13374        };
13375        let request_ctx = McpContext::new(cx, 1);
13376        let result = r
13377            .handle_initialize(&request_ctx, &mut session, params, None)
13378            .unwrap();
13379        assert!(result.instructions.is_none());
13380    }
13381
13382    // ── add_resource_with_behavior (Warn / Replace) ─────────────────────
13383
13384    #[test]
13385    fn add_resource_behavior_warn_keeps_original() {
13386        let mut r = Router::new();
13387        r.add_resource(NamedResource::new("file:///a"));
13388        r.add_resource_with_behavior(
13389            NamedResource::new("file:///a"),
13390            crate::DuplicateBehavior::Warn,
13391        )
13392        .unwrap();
13393        assert_eq!(r.resources_count(), 1);
13394    }
13395
13396    #[test]
13397    fn add_resource_behavior_replace() {
13398        let mut r = Router::new();
13399        r.add_resource(NamedResource::new("file:///a"));
13400        r.add_resource_with_behavior(
13401            NamedResource::new("file:///a"),
13402            crate::DuplicateBehavior::Replace,
13403        )
13404        .unwrap();
13405        assert_eq!(r.resources_count(), 1);
13406    }
13407
13408    #[test]
13409    fn add_resource_behavior_new_resource_ok() {
13410        let mut r = Router::new();
13411        r.add_resource_with_behavior(
13412            NamedResource::new("file:///a"),
13413            crate::DuplicateBehavior::Error,
13414        )
13415        .unwrap();
13416        assert_eq!(r.resources_count(), 1);
13417    }
13418
13419    // ── add_prompt_with_behavior (Replace / Ignore / new) ───────────────
13420
13421    #[test]
13422    fn add_prompt_behavior_replace() {
13423        let mut r = Router::new();
13424        r.add_prompt(NamedPrompt::new("p"));
13425        r.add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Replace)
13426            .unwrap();
13427        assert_eq!(r.prompts_count(), 1);
13428    }
13429
13430    #[test]
13431    fn add_prompt_behavior_ignore() {
13432        let mut r = Router::new();
13433        r.add_prompt(NamedPrompt::new("p"));
13434        r.add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Ignore)
13435            .unwrap();
13436        assert_eq!(r.prompts_count(), 1);
13437    }
13438
13439    #[test]
13440    fn add_prompt_behavior_new_prompt_ok() {
13441        let mut r = Router::new();
13442        r.add_prompt_with_behavior(NamedPrompt::new("p"), crate::DuplicateBehavior::Error)
13443            .unwrap();
13444        assert_eq!(r.prompts_count(), 1);
13445    }
13446
13447    // ── add_resource / add_prompt duplicate replace ─────────────────────
13448
13449    #[test]
13450    fn add_resource_replaces_on_duplicate() {
13451        let mut r = Router::new();
13452        r.add_resource(NamedResource::new("file:///a"));
13453        r.add_resource(NamedResource::new("file:///a"));
13454        assert_eq!(r.resources_count(), 1);
13455        assert_eq!(r.resources().len(), 1);
13456    }
13457
13458    #[test]
13459    fn add_prompt_replaces_on_duplicate() {
13460        let mut r = Router::new();
13461        r.add_prompt(NamedPrompt::new("p"));
13462        r.add_prompt(NamedPrompt::new("p"));
13463        assert_eq!(r.prompts_count(), 1);
13464        assert_eq!(r.prompts().len(), 1);
13465    }
13466
13467    // ── resource_exists for template match ──────────────────────────────
13468
13469    #[test]
13470    fn resource_exists_for_template_match() {
13471        struct DbResource;
13472        impl ResourceHandler for DbResource {
13473            fn definition(&self) -> Resource {
13474                Resource {
13475                    uri: "db://placeholder".to_string(),
13476                    name: "db".to_string(),
13477                    description: None,
13478                    mime_type: Some("text/plain".to_string()),
13479                    icon: None,
13480                    version: None,
13481                    tags: vec![],
13482                }
13483            }
13484            fn template(&self) -> Option<ResourceTemplate> {
13485                Some(ResourceTemplate {
13486                    uri_template: "db://{table}".to_string(),
13487                    name: "db".to_string(),
13488                    description: None,
13489                    mime_type: None,
13490                    icon: None,
13491                    version: None,
13492                    tags: vec![],
13493                })
13494            }
13495            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<fastmcp_protocol::ResourceContent>> {
13496                Ok(vec![])
13497            }
13498        }
13499        let mut r = Router::new();
13500        r.add_resource(DbResource);
13501        assert!(r.resource_exists("db://users"));
13502        assert!(!r.resource_exists("file://other"));
13503    }
13504
13505    // ── resources_filtered by tags ──────────────────────────────────────
13506
13507    #[test]
13508    fn resources_filtered_by_tags() {
13509        let mut r = Router::new();
13510        r.add_resource(NamedResource::with_tags(
13511            "file:///a",
13512            vec!["internal".to_string()],
13513        ));
13514        r.add_resource(NamedResource::with_tags(
13515            "file:///b",
13516            vec!["public".to_string()],
13517        ));
13518        let include = vec!["public".to_string()];
13519        let filters = TagFilters::new(Some(&include), None);
13520        let res = r.resources_filtered(None, Some(&filters));
13521        assert_eq!(res.len(), 1);
13522        assert_eq!(res[0].uri, "file:///b");
13523    }
13524
13525    // ── resource_templates_filtered by tags ─────────────────────────────
13526
13527    #[test]
13528    fn resource_templates_filtered_by_tags() {
13529        let mut r = Router::new();
13530        r.add_resource_template(ResourceTemplate {
13531            uri_template: "db://{table}".to_string(),
13532            name: "db".to_string(),
13533            description: None,
13534            mime_type: None,
13535            icon: None,
13536            version: None,
13537            tags: vec!["admin".to_string()],
13538        });
13539        r.add_resource_template(ResourceTemplate {
13540            uri_template: "cache://{key}".to_string(),
13541            name: "cache".to_string(),
13542            description: None,
13543            mime_type: None,
13544            icon: None,
13545            version: None,
13546            tags: vec!["public".to_string()],
13547        });
13548        let exclude = vec!["admin".to_string()];
13549        let filters = TagFilters::new(None, Some(&exclude));
13550        let tmpls = r.resource_templates_filtered(None, Some(&filters));
13551        assert_eq!(tmpls.len(), 1);
13552        assert_eq!(tmpls[0].name, "cache");
13553    }
13554
13555    // ── handle_tools_list with session state ────────────────────────────
13556
13557    #[test]
13558    fn handle_tools_list_with_session_state_filter() {
13559        let mut r = Router::new();
13560        r.add_tool(NamedTool::new("a"))
13561            .expect("tool registration succeeds");
13562        r.add_tool(NamedTool::new("b"))
13563            .expect("tool registration succeeds");
13564        let cx = Cx::for_testing();
13565        let state = SessionState::new();
13566        let disabled: std::collections::HashSet<String> = ["a".to_string()].into_iter().collect();
13567        state.set("fastmcp.disabled_tools", &disabled);
13568        let params = ListToolsParams {
13569            cursor: None,
13570            include_tags: None,
13571            exclude_tags: None,
13572        };
13573        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
13574        let result = r
13575            .handle_tools_list(&request_ctx, params, Some(&state))
13576            .unwrap();
13577        assert_eq!(result.tools.len(), 1);
13578        assert_eq!(result.tools[0].name, "b");
13579    }
13580
13581    // ── handle_resources_list with tag filter ────────────────────────────
13582
13583    #[test]
13584    fn handle_resources_list_with_tag_filter() {
13585        let mut r = Router::new();
13586        r.add_resource(NamedResource::with_tags(
13587            "file:///a",
13588            vec!["db".to_string()],
13589        ));
13590        r.add_resource(NamedResource::with_tags(
13591            "file:///b",
13592            vec!["web".to_string()],
13593        ));
13594        let cx = Cx::for_testing();
13595        let params = ListResourcesParams {
13596            cursor: None,
13597            include_tags: Some(vec!["web".to_string()]),
13598            exclude_tags: None,
13599        };
13600        let request_ctx = McpContext::new(cx, 1);
13601        let result = r.handle_resources_list(&request_ctx, params, None).unwrap();
13602        assert_eq!(result.resources.len(), 1);
13603        assert_eq!(result.resources[0].uri, "file:///b");
13604    }
13605
13606    // ── handle_prompts_list with pagination ──────────────────────────────
13607
13608    #[test]
13609    fn handle_prompts_list_with_pagination() {
13610        let mut r = Router::new();
13611        r.set_list_page_size(Some(1));
13612        r.add_prompt(NamedPrompt::new("a"));
13613        r.add_prompt(NamedPrompt::new("b"));
13614        let cx = Cx::for_testing();
13615        let params = ListPromptsParams {
13616            cursor: None,
13617            include_tags: None,
13618            exclude_tags: None,
13619        };
13620        let request_ctx = McpContext::new(cx, 1);
13621        let result = r.handle_prompts_list(&request_ctx, params, None).unwrap();
13622        assert_eq!(result.prompts.len(), 1);
13623        assert_eq!(result.prompts[0].name, "a");
13624        assert!(result.next_cursor.is_some());
13625
13626        let params = ListPromptsParams {
13627            cursor: result.next_cursor,
13628            include_tags: None,
13629            exclude_tags: None,
13630        };
13631        let result = r.handle_prompts_list(&request_ctx, params, None).unwrap();
13632        assert_eq!(result.prompts.len(), 1);
13633        assert_eq!(result.prompts[0].name, "b");
13634        assert!(result.next_cursor.is_none());
13635    }
13636
13637    // ── handle_prompts_list with tag filter ──────────────────────────────
13638
13639    #[test]
13640    fn handle_prompts_list_with_tag_filter() {
13641        let mut r = Router::new();
13642        r.add_prompt(NamedPrompt::with_tags("a", vec!["internal".to_string()]));
13643        r.add_prompt(NamedPrompt::with_tags("b", vec!["public".to_string()]));
13644        let cx = Cx::for_testing();
13645        let params = ListPromptsParams {
13646            cursor: None,
13647            include_tags: None,
13648            exclude_tags: Some(vec!["internal".to_string()]),
13649        };
13650        let request_ctx = McpContext::new(cx, 1);
13651        let result = r.handle_prompts_list(&request_ctx, params, None).unwrap();
13652        assert_eq!(result.prompts.len(), 1);
13653        assert_eq!(result.prompts[0].name, "b");
13654    }
13655
13656    // ── handle_resource_templates_list with pagination ───────────────────
13657
13658    #[test]
13659    fn handle_resource_templates_list_with_pagination() {
13660        let mut r = Router::new();
13661        r.set_list_page_size(Some(1));
13662        r.add_resource_template(ResourceTemplate {
13663            uri_template: "db://{table}".to_string(),
13664            name: "db".to_string(),
13665            description: None,
13666            mime_type: None,
13667            icon: None,
13668            version: None,
13669            tags: vec![],
13670        });
13671        r.add_resource_template(ResourceTemplate {
13672            uri_template: "cache://{key}".to_string(),
13673            name: "cache".to_string(),
13674            description: None,
13675            mime_type: None,
13676            icon: None,
13677            version: None,
13678            tags: vec![],
13679        });
13680        let cx = Cx::for_testing();
13681        let request_ctx = McpContext::new(cx, 1);
13682        let params = ListResourceTemplatesParams {
13683            cursor: None,
13684            include_tags: None,
13685            exclude_tags: None,
13686        };
13687        let result = r
13688            .handle_resource_templates_list(&request_ctx, params, None)
13689            .unwrap();
13690        assert_eq!(result.resource_templates.len(), 1);
13691        assert!(result.next_cursor.is_some());
13692
13693        let params = ListResourceTemplatesParams {
13694            cursor: result.next_cursor,
13695            include_tags: None,
13696            exclude_tags: None,
13697        };
13698        let result = r
13699            .handle_resource_templates_list(&request_ctx, params, None)
13700            .unwrap();
13701        assert_eq!(result.resource_templates.len(), 1);
13702        assert!(result.next_cursor.is_none());
13703    }
13704
13705    // ── handle_resource_templates_list with tag filter ───────────────────
13706
13707    #[test]
13708    fn handle_resource_templates_list_with_tag_filter() {
13709        let mut r = Router::new();
13710        r.add_resource_template(ResourceTemplate {
13711            uri_template: "db://{table}".to_string(),
13712            name: "db".to_string(),
13713            description: None,
13714            mime_type: None,
13715            icon: None,
13716            version: None,
13717            tags: vec!["admin".to_string()],
13718        });
13719        r.add_resource_template(ResourceTemplate {
13720            uri_template: "cache://{key}".to_string(),
13721            name: "cache".to_string(),
13722            description: None,
13723            mime_type: None,
13724            icon: None,
13725            version: None,
13726            tags: vec!["public".to_string()],
13727        });
13728        let cx = Cx::for_testing();
13729        let params = ListResourceTemplatesParams {
13730            cursor: None,
13731            include_tags: Some(vec!["public".to_string()]),
13732            exclude_tags: None,
13733        };
13734        let request_ctx = McpContext::new(cx, 1);
13735        let result = r
13736            .handle_resource_templates_list(&request_ctx, params, None)
13737            .unwrap();
13738        assert_eq!(result.resource_templates.len(), 1);
13739        assert_eq!(result.resource_templates[0].name, "cache");
13740    }
13741
13742    // ── mount_resources (selective method) ───────────────────────────────
13743
13744    #[test]
13745    fn mount_resources_only() {
13746        let mut main = Router::new();
13747        let mut sub = Router::new();
13748        sub.add_resource(NamedResource::new("file:///a"));
13749        sub.add_tool(NamedTool::new("t1"))
13750            .expect("tool registration succeeds");
13751        sub.add_resource_template(ResourceTemplate {
13752            uri_template: "db://{t}".to_string(),
13753            name: "db".to_string(),
13754            description: None,
13755            mime_type: None,
13756            icon: None,
13757            version: None,
13758            tags: vec![],
13759        });
13760        let result = main.mount_resources(sub, Some("ns"));
13761        assert_eq!(result.resources, 1);
13762        assert_eq!(result.resource_templates, 1);
13763        assert!(main.get_resource("ns/file:///a").is_some());
13764        assert_eq!(main.tools_count(), 0); // tools not mounted
13765    }
13766
13767    // ── MountResult has_components with all fields ──────────────────────
13768
13769    #[test]
13770    fn mount_result_with_resources_has_components() {
13771        let mut r = MountResult::default();
13772        r.resources = 1;
13773        assert!(r.has_components());
13774    }
13775
13776    #[test]
13777    fn mount_result_with_templates_has_components() {
13778        let mut r = MountResult::default();
13779        r.resource_templates = 1;
13780        assert!(r.has_components());
13781    }
13782
13783    #[test]
13784    fn mount_result_with_prompts_has_components() {
13785        let mut r = MountResult::default();
13786        r.prompts = 1;
13787        assert!(r.has_components());
13788    }
13789
13790    #[test]
13791    fn mount_result_is_success_with_warnings() {
13792        let mut r = MountResult::default();
13793        r.warnings.push("something".to_string());
13794        assert!(r.is_success());
13795    }
13796
13797    #[test]
13798    fn mount_result_reports_errors_as_failure() {
13799        let mut result = MountResult::default();
13800        result.errors.push("mount rejected".to_string());
13801        assert!(!result.is_success());
13802        assert!(!result.has_components());
13803    }
13804
13805    // ── mount with all component types ──────────────────────────────────
13806
13807    #[test]
13808    fn mount_all_component_types() {
13809        let mut main = Router::new();
13810        let mut sub = Router::new();
13811        sub.add_tool(NamedTool::new("t1"))
13812            .expect("tool registration succeeds");
13813        sub.add_resource(NamedResource::new("file:///r1"));
13814        sub.add_prompt(NamedPrompt::new("p1"));
13815        sub.add_resource_template(ResourceTemplate {
13816            uri_template: "db://{table}".to_string(),
13817            name: "db".to_string(),
13818            description: None,
13819            mime_type: None,
13820            icon: None,
13821            version: None,
13822            tags: vec![],
13823        });
13824        let result = main.mount(sub, Some("ns"));
13825        assert_eq!(result.tools, 1);
13826        assert_eq!(result.resources, 1);
13827        assert_eq!(result.prompts, 1);
13828        assert_eq!(result.resource_templates, 1);
13829        assert!(result.has_components());
13830        assert!(main.get_tool("ns/t1").is_some());
13831        assert!(main.get_resource("ns/file:///r1").is_some());
13832        assert!(main.get_prompt("ns/p1").is_some());
13833    }
13834
13835    // ── mount resource conflict warnings ────────────────────────────────
13836
13837    #[test]
13838    fn mount_warns_on_resource_conflict() {
13839        let mut main = Router::new();
13840        main.add_resource(NamedResource::new("file:///a"));
13841        let mut sub = Router::new();
13842        sub.add_resource(NamedResource::new("file:///a"));
13843        let result = main.mount(sub, None);
13844        assert!(!result.warnings.is_empty());
13845        assert!(result.warnings[0].contains("Resource"));
13846    }
13847
13848    #[test]
13849    fn mount_warns_on_prompt_conflict() {
13850        let mut main = Router::new();
13851        main.add_prompt(NamedPrompt::new("p"));
13852        let mut sub = Router::new();
13853        sub.add_prompt(NamedPrompt::new("p"));
13854        let result = main.mount(sub, None);
13855        assert!(!result.warnings.is_empty());
13856        assert!(result.warnings[0].contains("Prompt"));
13857    }
13858
13859    // ── TagFilters::clone ───────────────────────────────────────────────
13860
13861    #[test]
13862    fn tag_filters_clone() {
13863        let include = vec!["a".to_string()];
13864        let f = TagFilters::new(Some(&include), None);
13865        let cloned = f.clone();
13866        assert!(cloned.matches(&["a".to_string()]));
13867        assert!(!cloned.matches(&["b".to_string()]));
13868    }
13869
13870    // ── handle_tools_list with pagination AND tags ───────────────────────
13871
13872    #[test]
13873    fn handle_tools_list_pagination_with_tags() {
13874        let mut r = Router::new();
13875        r.set_list_page_size(Some(1));
13876        r.add_tool(NamedTool::with_tags("a", vec!["db".to_string()]))
13877            .expect("tool registration succeeds");
13878        r.add_tool(NamedTool::with_tags("b", vec!["db".to_string()]))
13879            .expect("tool registration succeeds");
13880        r.add_tool(NamedTool::with_tags("c", vec!["web".to_string()]))
13881            .expect("tool registration succeeds");
13882        let cx = Cx::for_testing();
13883        let request_ctx = McpContext::new(cx, 1);
13884
13885        // Only "db" tagged tools, page 1
13886        let params = ListToolsParams {
13887            cursor: None,
13888            include_tags: Some(vec!["db".to_string()]),
13889            exclude_tags: None,
13890        };
13891        let result = r.handle_tools_list(&request_ctx, params, None).unwrap();
13892        assert_eq!(result.tools.len(), 1);
13893        assert_eq!(result.tools[0].name, "a");
13894        assert!(result.next_cursor.is_some());
13895
13896        // Page 2
13897        let params = ListToolsParams {
13898            cursor: result.next_cursor,
13899            include_tags: Some(vec!["db".to_string()]),
13900            exclude_tags: None,
13901        };
13902        let result = r.handle_tools_list(&request_ctx, params, None).unwrap();
13903        assert_eq!(result.tools.len(), 1);
13904        assert_eq!(result.tools[0].name, "b");
13905        assert!(result.next_cursor.is_none());
13906    }
13907
13908    #[test]
13909    fn final_mounted_tools_preserve_admitted_order_before_cursor_pagination() {
13910        let mut router = mounted_tool_router();
13911        let cx = Cx::for_testing();
13912        let state = SessionState::new();
13913        let request_ctx = request_context(&cx, 164, Budget::INFINITE, &state);
13914
13915        let legacy = router
13916            .handle_tools_list(
13917                &request_ctx,
13918                ListToolsParams {
13919                    cursor: None,
13920                    include_tags: Some(vec!["visible".to_owned()]),
13921                    exclude_tags: Some(vec!["excluded".to_owned()]),
13922                },
13923                None,
13924            )
13925            .expect("legacy filtering retains every registered tool");
13926        assert_eq!(
13927            legacy
13928                .tools
13929                .iter()
13930                .map(|tool| tool.name.as_str())
13931                .collect::<Vec<_>>(),
13932            vec!["peer/first", "peer/second"],
13933            "the legacy catalog retains mounted tools in source order"
13934        );
13935
13936        router.set_list_page_size(Some(1));
13937        let first_page = router
13938            .dispatch_stateless(
13939                &request_ctx,
13940                &final_tools_list_request(
13941                    None,
13942                    Some(vec!["visible"]),
13943                    Some(vec!["excluded"]),
13944                    164_i64,
13945                ),
13946            )
13947            .expect("the first final page contains the first admitted entry");
13948        assert_eq!(first_page["tools"][0]["name"], "peer/first");
13949        assert_eq!(first_page["tools"].as_array().map(Vec::len), Some(1));
13950        let cursor = first_page["nextCursor"]
13951            .as_str()
13952            .expect("the first admitted page has a continuation cursor");
13953        let include_tags = vec!["visible".to_owned()];
13954        let exclude_tags = vec!["excluded".to_owned()];
13955        let query = FinalCatalogQuery::from_tag_filters(Some(&include_tags), Some(&exclude_tags));
13956        assert_eq!(
13957            decode_final_catalog_cursor_offset(
13958                Some(cursor),
13959                FinalCatalogKind::Tools,
13960                router.final_catalog_revision,
13961                &query,
13962                2,
13963            )
13964            .expect("cursor is router-generated for this exact final catalog revision"),
13965            1,
13966            "the cursor advances across admitted entries"
13967        );
13968
13969        let query_mismatch = router
13970            .dispatch_stateless(
13971                &request_ctx,
13972                &final_tools_list_request(
13973                    Some(cursor),
13974                    Some(vec!["other"]),
13975                    Some(vec!["excluded"]),
13976                    165_i64,
13977                ),
13978            )
13979            .expect_err("changing only the final list filter rejects the continuation");
13980        assert_eq!(query_mismatch.code, McpErrorCode::InvalidParams);
13981        assert!(query_mismatch.message.contains("query filters"));
13982
13983        let second_page = router
13984            .dispatch_stateless(
13985                &request_ctx,
13986                &final_tools_list_request(
13987                    Some(cursor),
13988                    Some(vec!["visible"]),
13989                    Some(vec!["excluded"]),
13990                    166_i64,
13991                ),
13992            )
13993            .expect("the continuation page keeps admitted source order");
13994        assert_eq!(second_page["tools"][0]["name"], "peer/second");
13995        assert_eq!(second_page["tools"].as_array().map(Vec::len), Some(1));
13996        assert!(
13997            second_page.get("nextCursor").is_none(),
13998            "the second admitted entry terminates the filtered final sequence"
13999        );
14000    }
14001
14002    #[test]
14003    fn final_default_list_validates_every_supplied_catalog_cursor() {
14004        let mut router = Router::new();
14005        router
14006            .add_tool(NamedTool::new("default-cursor-first"))
14007            .expect("first final tool registers");
14008        router
14009            .add_tool(NamedTool::new("default-cursor-second"))
14010            .expect("second final tool registers");
14011        assert!(
14012            router.list_page_size.is_none(),
14013            "the default final list path has pagination disabled"
14014        );
14015
14016        let cx = Cx::for_testing();
14017        let state = SessionState::new();
14018        let request_ctx = request_context(&cx, 167, Budget::INFINITE, &state);
14019        let full = router
14020            .dispatch_stateless(
14021                &request_ctx,
14022                &final_tools_list_request(None, None, None, 167_i64),
14023            )
14024            .expect("a cursor-free default final list returns the full catalog");
14025        assert_eq!(full["tools"].as_array().map(Vec::len), Some(2));
14026        assert!(full.get("nextCursor").is_none());
14027
14028        let query = FinalCatalogQuery::from_tag_filters(None, None);
14029        let valid_cursor = encode_final_catalog_cursor(
14030            FinalCatalogKind::Tools,
14031            router.final_catalog_revision,
14032            &query,
14033            0,
14034        );
14035        let validated_full = router
14036            .dispatch_stateless(
14037                &request_ctx,
14038                &final_tools_list_request(Some(&valid_cursor), None, None, 168_i64),
14039            )
14040            .expect(
14041                "a valid cursor remains admitted while default listing returns the full catalog",
14042            );
14043        assert_eq!(validated_full["tools"].as_array().map(Vec::len), Some(2));
14044        assert!(validated_full.get("nextCursor").is_none());
14045
14046        let stale_revision = router
14047            .final_catalog_revision
14048            .checked_sub(1)
14049            .expect("registered tools advance the final catalog revision");
14050        let stale_cursor =
14051            encode_final_catalog_cursor(FinalCatalogKind::Tools, stale_revision, &query, 0);
14052        let stale = router
14053            .dispatch_stateless(
14054                &request_ctx,
14055                &final_tools_list_request(Some(&stale_cursor), None, None, 169_i64),
14056            )
14057            .expect_err("changing only the revision rejects a default-list cursor");
14058        assert_eq!(stale.code, McpErrorCode::InvalidParams);
14059        assert!(stale.message.contains("stale catalog revision"));
14060
14061        let wrong_kind_cursor = encode_final_catalog_cursor(
14062            FinalCatalogKind::Prompts,
14063            router.final_catalog_revision,
14064            &query,
14065            0,
14066        );
14067        let wrong_kind = router
14068            .dispatch_stateless(
14069                &request_ctx,
14070                &final_tools_list_request(Some(&wrong_kind_cursor), None, None, 170_i64),
14071            )
14072            .expect_err("changing only the kind rejects a default-list cursor");
14073        assert_eq!(wrong_kind.code, McpErrorCode::InvalidParams);
14074        assert!(wrong_kind.message.contains("another list method"));
14075
14076        let other_tags = vec!["other".to_owned()];
14077        let other_query = FinalCatalogQuery::from_tag_filters(Some(&other_tags), None);
14078        let wrong_query_cursor = encode_final_catalog_cursor(
14079            FinalCatalogKind::Tools,
14080            router.final_catalog_revision,
14081            &other_query,
14082            0,
14083        );
14084        let wrong_query = router
14085            .dispatch_stateless(
14086                &request_ctx,
14087                &final_tools_list_request(Some(&wrong_query_cursor), None, None, 171_i64),
14088            )
14089            .expect_err("changing only the filters rejects a default-list cursor");
14090        assert_eq!(wrong_query.code, McpErrorCode::InvalidParams);
14091        assert!(wrong_query.message.contains("query filters"));
14092
14093        let out_of_range_cursor = encode_final_catalog_cursor(
14094            FinalCatalogKind::Tools,
14095            router.final_catalog_revision,
14096            &query,
14097            2,
14098        );
14099        let out_of_range = router
14100            .dispatch_stateless(
14101                &request_ctx,
14102                &final_tools_list_request(Some(&out_of_range_cursor), None, None, 172_i64),
14103            )
14104            .expect_err("changing only the offset rejects a default-list cursor");
14105        assert_eq!(out_of_range.code, McpErrorCode::InvalidParams);
14106        assert!(
14107            out_of_range
14108                .message
14109                .contains("outside the requested catalog page")
14110        );
14111    }
14112
14113    #[test]
14114    fn final_resource_and_prompt_cursors_reject_stale_catalog_revisions() {
14115        fn final_list_request(method: &str, cursor: Option<&str>, id: i64) -> JsonRpcRequest {
14116            let mut params = serde_json::json!({
14117                "_meta": {
14118                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
14119                    "io.modelcontextprotocol/clientCapabilities": {},
14120                },
14121            });
14122            if let Some(cursor) = cursor {
14123                params["cursor"] = serde_json::json!(cursor);
14124            }
14125            JsonRpcRequest::new(method, Some(params), id)
14126        }
14127
14128        let mut router = Router::new();
14129        router.set_list_page_size(Some(1));
14130        router.add_resource(NamedResource::new("file:///cursor-resource-a"));
14131        router.add_resource(NamedResource::new("file:///cursor-resource-b"));
14132        router.add_prompt(NamedPrompt::new("cursor-prompt-a"));
14133        router.add_prompt(NamedPrompt::new("cursor-prompt-b"));
14134
14135        let cx = Cx::for_testing();
14136        let state = SessionState::new();
14137        let request_ctx = request_context(&cx, 166, Budget::INFINITE, &state);
14138
14139        let resource_first = router
14140            .dispatch_stateless(
14141                &request_ctx,
14142                &final_list_request("resources/list", None, 166_i64),
14143            )
14144            .expect("the first final resource page is emitted");
14145        let resource_cursor = resource_first["nextCursor"]
14146            .as_str()
14147            .expect("the first resource page has a continuation")
14148            .to_owned();
14149        let resource_second = router
14150            .dispatch_stateless(
14151                &request_ctx,
14152                &final_list_request("resources/list", Some(&resource_cursor), 167_i64),
14153            )
14154            .expect("an unchanged final resource catalog accepts its cursor");
14155        assert_eq!(
14156            resource_second["resources"][0]["uri"],
14157            "file:///cursor-resource-b"
14158        );
14159
14160        router.add_resource(NamedResource::new("file:///cursor-resource-c"));
14161        let resource_stale = router
14162            .dispatch_stateless(
14163                &request_ctx,
14164                &final_list_request("resources/list", Some(&resource_cursor), 168_i64),
14165            )
14166            .expect_err("adding only one catalog resource invalidates the old continuation");
14167        assert_eq!(resource_stale.code, McpErrorCode::InvalidParams);
14168        assert!(resource_stale.message.contains("stale catalog revision"));
14169
14170        let prompt_first = router
14171            .dispatch_stateless(
14172                &request_ctx,
14173                &final_list_request("prompts/list", None, 169_i64),
14174            )
14175            .expect("the first final prompt page is emitted");
14176        let prompt_cursor = prompt_first["nextCursor"]
14177            .as_str()
14178            .expect("the first prompt page has a continuation")
14179            .to_owned();
14180        let prompt_second = router
14181            .dispatch_stateless(
14182                &request_ctx,
14183                &final_list_request("prompts/list", Some(&prompt_cursor), 170_i64),
14184            )
14185            .expect("an unchanged final prompt catalog accepts its cursor");
14186        assert_eq!(prompt_second["prompts"][0]["name"], "cursor-prompt-b");
14187
14188        router.add_prompt(NamedPrompt::new("cursor-prompt-c"));
14189        let prompt_stale = router
14190            .dispatch_stateless(
14191                &request_ctx,
14192                &final_list_request("prompts/list", Some(&prompt_cursor), 171_i64),
14193            )
14194            .expect_err("adding only one catalog prompt invalidates the old continuation");
14195        assert_eq!(prompt_stale.code, McpErrorCode::InvalidParams);
14196        assert!(prompt_stale.message.contains("stale catalog revision"));
14197    }
14198
14199    #[test]
14200    fn final_mounted_tools_apply_tag_filters_in_admitted_order() {
14201        let router = mounted_tool_router();
14202        let cx = Cx::for_testing();
14203        let state = SessionState::new();
14204        let request_ctx = request_context(&cx, 166, Budget::INFINITE, &state);
14205
14206        let visible = router
14207            .dispatch_stateless(
14208                &request_ctx,
14209                &final_tools_list_request(None, Some(vec!["visible"]), None, 166_i64),
14210            )
14211            .expect("the final include filter projects only admitted visible entries");
14212        assert_eq!(
14213            visible["tools"]
14214                .as_array()
14215                .expect("final tools are an array")
14216                .iter()
14217                .map(|tool| tool["name"].as_str().expect("tool name is a string"))
14218                .collect::<Vec<_>>(),
14219            vec!["peer/first", "peer/second", "peer/excluded"],
14220            "tag filtering preserves admitted insertion order"
14221        );
14222
14223        let other = router
14224            .dispatch_stateless(
14225                &request_ctx,
14226                &final_tools_list_request(None, Some(vec!["other"]), None, 167_i64),
14227            )
14228            .expect("the include filter is evaluated after final admission");
14229        assert_eq!(other["tools"][0]["name"], "peer/other");
14230        assert_eq!(other["tools"].as_array().map(Vec::len), Some(1));
14231    }
14232
14233    // ── handle_resources_list with session state filter ──────────────────
14234
14235    #[test]
14236    fn handle_resources_list_with_session_state_filter() {
14237        let mut r = Router::new();
14238        r.add_resource(NamedResource::new("file:///a"));
14239        r.add_resource(NamedResource::new("file:///b"));
14240        let cx = Cx::for_testing();
14241        let state = SessionState::new();
14242        let disabled: std::collections::HashSet<String> =
14243            ["file:///a".to_string()].into_iter().collect();
14244        state.set("fastmcp.disabled_resources", &disabled);
14245        let params = ListResourcesParams {
14246            cursor: None,
14247            include_tags: None,
14248            exclude_tags: None,
14249        };
14250        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
14251        let result = r
14252            .handle_resources_list(&request_ctx, params, Some(&state))
14253            .unwrap();
14254        assert_eq!(result.resources.len(), 1);
14255        assert_eq!(result.resources[0].uri, "file:///b");
14256    }
14257
14258    // ── handle_prompts_list with session state filter ────────────────────
14259
14260    #[test]
14261    fn handle_prompts_list_with_session_state_filter() {
14262        let mut r = Router::new();
14263        r.add_prompt(NamedPrompt::new("a"));
14264        r.add_prompt(NamedPrompt::new("b"));
14265        let cx = Cx::for_testing();
14266        let state = SessionState::new();
14267        let disabled: std::collections::HashSet<String> = ["a".to_string()].into_iter().collect();
14268        state.set("fastmcp.disabled_prompts", &disabled);
14269        let params = ListPromptsParams {
14270            cursor: None,
14271            include_tags: None,
14272            exclude_tags: None,
14273        };
14274        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
14275        let result = r
14276            .handle_prompts_list(&request_ctx, params, Some(&state))
14277            .unwrap();
14278        assert_eq!(result.prompts.len(), 1);
14279        assert_eq!(result.prompts[0].name, "b");
14280    }
14281
14282    // ── resource_templates_filtered by session + tags combined ───────────
14283
14284    #[test]
14285    fn resource_templates_filtered_session_and_tags_combined() {
14286        let mut r = Router::new();
14287        r.add_resource_template(ResourceTemplate {
14288            uri_template: "db://{table}".to_string(),
14289            name: "db".to_string(),
14290            description: None,
14291            mime_type: None,
14292            icon: None,
14293            version: None,
14294            tags: vec!["admin".to_string()],
14295        });
14296        r.add_resource_template(ResourceTemplate {
14297            uri_template: "cache://{key}".to_string(),
14298            name: "cache".to_string(),
14299            description: None,
14300            mime_type: None,
14301            icon: None,
14302            version: None,
14303            tags: vec!["admin".to_string()],
14304        });
14305        r.add_resource_template(ResourceTemplate {
14306            uri_template: "log://{entry}".to_string(),
14307            name: "log".to_string(),
14308            description: None,
14309            mime_type: None,
14310            icon: None,
14311            version: None,
14312            tags: vec!["public".to_string()],
14313        });
14314        // Disable db template via session state
14315        let state = SessionState::new();
14316        let disabled: std::collections::HashSet<String> =
14317            ["db://{table}".to_string()].into_iter().collect();
14318        state.set("fastmcp.disabled_resources", &disabled);
14319        // Also filter by admin tag
14320        let include = vec!["admin".to_string()];
14321        let filters = TagFilters::new(Some(&include), None);
14322        let tmpls = r.resource_templates_filtered(Some(&state), Some(&filters));
14323        // db is disabled, log doesn't have admin tag => only cache
14324        assert_eq!(tmpls.len(), 1);
14325        assert_eq!(tmpls[0].name, "cache");
14326    }
14327
14328    // ── mount_tools warns on template conflict ──────────────────────────
14329
14330    #[test]
14331    fn mount_resource_template_warns_on_conflict() {
14332        let mut main = Router::new();
14333        main.add_resource_template(ResourceTemplate {
14334            uri_template: "db://{table}".to_string(),
14335            name: "db".to_string(),
14336            description: None,
14337            mime_type: None,
14338            icon: None,
14339            version: None,
14340            tags: vec![],
14341        });
14342        let mut sub = Router::new();
14343        sub.add_resource_template(ResourceTemplate {
14344            uri_template: "db://{table}".to_string(),
14345            name: "db2".to_string(),
14346            description: None,
14347            mime_type: None,
14348            icon: None,
14349            version: None,
14350            tags: vec![],
14351        });
14352        let result = main.mount(sub, None);
14353        assert!(!result.warnings.is_empty());
14354        assert!(result.warnings[0].contains("Resource template"));
14355    }
14356
14357    // ── handle_tools_call: tool disabled via session ─────────────────────
14358
14359    #[test]
14360    fn handle_tools_call_disabled_tool_returns_error() {
14361        let mut r = Router::new();
14362        r.add_tool(NamedTool::new("my_tool"))
14363            .expect("tool registration succeeds");
14364        let cx = Cx::for_testing();
14365        let budget = Budget::INFINITE;
14366        let state = SessionState::new();
14367        let disabled: std::collections::HashSet<String> =
14368            ["my_tool".to_string()].into_iter().collect();
14369        state.set("fastmcp.disabled_tools", &disabled);
14370        let params = CallToolParams {
14371            name: "my_tool".to_string(),
14372            arguments: None,
14373            meta: None,
14374        };
14375        let request_ctx = request_context(&cx, 1, budget, &state);
14376        let err = r
14377            .handle_tools_call(&request_ctx, params, state, None, None)
14378            .unwrap_err();
14379        assert!(err.message.contains("disabled"));
14380    }
14381
14382    // ── handle_tools_call: success path ──────────────────────────────────
14383
14384    #[test]
14385    fn handle_tools_call_success() {
14386        let mut r = Router::new();
14387        r.add_tool(NamedTool::new("echo"))
14388            .expect("tool registration succeeds");
14389        let cx = Cx::for_testing();
14390        let budget = Budget::INFINITE;
14391        let params = CallToolParams {
14392            name: "echo".to_string(),
14393            arguments: None,
14394            meta: None,
14395        };
14396        let state = SessionState::new();
14397        let request_ctx = request_context(&cx, 1, budget, &state);
14398        let result = r
14399            .handle_tools_call(&request_ctx, params, state, None, None)
14400            .unwrap();
14401        assert!(!result.is_error);
14402        assert!(!result.content.is_empty());
14403    }
14404
14405    // ── handle_tools_call: not found ─────────────────────────────────────
14406
14407    #[test]
14408    fn handle_tools_call_not_found() {
14409        let r = Router::new();
14410        let cx = Cx::for_testing();
14411        let budget = Budget::INFINITE;
14412        let params = CallToolParams {
14413            name: "missing".to_string(),
14414            arguments: None,
14415            meta: None,
14416        };
14417        let state = SessionState::new();
14418        let request_ctx = request_context(&cx, 1, budget, &state);
14419        let err = r
14420            .handle_tools_call(&request_ctx, params, state, None, None)
14421            .unwrap_err();
14422        // The refusal deliberately does not echo the peer-controlled tool
14423        // name; only the sanitized method-not-found classification surfaces.
14424        assert_eq!(err.code, McpErrorCode::MethodNotFound);
14425        assert!(!err.message.contains("missing"));
14426    }
14427
14428    // ── handle_tools_call: zero poll balance without poll admission ──────
14429
14430    #[test]
14431    fn handle_tools_call_zero_poll_balance_allows_handler_without_checkpoint() {
14432        let mut r = Router::new();
14433        r.add_tool(NamedTool::new("t"))
14434            .expect("tool registration succeeds");
14435        let cx = Cx::for_testing();
14436        let budget = Budget::unlimited().with_poll_quota(0);
14437        let params = CallToolParams {
14438            name: "t".to_string(),
14439            arguments: None,
14440            meta: None,
14441        };
14442        let state = SessionState::new();
14443        let request_ctx = request_context(&cx, 1, budget, &state);
14444        let result = r
14445            .handle_tools_call(&request_ctx, params, state, None, None)
14446            .expect("a zero balance is not a retroactive failure");
14447        assert!(!result.is_error);
14448    }
14449
14450    #[test]
14451    fn handle_tools_call_defers_pending_cancellation_inside_context_mask() {
14452        let mut router = Router::new();
14453        router
14454            .add_tool(NamedTool::new("t"))
14455            .expect("tool registration succeeds");
14456        let cx = Cx::for_testing();
14457        cx.set_cancel_requested(true);
14458        let state = SessionState::new();
14459        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
14460        let params = || CallToolParams {
14461            name: "t".to_string(),
14462            arguments: None,
14463            meta: None,
14464        };
14465
14466        let masked_result = request_ctx
14467            .masked(|| router.handle_tools_call(&request_ctx, params(), state.clone(), None, None))
14468            .expect("mask should be admitted");
14469        assert!(masked_result.is_ok());
14470
14471        let unmasked_error = router
14472            .handle_tools_call(&request_ctx, params(), state, None, None)
14473            .expect_err("pending cancellation should surface after mask exit");
14474        assert_eq!(unmasked_error.code, McpErrorCode::RequestCancelled);
14475    }
14476
14477    // ── handle_resources_read: resource disabled via session ──────────────
14478
14479    #[test]
14480    fn handle_resources_read_disabled_resource_returns_error() {
14481        let mut r = Router::new();
14482        r.add_resource(NamedResource::new("file:///secret"));
14483        let cx = Cx::for_testing();
14484        let budget = Budget::INFINITE;
14485        let state = SessionState::new();
14486        let disabled: std::collections::HashSet<String> =
14487            ["file:///secret".to_string()].into_iter().collect();
14488        state.set("fastmcp.disabled_resources", &disabled);
14489        let params = ReadResourceParams {
14490            uri: "file:///secret".to_string(),
14491            meta: None,
14492        };
14493        let request_ctx = request_context(&cx, 1, budget, &state);
14494        let err = r
14495            .handle_resources_read(&request_ctx, &params, state, None, None)
14496            .unwrap_err();
14497        assert!(err.message.contains("disabled"));
14498    }
14499
14500    // ── handle_resources_read: success path ──────────────────────────────
14501
14502    #[test]
14503    fn handle_resources_read_success() {
14504        let mut r = Router::new();
14505        r.add_resource(NamedResource::new("file:///a"));
14506        let cx = Cx::for_testing();
14507        let budget = Budget::INFINITE;
14508        let params = ReadResourceParams {
14509            uri: "file:///a".to_string(),
14510            meta: None,
14511        };
14512        let state = SessionState::new();
14513        let request_ctx = request_context(&cx, 1, budget, &state);
14514        let result = r
14515            .handle_resources_read(&request_ctx, &params, state, None, None)
14516            .unwrap();
14517        assert_eq!(result.contents.len(), 1);
14518        let [
14519            LegacyResourceContent::Text {
14520                uri,
14521                text,
14522                additional,
14523                ..
14524            },
14525        ] = result.contents.as_slice()
14526        else {
14527            panic!("resource read must retain the exact legacy text shape");
14528        };
14529        assert_eq!(uri, "file:///a");
14530        assert_eq!(text, "content");
14531        assert!(additional.is_empty());
14532    }
14533
14534    // ── handle_resources_read: not found ─────────────────────────────────
14535
14536    #[test]
14537    fn handle_resources_read_not_found() {
14538        let r = Router::new();
14539        let cx = Cx::for_testing();
14540        let budget = Budget::INFINITE;
14541        let params = ReadResourceParams {
14542            uri: "file:///nonexistent".to_string(),
14543            meta: None,
14544        };
14545        let state = SessionState::new();
14546        let request_ctx = request_context(&cx, 1, budget, &state);
14547        let err = r
14548            .handle_resources_read(&request_ctx, &params, state, None, None)
14549            .unwrap_err();
14550        assert!(err.message.contains("nonexistent") || err.message.contains("not found"));
14551    }
14552
14553    // ── handle_resources_read: zero poll balance without admission ───────
14554
14555    #[test]
14556    fn handle_resources_read_zero_poll_balance_allows_handler_without_checkpoint() {
14557        let mut r = Router::new();
14558        r.add_resource(NamedResource::new("file:///a"));
14559        let cx = Cx::for_testing();
14560        let budget = Budget::unlimited().with_poll_quota(0);
14561        let params = ReadResourceParams {
14562            uri: "file:///a".to_string(),
14563            meta: None,
14564        };
14565        let state = SessionState::new();
14566        let request_ctx = request_context(&cx, 1, budget, &state);
14567        let result = r
14568            .handle_resources_read(&request_ctx, &params, state, None, None)
14569            .expect("a zero balance is not a retroactive failure");
14570        assert_eq!(result.contents.len(), 1);
14571    }
14572
14573    // ── handle_prompts_get: prompt disabled via session ───────────────────
14574
14575    #[test]
14576    fn handle_prompts_get_disabled_prompt_returns_error() {
14577        let mut r = Router::new();
14578        r.add_prompt(NamedPrompt::new("secret_prompt"));
14579        let cx = Cx::for_testing();
14580        let budget = Budget::INFINITE;
14581        let state = SessionState::new();
14582        let disabled: std::collections::HashSet<String> =
14583            ["secret_prompt".to_string()].into_iter().collect();
14584        state.set("fastmcp.disabled_prompts", &disabled);
14585        let params = GetPromptParams {
14586            name: "secret_prompt".to_string(),
14587            arguments: None,
14588            meta: None,
14589        };
14590        let request_ctx = request_context(&cx, 1, budget, &state);
14591        let err = r
14592            .handle_prompts_get(&request_ctx, params, state, None, None)
14593            .unwrap_err();
14594        assert!(err.message.contains("disabled"));
14595    }
14596
14597    // ── handle_prompts_get: success path ─────────────────────────────────
14598
14599    #[test]
14600    fn nested_get_prompt_returns_handler_text_and_refuses_missing() {
14601        let mut router = Router::new();
14602        router.add_prompt(NamedPrompt::new("greet"));
14603        let router = Arc::new(router);
14604        let cx = Cx::for_testing();
14605        let state = SessionState::new();
14606        let request_ctx = McpContext::with_state(cx, 1, state.clone()).with_prompt_caller(
14607            Arc::new(RouterPromptCaller::new(Arc::clone(&router), state)),
14608        );
14609
14610        let missing = block_on(request_ctx.get_prompt("missing", HashMap::new()))
14611            .expect_err("an unknown nested prompt must stay refused");
14612        assert_eq!(missing.code, McpErrorCode::PromptNotFound);
14613
14614        let result = block_on(request_ctx.get_prompt("greet", HashMap::new()))
14615            .expect("a registered nested prompt must complete");
14616        assert_eq!(result.description.as_deref(), Some("Prompt greet"));
14617        assert!(result.messages.is_empty());
14618    }
14619
14620    fn handle_prompts_get_success() {
14621        let mut r = Router::new();
14622        r.add_prompt(NamedPrompt::new("greet"));
14623        let cx = Cx::for_testing();
14624        let budget = Budget::INFINITE;
14625        let params = GetPromptParams {
14626            name: "greet".to_string(),
14627            arguments: None,
14628            meta: None,
14629        };
14630        let state = SessionState::new();
14631        let request_ctx = request_context(&cx, 1, budget, &state);
14632        let result = r
14633            .handle_prompts_get(&request_ctx, params, state, None, None)
14634            .unwrap();
14635        assert!(result.description.is_some());
14636    }
14637
14638    // ── handle_prompts_get: not found ────────────────────────────────────
14639
14640    #[test]
14641    fn handle_prompts_get_not_found() {
14642        let r = Router::new();
14643        let cx = Cx::for_testing();
14644        let budget = Budget::INFINITE;
14645        let params = GetPromptParams {
14646            name: "missing".to_string(),
14647            arguments: None,
14648            meta: None,
14649        };
14650        let state = SessionState::new();
14651        let request_ctx = request_context(&cx, 1, budget, &state);
14652        let err = r
14653            .handle_prompts_get(&request_ctx, params, state, None, None)
14654            .unwrap_err();
14655        assert!(err.message.contains("missing") || err.message.contains("not found"));
14656    }
14657
14658    // ── handle_prompts_get: zero poll balance without admission ──────────
14659
14660    #[test]
14661    fn handle_prompts_get_zero_poll_balance_allows_handler_without_checkpoint() {
14662        let mut r = Router::new();
14663        r.add_prompt(NamedPrompt::new("p"));
14664        let cx = Cx::for_testing();
14665        let budget = Budget::unlimited().with_poll_quota(0);
14666        let params = GetPromptParams {
14667            name: "p".to_string(),
14668            arguments: None,
14669            meta: None,
14670        };
14671        let state = SessionState::new();
14672        let request_ctx = request_context(&cx, 1, budget, &state);
14673        let result = r
14674            .handle_prompts_get(&request_ctx, params, state, None, None)
14675            .expect("a zero balance is not a retroactive failure");
14676        assert!(result.messages.is_empty());
14677    }
14678
14679    #[test]
14680    fn handler_budget_composition_uses_exact_earliest_deadline() {
14681        let now = Time::from_secs(100);
14682        let at = |seconds| Budget::new().with_deadline(Time::from_secs(seconds));
14683        let cases = [
14684            (Budget::INFINITE, Budget::INFINITE, None, None),
14685            (at(110), Budget::INFINITE, None, Some(Time::from_secs(110))),
14686            (Budget::INFINITE, at(115), None, Some(Time::from_secs(115))),
14687            (
14688                at(110),
14689                at(115),
14690                Some(Duration::from_secs(30)),
14691                Some(Time::from_secs(110)),
14692            ),
14693            (
14694                at(140),
14695                at(130),
14696                Some(Duration::from_secs(5)),
14697                Some(Time::from_secs(105)),
14698            ),
14699            (
14700                at(110),
14701                at(115),
14702                Some(Duration::ZERO),
14703                Some(Time::from_secs(110)),
14704            ),
14705            (
14706                at(90),
14707                at(115),
14708                Some(Duration::from_secs(5)),
14709                Some(Time::from_secs(90)),
14710            ),
14711            (
14712                Budget::INFINITE,
14713                Budget::new().with_deadline(Time::from_nanos(u64::MAX - 1)),
14714                Some(Duration::MAX),
14715                Some(Time::from_nanos(u64::MAX - 1)),
14716            ),
14717        ];
14718
14719        for (ambient, request, handler, expected) in cases {
14720            assert_eq!(
14721                compose_handler_budget(ambient, request, handler, now).deadline,
14722                expected
14723            );
14724        }
14725    }
14726
14727    #[test]
14728    fn alternating_tool_resource_recursion_uses_one_effective_depth() {
14729        let calls = Arc::new(AtomicU64::new(0));
14730        let mut router = Router::new();
14731        router
14732            .add_tool(AlternatingTool {
14733                calls: Arc::clone(&calls),
14734            })
14735            .expect("tool registration succeeds");
14736        router.add_resource(AlternatingResource {
14737            calls: Arc::clone(&calls),
14738        });
14739        let router = Arc::new(router);
14740        let cx = Cx::for_testing();
14741        let state = SessionState::new();
14742        let request_ctx = McpContext::with_state(cx, 1, state.clone())
14743            .with_tool_caller(Arc::new(RouterToolCaller::new(
14744                Arc::clone(&router),
14745                state.clone(),
14746            )))
14747            .with_resource_reader(Arc::new(RouterResourceReader::new(
14748                Arc::clone(&router),
14749                state.clone(),
14750            )));
14751
14752        let error = router
14753            .handle_tools_call(
14754                &request_ctx,
14755                CallToolParams {
14756                    name: "alternating_tool".to_string(),
14757                    arguments: None,
14758                    meta: None,
14759                },
14760                state,
14761                None,
14762                None,
14763            )
14764            .expect_err("alternating recursion must stop at the shared depth limit");
14765
14766        assert_eq!(error.code, McpErrorCode::InternalError);
14767        // Internal errors crossing the handler boundary are sanitized, so the
14768        // depth diagnosis never reaches the peer; the call count proves the
14769        // shared limit stopped the alternating recursion.
14770        assert_eq!(error.message, SANITIZED_HANDLER_PANIC_MESSAGE);
14771        assert_eq!(calls.load(Ordering::Relaxed), 11);
14772    }
14773
14774    #[test]
14775    fn nested_tool_resource_call_shares_parent_cost_ledger() {
14776        let remaining_after_parent_debit = Arc::new(AtomicU64::new(u64::MAX));
14777        let remaining_after_nested_debit = Arc::new(AtomicU64::new(u64::MAX));
14778        let remaining_after_nested_read = Arc::new(AtomicU64::new(u64::MAX));
14779        let mut router = Router::new();
14780        router
14781            .add_tool(CostLedgerTool {
14782                remaining_after_parent_debit: Arc::clone(&remaining_after_parent_debit),
14783                remaining_after_nested_read: Arc::clone(&remaining_after_nested_read),
14784            })
14785            .expect("tool registration succeeds");
14786        router.add_resource(CostLedgerResource {
14787            remaining_after_nested_debit: Arc::clone(&remaining_after_nested_debit),
14788        });
14789
14790        let router = Arc::new(router);
14791        let state = SessionState::new();
14792        let cx = Cx::for_testing_with_budget(Budget::new().with_cost_quota(3));
14793        let request_ctx = McpContext::with_state(cx, 77, state.clone())
14794            .with_tool_caller(Arc::new(RouterToolCaller::new(
14795                Arc::clone(&router),
14796                state.clone(),
14797            )))
14798            .with_resource_reader(Arc::new(RouterResourceReader::new(router, state)));
14799
14800        let result = block_on(request_ctx.call_tool("cost_ledger_tool", serde_json::json!({})))
14801            .expect("parent and nested debits fit the shared cost quota");
14802
14803        assert!(!result.is_error);
14804        assert_eq!(remaining_after_parent_debit.load(Ordering::Relaxed), 2);
14805        assert_eq!(remaining_after_nested_debit.load(Ordering::Relaxed), 1);
14806        assert_eq!(remaining_after_nested_read.load(Ordering::Relaxed), 1);
14807        assert_eq!(request_ctx.budget().cost_quota, Some(1));
14808    }
14809
14810    #[test]
14811    fn nested_tool_calls_preserve_framework_terminal_errors() {
14812        let mut router = Router::new();
14813        router
14814            .add_tool(ErrorTool {
14815                name: "nested_cancelled",
14816                code: McpErrorCode::RequestCancelled,
14817            })
14818            .expect("tool registration succeeds");
14819        router
14820            .add_tool(ErrorTool {
14821                name: "nested_internal",
14822                code: McpErrorCode::InternalError,
14823            })
14824            .expect("tool registration succeeds");
14825        router
14826            .add_tool(ErrorTool {
14827                name: "nested_tool_failure",
14828                code: McpErrorCode::ToolExecutionError,
14829            })
14830            .expect("tool registration succeeds");
14831        let router = Arc::new(router);
14832        let cx = Cx::for_testing();
14833        let state = SessionState::new();
14834        let request_ctx = McpContext::with_state(cx, 88, state.clone())
14835            .with_tool_caller(Arc::new(RouterToolCaller::new(Arc::clone(&router), state)));
14836
14837        for (name, expected) in [
14838            ("nested_cancelled", McpErrorCode::RequestCancelled),
14839            ("nested_internal", McpErrorCode::InternalError),
14840        ] {
14841            let error = block_on(request_ctx.call_tool(name, serde_json::json!({})))
14842                .expect_err("framework terminal errors must remain outer failures");
14843            assert_eq!(error.code, expected);
14844        }
14845
14846        let tool_failure =
14847            block_on(request_ctx.call_tool("nested_tool_failure", serde_json::json!({})))
14848                .expect("ordinary tool failures remain protocol-level tool results");
14849        assert!(tool_failure.is_error);
14850    }
14851
14852    #[test]
14853    fn manual_handler_timeout_is_read_exposed_and_enforced() {
14854        let observed_deadline = Arc::new(Mutex::new(None));
14855        let timeout_read = Arc::new(std::sync::atomic::AtomicBool::new(false));
14856        let mut router = Router::new();
14857        // The timeout must outlive dispatch admission (a too-tight deadline
14858        // rejects before the handler starts, proving nothing about read
14859        // exposure) while still expiring before the handler's delay finishes.
14860        router
14861            .add_tool(BudgetProbeTool {
14862                timeout: Some(Duration::from_millis(10)),
14863                delay: Duration::from_millis(100),
14864                observed_deadline: Arc::clone(&observed_deadline),
14865                timeout_read: Arc::clone(&timeout_read),
14866            })
14867            .expect("tool registration succeeds");
14868        let cx = Cx::for_testing();
14869        let state = SessionState::new();
14870        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
14871        let error = router
14872            .handle_tools_call(
14873                &request_ctx,
14874                CallToolParams {
14875                    name: "budget_probe".to_string(),
14876                    arguments: None,
14877                    meta: None,
14878                },
14879                state,
14880                None,
14881                None,
14882            )
14883            .expect_err("the handler deadline must reject a late completion");
14884
14885        assert!(timeout_read.load(Ordering::Relaxed));
14886        assert!(
14887            observed_deadline
14888                .lock()
14889                .unwrap_or_else(std::sync::PoisonError::into_inner)
14890                .is_some()
14891        );
14892        assert_eq!(error.code, McpErrorCode::RequestCancelled);
14893        assert_eq!(error.message, "Request timeout exceeded");
14894        assert!(!cx.is_cancel_requested());
14895    }
14896
14897    #[test]
14898    fn admitted_definition_is_not_requeried_inside_handler_budget() {
14899        let definition_reads = Arc::new(AtomicU64::new(0));
14900        let called = Arc::new(std::sync::atomic::AtomicBool::new(false));
14901        let mut router = Router::new();
14902        router
14903            .add_tool(SlowDefinitionTool {
14904                definition_reads: Arc::clone(&definition_reads),
14905                called: Arc::clone(&called),
14906            })
14907            .expect("the definition snapshot is admitted once");
14908        let cx = Cx::for_testing();
14909        let state = SessionState::new();
14910        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
14911
14912        router
14913            .handle_tools_call(
14914                &request_ctx,
14915                CallToolParams {
14916                    name: "slow_definition".to_string(),
14917                    arguments: None,
14918                    meta: None,
14919                },
14920                state,
14921                None,
14922                None,
14923            )
14924            .expect("dispatch uses the admitted snapshot and reaches the handler");
14925
14926        assert!(called.load(Ordering::Relaxed));
14927        assert_eq!(definition_reads.load(Ordering::Relaxed), 1);
14928    }
14929
14930    #[test]
14931    fn zero_handler_timeout_cannot_relax_request_deadline() {
14932        let observed_deadline = Arc::new(Mutex::new(None));
14933        let timeout_read = Arc::new(std::sync::atomic::AtomicBool::new(false));
14934        let request_deadline = wall_now().saturating_add_nanos(5_000_000_000);
14935        let mut router = Router::new();
14936        router
14937            .add_tool(BudgetProbeTool {
14938                timeout: Some(Duration::ZERO),
14939                delay: Duration::ZERO,
14940                observed_deadline: Arc::clone(&observed_deadline),
14941                timeout_read: Arc::clone(&timeout_read),
14942            })
14943            .expect("tool registration succeeds");
14944        let cx = Cx::for_testing();
14945        let state = SessionState::new();
14946        let request_ctx = request_context(
14947            &cx,
14948            1,
14949            Budget::new().with_deadline(request_deadline),
14950            &state,
14951        );
14952        router
14953            .handle_tools_call(
14954                &request_ctx,
14955                CallToolParams {
14956                    name: "budget_probe".to_string(),
14957                    arguments: None,
14958                    meta: None,
14959                },
14960                state,
14961                None,
14962                None,
14963            )
14964            .expect("zero adds no ceiling but preserves the request deadline");
14965
14966        assert!(timeout_read.load(Ordering::Relaxed));
14967        assert_eq!(
14968            *observed_deadline
14969                .lock()
14970                .unwrap_or_else(std::sync::PoisonError::into_inner),
14971            Some(request_deadline)
14972        );
14973    }
14974
14975    #[test]
14976    fn ambient_deadline_remains_visible_when_server_and_handler_are_looser() {
14977        let observed_deadline = Arc::new(Mutex::new(None));
14978        let timeout_read = Arc::new(std::sync::atomic::AtomicBool::new(false));
14979        let ambient_deadline = wall_now().saturating_add_nanos(2_000_000_000);
14980        let request_deadline = ambient_deadline.saturating_add_nanos(2_000_000_000);
14981        let mut router = Router::new();
14982        router
14983            .add_tool(BudgetProbeTool {
14984                timeout: Some(Duration::from_secs(10)),
14985                delay: Duration::ZERO,
14986                observed_deadline: Arc::clone(&observed_deadline),
14987                timeout_read,
14988            })
14989            .expect("tool registration succeeds");
14990        let cx = Cx::for_testing_with_budget(Budget::new().with_deadline(ambient_deadline));
14991        let state = SessionState::new();
14992        let request_ctx = request_context(
14993            &cx,
14994            1,
14995            Budget::new().with_deadline(request_deadline),
14996            &state,
14997        );
14998        router
14999            .handle_tools_call(
15000                &request_ctx,
15001                CallToolParams {
15002                    name: "budget_probe".to_string(),
15003                    arguments: None,
15004                    meta: None,
15005                },
15006                state,
15007                None,
15008                None,
15009            )
15010            .expect("looser inner limits must preserve the ambient deadline");
15011
15012        assert_eq!(
15013            *observed_deadline
15014                .lock()
15015                .unwrap_or_else(std::sync::PoisonError::into_inner),
15016            Some(ambient_deadline)
15017        );
15018    }
15019
15020    fn assert_sanitized_panic_tool(handler: impl ToolHandler + 'static, name: &str) {
15021        let mut router = Router::new();
15022        router
15023            .add_tool(handler)
15024            .expect("panic-test tool registration succeeds");
15025        let cx = Cx::for_testing();
15026        let state = SessionState::new();
15027        let request_ctx = request_context(&cx, 1, Budget::INFINITE, &state);
15028        let error = router
15029            .handle_tools_call(
15030                &request_ctx,
15031                CallToolParams {
15032                    name: name.to_string(),
15033                    arguments: None,
15034                    meta: None,
15035                },
15036                state,
15037                None,
15038                None,
15039            )
15040            .expect_err("panic must terminate as a sanitized protocol error");
15041        let wire = serde_json::to_string(&error).expect("error serializes");
15042        assert_eq!(error.code, McpErrorCode::InternalError);
15043        assert_eq!(error.message, SANITIZED_HANDLER_PANIC_MESSAGE);
15044        assert_eq!(error.data, None);
15045        assert!(!wire.contains(PANIC_CANARY));
15046        assert!(!wire.contains("Bearer"));
15047        assert!(!wire.contains("secret"));
15048        assert!(!wire.contains("peer-secret"));
15049        assert!(!wire.contains('\u{001b}'));
15050        assert!(wire.len() < 256);
15051        assert!(!cx.is_cancel_requested());
15052    }
15053
15054    #[test]
15055    fn unwinding_string_and_non_string_panics_have_one_fixed_peer_error() {
15056        assert_sanitized_panic_tool(
15057            UnwindingPanicTool {
15058                payload: "Bearer actual-secret".to_string(),
15059                non_string: false,
15060            },
15061            "panic_tool",
15062        );
15063        assert_sanitized_panic_tool(
15064            UnwindingPanicTool {
15065                payload: PANIC_CANARY.to_string(),
15066                non_string: true,
15067            },
15068            "panic_tool",
15069        );
15070    }
15071
15072    #[test]
15073    fn four_valued_panic_payload_is_never_rendered_for_peer() {
15074        assert_sanitized_panic_tool(
15075            OutcomePanicTool(format!("{PANIC_CANARY}{}", "y".repeat(64 * 1024))),
15076            "outcome_panic_tool",
15077        );
15078    }
15079
15080    #[test]
15081    fn opaque_internal_handler_errors_use_the_fixed_peer_contract() {
15082        let cx = Cx::for_testing();
15083
15084        let mut tool_router = Router::new();
15085        tool_router
15086            .add_tool(OpaqueInternalTool)
15087            .expect("tool registration succeeds");
15088        let tool_state = SessionState::new();
15089        let tool_ctx = request_context(&cx, 1, Budget::INFINITE, &tool_state);
15090        let tool_error = tool_router
15091            .handle_tools_call(
15092                &tool_ctx,
15093                CallToolParams {
15094                    name: "opaque_internal_tool".to_string(),
15095                    arguments: None,
15096                    meta: None,
15097                },
15098                tool_state,
15099                None,
15100                None,
15101            )
15102            .expect_err("opaque internal tool errors must remain protocol failures");
15103
15104        let mut resource_router = Router::new();
15105        resource_router.add_resource(OpaqueInternalResource);
15106        let resource_state = SessionState::new();
15107        let resource_ctx = request_context(&cx, 2, Budget::INFINITE, &resource_state);
15108        let resource_error = resource_router
15109            .handle_resources_read(
15110                &resource_ctx,
15111                &ReadResourceParams {
15112                    uri: "opaque://internal".to_string(),
15113                    meta: None,
15114                },
15115                resource_state,
15116                None,
15117                None,
15118            )
15119            .expect_err("opaque internal resource errors must be sanitized");
15120
15121        let mut prompt_router = Router::new();
15122        prompt_router.add_prompt(OpaqueInternalPrompt);
15123        let prompt_state = SessionState::new();
15124        let prompt_ctx = request_context(&cx, 3, Budget::INFINITE, &prompt_state);
15125        let prompt_error = prompt_router
15126            .handle_prompts_get(
15127                &prompt_ctx,
15128                GetPromptParams {
15129                    name: "opaque_internal_prompt".to_string(),
15130                    arguments: None,
15131                    meta: None,
15132                },
15133                prompt_state,
15134                None,
15135                None,
15136            )
15137            .expect_err("opaque internal prompt errors must be sanitized");
15138
15139        for error in [tool_error, resource_error, prompt_error] {
15140            let wire = serde_json::to_string(&error).expect("error serializes");
15141            assert_eq!(error.code, McpErrorCode::InternalError);
15142            assert_eq!(error.message, SANITIZED_HANDLER_PANIC_MESSAGE);
15143            assert_eq!(error.data, None);
15144            assert!(!wire.contains(PANIC_CANARY));
15145            assert!(wire.len() < 256);
15146        }
15147    }
15148
15149    #[test]
15150    fn resource_and_prompt_panics_use_same_sanitized_contract() {
15151        let mut resource_router = Router::new();
15152        resource_router.add_resource(PanicResource);
15153        let resource_cx = Cx::for_testing();
15154        let resource_state = SessionState::new();
15155        let resource_ctx = request_context(&resource_cx, 1, Budget::INFINITE, &resource_state);
15156        let resource_error = resource_router
15157            .handle_resources_read(
15158                &resource_ctx,
15159                &ReadResourceParams {
15160                    uri: "panic://resource".to_string(),
15161                    meta: None,
15162                },
15163                resource_state,
15164                None,
15165                None,
15166            )
15167            .expect_err("resource panic must be sanitized");
15168        assert_eq!(resource_error.message, SANITIZED_HANDLER_PANIC_MESSAGE);
15169
15170        let mut prompt_router = Router::new();
15171        prompt_router.add_prompt(PanicPrompt);
15172        let prompt_cx = Cx::for_testing();
15173        let prompt_state = SessionState::new();
15174        let prompt_ctx = request_context(&prompt_cx, 1, Budget::INFINITE, &prompt_state);
15175        let prompt_error = prompt_router
15176            .handle_prompts_get(
15177                &prompt_ctx,
15178                GetPromptParams {
15179                    name: "panic_prompt".to_string(),
15180                    arguments: None,
15181                    meta: None,
15182                },
15183                prompt_state,
15184                None,
15185                None,
15186            )
15187            .expect_err("prompt panic must be sanitized");
15188        assert_eq!(prompt_error.message, SANITIZED_HANDLER_PANIC_MESSAGE);
15189
15190        for error in [resource_error, prompt_error] {
15191            let wire = serde_json::to_string(&error).expect("error serializes");
15192            assert!(!wire.contains(PANIC_CANARY));
15193            assert!(wire.len() < 256);
15194        }
15195    }
15196
15197    #[test]
15198    fn admitted_tool_definition_is_snapshotted_while_other_list_panics_are_sanitized() {
15199        let cx = Cx::for_testing();
15200        let request_ctx = McpContext::new(cx, 1);
15201
15202        let mut tool_router = Router::new();
15203        tool_router
15204            .add_tool(DefinitionPanicTool(std::sync::atomic::AtomicBool::new(
15205                false,
15206            )))
15207            .expect("the definition hook is read exactly once during admission");
15208        let tools = tool_router
15209            .handle_tools_list(&request_ctx, ListToolsParams::default(), None)
15210            .expect("listing clones the immutable admitted definition snapshot");
15211        assert_eq!(tools.tools.len(), 1);
15212        assert_eq!(tools.tools[0].name, "definition_panic_tool");
15213
15214        let mut resource_router = Router::new();
15215        resource_router.add_resource(DefinitionPanicResource(std::sync::atomic::AtomicBool::new(
15216            false,
15217        )));
15218        let resource_error = resource_router
15219            .handle_resources_list(&request_ctx, ListResourcesParams::default(), None)
15220            .expect_err("resource definition panic must be sanitized");
15221
15222        let mut prompt_router = Router::new();
15223        prompt_router.add_prompt(DefinitionPanicPrompt(std::sync::atomic::AtomicBool::new(
15224            false,
15225        )));
15226        let prompt_error = prompt_router
15227            .handle_prompts_list(&request_ctx, ListPromptsParams::default(), None)
15228            .expect_err("prompt definition panic must be sanitized");
15229
15230        for error in [resource_error, prompt_error] {
15231            let wire = serde_json::to_string(&error).expect("error serializes");
15232            assert_eq!(error.code, McpErrorCode::InternalError);
15233            assert_eq!(error.message, SANITIZED_HANDLER_PANIC_MESSAGE);
15234            assert_eq!(error.data, None);
15235            assert!(!wire.contains(PANIC_CANARY));
15236            assert!(wire.len() < 256);
15237        }
15238    }
15239
15240    // ── add_resource_with_behavior: template resource Error ───────────────
15241
15242    #[test]
15243    fn add_resource_with_behavior_template_error_on_duplicate() {
15244        struct TmplResource;
15245        impl ResourceHandler for TmplResource {
15246            fn definition(&self) -> Resource {
15247                Resource {
15248                    uri: "db://placeholder".to_string(),
15249                    name: "db".to_string(),
15250                    description: None,
15251                    mime_type: None,
15252                    icon: None,
15253                    version: None,
15254                    tags: vec![],
15255                }
15256            }
15257            fn template(&self) -> Option<ResourceTemplate> {
15258                Some(ResourceTemplate {
15259                    uri_template: "db://{table}".to_string(),
15260                    name: "db".to_string(),
15261                    description: None,
15262                    mime_type: None,
15263                    icon: None,
15264                    version: None,
15265                    tags: vec![],
15266                })
15267            }
15268            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
15269                Ok(vec![])
15270            }
15271        }
15272        let mut r = Router::new();
15273        r.add_resource(TmplResource);
15274        let err = r
15275            .add_resource_with_behavior(TmplResource, crate::DuplicateBehavior::Error)
15276            .unwrap_err();
15277        assert!(err.message.contains("already exists"));
15278    }
15279
15280    // ── add_resource_with_behavior: template resource Ignore ─────────────
15281
15282    #[test]
15283    fn add_resource_with_behavior_template_ignore_on_duplicate() {
15284        struct TmplResource2;
15285        impl ResourceHandler for TmplResource2 {
15286            fn definition(&self) -> Resource {
15287                Resource {
15288                    uri: "cache://placeholder".to_string(),
15289                    name: "cache".to_string(),
15290                    description: None,
15291                    mime_type: None,
15292                    icon: None,
15293                    version: None,
15294                    tags: vec![],
15295                }
15296            }
15297            fn template(&self) -> Option<ResourceTemplate> {
15298                Some(ResourceTemplate {
15299                    uri_template: "cache://{key}".to_string(),
15300                    name: "cache".to_string(),
15301                    description: None,
15302                    mime_type: None,
15303                    icon: None,
15304                    version: None,
15305                    tags: vec![],
15306                })
15307            }
15308            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
15309                Ok(vec![])
15310            }
15311        }
15312        let mut r = Router::new();
15313        r.add_resource(TmplResource2);
15314        r.add_resource_with_behavior(TmplResource2, crate::DuplicateBehavior::Ignore)
15315            .unwrap();
15316        assert_eq!(r.resource_templates_count(), 1);
15317    }
15318
15319    // ── add_resource_with_behavior: template resource Warn ───────────────
15320
15321    #[test]
15322    fn add_resource_with_behavior_template_warn_on_duplicate() {
15323        struct TmplResource3;
15324        impl ResourceHandler for TmplResource3 {
15325            fn definition(&self) -> Resource {
15326                Resource {
15327                    uri: "log://placeholder".to_string(),
15328                    name: "log".to_string(),
15329                    description: None,
15330                    mime_type: None,
15331                    icon: None,
15332                    version: None,
15333                    tags: vec![],
15334                }
15335            }
15336            fn template(&self) -> Option<ResourceTemplate> {
15337                Some(ResourceTemplate {
15338                    uri_template: "log://{entry}".to_string(),
15339                    name: "log".to_string(),
15340                    description: None,
15341                    mime_type: None,
15342                    icon: None,
15343                    version: None,
15344                    tags: vec![],
15345                })
15346            }
15347            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
15348                Ok(vec![])
15349            }
15350        }
15351        let mut r = Router::new();
15352        r.add_resource(TmplResource3);
15353        r.add_resource_with_behavior(TmplResource3, crate::DuplicateBehavior::Warn)
15354            .unwrap();
15355        assert_eq!(r.resource_templates_count(), 1);
15356    }
15357
15358    // ── mount_tools warns on conflict ────────────────────────────────
15359
15360    #[test]
15361    fn mount_tools_warns_on_tool_conflict() {
15362        let mut main = Router::new();
15363        main.add_tool(NamedTool::new("t"))
15364            .expect("tool registration succeeds");
15365        let mut sub = Router::new();
15366        sub.add_tool(NamedTool::new("t"))
15367            .expect("tool registration succeeds");
15368        let result = main.mount_tools(sub, None);
15369        assert!(!result.warnings.is_empty());
15370        assert!(result.warnings[0].contains("Tool"));
15371    }
15372
15373    // ── mount_prompts warns on conflict ──────────────────────────────────
15374
15375    #[test]
15376    fn mount_prompts_warns_on_prompt_conflict() {
15377        let mut main = Router::new();
15378        main.add_prompt(NamedPrompt::new("p"));
15379        let mut sub = Router::new();
15380        sub.add_prompt(NamedPrompt::new("p"));
15381        let result = main.mount_prompts(sub, None);
15382        assert!(!result.warnings.is_empty());
15383        assert!(result.warnings[0].contains("Prompt"));
15384    }
15385
15386    // ── invalid cursor returns error ─────────────────────────────────────
15387
15388    #[test]
15389    fn invalid_cursor_returns_error() {
15390        let mut r = Router::new();
15391        r.set_list_page_size(Some(1));
15392        r.add_tool(NamedTool::new("a"))
15393            .expect("tool registration succeeds");
15394        let cx = Cx::for_testing();
15395        let params = ListToolsParams {
15396            cursor: Some("not-valid-base64!!!".to_string()),
15397            include_tags: None,
15398            exclude_tags: None,
15399        };
15400        let request_ctx = McpContext::new(cx, 1);
15401        let err = r.handle_tools_list(&request_ctx, params, None).unwrap_err();
15402        assert!(err.message.contains("cursor") || err.message.contains("Invalid"));
15403    }
15404
15405    // ── set_list_page_size zero is treated as None ───────────────────────
15406
15407    #[test]
15408    fn set_list_page_size_zero_disables_pagination() {
15409        let mut r = Router::new();
15410        r.set_list_page_size(Some(0));
15411        r.add_tool(NamedTool::new("a"))
15412            .expect("tool registration succeeds");
15413        r.add_tool(NamedTool::new("b"))
15414            .expect("tool registration succeeds");
15415        let cx = Cx::for_testing();
15416        let params = ListToolsParams {
15417            cursor: None,
15418            include_tags: None,
15419            exclude_tags: None,
15420        };
15421        let request_ctx = McpContext::new(cx, 1);
15422        let result = r.handle_tools_list(&request_ctx, params, None).unwrap();
15423        // With page_size = 0, all items returned (no pagination)
15424        assert_eq!(result.tools.len(), 2);
15425        assert!(result.next_cursor.is_none());
15426    }
15427
15428    // ── strict_input_validation getter ───────────────────────────────────
15429
15430    #[test]
15431    fn strict_input_validation_toggle() {
15432        let mut r = Router::new();
15433        assert!(!r.strict_input_validation());
15434        r.set_strict_input_validation(true);
15435        assert!(r.strict_input_validation());
15436        r.set_strict_input_validation(false);
15437        assert!(!r.strict_input_validation());
15438    }
15439
15440    // ── cx-cancelled early return paths ──────────────────────────────────
15441
15442    #[test]
15443    fn handle_tools_call_cancelled_cx_returns_error() {
15444        let mut r = Router::new();
15445        r.add_tool(NamedTool::new("t"))
15446            .expect("tool registration succeeds");
15447        let cx = Cx::for_testing();
15448        cx.set_cancel_requested(true);
15449        let budget = Budget::INFINITE;
15450        let params = CallToolParams {
15451            name: "t".to_string(),
15452            arguments: None,
15453            meta: None,
15454        };
15455        let state = SessionState::new();
15456        let request_ctx = request_context(&cx, 1, budget, &state);
15457        let err = r
15458            .handle_tools_call(&request_ctx, params, state, None, None)
15459            .unwrap_err();
15460        assert_eq!(err.code, McpErrorCode::RequestCancelled);
15461    }
15462
15463    #[test]
15464    fn handle_resources_read_cancelled_cx_returns_error() {
15465        let mut r = Router::new();
15466        r.add_resource(NamedResource::new("file:///a.txt"));
15467        let cx = Cx::for_testing();
15468        cx.set_cancel_requested(true);
15469        let budget = Budget::INFINITE;
15470        let params = ReadResourceParams {
15471            uri: "file:///a.txt".to_string(),
15472            meta: None,
15473        };
15474        let state = SessionState::new();
15475        let request_ctx = request_context(&cx, 1, budget, &state);
15476        let err = r
15477            .handle_resources_read(&request_ctx, &params, state, None, None)
15478            .unwrap_err();
15479        assert_eq!(err.code, McpErrorCode::RequestCancelled);
15480    }
15481
15482    #[test]
15483    fn handle_prompts_get_cancelled_cx_returns_error() {
15484        let mut r = Router::new();
15485        r.add_prompt(NamedPrompt::new("p"));
15486        let cx = Cx::for_testing();
15487        cx.set_cancel_requested(true);
15488        let budget = Budget::INFINITE;
15489        let params = GetPromptParams {
15490            name: "p".to_string(),
15491            arguments: None,
15492            meta: None,
15493        };
15494        let state = SessionState::new();
15495        let request_ctx = request_context(&cx, 1, budget, &state);
15496        let err = r
15497            .handle_prompts_get(&request_ctx, params, state, None, None)
15498            .unwrap_err();
15499        assert_eq!(err.code, McpErrorCode::RequestCancelled);
15500    }
15501
15502    #[test]
15503    fn completion_handler_dispatches_exact_legacy_and_final_contracts() {
15504        let mut router = Router::new();
15505        assert!(!router.has_completion_handler());
15506        router.add_completion_handler(EchoCompletion);
15507        router.add_prompt(PromptArgumentBoundary {
15508            final_calls: Arc::new(AtomicUsize::new(0)),
15509            legacy_calls: Arc::new(AtomicUsize::new(0)),
15510        });
15511        assert!(router.has_completion_handler());
15512        assert!(
15513            router
15514                .server_discovery_behavior_registry()
15515                .contains(ServerBehavior::CompletionComplete),
15516            "discovery advertises completion only after the handler is installed"
15517        );
15518
15519        let cx = Cx::for_testing();
15520        let state = SessionState::new();
15521        let request_ctx = request_context(&cx, 87, Budget::INFINITE, &state);
15522        let legacy_request = JsonRpcRequest::new(
15523            COMPLETION_COMPLETE,
15524            Some(serde_json::json!({
15525                "ref": {"type": "ref/prompt", "name": "prompt-argument-boundary"},
15526                "argument": {"name": "topic", "value": "sta"},
15527            })),
15528            87_i64,
15529        );
15530        let legacy = router
15531            .dispatch_legacy_completion(&request_ctx, &legacy_request)
15532            .expect("the exact legacy request reaches the registered completion handler");
15533        assert!(
15534            legacy.get("resultType").is_none(),
15535            "the exact legacy completion result remains discriminator-free"
15536        );
15537        assert_eq!(
15538            legacy["completion"]["values"],
15539            serde_json::json!(["staging"])
15540        );
15541
15542        let modern = router
15543            .dispatch_stateless(
15544                &request_ctx,
15545                &JsonRpcRequest::new(
15546                    COMPLETION_COMPLETE,
15547                    Some(serde_json::json!({
15548                        "_meta": {
15549                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
15550                            "io.modelcontextprotocol/clientCapabilities": {},
15551                        },
15552                        "ref": {"type": "ref/prompt", "name": "prompt-argument-boundary"},
15553                        "argument": {"name": "topic", "value": "sta"},
15554                    })),
15555                    88_i64,
15556                ),
15557            )
15558            .expect("the final request reaches the same registered completion handler");
15559        assert_eq!(
15560            modern.get("resultType"),
15561            Some(&serde_json::json!("complete"))
15562        );
15563        assert_eq!(modern.get("completion"), legacy.get("completion"));
15564    }
15565
15566    #[test]
15567    fn final_completion_routes_to_the_registered_prompt_or_resource_provider() {
15568        let prompt_calls = Arc::new(AtomicUsize::new(0));
15569        let resource_calls = Arc::new(AtomicUsize::new(0));
15570        let fallback_calls = Arc::new(AtomicUsize::new(0));
15571        let mut router = Router::new();
15572        router.add_completion_handler(ProviderCompletion {
15573            value: "fallback-provider",
15574            final_calls: Arc::clone(&fallback_calls),
15575        });
15576        router.add_prompt(PromptArgumentBoundary {
15577            final_calls: Arc::new(AtomicUsize::new(0)),
15578            legacy_calls: Arc::new(AtomicUsize::new(0)),
15579        });
15580        router.add_resource_template(marked_template("resource://first/{id}", "first"));
15581        router.add_resource_template(marked_template("resource://second/{id}", "second"));
15582        router.add_prompt_completion_handler(
15583            "prompt-argument-boundary",
15584            ProviderCompletion {
15585                value: "prompt-provider",
15586                final_calls: Arc::clone(&prompt_calls),
15587            },
15588        );
15589        router.add_resource_template_completion_handler(
15590            "resource://first/{id}",
15591            ProviderCompletion {
15592                value: "resource-provider",
15593                final_calls: Arc::clone(&resource_calls),
15594            },
15595        );
15596
15597        assert!(
15598            router
15599                .server_discovery_behavior_registry()
15600                .contains(ServerBehavior::CompletionComplete),
15601            "a registered final completion provider enables discovery"
15602        );
15603
15604        let cx = Cx::for_testing();
15605        let state = SessionState::new();
15606        let request_ctx = request_context(&cx, 187, Budget::INFINITE, &state);
15607        let metadata = serde_json::json!({
15608            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
15609            "io.modelcontextprotocol/clientCapabilities": {},
15610        });
15611        let prompt_request = JsonRpcRequest::new(
15612            COMPLETION_COMPLETE,
15613            Some(serde_json::json!({
15614                "_meta": metadata.clone(),
15615                "ref": {"type": "ref/prompt", "name": "prompt-argument-boundary"},
15616                "argument": {"name": "topic", "value": "pro"},
15617            })),
15618            187_i64,
15619        );
15620        let resource_request = JsonRpcRequest::new(
15621            COMPLETION_COMPLETE,
15622            Some(serde_json::json!({
15623                "_meta": metadata,
15624                "ref": {"type": "ref/resource", "uri": "resource://first/{id}"},
15625                "argument": {"name": "id", "value": "pro"},
15626            })),
15627            188_i64,
15628        );
15629
15630        let prompt = router
15631            .dispatch_stateless(&request_ctx, &prompt_request)
15632            .expect("the registered prompt provider handles its exact target");
15633        let resource = router
15634            .dispatch_stateless(&request_ctx, &resource_request)
15635            .expect("the registered resource provider handles its exact target");
15636        assert_eq!(
15637            prompt["completion"]["values"],
15638            serde_json::json!(["prompt-provider"])
15639        );
15640        assert_eq!(
15641            resource["completion"]["values"],
15642            serde_json::json!(["resource-provider"])
15643        );
15644        assert_eq!(prompt_calls.load(Ordering::SeqCst), 1);
15645        assert_eq!(resource_calls.load(Ordering::SeqCst), 1);
15646        assert_eq!(
15647            fallback_calls.load(Ordering::SeqCst),
15648            0,
15649            "a target-specific provider takes precedence over the installed fallback"
15650        );
15651
15652        let mut unregistered_provider = resource_request.clone();
15653        unregistered_provider
15654            .params
15655            .as_mut()
15656            .and_then(serde_json::Value::as_object_mut)
15657            .and_then(|params| params.get_mut("ref"))
15658            .and_then(serde_json::Value::as_object_mut)
15659            .expect("completion reference is an object")
15660            .insert(
15661                "uri".to_owned(),
15662                serde_json::json!("resource://second/{id}"),
15663            );
15664        assert_eq!(resource_request.method, unregistered_provider.method);
15665        assert_eq!(resource_request.id, unregistered_provider.id);
15666        assert_eq!(
15667            resource_request
15668                .params
15669                .as_ref()
15670                .and_then(|params| params.get("argument")),
15671            unregistered_provider
15672                .params
15673                .as_ref()
15674                .and_then(|params| params.get("argument")),
15675            "the referenced resource template is the sole planted dimension"
15676        );
15677        let fallback = router
15678            .dispatch_stateless(&request_ctx, &unregistered_provider)
15679            .expect("an admitted target without a provider-specific handler reaches the fallback");
15680        assert_eq!(
15681            fallback["completion"]["values"],
15682            serde_json::json!(["fallback-provider"])
15683        );
15684        assert_eq!(prompt_calls.load(Ordering::SeqCst), 1);
15685        assert_eq!(resource_calls.load(Ordering::SeqCst), 1);
15686        assert_eq!(fallback_calls.load(Ordering::SeqCst), 1);
15687    }
15688
15689    #[test]
15690    fn modern_connection_visibility_blocks_final_completion_before_provider_invocation() {
15691        let final_calls = Arc::new(AtomicUsize::new(0));
15692        let mut router = Router::new();
15693        router.add_completion_handler(CountingCompletion {
15694            final_calls: Arc::clone(&final_calls),
15695        });
15696        router.add_prompt(PromptArgumentBoundary {
15697            final_calls: Arc::new(AtomicUsize::new(0)),
15698            legacy_calls: Arc::new(AtomicUsize::new(0)),
15699        });
15700        const TEMPLATE_URI: &str = "resource://completion-visibility/{id}";
15701        router.add_resource_template(marked_template(TEMPLATE_URI, "completion-visibility"));
15702
15703        let prompt_catalog_before = serde_json::to_vec(&router.prompts())
15704            .expect("prompt catalog serializes before completion dispatch");
15705        let template_catalog_before = serde_json::to_vec(&router.resource_templates())
15706            .expect("resource-template catalog serializes before completion dispatch");
15707        let prompt_request = JsonRpcRequest::new(
15708            COMPLETION_COMPLETE,
15709            Some(serde_json::json!({
15710                "_meta": {
15711                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
15712                    "io.modelcontextprotocol/clientCapabilities": {},
15713                },
15714                "ref": {"type": "ref/prompt", "name": "prompt-argument-boundary"},
15715                "argument": {"name": "topic", "value": "sta"},
15716            })),
15717            1_871_i64,
15718        );
15719        let resource_request = JsonRpcRequest::new(
15720            COMPLETION_COMPLETE,
15721            Some(serde_json::json!({
15722                "_meta": {
15723                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
15724                    "io.modelcontextprotocol/clientCapabilities": {},
15725                },
15726                "ref": {"type": "ref/resource", "uri": TEMPLATE_URI},
15727                "argument": {"name": "id", "value": "sta"},
15728            })),
15729            1_872_i64,
15730        );
15731        let prompt_request_bytes =
15732            serde_json::to_vec(&prompt_request).expect("prompt completion request serializes");
15733        let resource_request_bytes = serde_json::to_vec(&resource_request)
15734            .expect("resource-template completion request serializes");
15735
15736        let cx = Cx::for_testing();
15737        let allowed_connection = ModernConnection::new();
15738        let allowed_inbound = InboundRequestContext::with_modern_connection(
15739            cx.clone(),
15740            1_871,
15741            InboundRequestTransport::Stdio,
15742            &allowed_connection,
15743        );
15744        let allowed_context = allowed_inbound.request_context();
15745        for request in [&prompt_request, &resource_request] {
15746            let result = router
15747                .dispatch_stateless(&allowed_context, request)
15748                .expect("a visible completion target reaches the fallback provider");
15749            assert_eq!(
15750                result["completion"]["values"],
15751                serde_json::json!(["staging"])
15752            );
15753        }
15754        assert_eq!(final_calls.load(Ordering::SeqCst), 2);
15755
15756        let denied_connection = ModernConnection::new();
15757        let denied_inbound = InboundRequestContext::with_modern_connection(
15758            cx,
15759            1_871,
15760            InboundRequestTransport::Stdio,
15761            &denied_connection,
15762        );
15763        let denied_context = denied_inbound.request_context();
15764        assert!(denied_context.disable_prompt("prompt-argument-boundary"));
15765        assert!(denied_context.disable_resource(TEMPLATE_URI));
15766
15767        assert_eq!(
15768            serde_json::to_vec(&prompt_request)
15769                .expect("prompt completion request remains serializable"),
15770            prompt_request_bytes,
15771            "connection visibility is the sole planted prompt-completion dimension"
15772        );
15773        let hidden_prompt = router
15774            .dispatch_stateless(&denied_context, &prompt_request)
15775            .expect_err("a hidden prompt cannot be used as a completion reference");
15776        assert_eq!(hidden_prompt.code, McpErrorCode::InvalidParams);
15777        assert_eq!(
15778            hidden_prompt.message,
15779            "completion prompt reference is not registered"
15780        );
15781
15782        assert_eq!(
15783            serde_json::to_vec(&resource_request)
15784                .expect("resource-template completion request remains serializable"),
15785            resource_request_bytes,
15786            "connection visibility is the sole planted resource-template-completion dimension"
15787        );
15788        let hidden_resource = router
15789            .dispatch_stateless(&denied_context, &resource_request)
15790            .expect_err("a hidden resource template cannot reach the fallback provider");
15791        assert_eq!(hidden_resource.code, McpErrorCode::InvalidParams);
15792        assert_eq!(
15793            hidden_resource.message,
15794            "completion resource reference is not registered"
15795        );
15796        assert_eq!(
15797            final_calls.load(Ordering::SeqCst),
15798            2,
15799            "refused completion references must not invoke the fallback provider"
15800        );
15801        assert_eq!(
15802            serde_json::to_vec(&router.prompts())
15803                .expect("prompt catalog serializes after completion refusal"),
15804            prompt_catalog_before,
15805            "completion refusal cannot mutate the prompt catalog"
15806        );
15807        assert_eq!(
15808            serde_json::to_vec(&router.resource_templates())
15809                .expect("resource-template catalog serializes after completion refusal"),
15810            template_catalog_before,
15811            "completion refusal cannot mutate the resource-template catalog"
15812        );
15813    }
15814
15815    #[test]
15816    fn explicit_legacy_completion_is_not_discovered_or_dispatched_as_final() {
15817        let mut router = Router::new();
15818        router.add_legacy_completion_handler(EchoCompletion);
15819        router.add_prompt(NamedPrompt::new("deploy"));
15820        let cx = Cx::for_testing();
15821        let state = SessionState::new();
15822        let request_ctx = request_context(&cx, 184, Budget::INFINITE, &state);
15823        let request = JsonRpcRequest::new(
15824            COMPLETION_COMPLETE,
15825            Some(serde_json::json!({
15826                "ref": {"type": "ref/prompt", "name": "deploy"},
15827                "argument": {"name": "environment", "value": "sta"},
15828            })),
15829            184_i64,
15830        );
15831        assert!(
15832            router
15833                .dispatch_legacy_completion(&request_ctx, &request)
15834                .is_ok()
15835        );
15836
15837        let mut final_request = request.clone();
15838        final_request
15839            .params
15840            .as_mut()
15841            .and_then(serde_json::Value::as_object_mut)
15842            .expect("completion parameters are an object")
15843            .insert(
15844                "_meta".to_owned(),
15845                serde_json::json!({
15846                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
15847                    "io.modelcontextprotocol/clientCapabilities": {},
15848                }),
15849            );
15850        let error = router
15851            .dispatch_stateless(&request_ctx, &final_request)
15852            .expect_err("only the selected protocol era changes completion availability");
15853        assert_eq!(error.code, McpErrorCode::MethodNotFound);
15854        assert!(
15855            !router
15856                .server_discovery_behavior_registry()
15857                .contains(ServerBehavior::CompletionComplete)
15858        );
15859    }
15860
15861    #[test]
15862    fn final_completion_resource_template_reference_and_argument_are_validated_before_handler() {
15863        let final_calls = Arc::new(AtomicUsize::new(0));
15864        let mut router = Router::new();
15865        router.add_completion_handler(CountingCompletion {
15866            final_calls: Arc::clone(&final_calls),
15867        });
15868        router.add_resource(NamedResource::new("resource://static"));
15869        router.add_resource_template(marked_template("resource://{id}", "registered"));
15870        router
15871            .add_legacy_resource_template(marked_template("resource://{legacy_id}", "legacy-only"));
15872        let cx = Cx::for_testing();
15873        let state = SessionState::new();
15874        let request_ctx = request_context(&cx, 188, Budget::INFINITE, &state);
15875        let baseline = JsonRpcRequest::new(
15876            COMPLETION_COMPLETE,
15877            Some(serde_json::json!({
15878                "_meta": {
15879                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
15880                    "io.modelcontextprotocol/clientCapabilities": {},
15881                },
15882                "ref": {"type": "ref/resource", "uri": "resource://{id}"},
15883                "argument": {"name": "id", "value": "sta"},
15884            })),
15885            188_i64,
15886        );
15887        let templates_before = serde_json::to_vec(&router.resource_templates())
15888            .expect("resource-template catalog serializes");
15889        let template_accepted = router
15890            .dispatch_stateless(&request_ctx, &baseline)
15891            .expect("a registered final resource-template reference is accepted");
15892        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
15893
15894        let mut static_reference = baseline.clone();
15895        static_reference
15896            .params
15897            .as_mut()
15898            .and_then(serde_json::Value::as_object_mut)
15899            .and_then(|params| params.get_mut("ref"))
15900            .and_then(serde_json::Value::as_object_mut)
15901            .expect("completion reference is an object")
15902            .insert("uri".to_owned(), serde_json::json!("resource://static"));
15903        let static_error = router
15904            .dispatch_stateless(&request_ctx, &static_reference)
15905            .expect_err("a static resource is not a final completion-template target");
15906        assert_eq!(static_error.code, McpErrorCode::InvalidParams);
15907        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
15908
15909        let mut legacy_template = baseline.clone();
15910        legacy_template
15911            .params
15912            .as_mut()
15913            .and_then(serde_json::Value::as_object_mut)
15914            .and_then(|params| params.get_mut("ref"))
15915            .and_then(serde_json::Value::as_object_mut)
15916            .expect("completion reference is an object")
15917            .insert(
15918                "uri".to_owned(),
15919                serde_json::json!("resource://{legacy_id}"),
15920            );
15921        assert_eq!(baseline.method, legacy_template.method);
15922        assert_eq!(baseline.id, legacy_template.id);
15923        assert_eq!(
15924            baseline
15925                .params
15926                .as_ref()
15927                .and_then(|params| params.get("argument")),
15928            legacy_template
15929                .params
15930                .as_ref()
15931                .and_then(|params| params.get("argument")),
15932            "the target URI is the sole planted visibility dimension"
15933        );
15934        let legacy_template_error = router
15935            .dispatch_stateless(&request_ctx, &legacy_template)
15936            .expect_err("an exact-2024-only template is not final-visible");
15937        assert_eq!(legacy_template_error.code, McpErrorCode::InvalidParams);
15938        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
15939
15940        let mut unknown_argument = baseline.clone();
15941        unknown_argument
15942            .params
15943            .as_mut()
15944            .and_then(serde_json::Value::as_object_mut)
15945            .and_then(|params| params.get_mut("argument"))
15946            .and_then(serde_json::Value::as_object_mut)
15947            .expect("completion argument is an object")
15948            .insert("name".to_owned(), serde_json::json!("unknown"));
15949        assert_eq!(baseline.method, unknown_argument.method);
15950        assert_eq!(baseline.id, unknown_argument.id);
15951        assert_eq!(
15952            baseline
15953                .params
15954                .as_ref()
15955                .and_then(|params| params.get("ref")),
15956            unknown_argument
15957                .params
15958                .as_ref()
15959                .and_then(|params| params.get("ref")),
15960            "the argument name is the sole planted validation dimension"
15961        );
15962        let unknown_argument_error = router
15963            .dispatch_stateless(&request_ctx, &unknown_argument)
15964            .expect_err("an undeclared template argument is rejected before the handler");
15965        assert_eq!(unknown_argument_error.code, McpErrorCode::InvalidParams);
15966        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
15967        assert_eq!(
15968            serde_json::to_vec(&router.resource_templates())
15969                .expect("resource-template catalog serializes after refusal"),
15970            templates_before,
15971            "completion reference refusal cannot mutate the registered catalog"
15972        );
15973        assert_eq!(
15974            router
15975                .dispatch_stateless(&request_ctx, &baseline)
15976                .expect("the registered reference remains accepted after refusal"),
15977            template_accepted,
15978            "rejected target or argument changes cannot alter the accepted completion"
15979        );
15980        assert_eq!(final_calls.load(Ordering::SeqCst), 2);
15981    }
15982
15983    #[test]
15984    fn final_completion_prompt_reference_and_argument_are_validated_before_handler() {
15985        let final_calls = Arc::new(AtomicUsize::new(0));
15986        let mut router = Router::new();
15987        router.add_completion_handler(CountingCompletion {
15988            final_calls: Arc::clone(&final_calls),
15989        });
15990        router.add_prompt(PromptArgumentBoundary {
15991            final_calls: Arc::new(AtomicUsize::new(0)),
15992            legacy_calls: Arc::new(AtomicUsize::new(0)),
15993        });
15994        router.add_legacy_prompt(NamedPrompt::new("legacy-completion-prompt"));
15995        let cx = Cx::for_testing();
15996        let state = SessionState::new();
15997        let request_ctx = request_context(&cx, 189, Budget::INFINITE, &state);
15998        let baseline = JsonRpcRequest::new(
15999            COMPLETION_COMPLETE,
16000            Some(serde_json::json!({
16001                "_meta": {
16002                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
16003                    "io.modelcontextprotocol/clientCapabilities": {},
16004                },
16005                "ref": {"type": "ref/prompt", "name": "prompt-argument-boundary"},
16006                "argument": {"name": "topic", "value": "sta"},
16007            })),
16008            189_i64,
16009        );
16010        let accepted = router
16011            .dispatch_stateless(&request_ctx, &baseline)
16012            .expect("a final-visible prompt and its declared argument are accepted");
16013        assert_eq!(accepted["resultType"], "complete");
16014        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
16015
16016        let mut legacy_prompt = baseline.clone();
16017        legacy_prompt
16018            .params
16019            .as_mut()
16020            .and_then(serde_json::Value::as_object_mut)
16021            .and_then(|params| params.get_mut("ref"))
16022            .and_then(serde_json::Value::as_object_mut)
16023            .expect("completion reference is an object")
16024            .insert(
16025                "name".to_owned(),
16026                serde_json::json!("legacy-completion-prompt"),
16027            );
16028        assert_eq!(baseline.method, legacy_prompt.method);
16029        assert_eq!(baseline.id, legacy_prompt.id);
16030        assert_eq!(
16031            baseline
16032                .params
16033                .as_ref()
16034                .and_then(|params| params.get("argument")),
16035            legacy_prompt
16036                .params
16037                .as_ref()
16038                .and_then(|params| params.get("argument")),
16039            "the prompt name is the sole planted visibility dimension"
16040        );
16041        let legacy_prompt_error = router
16042            .dispatch_stateless(&request_ctx, &legacy_prompt)
16043            .expect_err("an exact-2024-only prompt is not final-visible");
16044        assert_eq!(legacy_prompt_error.code, McpErrorCode::InvalidParams);
16045        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
16046
16047        let mut unknown_argument = baseline.clone();
16048        unknown_argument
16049            .params
16050            .as_mut()
16051            .and_then(serde_json::Value::as_object_mut)
16052            .and_then(|params| params.get_mut("argument"))
16053            .and_then(serde_json::Value::as_object_mut)
16054            .expect("completion argument is an object")
16055            .insert("name".to_owned(), serde_json::json!("unknown"));
16056        assert_eq!(baseline.method, unknown_argument.method);
16057        assert_eq!(baseline.id, unknown_argument.id);
16058        assert_eq!(
16059            baseline
16060                .params
16061                .as_ref()
16062                .and_then(|params| params.get("ref")),
16063            unknown_argument
16064                .params
16065                .as_ref()
16066                .and_then(|params| params.get("ref")),
16067            "the argument name is the sole planted validation dimension"
16068        );
16069        let unknown_argument_error = router
16070            .dispatch_stateless(&request_ctx, &unknown_argument)
16071            .expect_err("an undeclared prompt argument is rejected before the handler");
16072        assert_eq!(unknown_argument_error.code, McpErrorCode::InvalidParams);
16073        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
16074
16075        let legacy = router
16076            .dispatch_legacy_completion(
16077                &request_ctx,
16078                &JsonRpcRequest::new(
16079                    COMPLETION_COMPLETE,
16080                    Some(serde_json::json!({
16081                        "ref": {"type": "ref/prompt", "name": "legacy-completion-prompt"},
16082                        "argument": {"name": "unknown", "value": "sta"},
16083                    })),
16084                    190_i64,
16085                ),
16086            )
16087            .expect("exact-2024 completion retains its unvalidated target argument behavior");
16088        assert!(legacy.get("resultType").is_none());
16089        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
16090    }
16091
16092    #[test]
16093    fn final_completion_rejects_invalid_local_handler_results() {
16094        let final_calls = Arc::new(AtomicUsize::new(0));
16095        let mut router = Router::new();
16096        router.add_completion_handler(CompletionValueBoundary {
16097            final_calls: Arc::clone(&final_calls),
16098        });
16099        router.add_prompt(PromptArgumentBoundary {
16100            final_calls: Arc::new(AtomicUsize::new(0)),
16101            legacy_calls: Arc::new(AtomicUsize::new(0)),
16102        });
16103        let cx = Cx::for_testing();
16104        let state = SessionState::new();
16105        let request_ctx = request_context(&cx, 191, Budget::INFINITE, &state);
16106        let baseline = JsonRpcRequest::new(
16107            COMPLETION_COMPLETE,
16108            Some(serde_json::json!({
16109                "_meta": {
16110                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
16111                    "io.modelcontextprotocol/clientCapabilities": {},
16112                },
16113                "ref": {"type": "ref/prompt", "name": "prompt-argument-boundary"},
16114                "argument": {"name": "topic", "value": "at-bound"},
16115            })),
16116            191_i64,
16117        );
16118        let accepted = router
16119            .dispatch_stateless(&request_ctx, &baseline)
16120            .expect("a local handler result at the 100-value limit is accepted");
16121        assert_eq!(
16122            accepted["completion"]["values"].as_array().map(Vec::len),
16123            Some(fastmcp_protocol::MAX_COMPLETION_VALUES)
16124        );
16125        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
16126
16127        let mut one_over = baseline.clone();
16128        one_over
16129            .params
16130            .as_mut()
16131            .and_then(serde_json::Value::as_object_mut)
16132            .and_then(|params| params.get_mut("argument"))
16133            .and_then(serde_json::Value::as_object_mut)
16134            .expect("completion argument is an object")
16135            .insert("value".to_owned(), serde_json::json!("one-over"));
16136        assert_eq!(baseline.method, one_over.method);
16137        assert_eq!(baseline.id, one_over.id);
16138        assert_eq!(
16139            baseline
16140                .params
16141                .as_ref()
16142                .and_then(|params| params.get("ref")),
16143            one_over
16144                .params
16145                .as_ref()
16146                .and_then(|params| params.get("ref")),
16147            "the handler result boundary is the sole planted dimension"
16148        );
16149        let error = router
16150            .dispatch_stateless(&request_ctx, &one_over)
16151            .expect_err("a local handler cannot return a 101st completion value");
16152        assert_eq!(error.code, McpErrorCode::InternalError);
16153        assert_eq!(
16154            error.message,
16155            "completion handler returned more than 100 values"
16156        );
16157        assert_eq!(final_calls.load(Ordering::SeqCst), 2);
16158
16159        let mut negative_total = baseline;
16160        negative_total
16161            .params
16162            .as_mut()
16163            .and_then(serde_json::Value::as_object_mut)
16164            .and_then(|params| params.get_mut("argument"))
16165            .and_then(serde_json::Value::as_object_mut)
16166            .expect("completion argument is an object")
16167            .insert("value".to_owned(), serde_json::json!("negative-total"));
16168        let error = router
16169            .dispatch_stateless(&request_ctx, &negative_total)
16170            .expect_err("a local handler cannot return a negative final completion total");
16171        assert_eq!(error.code, McpErrorCode::InternalError);
16172        assert_eq!(
16173            error.message,
16174            "final completion total must be a nonnegative JSON integer"
16175        );
16176        assert_eq!(final_calls.load(Ordering::SeqCst), 3);
16177    }
16178
16179    #[test]
16180    fn resource_template_registration_is_final_visible_via_protocol_rfc6570_matcher() {
16181        struct AmbiguousTemplateResource;
16182
16183        impl ResourceHandler for AmbiguousTemplateResource {
16184            fn definition(&self) -> Resource {
16185                Resource {
16186                    uri: "mcp://resource/ambiguous".to_owned(),
16187                    name: "ambiguous-template".to_owned(),
16188                    description: None,
16189                    mime_type: None,
16190                    icon: None,
16191                    version: None,
16192                    tags: Vec::new(),
16193                }
16194            }
16195
16196            fn template(&self) -> Option<ResourceTemplate> {
16197                Some(marked_template(
16198                    "mcp://resource/{first}{second}",
16199                    "ambiguous",
16200                ))
16201            }
16202
16203            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
16204                Ok(Vec::new())
16205            }
16206        }
16207
16208        let mut router = Router::new();
16209        let read_calls = Arc::new(AtomicUsize::new(0));
16210        let completion_calls = Arc::new(AtomicUsize::new(0));
16211        router.add_completion_handler(CountingCompletion {
16212            final_calls: Arc::clone(&completion_calls),
16213        });
16214        router
16215            .add_resource_with_behavior(
16216                ReversibleLevelFourTemplateResource {
16217                    read_calls: Arc::clone(&read_calls),
16218                },
16219                crate::DuplicateBehavior::Replace,
16220            )
16221            .expect("a reversible level-four template is admitted");
16222        let cx = Cx::for_testing();
16223        let state = SessionState::new();
16224        let request_ctx = request_context(&cx, 189, Budget::INFINITE, &state);
16225        let legacy_error = router
16226            .handle_resources_read(
16227                &request_ctx,
16228                &fastmcp_protocol::ReadResourceParams {
16229                    uri: "mcp://resource/books/manifest?revision=stable".to_owned(),
16230                    meta: None,
16231                },
16232                state,
16233                None,
16234                None,
16235            )
16236            .expect_err("scalar explode remains unavailable to exact-2024 routing");
16237        assert_eq!(legacy_error.code, McpErrorCode::ResourceNotFound);
16238        assert_eq!(read_calls.load(Ordering::SeqCst), 0);
16239
16240        let final_metadata = serde_json::json!({
16241            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
16242            "io.modelcontextprotocol/clientCapabilities": {},
16243        });
16244        let templates = router
16245            .dispatch_stateless(
16246                &request_ctx,
16247                &JsonRpcRequest::new(
16248                    "resources/templates/list",
16249                    Some(serde_json::json!({"_meta": final_metadata.clone()})),
16250                    189_i64,
16251                ),
16252            )
16253            .expect("the admitted template is final-visible");
16254        assert_eq!(templates["resultType"], "complete");
16255        assert_eq!(
16256            templates["resourceTemplates"][0]["uriTemplate"],
16257            "mcp://resource{/collection*}/manifest{?revision*}"
16258        );
16259
16260        let final_read = router
16261            .dispatch_stateless(
16262                &request_ctx,
16263                &JsonRpcRequest::new(
16264                    "resources/read",
16265                    Some(serde_json::json!({
16266                        "_meta": final_metadata.clone(),
16267                        "uri": "mcp://resource/books%2Ffiction/manifest?revision=stable",
16268                    })),
16269                    189_i64,
16270                ),
16271            )
16272            .expect("the final route uses the same reversible matcher");
16273        assert_eq!(final_read["resultType"], "complete");
16274        assert_eq!(
16275            final_read["contents"][0]["text"], "books/fiction:stable",
16276            "the final route decodes the scalar capture exactly once"
16277        );
16278        assert_eq!(read_calls.load(Ordering::SeqCst), 1);
16279
16280        let completion = router
16281            .dispatch_stateless(
16282                &request_ctx,
16283                &JsonRpcRequest::new(
16284                    COMPLETION_COMPLETE,
16285                    Some(serde_json::json!({
16286                        "_meta": final_metadata,
16287                        "ref": {
16288                            "type": "ref/resource",
16289                            "uri": "mcp://resource{/collection*}/manifest{?revision*}",
16290                        },
16291                        "argument": {"name": "revision", "value": "sta"},
16292                    })),
16293                    189_i64,
16294                ),
16295            )
16296            .expect("the final completion target exposes protocol-derived variables");
16297        assert_eq!(completion["resultType"], "complete");
16298        assert_eq!(
16299            completion["completion"]["values"],
16300            serde_json::json!(["staging"])
16301        );
16302        assert_eq!(completion_calls.load(Ordering::SeqCst), 1);
16303
16304        let template_count = router.resource_templates_count();
16305        let error = router
16306            .add_resource_with_behavior(
16307                AmbiguousTemplateResource,
16308                crate::DuplicateBehavior::Replace,
16309            )
16310            .expect_err("changing only the adjacent scalar boundary remains ambiguous");
16311        assert_eq!(error.code, McpErrorCode::InvalidParams);
16312        assert_eq!(
16313            router.resource_templates_count(),
16314            template_count,
16315            "rejected template admission cannot mutate the registered catalog"
16316        );
16317    }
16318
16319    #[test]
16320    fn final_resource_registration_rejects_template_language_collisions_atomically() {
16321        let mut template_router = Router::new();
16322        template_router
16323            .add_resource_template_with_behavior(
16324                marked_template("mcp://resource/{first}", "first"),
16325                crate::DuplicateBehavior::Replace,
16326            )
16327            .expect("the first reversible template is admitted");
16328        let templates_before = serde_json::to_vec(&template_router.resource_templates())
16329            .expect("the admitted template catalog serializes");
16330
16331        let error = template_router
16332            .add_resource_template_with_behavior(
16333                marked_template("mcp://resource/{second}", "second"),
16334                crate::DuplicateBehavior::Replace,
16335            )
16336            .expect_err("renaming only the capture must not create a tie-broken route");
16337        assert_eq!(error.code, McpErrorCode::InvalidParams);
16338        assert_eq!(
16339            serde_json::to_vec(&template_router.resource_templates())
16340                .expect("a rejected collision leaves the template catalog serializable"),
16341            templates_before,
16342            "template-template collision rejection leaves the catalog unchanged"
16343        );
16344
16345        let mut exact_router = Router::new();
16346        exact_router
16347            .add_resource_with_behavior(
16348                NamedResource::new("mcp://resource/books"),
16349                crate::DuplicateBehavior::Replace,
16350            )
16351            .expect("the exact final resource is admitted");
16352        let exact_count = exact_router.resources_count();
16353        let error = exact_router
16354            .add_resource_template_with_behavior(
16355                marked_template("mcp://resource/{id}", "template"),
16356                crate::DuplicateBehavior::Replace,
16357            )
16358            .expect_err("a template cannot shadow an exact final resource");
16359        assert_eq!(error.code, McpErrorCode::InvalidParams);
16360        assert_eq!(exact_router.resources_count(), exact_count);
16361        assert_eq!(exact_router.resource_templates_count(), 0);
16362
16363        let mut reverse_router = Router::new();
16364        reverse_router
16365            .add_resource_template_with_behavior(
16366                marked_template("mcp://resource/{id}", "template"),
16367                crate::DuplicateBehavior::Replace,
16368            )
16369            .expect("the reversible template is admitted before the planted exact collision");
16370        let templates_before = serde_json::to_vec(&reverse_router.resource_templates())
16371            .expect("the admitted template catalog serializes");
16372        let error = reverse_router
16373            .add_resource_with_behavior(
16374                NamedResource::new("mcp://resource/books"),
16375                crate::DuplicateBehavior::Replace,
16376            )
16377            .expect_err("an exact final resource cannot be hidden behind a template");
16378        assert_eq!(error.code, McpErrorCode::InvalidParams);
16379        assert_eq!(reverse_router.resources_count(), 0);
16380        assert_eq!(
16381            serde_json::to_vec(&reverse_router.resource_templates())
16382                .expect("the template catalog remains serializable after refusal"),
16383            templates_before,
16384            "exact-resource collision rejection leaves the existing template unchanged"
16385        );
16386
16387        let mut disjoint_router = Router::new();
16388        disjoint_router
16389            .add_resource_template_with_behavior(
16390                marked_template("mcp://alpha/{id}", "alpha"),
16391                crate::DuplicateBehavior::Replace,
16392            )
16393            .expect("a disjoint template is admitted");
16394        disjoint_router
16395            .add_resource_template_with_behavior(
16396                marked_template("mcp://beta/{id}", "beta"),
16397                crate::DuplicateBehavior::Replace,
16398            )
16399            .expect("a template with a conflicting literal prefix remains independent");
16400        assert_eq!(disjoint_router.resource_templates_count(), 2);
16401    }
16402
16403    #[test]
16404    fn final_resource_mount_rejects_cross_router_language_collisions_atomically() {
16405        let exact_uri = "mcp://resource/books";
16406
16407        let mut exact_destination = Router::new();
16408        exact_destination
16409            .add_resource_with_behavior(
16410                NamedResource::new(exact_uri),
16411                crate::DuplicateBehavior::Replace,
16412            )
16413            .expect("the exact final resource is admitted before mounting");
16414        let exact_before = serde_json::to_vec(&exact_destination.resources())
16415            .expect("the exact destination catalog serializes");
16416        let mut template_source = Router::new();
16417        template_source
16418            .add_resource_template_with_behavior(
16419                marked_template("mcp://resource/{id}", "mounted-template"),
16420                crate::DuplicateBehavior::Replace,
16421            )
16422            .expect("the independently admitted source template is valid");
16423
16424        let result = exact_destination.mount_resources(template_source, None);
16425        assert!(!result.is_success());
16426        assert_eq!(
16427            serde_json::to_vec(&exact_destination.resources())
16428                .expect("a rejected mount preserves the destination catalog"),
16429            exact_before,
16430            "template-after-exact mount rejection leaves every destination resource unchanged"
16431        );
16432        assert_eq!(exact_destination.resource_templates_count(), 0);
16433
16434        let mut template_destination = Router::new();
16435        template_destination
16436            .add_resource_template_with_behavior(
16437                marked_template("mcp://resource/{id}", "destination-template"),
16438                crate::DuplicateBehavior::Replace,
16439            )
16440            .expect("the final template is admitted before mounting");
16441        let templates_before = serde_json::to_vec(&template_destination.resource_templates())
16442            .expect("the template destination catalog serializes");
16443        let mut exact_source = Router::new();
16444        exact_source
16445            .add_resource_with_behavior(
16446                NamedResource::new(exact_uri),
16447                crate::DuplicateBehavior::Replace,
16448            )
16449            .expect("the independently admitted source resource is valid");
16450
16451        let result = template_destination.mount_resources(exact_source, None);
16452        assert!(!result.is_success());
16453        assert_eq!(template_destination.resources_count(), 0);
16454        assert_eq!(
16455            serde_json::to_vec(&template_destination.resource_templates())
16456                .expect("a rejected mount preserves the destination template catalog"),
16457            templates_before,
16458            "exact-after-template mount rejection leaves every destination template unchanged"
16459        );
16460
16461        let mut first_template_destination = Router::new();
16462        first_template_destination
16463            .add_resource_template_with_behavior(
16464                marked_template("mcp://resource/{first}", "first-template"),
16465                crate::DuplicateBehavior::Replace,
16466            )
16467            .expect("the first final template is admitted");
16468        let templates_before = serde_json::to_vec(&first_template_destination.resource_templates())
16469            .expect("the first template destination catalog serializes");
16470        let mut second_template_source = Router::new();
16471        second_template_source
16472            .add_resource_template_with_behavior(
16473                marked_template("mcp://resource/{second}", "second-template"),
16474                crate::DuplicateBehavior::Replace,
16475            )
16476            .expect("the independently admitted second template is valid");
16477
16478        let result = first_template_destination.mount_resources(second_template_source, None);
16479        assert!(!result.is_success());
16480        assert_eq!(
16481            serde_json::to_vec(&first_template_destination.resource_templates())
16482                .expect("a rejected mount preserves the original template catalog"),
16483            templates_before,
16484            "template-language overlap cannot make mount order a dispatch authority"
16485        );
16486    }
16487
16488    #[test]
16489    fn empty_prefix_mount_matches_unprefixed_final_route_projection() {
16490        const EXACT_URI: &str = "mcp://resource/books";
16491
16492        for prefix in [None, Some("")] {
16493            let mut destination = Router::new();
16494            destination
16495                .add_resource_with_behavior(
16496                    NamedResource::new(EXACT_URI),
16497                    crate::DuplicateBehavior::Replace,
16498                )
16499                .expect("the destination exact final resource is admitted");
16500            let resources_before = serde_json::to_vec(&destination.resources())
16501                .expect("the destination catalog serializes before the mount");
16502            let mut source = Router::new();
16503            source
16504                .add_resource_template_with_behavior(
16505                    marked_template("mcp://resource/{id}", "one-variable-template"),
16506                    crate::DuplicateBehavior::Replace,
16507                )
16508                .expect("the source one-variable final template is admitted");
16509
16510            let result = destination.mount_resources(source, prefix);
16511
16512            assert!(
16513                !result.is_success(),
16514                "an unchanged-key mount must reject the final route collision for {prefix:?}"
16515            );
16516            assert!(
16517                result
16518                    .errors
16519                    .iter()
16520                    .any(|error| error.contains("collides with an exact final resource")),
16521                "the rejection must retain the exact/template collision reason for {prefix:?}"
16522            );
16523            assert_eq!(
16524                serde_json::to_vec(&destination.resources())
16525                    .expect("the rejected mount leaves the destination catalog serializable"),
16526                resources_before,
16527                "the one-variable collision leaves the destination unchanged for {prefix:?}"
16528            );
16529            assert_eq!(destination.resource_templates_count(), 0);
16530            assert!(destination.final_resources.contains_key(EXACT_URI));
16531
16532            let mut template_destination = Router::new();
16533            template_destination
16534                .add_resource_template_with_behavior(
16535                    marked_template("mcp://resource/{id}", "one-variable-destination-template"),
16536                    crate::DuplicateBehavior::Replace,
16537                )
16538                .expect("the destination one-variable final template is admitted");
16539            let templates_before = serde_json::to_vec(&template_destination.resource_templates())
16540                .expect("the destination template catalog serializes before the mount");
16541            let mut exact_source = Router::new();
16542            exact_source
16543                .add_resource_with_behavior(
16544                    NamedResource::new(EXACT_URI),
16545                    crate::DuplicateBehavior::Replace,
16546                )
16547                .expect("the source exact final resource is admitted");
16548
16549            let result = template_destination.mount_resources(exact_source, prefix);
16550
16551            assert!(
16552                !result.is_success(),
16553                "the reverse unchanged-key collision must reject for {prefix:?}"
16554            );
16555            assert!(
16556                result
16557                    .errors
16558                    .iter()
16559                    .any(|error| error.contains("collides with an exact final resource")),
16560                "the reverse rejection must retain the exact/template collision reason for {prefix:?}"
16561            );
16562            assert_eq!(
16563                serde_json::to_vec(&template_destination.resource_templates())
16564                    .expect("the rejected reverse mount leaves the template catalog serializable"),
16565                templates_before,
16566                "the reverse one-variable collision leaves the destination unchanged for {prefix:?}"
16567            );
16568            assert_eq!(template_destination.resources_count(), 0);
16569            assert!(
16570                template_destination.resource_templates["mcp://resource/{id}"]
16571                    .final_definition
16572                    .is_some()
16573            );
16574        }
16575
16576        for prefix in [None, Some("")] {
16577            let mut destination = Router::new();
16578            destination
16579                .add_resource_template_with_behavior(
16580                    marked_template("mcp://alpha/{id}", "destination-disjoint"),
16581                    crate::DuplicateBehavior::Replace,
16582                )
16583                .expect("the destination disjoint final template is admitted");
16584            let mut source = Router::new();
16585            source
16586                .add_resource_template_with_behavior(
16587                    marked_template("mcp://beta/{id}", "source-disjoint"),
16588                    crate::DuplicateBehavior::Replace,
16589                )
16590                .expect("the source disjoint final template is admitted");
16591
16592            let result = destination.mount_resources(source, prefix);
16593
16594            assert!(
16595                result.is_success(),
16596                "a disjoint unchanged-key mount remains admissible for {prefix:?}"
16597            );
16598            assert_eq!(destination.resource_templates_count(), 2);
16599            assert!(
16600                destination.resource_templates["mcp://beta/{id}"]
16601                    .final_definition
16602                    .is_some(),
16603                "an empty prefix cannot downgrade a disjoint final template for {prefix:?}"
16604            );
16605        }
16606
16607        for prefix in [None, Some("")] {
16608            let mut destination = Router::new();
16609            destination
16610                .add_final_resource_with_behavior(
16611                    NamedResource::new(EXACT_URI),
16612                    crate::DuplicateBehavior::Replace,
16613                )
16614                .expect("the destination final-only exact resource is admitted");
16615            let mut source = Router::new();
16616            source
16617                .add_legacy_resource_template_with_behavior(
16618                    marked_template("mcp://resource/{id}", "legacy-template"),
16619                    crate::DuplicateBehavior::Replace,
16620                )
16621                .expect("the source legacy-only template is admitted");
16622
16623            let result = destination.mount_resources(source, prefix);
16624
16625            assert!(
16626                result.is_success(),
16627                "a legacy-only template remains isolated for {prefix:?}"
16628            );
16629            assert!(destination.final_resources.contains_key(EXACT_URI));
16630            assert!(
16631                destination.resource_templates["mcp://resource/{id}"]
16632                    .final_definition
16633                    .is_none(),
16634                "legacy-only template mounting cannot enter final routing for {prefix:?}"
16635            );
16636        }
16637    }
16638
16639    #[test]
16640    fn legacy_only_template_mount_remains_final_isolated() {
16641        let exact_uri = "mcp://resource/books";
16642        let mut destination = Router::new();
16643        destination
16644            .add_final_resource_with_behavior(
16645                NamedResource::new(exact_uri),
16646                crate::DuplicateBehavior::Replace,
16647            )
16648            .expect("the final-only exact resource is admitted");
16649        let mut legacy_source = Router::new();
16650        legacy_source
16651            .add_legacy_resource_template_with_behavior(
16652                marked_template("mcp://resource/{id}", "legacy-template"),
16653                crate::DuplicateBehavior::Replace,
16654            )
16655            .expect("the frozen exact-2024 template is admitted in its own catalog");
16656
16657        let result = destination.mount_resources(legacy_source, None);
16658        assert!(result.is_success());
16659        assert_eq!(destination.resources_count(), 1);
16660        assert_eq!(destination.resource_templates_count(), 1);
16661        assert!(destination.final_resources.contains_key(exact_uri));
16662        assert!(
16663            destination.resource_templates["mcp://resource/{id}"]
16664                .final_definition
16665                .is_none(),
16666            "a legacy-only template remains absent from final dispatch after mounting"
16667        );
16668    }
16669
16670    #[test]
16671    fn legacy_resource_template_is_inert_on_final_list_read_and_completion() {
16672        let mut router = Router::new();
16673        let read_calls = Arc::new(AtomicUsize::new(0));
16674        let completion_calls = Arc::new(AtomicUsize::new(0));
16675        router.add_completion_handler(CountingCompletion {
16676            final_calls: Arc::clone(&completion_calls),
16677        });
16678        router
16679            .add_legacy_resource_with_behavior(
16680                LegacyTemplateResource {
16681                    read_calls: Arc::clone(&read_calls),
16682                },
16683                crate::DuplicateBehavior::Replace,
16684            )
16685            .expect("the exact-2024 template is admitted for its own route");
16686        let cx = Cx::for_testing();
16687        let state = SessionState::new();
16688        let request_ctx = request_context(&cx, 190, Budget::INFINITE, &state);
16689        let uri = "mcp://resource/books/manifest?revision=stable";
16690        let legacy_read = router
16691            .handle_resources_read(
16692                &request_ctx,
16693                &ReadResourceParams {
16694                    uri: uri.to_owned(),
16695                    meta: None,
16696                },
16697                state,
16698                None,
16699                None,
16700            )
16701            .expect("the exact legacy route retains the registered template");
16702        let legacy_wire =
16703            serde_json::to_value(legacy_read).expect("legacy resource result serializes");
16704        assert_eq!(legacy_wire["contents"][0]["text"], "books:stable");
16705        assert!(legacy_wire.get("resultType").is_none());
16706        assert_eq!(read_calls.load(Ordering::SeqCst), 1);
16707
16708        let final_metadata = serde_json::json!({
16709            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
16710            "io.modelcontextprotocol/clientCapabilities": {},
16711        });
16712        let templates = router
16713            .dispatch_stateless(
16714                &request_ctx,
16715                &JsonRpcRequest::new(
16716                    "resources/templates/list",
16717                    Some(serde_json::json!({"_meta": final_metadata.clone()})),
16718                    190_i64,
16719                ),
16720            )
16721            .expect("final template discovery remains valid with only exact-2024 templates");
16722        assert_eq!(templates["resultType"], "complete");
16723        assert_eq!(templates["resourceTemplates"], serde_json::json!([]));
16724
16725        let final_read = router
16726            .dispatch_stateless(
16727                &request_ctx,
16728                &JsonRpcRequest::new(
16729                    "resources/read",
16730                    Some(serde_json::json!({
16731                        "_meta": final_metadata.clone(),
16732                        "uri": uri,
16733                    })),
16734                    190_i64,
16735                ),
16736            )
16737            .expect_err("changing only the dispatch era cannot invoke a legacy-only template");
16738        assert_eq!(final_read.code, McpErrorCode::InvalidParams);
16739        assert_eq!(read_calls.load(Ordering::SeqCst), 1);
16740
16741        let completion = router
16742            .dispatch_stateless(
16743                &request_ctx,
16744                &JsonRpcRequest::new(
16745                    COMPLETION_COMPLETE,
16746                    Some(serde_json::json!({
16747                        "_meta": final_metadata,
16748                        "ref": {
16749                            "type": "ref/resource",
16750                            "uri": "mcp://resource/{collection}/manifest?revision={revision}",
16751                        },
16752                        "argument": {"name": "revision", "value": "sta"},
16753                    })),
16754                    190_i64,
16755                ),
16756            )
16757            .expect_err("a legacy-only template is not a final completion target");
16758        assert_eq!(completion.code, McpErrorCode::InvalidParams);
16759        assert_eq!(completion_calls.load(Ordering::SeqCst), 0);
16760    }
16761
16762    #[test]
16763    fn exact_2024_resource_template_admission_rejects_final_only_star_and_multi_variable_forms() {
16764        let mut legacy_router = Router::new();
16765        legacy_router
16766            .add_legacy_resource_with_behavior(
16767                LegacyTemplateResource {
16768                    read_calls: Arc::new(AtomicUsize::new(0)),
16769                },
16770                crate::DuplicateBehavior::Replace,
16771            )
16772            .expect("the frozen {name} legacy grammar remains admitted");
16773        let catalog_before = serde_json::to_vec(&legacy_router.resource_templates())
16774            .expect("the admitted legacy catalog serializes");
16775
16776        let star_error = legacy_router
16777            .add_legacy_resource_with_behavior(
16778                ReversibleLevelFourTemplateResource {
16779                    read_calls: Arc::new(AtomicUsize::new(0)),
16780                },
16781                crate::DuplicateBehavior::Replace,
16782            )
16783            .expect_err("changing only a legacy variable to scalar explode is final-only");
16784        assert_eq!(star_error.code, McpErrorCode::InvalidParams);
16785        assert_eq!(
16786            serde_json::to_vec(&legacy_router.resource_templates())
16787                .expect("rejected star admission leaves the catalog serializable"),
16788            catalog_before,
16789            "legacy rejection cannot mutate the existing catalog"
16790        );
16791
16792        let multi_variable_error = legacy_router
16793            .add_legacy_resource_template_with_behavior(
16794                marked_template("mcp://resource{?collection*,revision*}", "final-only-multi"),
16795                crate::DuplicateBehavior::Replace,
16796            )
16797            .expect_err("changing only to a named multi-variable expression is final-only");
16798        assert_eq!(multi_variable_error.code, McpErrorCode::InvalidParams);
16799        assert_eq!(
16800            serde_json::to_vec(&legacy_router.resource_templates())
16801                .expect("rejected multi-variable admission leaves the catalog serializable"),
16802            catalog_before,
16803            "the near-identical final syntax cannot alter an exact-2024 catalog"
16804        );
16805
16806        let mut final_router = Router::new();
16807        final_router
16808            .add_final_resource_with_behavior(
16809                ReversibleLevelFourTemplateResource {
16810                    read_calls: Arc::new(AtomicUsize::new(0)),
16811                },
16812                crate::DuplicateBehavior::Replace,
16813            )
16814            .expect("the scalar-explode form remains admitted on the final route");
16815        final_router
16816            .add_resource_template_with_behavior(
16817                marked_template("mcp://resource{?collection*,revision*}", "final-multi"),
16818                crate::DuplicateBehavior::Replace,
16819            )
16820            .expect("the named multi-variable form remains admitted on the final route");
16821        assert_eq!(final_router.resource_templates_count(), 2);
16822    }
16823
16824    #[test]
16825    fn frozen_exact_2024_matcher_preserves_plus_capture_and_percent_decoding() {
16826        let matcher = admit_legacy_resource_template("legacy://resource/{+path}")
16827            .expect("the frozen legacy grammar retains {+name}");
16828        let params = matcher
16829            .matches("legacy://resource/books%2Ffiction")
16830            .expect("legacy plus capture matches a percent-encoded path");
16831        assert_eq!(
16832            params.get("path").map(String::as_str),
16833            Some("books/fiction")
16834        );
16835
16836        assert!(
16837            admit_legacy_resource_template("legacy://resource/{+path*}").is_err(),
16838            "changing only the scalar modifier keeps the new syntax final-only"
16839        );
16840    }
16841
16842    #[test]
16843    fn resource_template_admission_rejects_bare_literal_percent_without_catalog_mutation() {
16844        let accepted = marked_template("mcp://percent/reports%2Fdaily", "percent-template");
16845        let rejected = marked_template("mcp://percent/reports%Qdaily", "percent-template");
16846        let mut router = Router::new();
16847
16848        router
16849            .add_resource_template_with_behavior(accepted, crate::DuplicateBehavior::Replace)
16850            .expect("a complete literal percent triplet is admitted");
16851        let catalog_before = serde_json::to_vec(&router.resource_templates())
16852            .expect("accepted resource-template catalog serializes");
16853
16854        let error = router
16855            .add_resource_template_with_behavior(rejected, crate::DuplicateBehavior::Replace)
16856            .expect_err("changing only the percent triplet to a bare percent is refused");
16857        assert_eq!(error.code, McpErrorCode::InvalidParams);
16858        assert_eq!(
16859            serde_json::to_vec(&router.resource_templates())
16860                .expect("rejected admission leaves the catalog serializable"),
16861            catalog_before,
16862            "rejected literal syntax cannot rewrite the advertised template"
16863        );
16864    }
16865
16866    #[test]
16867    fn resource_resolution_skips_cross_era_static_shadows_for_matching_templates() {
16868        struct ShadowTemplate {
16869            label: &'static str,
16870        }
16871
16872        impl ResourceHandler for ShadowTemplate {
16873            fn definition(&self) -> Resource {
16874                Resource {
16875                    uri: "mcp://shadow/template".to_owned(),
16876                    name: self.label.to_owned(),
16877                    description: None,
16878                    mime_type: Some("text/plain".to_owned()),
16879                    icon: None,
16880                    version: None,
16881                    tags: Vec::new(),
16882                }
16883            }
16884
16885            fn template(&self) -> Option<ResourceTemplate> {
16886                Some(marked_template("mcp://shadow/{id}", self.label))
16887            }
16888
16889            fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
16890                unreachable!("templated reads receive their URI parameters")
16891            }
16892
16893            fn read_with_uri(
16894                &self,
16895                _ctx: &McpContext,
16896                uri: &str,
16897                _params: &UriParams,
16898            ) -> McpResult<Vec<ResourceContent>> {
16899                Ok(vec![ResourceContent {
16900                    uri: uri.to_owned(),
16901                    mime_type: Some("text/plain".to_owned()),
16902                    text: Some(self.label.to_owned()),
16903                    blob: None,
16904                }])
16905            }
16906        }
16907
16908        let cx = Cx::for_testing();
16909        let state = SessionState::new();
16910        let request_ctx = request_context(&cx, 190, Budget::INFINITE, &state);
16911        let uri = "mcp://shadow/item";
16912
16913        let mut legacy_router = Router::new();
16914        legacy_router
16915            .add_legacy_resource_with_behavior(
16916                ShadowTemplate {
16917                    label: "legacy-template",
16918                },
16919                crate::DuplicateBehavior::Replace,
16920            )
16921            .expect("legacy template registers");
16922        legacy_router
16923            .add_final_resource_with_behavior(
16924                NamedResource::new(uri),
16925                crate::DuplicateBehavior::Replace,
16926            )
16927            .expect("final-only static resource registers");
16928        let legacy_read = legacy_router
16929            .handle_resources_read(
16930                &request_ctx,
16931                &ReadResourceParams {
16932                    uri: uri.to_owned(),
16933                    meta: None,
16934                },
16935                state.clone(),
16936                None,
16937                None,
16938            )
16939            .expect("a final-only static URI cannot hide a listed legacy template");
16940        assert_eq!(
16941            serde_json::to_value(legacy_read).expect("legacy result serializes")["contents"][0]["text"],
16942            "legacy-template"
16943        );
16944
16945        let mut final_router = Router::new();
16946        final_router
16947            .add_final_resource_with_behavior(
16948                ShadowTemplate {
16949                    label: "final-template",
16950                },
16951                crate::DuplicateBehavior::Replace,
16952            )
16953            .expect("final template registers");
16954        final_router
16955            .add_legacy_resource_with_behavior(
16956                NamedResource::new(uri),
16957                crate::DuplicateBehavior::Replace,
16958            )
16959            .expect("legacy-only static resource registers");
16960        let final_read = final_router
16961            .dispatch_stateless(
16962                &request_ctx,
16963                &JsonRpcRequest::new(
16964                    "resources/read",
16965                    Some(serde_json::json!({
16966                        "_meta": {
16967                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
16968                            "io.modelcontextprotocol/clientCapabilities": {},
16969                        },
16970                        "uri": uri,
16971                    })),
16972                    190_i64,
16973                ),
16974            )
16975            .expect("a legacy-only static URI cannot hide a listed final template");
16976        assert_eq!(final_read["contents"][0]["text"], "final-template");
16977    }
16978
16979    #[test]
16980    fn completion_handler_rejects_one_field_final_metadata_in_legacy_request() {
16981        let mut router = Router::new();
16982        router.add_completion_handler(EchoCompletion);
16983        let cx = Cx::for_testing();
16984        let state = SessionState::new();
16985        let request_ctx = request_context(&cx, 89, Budget::INFINITE, &state);
16986        let baseline = JsonRpcRequest::new(
16987            COMPLETION_COMPLETE,
16988            Some(serde_json::json!({
16989                "ref": {"type": "ref/prompt", "name": "deploy"},
16990                "argument": {"name": "environment", "value": "sta"},
16991            })),
16992            89_i64,
16993        );
16994        let mut planted = baseline.clone();
16995        planted
16996            .params
16997            .as_mut()
16998            .and_then(serde_json::Value::as_object_mut)
16999            .expect("completion parameters are an object")
17000            .insert(
17001                "_meta".to_string(),
17002                serde_json::json!({
17003                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
17004                }),
17005            );
17006
17007        assert_eq!(baseline.method, planted.method);
17008        assert_eq!(baseline.id, planted.id);
17009        assert_eq!(
17010            baseline
17011                .params
17012                .as_ref()
17013                .and_then(|params| params.get("ref")),
17014            planted.params.as_ref().and_then(|params| params.get("ref")),
17015            "the final metadata object is the sole planted dimension"
17016        );
17017        let catalog_before = router.has_completion_handler();
17018        let planted_before = serde_json::to_vec(&planted).expect("planted request serializes");
17019
17020        let baseline_result = router
17021            .dispatch_legacy_completion(&request_ctx, &baseline)
17022            .expect("the baseline legacy completion request is accepted");
17023        let error = router
17024            .dispatch_legacy_completion(&request_ctx, &planted)
17025            .expect_err("only final metadata is refused in the exact legacy request");
17026        assert_eq!(error.code, McpErrorCode::InvalidParams);
17027        assert_eq!(
17028            serde_json::to_vec(&planted).expect("rejected request serializes"),
17029            planted_before,
17030            "cross-era rejection cannot mutate caller-owned completion parameters"
17031        );
17032        assert_eq!(
17033            router.has_completion_handler(),
17034            catalog_before,
17035            "cross-era rejection cannot alter the installed completion handler"
17036        );
17037        assert_eq!(
17038            router
17039                .dispatch_legacy_completion(&request_ctx, &baseline)
17040                .expect("the baseline remains accepted after the planted rejection"),
17041            baseline_result,
17042            "the one-field rejection cannot alter the accepted legacy completion result"
17043        );
17044    }
17045
17046    #[test]
17047    fn macro_tool_dispatches_exact_legacy_and_final_complete_results() {
17048        let _counter_guard = MACRO_DUAL_ERA_TOOL_LOCK
17049            .lock()
17050            .unwrap_or_else(std::sync::PoisonError::into_inner);
17051        let mut router = Router::new();
17052        MACRO_DUAL_ERA_TOOL_CALLS.store(0, Ordering::SeqCst);
17053        router
17054            .add_tool(MacroDualEraTool)
17055            .expect("macro tool registration succeeds");
17056        let cx = Cx::for_testing();
17057        let state = SessionState::new();
17058        let request_ctx = request_context(&cx, 91, Budget::INFINITE, &state);
17059
17060        let legacy = router
17061            .handle_tools_call(
17062                &request_ctx,
17063                CallToolParams {
17064                    name: "macro_dual_era_tool".to_string(),
17065                    arguments: Some(serde_json::json!({})),
17066                    meta: None,
17067                },
17068                state,
17069                None,
17070                None,
17071            )
17072            .expect("the legacy adapter still invokes the registered handler");
17073        let legacy_wire = serde_json::to_value(&legacy).expect("legacy result serializes");
17074        assert!(
17075            legacy_wire.get("resultType").is_none(),
17076            "the exact legacy result shape remains unchanged"
17077        );
17078        assert_eq!(legacy_wire["content"][0]["text"], "macro final tool result");
17079
17080        let modern = router
17081            .dispatch_stateless(
17082                &request_ctx,
17083                &JsonRpcRequest::new(
17084                    "tools/call",
17085                    Some(serde_json::json!({
17086                        "_meta": {
17087                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
17088                            "io.modelcontextprotocol/clientCapabilities": {},
17089                        },
17090                        "name": "macro_dual_era_tool",
17091                        "arguments": {},
17092                    })),
17093                    91_i64,
17094                ),
17095            )
17096            .expect("the modern router invokes the same installed handler");
17097
17098        assert_eq!(
17099            modern.get("resultType"),
17100            Some(&serde_json::json!("complete"))
17101        );
17102        assert_eq!(modern.get("content"), legacy_wire.get("content"));
17103        assert_eq!(modern.get("isError"), legacy_wire.get("isError"));
17104        assert!(modern.get("serverInfo").is_none());
17105        assert_eq!(
17106            modern["structuredContent"],
17107            serde_json::json!({"weather": "clear"})
17108        );
17109        assert_eq!(MACRO_DUAL_ERA_TOOL_CALLS.load(Ordering::SeqCst), 2);
17110    }
17111
17112    #[test]
17113    fn final_tools_call_input_schema_failure_is_bounded_tool_error_without_handler_call() {
17114        let final_calls = Arc::new(AtomicUsize::new(0));
17115        let legacy_calls = Arc::new(AtomicUsize::new(0));
17116        let mut router = Router::new();
17117        router
17118            .add_tool(SchemaBoundaryTool {
17119                final_calls: Arc::clone(&final_calls),
17120                legacy_calls: Arc::clone(&legacy_calls),
17121                output_matches_schema: true,
17122                output_is_error: false,
17123                output_has_unevaluated_property: false,
17124                invalid_final_input_schema: false,
17125                missing_final_input_object_type: false,
17126                invalid_final_output_schema: false,
17127            })
17128            .expect("schema-boundary tool registration succeeds");
17129        let cx = Cx::for_testing();
17130        let state = SessionState::new();
17131        let request_ctx = request_context(&cx, 150, Budget::INFINITE, &state);
17132
17133        let rejected = router
17134            .dispatch_stateless(
17135                &request_ctx,
17136                &final_tools_call_request(
17137                    "schema-boundary-tool",
17138                    serde_json::json!({"value": 7}),
17139                    150_i64,
17140                ),
17141            )
17142            .expect("registered final tool input failures are tool results");
17143        assert_eq!(rejected["resultType"], "complete");
17144        assert_eq!(rejected["isError"], true);
17145        assert_eq!(
17146            rejected["content"][0]["text"],
17147            "Tool arguments do not match the declared input schema."
17148        );
17149        assert_eq!(final_calls.load(Ordering::SeqCst), 0);
17150        assert_eq!(legacy_calls.load(Ordering::SeqCst), 0);
17151
17152        let accepted = router
17153            .dispatch_stateless(
17154                &request_ctx,
17155                &final_tools_call_request(
17156                    "schema-boundary-tool",
17157                    serde_json::json!({"value": "accepted"}),
17158                    151_i64,
17159                ),
17160            )
17161            .expect("changing only the input value to match the schema is accepted");
17162        assert_eq!(accepted["resultType"], "complete");
17163        assert!(accepted.get("isError").is_none());
17164        assert_eq!(
17165            accepted["structuredContent"],
17166            serde_json::json!({"accepted": true})
17167        );
17168        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
17169        assert_eq!(legacy_calls.load(Ordering::SeqCst), 0);
17170    }
17171
17172    #[test]
17173    fn final_unknown_tool_is_invalid_params_while_legacy_unknown_tool_is_unchanged() {
17174        let router = Router::new();
17175        let cx = Cx::for_testing();
17176        let state = SessionState::new();
17177        let request_ctx = request_context(&cx, 152, Budget::INFINITE, &state);
17178
17179        let modern_error = router
17180            .dispatch_stateless(
17181                &request_ctx,
17182                &final_tools_call_request("unknown-tool", serde_json::json!({}), 152_i64),
17183            )
17184            .expect_err("an unknown final tool is an invalid-params protocol error");
17185        assert_eq!(modern_error.code, McpErrorCode::InvalidParams);
17186
17187        let legacy_error = router
17188            .handle_tools_call(
17189                &request_ctx,
17190                CallToolParams {
17191                    name: "unknown-tool".to_owned(),
17192                    arguments: Some(serde_json::json!({})),
17193                    meta: None,
17194                },
17195                state,
17196                None,
17197                None,
17198            )
17199            .expect_err("the exact legacy unknown-tool result remains method-not-found");
17200        assert_eq!(legacy_error.code, McpErrorCode::MethodNotFound);
17201    }
17202
17203    #[test]
17204    fn final_tool_output_schema_is_checked_before_success_and_legacy_is_unchanged() {
17205        let final_calls = Arc::new(AtomicUsize::new(0));
17206        let legacy_calls = Arc::new(AtomicUsize::new(0));
17207        let mut router = Router::new();
17208        router
17209            .add_tool(SchemaBoundaryTool {
17210                final_calls: Arc::clone(&final_calls),
17211                legacy_calls: Arc::clone(&legacy_calls),
17212                output_matches_schema: true,
17213                output_is_error: false,
17214                output_has_unevaluated_property: false,
17215                invalid_final_input_schema: false,
17216                missing_final_input_object_type: false,
17217                invalid_final_output_schema: false,
17218            })
17219            .expect("schema-boundary tool registration succeeds");
17220        let cx = Cx::for_testing();
17221        let state = SessionState::new();
17222        let request_ctx = request_context(&cx, 153, Budget::INFINITE, &state);
17223
17224        let accepted = router
17225            .dispatch_stateless(
17226                &request_ctx,
17227                &final_tools_call_request(
17228                    "schema-boundary-tool",
17229                    serde_json::json!({"value": "accepted"}),
17230                    153_i64,
17231                ),
17232            )
17233            .expect("a complete result matching the declared output schema is emitted");
17234        assert_eq!(
17235            accepted["structuredContent"],
17236            serde_json::json!({"accepted": true})
17237        );
17238        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
17239
17240        let legacy = router
17241            .handle_tools_call(
17242                &request_ctx,
17243                CallToolParams {
17244                    name: "schema-boundary-tool".to_owned(),
17245                    arguments: Some(serde_json::json!({"value": "accepted"})),
17246                    meta: None,
17247                },
17248                state,
17249                None,
17250                None,
17251            )
17252            .expect("the legacy tool path does not apply final output-schema validation");
17253        assert!(!legacy.is_error);
17254        let legacy_wire = serde_json::to_value(&legacy).expect("legacy result serializes");
17255        assert_eq!(
17256            legacy_wire["content"][0]["text"],
17257            "legacy schema-boundary result"
17258        );
17259        assert_eq!(legacy_calls.load(Ordering::SeqCst), 1);
17260
17261        let rejected_final_calls = Arc::new(AtomicUsize::new(0));
17262        let rejected_legacy_calls = Arc::new(AtomicUsize::new(0));
17263        let mut rejected_router = Router::new();
17264        rejected_router
17265            .add_tool(SchemaBoundaryTool {
17266                final_calls: Arc::clone(&rejected_final_calls),
17267                legacy_calls: Arc::clone(&rejected_legacy_calls),
17268                output_matches_schema: false,
17269                output_is_error: false,
17270                output_has_unevaluated_property: false,
17271                invalid_final_input_schema: false,
17272                missing_final_input_object_type: false,
17273                invalid_final_output_schema: false,
17274            })
17275            .expect("schema-boundary tool registration succeeds");
17276        let rejected = rejected_router
17277            .dispatch_stateless(
17278                &request_ctx,
17279                &final_tools_call_request(
17280                    "schema-boundary-tool",
17281                    serde_json::json!({"value": "accepted"}),
17282                    154_i64,
17283                ),
17284            )
17285            .expect_err("a complete result failing the declared output schema is not emitted");
17286        assert_eq!(rejected.code, McpErrorCode::InternalError);
17287        assert_eq!(
17288            rejected.message,
17289            "tool output does not match the declared output schema"
17290        );
17291        assert_eq!(rejected_final_calls.load(Ordering::SeqCst), 1);
17292        assert_eq!(rejected_legacy_calls.load(Ordering::SeqCst), 0);
17293    }
17294
17295    #[test]
17296    fn final_tool_output_schema_applies_to_complete_error_payloads() {
17297        let accepted_calls = Arc::new(AtomicUsize::new(0));
17298        let mut accepted_router = Router::new();
17299        accepted_router
17300            .add_tool(SchemaBoundaryTool {
17301                final_calls: Arc::clone(&accepted_calls),
17302                legacy_calls: Arc::new(AtomicUsize::new(0)),
17303                output_matches_schema: true,
17304                output_is_error: true,
17305                output_has_unevaluated_property: false,
17306                invalid_final_input_schema: false,
17307                missing_final_input_object_type: false,
17308                invalid_final_output_schema: false,
17309            })
17310            .expect("schema-boundary tool registration succeeds");
17311        let cx = Cx::for_testing();
17312        let state = SessionState::new();
17313        let request_ctx = request_context(&cx, 154, Budget::INFINITE, &state);
17314
17315        let accepted = accepted_router
17316            .dispatch_stateless(
17317                &request_ctx,
17318                &final_tools_call_request(
17319                    "schema-boundary-tool",
17320                    serde_json::json!({"value": "accepted"}),
17321                    154_i64,
17322                ),
17323            )
17324            .expect("a schema-conforming complete error payload is emitted");
17325        assert_eq!(accepted["resultType"], "complete");
17326        assert_eq!(accepted["isError"], true);
17327        assert_eq!(
17328            accepted["structuredContent"],
17329            serde_json::json!({"accepted": true})
17330        );
17331        assert_eq!(accepted_calls.load(Ordering::SeqCst), 1);
17332
17333        let rejected_calls = Arc::new(AtomicUsize::new(0));
17334        let mut rejected_router = Router::new();
17335        rejected_router
17336            .add_tool(SchemaBoundaryTool {
17337                final_calls: Arc::clone(&rejected_calls),
17338                legacy_calls: Arc::new(AtomicUsize::new(0)),
17339                output_matches_schema: false,
17340                output_is_error: true,
17341                output_has_unevaluated_property: false,
17342                invalid_final_input_schema: false,
17343                missing_final_input_object_type: false,
17344                invalid_final_output_schema: false,
17345            })
17346            .expect("schema-boundary tool registration succeeds");
17347
17348        let rejected = rejected_router
17349            .dispatch_stateless(
17350                &request_ctx,
17351                &final_tools_call_request(
17352                    "schema-boundary-tool",
17353                    serde_json::json!({"value": "accepted"}),
17354                    155_i64,
17355                ),
17356            )
17357            .expect_err("a nonconforming complete error payload is not emitted");
17358        assert_eq!(rejected.code, McpErrorCode::InternalError);
17359        assert_eq!(
17360            rejected.message,
17361            "tool output does not match the declared output schema"
17362        );
17363        assert_eq!(rejected_calls.load(Ordering::SeqCst), 1);
17364    }
17365
17366    #[test]
17367    fn final_tool_admitted_schemas_enforce_unevaluated_properties_on_shipped_path() {
17368        let input_final_calls = Arc::new(AtomicUsize::new(0));
17369        let input_legacy_calls = Arc::new(AtomicUsize::new(0));
17370        let mut input_router = Router::new();
17371        input_router
17372            .add_tool(SchemaBoundaryTool {
17373                final_calls: Arc::clone(&input_final_calls),
17374                legacy_calls: Arc::clone(&input_legacy_calls),
17375                output_matches_schema: true,
17376                output_is_error: false,
17377                output_has_unevaluated_property: false,
17378                invalid_final_input_schema: false,
17379                missing_final_input_object_type: false,
17380                invalid_final_output_schema: false,
17381            })
17382            .expect("schema-boundary tool registration succeeds");
17383        let cx = Cx::for_testing();
17384        let state = SessionState::new();
17385        let request_ctx = request_context(&cx, 155, Budget::INFINITE, &state);
17386
17387        let rejected_input = input_router
17388            .dispatch_stateless(
17389                &request_ctx,
17390                &final_tools_call_request(
17391                    "schema-boundary-tool",
17392                    serde_json::json!({"value": "accepted", "unexpected": true}),
17393                    155_i64,
17394                ),
17395            )
17396            .expect("unevaluated final input properties return the bounded tool error result");
17397        assert_eq!(rejected_input["resultType"], "complete");
17398        assert_eq!(rejected_input["isError"], true);
17399        assert_eq!(input_final_calls.load(Ordering::SeqCst), 0);
17400        assert_eq!(input_legacy_calls.load(Ordering::SeqCst), 0);
17401
17402        let accepted_input = input_router
17403            .dispatch_stateless(
17404                &request_ctx,
17405                &final_tools_call_request(
17406                    "schema-boundary-tool",
17407                    serde_json::json!({"value": "accepted"}),
17408                    156_i64,
17409                ),
17410            )
17411            .expect("removing only the unevaluated input property reaches the handler");
17412        assert_eq!(accepted_input["resultType"], "complete");
17413        assert_eq!(input_final_calls.load(Ordering::SeqCst), 1);
17414
17415        let output_final_calls = Arc::new(AtomicUsize::new(0));
17416        let output_legacy_calls = Arc::new(AtomicUsize::new(0));
17417        let mut output_router = Router::new();
17418        output_router
17419            .add_tool(SchemaBoundaryTool {
17420                final_calls: Arc::clone(&output_final_calls),
17421                legacy_calls: Arc::clone(&output_legacy_calls),
17422                output_matches_schema: true,
17423                output_is_error: false,
17424                output_has_unevaluated_property: true,
17425                invalid_final_input_schema: false,
17426                missing_final_input_object_type: false,
17427                invalid_final_output_schema: false,
17428            })
17429            .expect("schema-boundary tool registration succeeds");
17430
17431        let rejected_output = output_router
17432            .dispatch_stateless(
17433                &request_ctx,
17434                &final_tools_call_request(
17435                    "schema-boundary-tool",
17436                    serde_json::json!({"value": "accepted"}),
17437                    157_i64,
17438                ),
17439            )
17440            .expect_err("an unevaluated final output property is not emitted as success");
17441        assert_eq!(rejected_output.code, McpErrorCode::InternalError);
17442        assert_eq!(
17443            rejected_output.message,
17444            "tool output does not match the declared output schema"
17445        );
17446        assert_eq!(output_final_calls.load(Ordering::SeqCst), 1);
17447        assert_eq!(output_legacy_calls.load(Ordering::SeqCst), 0);
17448    }
17449
17450    #[test]
17451    fn valid_final_tool_replace_updates_both_catalogs_without_reordering() {
17452        let original_legacy_calls = Arc::new(AtomicUsize::new(0));
17453        let original_final_calls = Arc::new(AtomicUsize::new(0));
17454        let replacement_legacy_calls = Arc::new(AtomicUsize::new(0));
17455        let replacement_final_calls = Arc::new(AtomicUsize::new(0));
17456        let mut router = Router::new();
17457        router
17458            .add_tool(NamedTool::new("before"))
17459            .expect("tool registration succeeds");
17460        router
17461            .add_tool(AdmittedSchemaReplacementTool {
17462                legacy_calls: Arc::clone(&original_legacy_calls),
17463                final_calls: Arc::clone(&original_final_calls),
17464                legacy_label: "original",
17465                output_schema: serde_json::json!({"type": "string"}),
17466                structured_content: Some(serde_json::json!("original")),
17467            })
17468            .expect("original tool registration succeeds");
17469        router
17470            .add_tool(NamedTool::new("after"))
17471            .expect("tool registration succeeds");
17472        let order_before = router
17473            .tools()
17474            .into_iter()
17475            .map(|tool| tool.name)
17476            .collect::<Vec<_>>();
17477
17478        router
17479            .add_tool_with_behavior(
17480                AdmittedSchemaReplacementTool {
17481                    legacy_calls: Arc::clone(&replacement_legacy_calls),
17482                    final_calls: Arc::clone(&replacement_final_calls),
17483                    legacy_label: "replacement",
17484                    output_schema: serde_json::json!({"type": "boolean"}),
17485                    structured_content: Some(serde_json::json!(true)),
17486                },
17487                crate::DuplicateBehavior::Replace,
17488            )
17489            .expect("a fully admitted replacement commits both catalog views");
17490        assert_eq!(
17491            router
17492                .tools()
17493                .into_iter()
17494                .map(|tool| tool.name)
17495                .collect::<Vec<_>>(),
17496            order_before,
17497            "replacement retains the original registration position"
17498        );
17499
17500        let cx = Cx::for_testing();
17501        let state = SessionState::new();
17502        let request_ctx = request_context(&cx, 158, Budget::INFINITE, &state);
17503        let legacy = router
17504            .handle_tools_call(
17505                &request_ctx,
17506                CallToolParams {
17507                    name: "admitted-schema-replacement-tool".to_owned(),
17508                    arguments: Some(serde_json::json!({})),
17509                    meta: None,
17510                },
17511                state,
17512                None,
17513                None,
17514            )
17515            .expect("the replacement is installed for legacy dispatch");
17516        let legacy_wire = serde_json::to_value(&legacy).expect("legacy result serializes");
17517        assert_eq!(legacy_wire["content"][0]["text"], "replacement");
17518        assert_eq!(replacement_legacy_calls.load(Ordering::SeqCst), 1);
17519        assert_eq!(original_legacy_calls.load(Ordering::SeqCst), 0);
17520
17521        let modern = router
17522            .dispatch_stateless(
17523                &request_ctx,
17524                &final_tools_call_request(
17525                    "admitted-schema-replacement-tool",
17526                    serde_json::json!({}),
17527                    159_i64,
17528                ),
17529            )
17530            .expect("the replacement is installed for modern dispatch");
17531        assert_eq!(modern["structuredContent"], serde_json::json!(true));
17532        assert_eq!(replacement_final_calls.load(Ordering::SeqCst), 1);
17533        assert_eq!(original_final_calls.load(Ordering::SeqCst), 0);
17534
17535        let modern_catalog = router
17536            .dispatch_stateless(
17537                &request_ctx,
17538                &final_tools_list_request(None, None, None, 160_i64),
17539            )
17540            .expect("the admitted replacement remains visible to the modern catalog");
17541        assert_eq!(
17542            modern_catalog["tools"]
17543                .as_array()
17544                .expect("modern tools remain an array")
17545                .iter()
17546                .map(|tool| tool["name"].as_str().expect("tool name is a string"))
17547                .collect::<Vec<_>>(),
17548            vec!["before", "admitted-schema-replacement-tool", "after"]
17549        );
17550        assert_eq!(
17551            modern_catalog["tools"][1]["outputSchema"]["type"],
17552            "boolean"
17553        );
17554    }
17555
17556    #[test]
17557    fn invalid_final_schema_new_tool_leaves_both_catalogs_unchanged() {
17558        let mut router = Router::new();
17559        router
17560            .add_tool(NamedTool::new("existing"))
17561            .expect("baseline tool registration succeeds");
17562        let cx = Cx::for_testing();
17563        let state = SessionState::new();
17564        let request_ctx = request_context(&cx, 161, Budget::INFINITE, &state);
17565        let legacy_before =
17566            serde_json::to_value(router.tools()).expect("legacy catalog serializes");
17567        let modern_before = router
17568            .dispatch_stateless(
17569                &request_ctx,
17570                &final_tools_list_request(None, None, None, 161_i64),
17571            )
17572            .expect("baseline modern catalog is available");
17573
17574        let error = router
17575            .add_tool_with_behavior(
17576                InvalidFinalSchemaNamedTool::with_tags("new-invalid", Vec::new()),
17577                crate::DuplicateBehavior::Error,
17578            )
17579            .expect_err("a new normal tool with a scalar outputSchema is rejected");
17580        assert_eq!(error.code, McpErrorCode::InternalError);
17581        assert_eq!(
17582            serde_json::to_value(router.tools()).expect("legacy catalog serializes"),
17583            legacy_before,
17584            "failed admission cannot add a legacy-only entry"
17585        );
17586        assert_eq!(
17587            router
17588                .dispatch_stateless(
17589                    &request_ctx,
17590                    &final_tools_list_request(None, None, None, 162_i64),
17591                )
17592                .expect("modern catalog remains available"),
17593            modern_before,
17594            "failed admission cannot alter the modern catalog"
17595        );
17596        assert!(router.get_tool("new-invalid").is_none());
17597    }
17598
17599    #[test]
17600    fn only_tokenized_upstream_schema_registration_bypasses_local_validation() {
17601        let cx = Cx::for_testing();
17602        let state = SessionState::new();
17603        let request_ctx = request_context(&cx, 1601, Budget::INFINITE, &state);
17604        let mut registered_proxy_router = Router::new();
17605        registered_proxy_router
17606            .add_tool(UpstreamScalarSchemaTool {
17607                registered_proxy: true,
17608            })
17609            .expect("an upstream-owned scalar schema is retained without local admission");
17610
17611        let response = registered_proxy_router
17612            .dispatch_stateless(
17613                &request_ctx,
17614                &final_tools_call_request(
17615                    "upstream-scalar-schema-tool",
17616                    serde_json::json!({}),
17617                    1601_i64,
17618                ),
17619            )
17620            .expect("upstream-owned structured content is not locally revalidated");
17621        assert_eq!(
17622            response["structuredContent"],
17623            serde_json::json!({"upstream": true})
17624        );
17625
17626        let mut forged_router = Router::new();
17627        let error = forged_router
17628            .add_tool(UpstreamScalarSchemaTool {
17629                registered_proxy: false,
17630            })
17631            .expect_err("a forgeable authority label cannot bypass local schema admission");
17632        assert_eq!(error.code, McpErrorCode::InternalError);
17633        assert!(
17634            forged_router
17635                .get_tool("upstream-scalar-schema-tool")
17636                .is_none()
17637        );
17638    }
17639
17640    #[test]
17641    fn tokenized_upstream_schema_still_honors_strict_input_validation() {
17642        let cx = Cx::for_testing();
17643        let state = SessionState::new();
17644        let request_ctx = request_context(&cx, 1602, Budget::INFINITE, &state);
17645        let mut router = Router::new();
17646        router
17647            .add_tool(UpstreamScalarSchemaTool {
17648                registered_proxy: true,
17649            })
17650            .expect("an upstream-owned object schema is retained without local admission");
17651        router.set_strict_input_validation(true);
17652
17653        let refused = router
17654            .dispatch_stateless(
17655                &request_ctx,
17656                &final_tools_call_request(
17657                    "upstream-scalar-schema-tool",
17658                    serde_json::json!({"extra": 1}),
17659                    1602_i64,
17660                ),
17661            )
17662            .expect("gateway strict mode returns a complete tools/call result");
17663        assert_eq!(refused["resultType"], "complete");
17664        assert_eq!(refused["isError"], true);
17665        assert_eq!(
17666            refused["content"][0]["text"],
17667            "Tool arguments do not match the declared input schema."
17668        );
17669
17670        let admitted = router
17671            .dispatch_stateless(
17672                &request_ctx,
17673                &final_tools_call_request(
17674                    "upstream-scalar-schema-tool",
17675                    serde_json::json!({}),
17676                    1603_i64,
17677                ),
17678            )
17679            .expect("declared empty object arguments still reach the proxy handler");
17680        assert_eq!(
17681            admitted["structuredContent"],
17682            serde_json::json!({"upstream": true})
17683        );
17684
17685        router.set_strict_input_validation(false);
17686        let extra = router
17687            .dispatch_stateless(
17688                &request_ctx,
17689                &final_tools_call_request(
17690                    "upstream-scalar-schema-tool",
17691                    serde_json::json!({"extra": 1}),
17692                    1604_i64,
17693                ),
17694            )
17695            .expect("changing only the strict flag must admit the extra property");
17696        assert_eq!(
17697            extra["structuredContent"],
17698            serde_json::json!({"upstream": true})
17699        );
17700    }
17701
17702    #[test]
17703    fn normal_registration_rejects_missing_input_schema_type_without_catalog_mutation() {
17704        let mut router = Router::new();
17705        router
17706            .add_tool(NamedTool::new("existing"))
17707            .expect("baseline tool registration succeeds");
17708        let legacy_before =
17709            serde_json::to_value(router.tools()).expect("legacy catalog serializes");
17710
17711        let error = router
17712            .add_tool_with_behavior(
17713                SchemaBoundaryTool {
17714                    final_calls: Arc::new(AtomicUsize::new(0)),
17715                    legacy_calls: Arc::new(AtomicUsize::new(0)),
17716                    output_matches_schema: true,
17717                    output_is_error: false,
17718                    output_has_unevaluated_property: false,
17719                    invalid_final_input_schema: false,
17720                    missing_final_input_object_type: true,
17721                    invalid_final_output_schema: false,
17722                },
17723                crate::DuplicateBehavior::Error,
17724            )
17725            .expect_err("a normal inputSchema must declare type object");
17726        assert_eq!(error.code, McpErrorCode::InternalError);
17727        assert_eq!(
17728            serde_json::to_value(router.tools()).expect("legacy catalog serializes"),
17729            legacy_before
17730        );
17731        assert!(router.get_tool("schema-boundary-tool").is_none());
17732    }
17733
17734    #[test]
17735    fn final_tool_scalar_and_null_structured_content_are_present_and_validated() {
17736        let cx = Cx::for_testing();
17737        let state = SessionState::new();
17738        let request_ctx = request_context(&cx, 163, Budget::INFINITE, &state);
17739
17740        let mut scalar_router = Router::new();
17741        scalar_router
17742            .add_tool(AdmittedSchemaReplacementTool {
17743                legacy_calls: Arc::new(AtomicUsize::new(0)),
17744                final_calls: Arc::new(AtomicUsize::new(0)),
17745                legacy_label: "scalar",
17746                output_schema: serde_json::json!({"type": "string"}),
17747                structured_content: Some(serde_json::json!("")),
17748            })
17749            .expect("scalar-output tool registration succeeds");
17750        let scalar = scalar_router
17751            .dispatch_stateless(
17752                &request_ctx,
17753                &final_tools_call_request(
17754                    "admitted-schema-replacement-tool",
17755                    serde_json::json!({}),
17756                    163_i64,
17757                ),
17758            )
17759            .expect("an object-valued schema document may describe a scalar result");
17760        assert_eq!(
17761            scalar.get("structuredContent"),
17762            Some(&serde_json::json!("")),
17763            "an empty string is present structured content, not an omitted value"
17764        );
17765
17766        let mut null_router = Router::new();
17767        null_router
17768            .add_tool(AdmittedSchemaReplacementTool {
17769                legacy_calls: Arc::new(AtomicUsize::new(0)),
17770                final_calls: Arc::new(AtomicUsize::new(0)),
17771                legacy_label: "null",
17772                output_schema: serde_json::json!({"type": "null"}),
17773                structured_content: Some(serde_json::Value::Null),
17774            })
17775            .expect("null-output tool registration succeeds");
17776        let null = null_router
17777            .dispatch_stateless(
17778                &request_ctx,
17779                &final_tools_call_request(
17780                    "admitted-schema-replacement-tool",
17781                    serde_json::json!({}),
17782                    164_i64,
17783                ),
17784            )
17785            .expect("present JSON null is validated against a null output schema");
17786        assert_eq!(
17787            null.get("structuredContent"),
17788            Some(&serde_json::Value::Null),
17789            "explicit JSON null remains present on the server-emission path"
17790        );
17791    }
17792
17793    #[test]
17794    fn final_tool_declared_output_schema_rejects_absent_structured_content() {
17795        let mut router = Router::new();
17796        router
17797            .add_tool(AdmittedSchemaReplacementTool {
17798                legacy_calls: Arc::new(AtomicUsize::new(0)),
17799                final_calls: Arc::new(AtomicUsize::new(0)),
17800                legacy_label: "missing",
17801                output_schema: serde_json::json!({"type": "string"}),
17802                structured_content: None,
17803            })
17804            .expect("mapped tool registration succeeds");
17805        let cx = Cx::for_testing();
17806        let state = SessionState::new();
17807        let request_ctx = request_context(&cx, 165, Budget::INFINITE, &state);
17808
17809        let error = router
17810            .dispatch_stateless(
17811                &request_ctx,
17812                &final_tools_call_request(
17813                    "admitted-schema-replacement-tool",
17814                    serde_json::json!({}),
17815                    165_i64,
17816                ),
17817            )
17818            .expect_err("a declared output schema requires structured content on complete output");
17819        assert_eq!(error.code, McpErrorCode::InternalError);
17820        assert_eq!(
17821            error.message,
17822            "tool output is missing structuredContent required by the declared output schema"
17823        );
17824    }
17825
17826    #[test]
17827    fn final_tool_error_mapper_covers_input_validation_and_handler_errors() {
17828        let calls = Arc::new(AtomicUsize::new(0));
17829        let mut router = Router::new();
17830        router
17831            .add_tool(ErrorMappedTool {
17832                mode: ErrorMapperMode::Complete,
17833                calls: Arc::clone(&calls),
17834            })
17835            .expect("both bounded mapper branches satisfy outputSchema");
17836        let cx = Cx::for_testing();
17837        let state = SessionState::new();
17838        let request_ctx = request_context(&cx, 169, Budget::INFINITE, &state);
17839
17840        let input_error = router
17841            .dispatch_stateless(
17842                &request_ctx,
17843                &final_tools_call_request(
17844                    "error-mapped-tool",
17845                    serde_json::json!({"value": 7}),
17846                    169_i64,
17847                ),
17848            )
17849            .expect("input rejection is a schema-valid complete tool error");
17850        assert_eq!(input_error["isError"], true);
17851        assert_eq!(
17852            input_error["structuredContent"],
17853            serde_json::json!({"error": "input-validation"})
17854        );
17855        assert_eq!(calls.load(Ordering::SeqCst), 0);
17856
17857        let handler_error = router
17858            .dispatch_stateless(
17859                &request_ctx,
17860                &final_tools_call_request(
17861                    "error-mapped-tool",
17862                    serde_json::json!({"value": "accepted"}),
17863                    170_i64,
17864                ),
17865            )
17866            .expect("handler rejection is a schema-valid complete tool error");
17867        assert_eq!(handler_error["isError"], true);
17868        assert_eq!(
17869            handler_error["structuredContent"],
17870            serde_json::json!({"error": "handler"})
17871        );
17872        assert_eq!(calls.load(Ordering::SeqCst), 1);
17873    }
17874
17875    #[test]
17876    fn incomplete_invalid_or_oversized_tool_error_mapper_is_rejected_atomically() {
17877        for (mode, expected_message) in [
17878            (
17879                ErrorMapperMode::MissingHandler,
17880                "tool declares outputSchema without a complete tool-error structured-content mapper",
17881            ),
17882            (
17883                ErrorMapperMode::InvalidHandler,
17884                "tool error structured-content mapper does not satisfy outputSchema",
17885            ),
17886            (
17887                ErrorMapperMode::OversizedHandler,
17888                "tool error structured-content mapper exceeded the registration limit",
17889            ),
17890        ] {
17891            let calls = Arc::new(AtomicUsize::new(0));
17892            let mut router = Router::new();
17893            router
17894                .add_tool(NamedTool::new("existing"))
17895                .expect("baseline tool admission succeeds");
17896            let cx = Cx::for_testing();
17897            let state = SessionState::new();
17898            let request_ctx = request_context(&cx, 171, Budget::INFINITE, &state);
17899            let legacy_before =
17900                serde_json::to_value(router.tools()).expect("legacy catalog serializes");
17901            let modern_before = router
17902                .dispatch_stateless(
17903                    &request_ctx,
17904                    &final_tools_list_request(None, None, None, 171_i64),
17905                )
17906                .expect("modern catalog is available");
17907
17908            let error = router
17909                .add_tool(ErrorMappedTool {
17910                    mode,
17911                    calls: Arc::clone(&calls),
17912                })
17913                .expect_err("public add_tool exposes mapper admission failure");
17914            assert_eq!(error.code, McpErrorCode::InternalError);
17915            assert_eq!(error.message, expected_message);
17916            assert_eq!(
17917                serde_json::to_value(router.tools()).expect("legacy catalog serializes"),
17918                legacy_before
17919            );
17920            assert_eq!(
17921                router
17922                    .dispatch_stateless(
17923                        &request_ctx,
17924                        &final_tools_list_request(None, None, None, 172_i64),
17925                    )
17926                    .expect("modern catalog remains available"),
17927                modern_before
17928            );
17929            assert!(router.get_tool("error-mapped-tool").is_none());
17930            assert_eq!(calls.load(Ordering::SeqCst), 0);
17931        }
17932    }
17933
17934    #[test]
17935    fn normal_registration_rejects_null_output_schema_without_catalog_mutation() {
17936        let mut router = Router::new();
17937        let error = router
17938            .add_tool_with_behavior(
17939                AdmittedSchemaReplacementTool {
17940                    legacy_calls: Arc::new(AtomicUsize::new(0)),
17941                    final_calls: Arc::new(AtomicUsize::new(0)),
17942                    legacy_label: "null-schema",
17943                    output_schema: serde_json::Value::Null,
17944                    structured_content: None,
17945                },
17946                crate::DuplicateBehavior::Error,
17947            )
17948            .expect_err("the outputSchema wire field itself must be an object");
17949        assert_eq!(error.code, McpErrorCode::InternalError);
17950        assert!(router.tools().is_empty());
17951        assert!(
17952            router
17953                .get_tool("admitted-schema-replacement-tool")
17954                .is_none()
17955        );
17956    }
17957
17958    #[test]
17959    fn invalid_final_schema_replace_leaves_handlers_and_catalogs_unchanged() {
17960        let original_legacy_calls = Arc::new(AtomicUsize::new(0));
17961        let original_final_calls = Arc::new(AtomicUsize::new(0));
17962        let mut router = Router::new();
17963        router
17964            .add_tool(AdmittedSchemaReplacementTool {
17965                legacy_calls: Arc::clone(&original_legacy_calls),
17966                final_calls: Arc::clone(&original_final_calls),
17967                legacy_label: "original",
17968                output_schema: serde_json::json!({"type": "string"}),
17969                structured_content: Some(serde_json::json!("original")),
17970            })
17971            .expect("original tool registration succeeds");
17972        let cx = Cx::for_testing();
17973        let state = SessionState::new();
17974        let request_ctx = request_context(&cx, 166, Budget::INFINITE, &state);
17975        let legacy_before =
17976            serde_json::to_value(router.tools()).expect("legacy catalog serializes");
17977        let modern_before = router
17978            .dispatch_stateless(
17979                &request_ctx,
17980                &final_tools_list_request(None, None, None, 166_i64),
17981            )
17982            .expect("baseline modern catalog is available");
17983
17984        let error = router
17985            .add_tool_with_behavior(
17986                InvalidFinalSchemaNamedTool::with_tags(
17987                    "admitted-schema-replacement-tool",
17988                    Vec::new(),
17989                ),
17990                crate::DuplicateBehavior::Replace,
17991            )
17992            .expect_err("changing only the candidate outputSchema rejects replacement");
17993        assert_eq!(error.code, McpErrorCode::InternalError);
17994        assert_eq!(
17995            serde_json::to_value(router.tools()).expect("legacy catalog serializes"),
17996            legacy_before,
17997            "a rejected replacement cannot replace the legacy handler"
17998        );
17999        assert_eq!(
18000            router
18001                .dispatch_stateless(
18002                    &request_ctx,
18003                    &final_tools_list_request(None, None, None, 167_i64),
18004                )
18005                .expect("modern catalog remains available"),
18006            modern_before,
18007            "a rejected replacement cannot remove the admitted modern entry"
18008        );
18009
18010        let legacy = router
18011            .handle_tools_call(
18012                &request_ctx,
18013                CallToolParams {
18014                    name: "admitted-schema-replacement-tool".to_owned(),
18015                    arguments: Some(serde_json::json!({})),
18016                    meta: None,
18017                },
18018                state,
18019                None,
18020                None,
18021            )
18022            .expect("the original legacy handler remains installed");
18023        let legacy_wire = serde_json::to_value(&legacy).expect("legacy result serializes");
18024        assert_eq!(legacy_wire["content"][0]["text"], "original");
18025        assert_eq!(original_legacy_calls.load(Ordering::SeqCst), 1);
18026
18027        let modern = router
18028            .dispatch_stateless(
18029                &request_ctx,
18030                &final_tools_call_request(
18031                    "admitted-schema-replacement-tool",
18032                    serde_json::json!({}),
18033                    168_i64,
18034                ),
18035            )
18036            .expect("the original modern handler remains installed");
18037        assert_eq!(modern["structuredContent"], serde_json::json!("original"));
18038        assert_eq!(original_final_calls.load(Ordering::SeqCst), 1);
18039    }
18040
18041    #[test]
18042    fn final_tool_and_prompt_argument_nulls_are_rejected_without_erasing_absence() {
18043        let tool_final_calls = Arc::new(AtomicUsize::new(0));
18044        let prompt_final_calls = Arc::new(AtomicUsize::new(0));
18045        let mut router = Router::new();
18046        router
18047            .add_tool(InputRequiredTool {
18048                legacy_calls: Arc::new(AtomicUsize::new(0)),
18049                final_calls: Arc::clone(&tool_final_calls),
18050            })
18051            .expect("tool registration succeeds");
18052        router.add_prompt(DirectFinalPrompt {
18053            final_calls: Arc::clone(&prompt_final_calls),
18054        });
18055        let cx = Cx::for_testing();
18056        let connection = ModernConnection::new();
18057        let inbound = InboundRequestContext::with_modern_connection(
18058            cx,
18059            160,
18060            InboundRequestTransport::Memory,
18061            &connection,
18062        );
18063        let request_ctx = inbound.request_context();
18064
18065        let absent_tool_arguments = JsonRpcRequest::new(
18066            "tools/call",
18067            Some(serde_json::json!({
18068                "_meta": {
18069                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
18070                    "io.modelcontextprotocol/clientCapabilities": {},
18071                },
18072                "name": "input-required-tool",
18073            })),
18074            160_i64,
18075        );
18076        let mut null_tool_arguments = absent_tool_arguments.clone();
18077        null_tool_arguments
18078            .params
18079            .as_mut()
18080            .and_then(serde_json::Value::as_object_mut)
18081            .expect("final tool parameters are an object")
18082            .insert("arguments".to_owned(), serde_json::Value::Null);
18083
18084        let CoreRequest::Final(FinalCoreRequest::ToolsCall(absent_tool_params)) =
18085            CoreRequest::decode(
18086                ProtocolEra::Modern2026,
18087                "tools/call",
18088                absent_tool_arguments.params.as_ref(),
18089            )
18090            .expect("absent final tool arguments decode")
18091        else {
18092            panic!("the final tool parameter shape is selected");
18093        };
18094        assert!(absent_tool_params.arguments.is_absent());
18095        // Final arguments deserialization now rejects an explicit null outright,
18096        // so a null `arguments` member fails at decode rather than reaching the
18097        // handler as an explicit-null presence marker.
18098        assert!(
18099            CoreRequest::decode(
18100                ProtocolEra::Modern2026,
18101                "tools/call",
18102                null_tool_arguments.params.as_ref(),
18103            )
18104            .is_err(),
18105            "explicit-null final tool arguments are rejected at decode"
18106        );
18107
18108        let absent_tool_result = router
18109            .dispatch_stateless(&request_ctx, &absent_tool_arguments)
18110            .expect("absent final tool arguments default to an empty object");
18111        assert_eq!(absent_tool_result["resultType"], "input_required");
18112        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
18113        let null_tool_error = router
18114            .dispatch_stateless(&request_ctx, &null_tool_arguments)
18115            .expect_err("explicit-null final tool arguments are rejected");
18116        assert_eq!(null_tool_error.code, McpErrorCode::InvalidParams);
18117        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
18118
18119        let absent_prompt_arguments = direct_final_prompt_request(161_i64);
18120        let mut null_prompt_arguments = absent_prompt_arguments.clone();
18121        null_prompt_arguments
18122            .params
18123            .as_mut()
18124            .and_then(serde_json::Value::as_object_mut)
18125            .expect("final prompt parameters are an object")
18126            .insert("arguments".to_owned(), serde_json::Value::Null);
18127
18128        let CoreRequest::Final(FinalCoreRequest::PromptsGet(absent_prompt_params)) =
18129            CoreRequest::decode(
18130                ProtocolEra::Modern2026,
18131                "prompts/get",
18132                absent_prompt_arguments.params.as_ref(),
18133            )
18134            .expect("absent final prompt arguments decode")
18135        else {
18136            panic!("the final prompt parameter shape is selected");
18137        };
18138        assert!(absent_prompt_params.arguments.is_absent());
18139        // As with tools/call, a null `arguments` member is rejected at decode.
18140        assert!(
18141            CoreRequest::decode(
18142                ProtocolEra::Modern2026,
18143                "prompts/get",
18144                null_prompt_arguments.params.as_ref(),
18145            )
18146            .is_err(),
18147            "explicit-null final prompt arguments are rejected at decode"
18148        );
18149
18150        let absent_prompt_result = router
18151            .dispatch_stateless(&request_ctx, &absent_prompt_arguments)
18152            .expect("absent final prompt arguments default to an empty map");
18153        assert_eq!(absent_prompt_result["resultType"], "complete");
18154        assert_eq!(prompt_final_calls.load(Ordering::SeqCst), 1);
18155        let null_prompt_error = router
18156            .dispatch_stateless(&request_ctx, &null_prompt_arguments)
18157            .expect_err("explicit-null final prompt arguments are rejected");
18158        assert_eq!(null_prompt_error.code, McpErrorCode::InvalidParams);
18159        assert_eq!(prompt_final_calls.load(Ordering::SeqCst), 1);
18160    }
18161
18162    #[cfg(feature = "tasks")]
18163    #[test]
18164    fn final_task_capable_tool_creates_work_bound_task_after_capability_and_service_admission() {
18165        let final_calls = Arc::new(AtomicUsize::new(0));
18166        let store = Arc::new(InMemoryFinalTaskStore::default());
18167        let runtime = task_runtime_for_router(Arc::clone(&store));
18168        let service_runner = runtime
18169            .install_task_service(1, Arc::new(NoopFinalTaskSupervisor))
18170            .expect("a bounded application-owned task service is installed");
18171        let service_cx = Cx::for_testing();
18172        let mut running_service = Box::pin(service_runner.run(&service_cx));
18173        let mut task_cx = std::task::Context::from_waker(std::task::Waker::noop());
18174        assert!(matches!(
18175            Future::poll(running_service.as_mut(), &mut task_cx),
18176            Poll::Pending
18177        ));
18178        let mut router = Router::new();
18179        router.set_final_task_runtime(Some(runtime));
18180        router
18181            .add_tool(TaskCapableRouterTool {
18182                final_calls: Arc::clone(&final_calls),
18183            })
18184            .expect("tool registration succeeds");
18185        let cx = Cx::for_testing();
18186        let state = SessionState::new();
18187        let request_ctx = request_context(&cx, 160, Budget::INFINITE, &state);
18188
18189        let result = router
18190            .dispatch_stateless(&request_ctx, &final_task_capable_tool_request(160_i64))
18191            .expect("the admitted task-capable outcome creates a work-bound task");
18192        assert_eq!(result["resultType"], "task");
18193        assert_eq!(result["status"], "working");
18194        assert_eq!(result["statusMessage"], "router task created");
18195        assert!(
18196            result["taskId"].as_str().is_some(),
18197            "the created task has a final task identifier"
18198        );
18199        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
18200        assert_eq!(store.task_count(), 1);
18201    }
18202
18203    #[cfg(feature = "tasks")]
18204    #[test]
18205    fn final_task_declaration_gates_only_the_create_task_outcome() {
18206        let final_calls = Arc::new(AtomicUsize::new(0));
18207        let store = Arc::new(InMemoryFinalTaskStore::default());
18208        let mut router = Router::new();
18209        router
18210            .add_tool(ConditionalTaskCapableRouterTool {
18211                final_calls: Arc::clone(&final_calls),
18212            })
18213            .expect("conditional task-capable tool registration succeeds");
18214        let cx = Cx::for_testing();
18215        let state = SessionState::new();
18216        let request_ctx = request_context(&cx, 164, Budget::INFINITE, &state);
18217
18218        let complete = final_tools_call_request(
18219            "conditional-task-capable-router-tool",
18220            serde_json::json!({ "createTask": false }),
18221            164_i64,
18222        );
18223        let task = final_tools_call_request(
18224            "conditional-task-capable-router-tool",
18225            serde_json::json!({ "createTask": true }),
18226            165_i64,
18227        );
18228        assert_eq!(complete.method, task.method);
18229        assert_eq!(
18230            complete
18231                .params
18232                .as_ref()
18233                .and_then(|params| params.get("_meta")),
18234            task.params.as_ref().and_then(|params| params.get("_meta")),
18235            "Tasks negotiation is unchanged between the paired requests"
18236        );
18237
18238        let result = router
18239            .dispatch_stateless(&request_ctx, &complete)
18240            .expect("a declared task-capable handler may complete without Tasks negotiation");
18241        assert_eq!(result["resultType"], "complete");
18242        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
18243        assert_eq!(store.task_count(), 0);
18244
18245        let error = router
18246            .dispatch_stateless(&request_ctx, &task)
18247            .expect_err("only the CreateTask outcome requires Tasks negotiation");
18248        assert!(matches!(error.code, McpErrorCode::Custom(_)));
18249        assert_eq!(final_calls.load(Ordering::SeqCst), 2);
18250        assert_eq!(
18251            store.task_count(),
18252            0,
18253            "a rejected CreateTask outcome must not mutate the task store"
18254        );
18255    }
18256
18257    #[cfg(feature = "tasks")]
18258    #[test]
18259    fn final_task_outcome_without_runtime_rejects_after_handler_before_store_mutation() {
18260        let final_calls = Arc::new(AtomicUsize::new(0));
18261        let store = Arc::new(InMemoryFinalTaskStore::default());
18262        let mut router = Router::new();
18263        router
18264            .add_tool(TaskCapableRouterTool {
18265                final_calls: Arc::clone(&final_calls),
18266            })
18267            .expect("tool registration succeeds");
18268        let cx = Cx::for_testing();
18269        let state = SessionState::new();
18270        let request_ctx = request_context(&cx, 161, Budget::INFINITE, &state);
18271
18272        let error = router
18273            .dispatch_stateless(&request_ctx, &final_task_capable_tool_request(161_i64))
18274            .expect_err("a CreateTask outcome cannot persist without a final Tasks runtime");
18275        assert_eq!(error.code, McpErrorCode::InternalError);
18276        assert_eq!(
18277            error.message,
18278            "task-capable tool requires an installed final Tasks runtime"
18279        );
18280        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
18281        assert_eq!(
18282            store.task_count(),
18283            0,
18284            "outcome-aware runtime admission rejects before task-store mutation"
18285        );
18286    }
18287
18288    #[cfg(feature = "tasks")]
18289    #[test]
18290    fn final_task_outcome_with_unready_service_rejects_after_handler_before_store_mutation() {
18291        let final_calls = Arc::new(AtomicUsize::new(0));
18292        let store = Arc::new(InMemoryFinalTaskStore::default());
18293        let runtime = task_runtime_for_router(Arc::clone(&store));
18294        let _unready_service = runtime
18295            .install_task_service(1, Arc::new(NoopFinalTaskSupervisor))
18296            .expect("an installed but unpolled task service remains unready");
18297        let mut router = Router::new();
18298        router.set_final_task_runtime(Some(runtime));
18299        router
18300            .add_tool(TaskCapableRouterTool {
18301                final_calls: Arc::clone(&final_calls),
18302            })
18303            .expect("tool registration succeeds");
18304        let cx = Cx::for_testing();
18305        let state = SessionState::new();
18306        let request_ctx = request_context(&cx, 162, Budget::INFINITE, &state);
18307
18308        let error = router
18309            .dispatch_stateless(&request_ctx, &final_task_capable_tool_request(162_i64))
18310            .expect_err("an installed but unready task service is refused before task creation");
18311        assert_eq!(error.code, McpErrorCode::InvalidParams);
18312        assert_eq!(
18313            error.message,
18314            "Final task creation requires an installed ready task service"
18315        );
18316        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
18317        assert_eq!(
18318            store.task_count(),
18319            0,
18320            "outcome-aware readiness admission cannot persist a task"
18321        );
18322    }
18323
18324    #[cfg(feature = "tasks")]
18325    #[test]
18326    fn final_task_outcome_requires_peer_capability_before_store_mutation() {
18327        let final_calls = Arc::new(AtomicUsize::new(0));
18328        let store = Arc::new(InMemoryFinalTaskStore::default());
18329        let runtime = task_runtime_for_router(Arc::clone(&store));
18330        let service_runner = runtime
18331            .install_task_service(1, Arc::new(NoopFinalTaskSupervisor))
18332            .expect("a bounded application-owned task service is installed");
18333        let service_cx = Cx::for_testing();
18334        let mut running_service = Box::pin(service_runner.run(&service_cx));
18335        let mut task_cx = std::task::Context::from_waker(std::task::Waker::noop());
18336        assert!(matches!(
18337            Future::poll(running_service.as_mut(), &mut task_cx),
18338            Poll::Pending
18339        ));
18340        let mut router = Router::new();
18341        router.set_final_task_runtime(Some(runtime));
18342        router
18343            .add_tool(TaskCapableRouterTool {
18344                final_calls: Arc::clone(&final_calls),
18345            })
18346            .expect("tool registration succeeds");
18347        let cx = Cx::for_testing();
18348        let state = SessionState::new();
18349        let request_ctx = request_context(&cx, 162, Budget::INFINITE, &state);
18350
18351        let error = router
18352            .dispatch_stateless(
18353                &request_ctx,
18354                &final_tools_call_request(
18355                    "task-capable-router-tool",
18356                    serde_json::json!({}),
18357                    162_i64,
18358                ),
18359            )
18360            .expect_err("a missing peer Tasks capability is refused before task-store mutation");
18361        assert!(matches!(error.code, McpErrorCode::Custom(_)));
18362        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
18363        assert_eq!(
18364            store.task_count(),
18365            0,
18366            "a rejected CreateTask outcome must not reach the task store"
18367        );
18368    }
18369
18370    #[cfg(feature = "tasks")]
18371    #[test]
18372    fn final_router_defensively_rejects_an_undeclared_task_outcome() {
18373        let final_calls = Arc::new(AtomicUsize::new(0));
18374        let mut router = Router::new();
18375        router
18376            .add_tool(UndeclaredTaskOutcomeRouterTool {
18377                final_calls: Arc::clone(&final_calls),
18378            })
18379            .expect("tool registration succeeds");
18380        let cx = Cx::for_testing();
18381        let state = SessionState::new();
18382        let request_ctx = request_context(&cx, 163, Budget::INFINITE, &state);
18383        let request = JsonRpcRequest::new(
18384            "tools/call",
18385            Some(serde_json::json!({
18386                "_meta": {
18387                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
18388                    "io.modelcontextprotocol/clientCapabilities": {
18389                        "extensions": {
18390                            "io.modelcontextprotocol/tasks": {}
18391                        }
18392                    },
18393                },
18394                "name": "undeclared-task-outcome-router-tool",
18395                "arguments": {},
18396            })),
18397            163_i64,
18398        );
18399
18400        let error = router
18401            .dispatch_stateless(&request_ctx, &request)
18402            .expect_err("the router refuses a task outcome without a preflight declaration");
18403        assert_eq!(error.code, McpErrorCode::InvalidRequest);
18404        assert_eq!(
18405            error.message,
18406            "tool returned CreateTask without declaring final Tasks capability"
18407        );
18408        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
18409    }
18410
18411    #[test]
18412    fn macro_tool_final_metadata_negative_is_non_mutating() {
18413        let _counter_guard = MACRO_DUAL_ERA_TOOL_LOCK
18414            .lock()
18415            .unwrap_or_else(std::sync::PoisonError::into_inner);
18416        let mut router = Router::new();
18417        MACRO_DUAL_ERA_TOOL_CALLS.store(0, Ordering::SeqCst);
18418        router
18419            .add_tool(MacroDualEraTool)
18420            .expect("macro tool registration succeeds");
18421        let cx = Cx::for_testing();
18422        let state = SessionState::new();
18423        let request_ctx = request_context(&cx, 92, Budget::INFINITE, &state);
18424        let baseline = JsonRpcRequest::new(
18425            "tools/call",
18426            Some(serde_json::json!({
18427                "_meta": {
18428                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
18429                    "io.modelcontextprotocol/clientCapabilities": {},
18430                },
18431                "name": "macro_dual_era_tool",
18432                "arguments": {},
18433            })),
18434            92_i64,
18435        );
18436        let mut planted = baseline.clone();
18437        planted
18438            .params
18439            .as_mut()
18440            .and_then(serde_json::Value::as_object_mut)
18441            .expect("tools/call parameters are an object")
18442            .remove("_meta");
18443
18444        assert_eq!(baseline.method, planted.method);
18445        assert_eq!(baseline.id, planted.id);
18446        assert_eq!(
18447            baseline
18448                .params
18449                .as_ref()
18450                .and_then(|params| params.get("name")),
18451            planted
18452                .params
18453                .as_ref()
18454                .and_then(|params| params.get("name")),
18455            "the final metadata object is the sole planted dimension"
18456        );
18457        let catalog_before = serde_json::to_vec(&router.tools()).expect("catalog serializes");
18458        let planted_before = serde_json::to_vec(&planted).expect("request serializes");
18459
18460        let baseline_result = router
18461            .dispatch_stateless(&request_ctx, &baseline)
18462            .expect("the baseline invokes the registered handler");
18463        assert_eq!(
18464            baseline_result.get("resultType"),
18465            Some(&serde_json::json!("complete"))
18466        );
18467        assert_eq!(MACRO_DUAL_ERA_TOOL_CALLS.load(Ordering::SeqCst), 1);
18468
18469        let error = router
18470            .dispatch_stateless(&request_ctx, &planted)
18471            .expect_err("only final metadata is refused");
18472        assert_eq!(error.code, McpErrorCode::InvalidParams);
18473        assert_eq!(
18474            serde_json::to_vec(&planted).expect("rejected request serializes"),
18475            planted_before,
18476            "typed refusal cannot mutate caller-owned input"
18477        );
18478        assert_eq!(
18479            serde_json::to_vec(&router.tools()).expect("catalog serializes"),
18480            catalog_before,
18481            "typed refusal cannot mutate the installed handler catalog"
18482        );
18483        assert_eq!(
18484            MACRO_DUAL_ERA_TOOL_CALLS.load(Ordering::SeqCst),
18485            1,
18486            "metadata refusal cannot invoke the macro-generated tool"
18487        );
18488        assert_eq!(
18489            router
18490                .dispatch_stateless(&request_ctx, &baseline)
18491                .expect("the unchanged baseline remains accepted after the rejection"),
18492            baseline_result,
18493            "the one-field rejection cannot alter the accepted final result"
18494        );
18495        assert_eq!(MACRO_DUAL_ERA_TOOL_CALLS.load(Ordering::SeqCst), 2);
18496    }
18497
18498    #[test]
18499    fn public_final_dispatch_encodes_input_required_handler_outcomes() {
18500        let tool_legacy_calls = Arc::new(AtomicUsize::new(0));
18501        let tool_final_calls = Arc::new(AtomicUsize::new(0));
18502        let resource_legacy_calls = Arc::new(AtomicUsize::new(0));
18503        let resource_final_calls = Arc::new(AtomicUsize::new(0));
18504        let prompt_legacy_calls = Arc::new(AtomicUsize::new(0));
18505        let prompt_final_calls = Arc::new(AtomicUsize::new(0));
18506        let mut router = Router::new();
18507        router
18508            .add_tool(InputRequiredTool {
18509                legacy_calls: Arc::clone(&tool_legacy_calls),
18510                final_calls: Arc::clone(&tool_final_calls),
18511            })
18512            .expect("tool registration succeeds");
18513        router.add_resource(InputRequiredResource {
18514            legacy_calls: Arc::clone(&resource_legacy_calls),
18515            final_calls: Arc::clone(&resource_final_calls),
18516        });
18517        router.add_prompt(InputRequiredPrompt {
18518            legacy_calls: Arc::clone(&prompt_legacy_calls),
18519            final_calls: Arc::clone(&prompt_final_calls),
18520        });
18521
18522        let cx = Cx::for_testing();
18523        let connection = ModernConnection::new();
18524        let inbound = InboundRequestContext::with_modern_connection(
18525            cx,
18526            141,
18527            InboundRequestTransport::Memory,
18528            &connection,
18529        );
18530        let request_ctx = inbound.request_context();
18531        let metadata = serde_json::json!({
18532            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
18533            "io.modelcontextprotocol/clientCapabilities": {},
18534        });
18535
18536        let tool_request = JsonRpcRequest::new(
18537            "tools/call",
18538            Some(serde_json::json!({
18539                "_meta": metadata.clone(),
18540                "name": "input-required-tool",
18541                "arguments": {},
18542            })),
18543            141_i64,
18544        );
18545        let tool_typed = CoreRequest::decode(
18546            ProtocolEra::Modern2026,
18547            "tools/call",
18548            tool_request.params.as_ref(),
18549        )
18550        .expect("final tools/call request decodes");
18551        let tool_response = router
18552            .dispatch_stateless(&request_ctx, &tool_request)
18553            .expect("public final tools/call dispatch encodes input_required");
18554        assert_eq!(tool_response["resultType"], "input_required");
18555        assert_ne!(tool_response["requestState"], "tool-retry-state");
18556        assert_eq!(
18557            tool_response["inputRequests"]["roots"]["method"],
18558            "roots/list"
18559        );
18560        let tool_wire = serde_json::to_string(&tool_response).expect("tool response serializes");
18561        assert!(matches!(
18562            tool_typed.decode_result(&tool_wire),
18563            Ok(CoreResult::Final(FinalCoreResult::ToolsCallInputRequired { result, .. }))
18564                if result.request_state().is_some()
18565        ));
18566
18567        let resource_request = JsonRpcRequest::new(
18568            "resources/read",
18569            Some(serde_json::json!({
18570                "_meta": metadata.clone(),
18571                "uri": "file:///input-required-resource",
18572            })),
18573            142_i64,
18574        );
18575        let resource_typed = CoreRequest::decode(
18576            ProtocolEra::Modern2026,
18577            "resources/read",
18578            resource_request.params.as_ref(),
18579        )
18580        .expect("final resources/read request decodes");
18581        let resource_response = router
18582            .dispatch_stateless(&request_ctx, &resource_request)
18583            .expect("public final resources/read dispatch encodes input_required");
18584        assert_eq!(resource_response["resultType"], "input_required");
18585        assert_ne!(resource_response["requestState"], "resource-retry-state");
18586        assert_eq!(
18587            resource_response["inputRequests"]["roots"]["method"],
18588            "roots/list"
18589        );
18590        let resource_wire =
18591            serde_json::to_string(&resource_response).expect("resource response serializes");
18592        assert!(matches!(
18593            resource_typed.decode_result(&resource_wire),
18594            Ok(CoreResult::Final(FinalCoreResult::ResourcesReadInputRequired { result, .. }))
18595                if result.request_state().is_some()
18596        ));
18597
18598        let prompt_request = JsonRpcRequest::new(
18599            "prompts/get",
18600            Some(serde_json::json!({
18601                "_meta": metadata,
18602                "name": "input-required-prompt",
18603            })),
18604            143_i64,
18605        );
18606        let prompt_typed = CoreRequest::decode(
18607            ProtocolEra::Modern2026,
18608            "prompts/get",
18609            prompt_request.params.as_ref(),
18610        )
18611        .expect("final prompts/get request decodes");
18612        let prompt_response = router
18613            .dispatch_stateless(&request_ctx, &prompt_request)
18614            .expect("public final prompts/get dispatch encodes input_required");
18615        assert_eq!(prompt_response["resultType"], "input_required");
18616        assert_ne!(prompt_response["requestState"], "prompt-retry-state");
18617        assert_eq!(
18618            prompt_response["inputRequests"]["roots"]["method"],
18619            "roots/list"
18620        );
18621        let prompt_wire =
18622            serde_json::to_string(&prompt_response).expect("prompt response serializes");
18623        assert!(matches!(
18624            prompt_typed.decode_result(&prompt_wire),
18625            Ok(CoreResult::Final(FinalCoreResult::PromptsGetInputRequired { result, .. }))
18626                if result.request_state().is_some()
18627        ));
18628
18629        assert_eq!(tool_legacy_calls.load(Ordering::SeqCst), 0);
18630        assert_eq!(resource_legacy_calls.load(Ordering::SeqCst), 0);
18631        assert_eq!(prompt_legacy_calls.load(Ordering::SeqCst), 0);
18632        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
18633        assert_eq!(resource_final_calls.load(Ordering::SeqCst), 1);
18634        assert_eq!(prompt_final_calls.load(Ordering::SeqCst), 1);
18635    }
18636
18637    #[test]
18638    fn final_context_elicitation_round_trips_through_tools_call_mrtr() {
18639        let initial_calls = Arc::new(AtomicUsize::new(0));
18640        let resumed_calls = Arc::new(AtomicUsize::new(0));
18641        let mut router = Router::new();
18642        router
18643            .add_tool(ContextElicitationTool {
18644                initial_calls: Arc::clone(&initial_calls),
18645                resumed_calls: Arc::clone(&resumed_calls),
18646            })
18647            .expect("context elicitation tool registers");
18648
18649        let cx = Cx::for_testing();
18650        let connection = ModernConnection::new();
18651        let mut initial_request =
18652            final_tools_call_request("context-elicitation-tool", serde_json::json!({}), 940_i64);
18653        initial_request
18654            .params
18655            .as_mut()
18656            .expect("tool params are present")["_meta"]["io.modelcontextprotocol/clientCapabilities"] = serde_json::json!({
18657            "elicitation": {"form": {}},
18658        });
18659        let initial_context = InboundRequestContext::with_modern_connection(
18660            cx.clone(),
18661            940,
18662            InboundRequestTransport::Memory,
18663            &connection,
18664        )
18665        .request_context()
18666        .with_client_capabilities(ClientCapabilityInfo::new().with_elicitation(true, false));
18667
18668        let initial = router
18669            .dispatch_stateless(&initial_context, &initial_request)
18670            .expect("an elicitation-capable final client receives MRTR input_required");
18671        assert_eq!(initial["resultType"], "input_required");
18672        assert_eq!(
18673            initial["inputRequests"]["approval"]["method"],
18674            "elicitation/create"
18675        );
18676        assert_eq!(
18677            initial["inputRequests"]["approval"]["params"]["mode"],
18678            "form"
18679        );
18680        assert!(
18681            initial["inputRequests"]["approval"]
18682                .get("jsonrpc")
18683                .is_none()
18684                && initial["inputRequests"]["approval"].get("id").is_none(),
18685            "final elicitation must be embedded input, never a reverse JSON-RPC request"
18686        );
18687        let request_state = initial["requestState"]
18688            .as_str()
18689            .expect("router mints an opaque MRTR request state")
18690            .to_owned();
18691
18692        let mut retry_request =
18693            final_tools_call_request("context-elicitation-tool", serde_json::json!({}), 941_i64);
18694        let retry_params = retry_request
18695            .params
18696            .as_mut()
18697            .expect("tool params are present");
18698        retry_params["_meta"]["io.modelcontextprotocol/clientCapabilities"] = serde_json::json!({
18699            "elicitation": {"form": {}},
18700        });
18701        retry_params["inputResponses"] = serde_json::json!({
18702            "approval": {"action": "accept", "content": {"approved": true}},
18703        });
18704        retry_params["requestState"] = serde_json::Value::String(request_state);
18705        let retry_context = InboundRequestContext::with_modern_connection(
18706            cx,
18707            941,
18708            InboundRequestTransport::Memory,
18709            &connection,
18710        )
18711        .request_context()
18712        .with_client_capabilities(ClientCapabilityInfo::new().with_elicitation(true, false));
18713
18714        let completed = router
18715            .dispatch_stateless(&retry_context, &retry_request)
18716            .expect("the accepted final elicitation resumes the original tools/call");
18717        assert_eq!(completed["resultType"], "complete");
18718        assert_eq!(completed["content"][0]["text"], "approved");
18719        assert_eq!(initial_calls.load(Ordering::SeqCst), 1);
18720        assert_eq!(resumed_calls.load(Ordering::SeqCst), 1);
18721    }
18722
18723    #[test]
18724    fn final_context_elicitation_without_capability_rejects_before_mrtr_state_mutation() {
18725        let initial_calls = Arc::new(AtomicUsize::new(0));
18726        let resumed_calls = Arc::new(AtomicUsize::new(0));
18727        let mut router = Router::new();
18728        router
18729            .add_tool(ContextElicitationTool {
18730                initial_calls: Arc::clone(&initial_calls),
18731                resumed_calls: Arc::clone(&resumed_calls),
18732            })
18733            .expect("context elicitation tool registers");
18734
18735        let cx = Cx::for_testing();
18736        let connection = ModernConnection::new();
18737        let request =
18738            final_tools_call_request("context-elicitation-tool", serde_json::json!({}), 942_i64);
18739        let context = InboundRequestContext::with_modern_connection(
18740            cx,
18741            942,
18742            InboundRequestTransport::Memory,
18743            &connection,
18744        )
18745        .request_context();
18746
18747        let error = router
18748            .dispatch_stateless(&context, &request)
18749            .expect_err("changing only client elicitation capability must refuse the request");
18750        assert_eq!(error.code, McpErrorCode::InvalidRequest);
18751        assert_eq!(initial_calls.load(Ordering::SeqCst), 1);
18752        assert_eq!(resumed_calls.load(Ordering::SeqCst), 0);
18753        assert_eq!(
18754            router.mrtr_exchanges.active_len(),
18755            0,
18756            "capability refusal cannot mint MRTR continuation state"
18757        );
18758    }
18759
18760    #[test]
18761    fn final_sampling_capability_preserves_tool_choice_and_rejection_leaves_registry_unchanged() {
18762        let calls = Arc::new(AtomicUsize::new(0));
18763        let mut router = Router::new();
18764        router
18765            .add_tool(ContextSamplingTool {
18766                calls: Arc::clone(&calls),
18767            })
18768            .expect("context sampling tool registers");
18769
18770        let cx = Cx::for_testing();
18771        let connection = ModernConnection::new();
18772        let request =
18773            final_tools_call_request("context-sampling-tool", serde_json::json!({}), 943_i64);
18774        let admitted_context = InboundRequestContext::with_modern_connection(
18775            cx.clone(),
18776            943,
18777            InboundRequestTransport::Memory,
18778            &connection,
18779        )
18780        .request_context()
18781        .with_client_capabilities(ClientCapabilityInfo::new().with_sampling());
18782        let admitted = router
18783            .dispatch_stateless(&admitted_context, &request)
18784            .expect("sampling capability admits final MRTR sampling");
18785        assert_eq!(admitted["resultType"], "input_required");
18786        assert_eq!(
18787            admitted["inputRequests"]["sample"]["params"]["toolChoice"],
18788            serde_json::json!({"mode": "required"})
18789        );
18790        assert_eq!(
18791            admitted["inputRequests"]["sample"]["params"]["messages"][0]["content"]["type"],
18792            "tool_use"
18793        );
18794        assert_eq!(router.mrtr_exchanges.active_len(), 1);
18795
18796        let removed_capability_context = InboundRequestContext::with_modern_connection(
18797            cx,
18798            944,
18799            InboundRequestTransport::Memory,
18800            &connection,
18801        )
18802        .request_context();
18803        let rejection = router
18804            .dispatch_stateless(&removed_capability_context, &request)
18805            .expect_err("removing only sampling capability rejects the descriptor");
18806        assert_eq!(rejection.code, McpErrorCode::InvalidRequest);
18807        assert_eq!(
18808            router.mrtr_exchanges.active_len(),
18809            1,
18810            "the one-field capability removal cannot mutate the admitted registry"
18811        );
18812        assert_eq!(calls.load(Ordering::SeqCst), 2);
18813    }
18814
18815    #[test]
18816    fn http_admission_raw_sidecar_reaches_tool_resource_and_prompt_mrtr_retries() {
18817        let tool_calls = Arc::new(AtomicUsize::new(0));
18818        let resource_calls = Arc::new(AtomicUsize::new(0));
18819        let prompt_calls = Arc::new(AtomicUsize::new(0));
18820        let mut router = Router::new();
18821        router
18822            .add_tool(InputRequiredTool {
18823                legacy_calls: Arc::new(AtomicUsize::new(0)),
18824                final_calls: Arc::clone(&tool_calls),
18825            })
18826            .expect("tool registers");
18827        router.add_resource(InputRequiredResource {
18828            legacy_calls: Arc::new(AtomicUsize::new(0)),
18829            final_calls: Arc::clone(&resource_calls),
18830        });
18831        router.add_prompt(InputRequiredPrompt {
18832            legacy_calls: Arc::new(AtomicUsize::new(0)),
18833            final_calls: Arc::clone(&prompt_calls),
18834        });
18835
18836        let cx = Cx::for_testing();
18837        let connection = ModernConnection::new();
18838        let inbound = InboundRequestContext::with_modern_connection(
18839            cx,
18840            1438,
18841            InboundRequestTransport::Http,
18842            &connection,
18843        );
18844        let request_ctx = inbound.request_context();
18845        let cancellation = inbound
18846            .mrtr_continuation_cancellation()
18847            .expect("HTTP-bound modern context owns continuations");
18848        let metadata = serde_json::json!({
18849            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
18850            "io.modelcontextprotocol/clientCapabilities": {},
18851        });
18852        let roots = serde_json::to_string(&router_roots_response_wire())
18853            .expect("roots response serializes");
18854
18855        let tool_initial_body = serde_json::to_vec(&serde_json::json!({
18856            "jsonrpc": "2.0", "id": 1438, "method": "tools/call",
18857            "params": {"_meta": metadata.clone(), "name": "input-required-tool", "arguments": {}},
18858        }))
18859        .expect("tool body serializes");
18860        let (tool_initial, tool_initial_raw) =
18861            admit_http_wire("tools/call", "input-required-tool", &tool_initial_body);
18862        let tool_issued = router
18863            .dispatch_stateless_with_continuation_cancellation_and_raw_params(
18864                &request_ctx,
18865                &tool_initial,
18866                tool_initial_raw.as_deref(),
18867                &cancellation,
18868            )
18869            .expect("HTTP-admitted tool issues state");
18870        let tool_retry_body = format!(
18871            r#"{{"jsonrpc":"2.0","id":1439,"method":"tools/call","params":{{"_meta":{},"name":"input-required-tool","arguments":{{}},"inputResponses":{{"inert":{},"roots":{}}},"requestState":{}}}}}"#,
18872            serde_json::to_string(&metadata).expect("metadata serializes"),
18873            roots,
18874            roots,
18875            serde_json::to_string(&tool_issued["requestState"]).expect("tool state serializes"),
18876        );
18877        let (tool_retry, tool_retry_raw) = admit_http_wire(
18878            "tools/call",
18879            "input-required-tool",
18880            tool_retry_body.as_bytes(),
18881        );
18882        router
18883            .dispatch_stateless_with_continuation_cancellation_and_raw_params(
18884                &request_ctx,
18885                &tool_retry,
18886                tool_retry_raw.as_deref(),
18887                &cancellation,
18888            )
18889            .expect("HTTP-admitted ordered tool retry reaches the handler");
18890
18891        let resource_initial_body = serde_json::to_vec(&serde_json::json!({
18892            "jsonrpc": "2.0", "id": 1440, "method": "resources/read",
18893            "params": {"_meta": metadata.clone(), "uri": "file:///input-required-resource"},
18894        }))
18895        .expect("resource body serializes");
18896        let (resource_initial, resource_initial_raw) = admit_http_wire(
18897            "resources/read",
18898            "file:///input-required-resource",
18899            &resource_initial_body,
18900        );
18901        let resource_issued = router
18902            .dispatch_stateless_with_continuation_cancellation_and_raw_params(
18903                &request_ctx,
18904                &resource_initial,
18905                resource_initial_raw.as_deref(),
18906                &cancellation,
18907            )
18908            .expect("HTTP-admitted resource issues state");
18909        let resource_retry_body = format!(
18910            r#"{{"jsonrpc":"2.0","id":1441,"method":"resources/read","params":{{"_meta":{},"uri":"file:///input-required-resource","inputResponses":{{"inert":{},"roots":{}}},"requestState":{}}}}}"#,
18911            serde_json::to_string(&metadata).expect("metadata serializes"),
18912            roots,
18913            roots,
18914            serde_json::to_string(&resource_issued["requestState"])
18915                .expect("resource state serializes"),
18916        );
18917        let (resource_retry, resource_retry_raw) = admit_http_wire(
18918            "resources/read",
18919            "file:///input-required-resource",
18920            resource_retry_body.as_bytes(),
18921        );
18922        let mut sanitized_resource_retry = resource_retry.clone();
18923        sanitized_resource_retry
18924            .params
18925            .as_mut()
18926            .and_then(serde_json::Value::as_object_mut)
18927            .expect("HTTP-admitted resource parameters are an object")
18928            .insert(
18929                "uri".to_owned(),
18930                serde_json::json!("file:///sanitized-resource"),
18931            );
18932        let resource_sidecar_error = router
18933            .dispatch_stateless_with_continuation_cancellation_and_raw_params(
18934                &request_ctx,
18935                &sanitized_resource_retry,
18936                resource_retry_raw.as_deref(),
18937                &cancellation,
18938            )
18939            .expect_err(
18940                "a resource raw sidecar cannot survive a one-field sanitized typed mismatch",
18941            );
18942        assert_eq!(resource_sidecar_error.code, McpErrorCode::InvalidParams);
18943        assert_eq!(
18944            resource_calls.load(Ordering::SeqCst),
18945            1,
18946            "the mismatched resource sidecar is rejected before handler invocation"
18947        );
18948        assert_eq!(
18949            router.mrtr_exchanges.active_len(),
18950            2,
18951            "the rejected resource sidecar leaves its continuation available"
18952        );
18953        router
18954            .dispatch_stateless_with_continuation_cancellation_and_raw_params(
18955                &request_ctx,
18956                &resource_retry,
18957                resource_retry_raw.as_deref(),
18958                &cancellation,
18959            )
18960            .expect("HTTP-admitted ordered resource retry reaches the handler");
18961
18962        let prompt_initial_body = serde_json::to_vec(&serde_json::json!({
18963            "jsonrpc": "2.0", "id": 1442, "method": "prompts/get",
18964            "params": {"_meta": metadata.clone(), "name": "input-required-prompt"},
18965        }))
18966        .expect("prompt body serializes");
18967        let (prompt_initial, prompt_initial_raw) =
18968            admit_http_wire("prompts/get", "input-required-prompt", &prompt_initial_body);
18969        let prompt_issued = router
18970            .dispatch_stateless_with_continuation_cancellation_and_raw_params(
18971                &request_ctx,
18972                &prompt_initial,
18973                prompt_initial_raw.as_deref(),
18974                &cancellation,
18975            )
18976            .expect("HTTP-admitted prompt issues state");
18977        let prompt_retry_body = format!(
18978            r#"{{"jsonrpc":"2.0","id":1443,"method":"prompts/get","params":{{"_meta":{},"name":"input-required-prompt","inputResponses":{{"inert":{},"roots":{}}},"requestState":{}}}}}"#,
18979            serde_json::to_string(&metadata).expect("metadata serializes"),
18980            roots,
18981            roots,
18982            serde_json::to_string(&prompt_issued["requestState"]).expect("prompt state serializes"),
18983        );
18984        let (prompt_retry, prompt_retry_raw) = admit_http_wire(
18985            "prompts/get",
18986            "input-required-prompt",
18987            prompt_retry_body.as_bytes(),
18988        );
18989        let mut sanitized_prompt_retry = prompt_retry.clone();
18990        sanitized_prompt_retry
18991            .params
18992            .as_mut()
18993            .and_then(serde_json::Value::as_object_mut)
18994            .expect("HTTP-admitted prompt parameters are an object")
18995            .insert("name".to_owned(), serde_json::json!("sanitized-prompt"));
18996        let prompt_sidecar_error = router
18997            .dispatch_stateless_with_continuation_cancellation_and_raw_params(
18998                &request_ctx,
18999                &sanitized_prompt_retry,
19000                prompt_retry_raw.as_deref(),
19001                &cancellation,
19002            )
19003            .expect_err("a prompt raw sidecar cannot survive a one-field sanitized typed mismatch");
19004        assert_eq!(prompt_sidecar_error.code, McpErrorCode::InvalidParams);
19005        assert_eq!(
19006            prompt_calls.load(Ordering::SeqCst),
19007            1,
19008            "the mismatched prompt sidecar is rejected before handler invocation"
19009        );
19010        assert_eq!(
19011            router.mrtr_exchanges.active_len(),
19012            3,
19013            "the rejected prompt sidecar leaves its continuation available"
19014        );
19015        router
19016            .dispatch_stateless_with_continuation_cancellation_and_raw_params(
19017                &request_ctx,
19018                &prompt_retry,
19019                prompt_retry_raw.as_deref(),
19020                &cancellation,
19021            )
19022            .expect("HTTP-admitted ordered prompt retry reaches the handler");
19023
19024        assert_eq!(tool_calls.load(Ordering::SeqCst), 2);
19025        assert_eq!(resource_calls.load(Ordering::SeqCst), 2);
19026        assert_eq!(prompt_calls.load(Ordering::SeqCst), 2);
19027    }
19028
19029    #[test]
19030    fn stateless_final_dispatch_keeps_complete_only_handlers_available_for_all_families() {
19031        let mut router = Router::new();
19032        router
19033            .add_tool(NamedTool::new("complete-only-tool"))
19034            .expect("complete-only tool registers");
19035        router.add_resource(NamedResource::new("file:///complete-only-resource"));
19036        router.add_prompt(NamedPrompt::new("complete-only-prompt"));
19037
19038        let cx = Cx::for_testing();
19039        let state = SessionState::new();
19040        let request_ctx = request_context(&cx, 1430, Budget::INFINITE, &state);
19041        let metadata = serde_json::json!({
19042            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
19043            "io.modelcontextprotocol/clientCapabilities": {},
19044        });
19045
19046        let tool = router
19047            .dispatch_stateless(
19048                &request_ctx,
19049                &final_tools_call_request("complete-only-tool", serde_json::json!({}), 1430_i64),
19050            )
19051            .expect("a complete-only tool remains available without a session partition");
19052        assert_eq!(tool["resultType"], "complete");
19053
19054        let resource = router
19055            .dispatch_stateless(
19056                &request_ctx,
19057                &JsonRpcRequest::new(
19058                    "resources/read",
19059                    Some(serde_json::json!({
19060                        "_meta": metadata.clone(),
19061                        "uri": "file:///complete-only-resource",
19062                    })),
19063                    1431_i64,
19064                ),
19065            )
19066            .expect("a complete-only resource remains available without a session partition");
19067        assert_eq!(resource["resultType"], "complete");
19068
19069        let prompt = router
19070            .dispatch_stateless(
19071                &request_ctx,
19072                &JsonRpcRequest::new(
19073                    "prompts/get",
19074                    Some(serde_json::json!({
19075                        "_meta": metadata,
19076                        "name": "complete-only-prompt",
19077                    })),
19078                    1432_i64,
19079                ),
19080            )
19081            .expect("a complete-only prompt remains available without a session partition");
19082        assert_eq!(prompt["resultType"], "complete");
19083    }
19084
19085    #[test]
19086    fn stateless_mrtr_handlers_are_rejected_before_all_family_invocations() {
19087        let tool_calls = Arc::new(AtomicUsize::new(0));
19088        let resource_calls = Arc::new(AtomicUsize::new(0));
19089        let prompt_calls = Arc::new(AtomicUsize::new(0));
19090        let mut router = Router::new();
19091        router
19092            .add_tool(InputRequiredTool {
19093                legacy_calls: Arc::new(AtomicUsize::new(0)),
19094                final_calls: Arc::clone(&tool_calls),
19095            })
19096            .expect("MRTR tool registers");
19097        router.add_resource(InputRequiredResource {
19098            legacy_calls: Arc::new(AtomicUsize::new(0)),
19099            final_calls: Arc::clone(&resource_calls),
19100        });
19101        router.add_prompt(InputRequiredPrompt {
19102            legacy_calls: Arc::new(AtomicUsize::new(0)),
19103            final_calls: Arc::clone(&prompt_calls),
19104        });
19105
19106        let cx = Cx::for_testing();
19107        let state = SessionState::new();
19108        let request_ctx = request_context(&cx, 1433, Budget::INFINITE, &state);
19109        let metadata = serde_json::json!({
19110            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
19111            "io.modelcontextprotocol/clientCapabilities": {},
19112        });
19113
19114        let tool_error = router
19115            .dispatch_stateless(
19116                &request_ctx,
19117                &final_tools_call_request("input-required-tool", serde_json::json!({}), 1433_i64),
19118            )
19119            .expect_err("a stateless MRTR tool is rejected before its handler runs");
19120        assert_eq!(tool_error.code, McpErrorCode::InvalidParams);
19121
19122        let resource_error = router
19123            .dispatch_stateless(
19124                &request_ctx,
19125                &JsonRpcRequest::new(
19126                    "resources/read",
19127                    Some(serde_json::json!({
19128                        "_meta": metadata.clone(),
19129                        "uri": "file:///input-required-resource",
19130                    })),
19131                    1434_i64,
19132                ),
19133            )
19134            .expect_err("a stateless MRTR resource is rejected before its handler runs");
19135        assert_eq!(resource_error.code, McpErrorCode::InvalidParams);
19136
19137        let prompt_error = router
19138            .dispatch_stateless(
19139                &request_ctx,
19140                &JsonRpcRequest::new(
19141                    "prompts/get",
19142                    Some(serde_json::json!({
19143                        "_meta": metadata,
19144                        "name": "input-required-prompt",
19145                    })),
19146                    1435_i64,
19147                ),
19148            )
19149            .expect_err("a stateless MRTR prompt is rejected before its handler runs");
19150        assert_eq!(prompt_error.code, McpErrorCode::InvalidParams);
19151        assert_eq!(tool_calls.load(Ordering::SeqCst), 0);
19152        assert_eq!(resource_calls.load(Ordering::SeqCst), 0);
19153        assert_eq!(prompt_calls.load(Ordering::SeqCst), 0);
19154        assert_eq!(router.mrtr_exchanges.active_len(), 0);
19155    }
19156
19157    #[test]
19158    fn bound_owned_mrtr_retry_rejects_foreign_and_stale_state_without_mutation_then_resumes() {
19159        let calls = Arc::new(AtomicUsize::new(0));
19160        let mut unshared_router = Router::new();
19161        unshared_router
19162            .add_tool(OneShotMrtrTool {
19163                calls: Arc::clone(&calls),
19164            })
19165            .expect("one-shot MRTR tool registers");
19166        let router = Arc::new(unshared_router);
19167        let cx = Cx::for_testing();
19168        let origin_connection = ModernConnection::new();
19169        let origin_inbound = InboundRequestContext::with_modern_connection(
19170            cx.clone(),
19171            1436,
19172            InboundRequestTransport::Memory,
19173            &origin_connection,
19174        );
19175        let origin_ctx = origin_inbound.request_context();
19176        let origin_cancellation = origin_inbound
19177            .mrtr_continuation_cancellation()
19178            .expect("a bound origin supplies continuation ownership");
19179        let initial =
19180            final_tools_call_request("one-shot-mrtr-tool", serde_json::json!({}), 1436_i64);
19181        let issued = block_on(
19182            Arc::clone(&router).dispatch_stateless_owned_with_continuation_cancellation(
19183                origin_ctx,
19184                initial,
19185                origin_cancellation.clone(),
19186            ),
19187        )
19188        .expect("the bound owned path issues one continuation");
19189        let request_state = issued["requestState"]
19190            .as_str()
19191            .expect("issued continuation has opaque state")
19192            .to_owned();
19193        assert_eq!(calls.load(Ordering::SeqCst), 1);
19194        assert_eq!(router.mrtr_exchanges.active_len(), 1);
19195
19196        let retry = JsonRpcRequest::new(
19197            "tools/call",
19198            Some(serde_json::json!({
19199                "_meta": {
19200                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
19201                    "io.modelcontextprotocol/clientCapabilities": {},
19202                },
19203                "name": "one-shot-mrtr-tool",
19204                "arguments": {},
19205                "inputResponses": {"roots": router_roots_response_wire()},
19206                "requestState": request_state,
19207            })),
19208            1437_i64,
19209        );
19210
19211        let mismatched_sidecar = serde_json::to_string(
19212            retry
19213                .params
19214                .as_ref()
19215                .expect("retry has materialized parameters"),
19216        )
19217        .expect("retry parameters serialize")
19218        .replacen("one-shot-mrtr-tool", "other-tool", 1);
19219        let sidecar_error = block_on(
19220            Arc::clone(&router)
19221                .dispatch_stateless_owned_with_continuation_cancellation_and_raw_params(
19222                    origin_inbound.request_context(),
19223                    retry.clone(),
19224                    Some(Arc::<str>::from(mismatched_sidecar)),
19225                    origin_cancellation.clone(),
19226                ),
19227        )
19228        .expect_err("a mismatched raw sidecar cannot reach continuation admission");
19229        assert_eq!(sidecar_error.code, McpErrorCode::InvalidParams);
19230        assert_eq!(calls.load(Ordering::SeqCst), 1);
19231        assert_eq!(router.mrtr_exchanges.active_len(), 1);
19232
19233        let foreign_connection = ModernConnection::new();
19234        let foreign_inbound = InboundRequestContext::with_modern_connection(
19235            cx.clone(),
19236            1437,
19237            InboundRequestTransport::Memory,
19238            &foreign_connection,
19239        );
19240        let foreign_error = block_on(
19241            Arc::clone(&router).dispatch_stateless_owned_with_continuation_cancellation(
19242                foreign_inbound.request_context(),
19243                retry.clone(),
19244                foreign_inbound
19245                    .mrtr_continuation_cancellation()
19246                    .expect("a foreign connection still supplies its own cancellation"),
19247            ),
19248        )
19249        .expect_err("a foreign connection cannot consume origin continuation state");
19250        assert_eq!(foreign_error.code, McpErrorCode::InvalidParams);
19251        assert_eq!(calls.load(Ordering::SeqCst), 1);
19252        assert_eq!(router.mrtr_exchanges.active_len(), 1);
19253
19254        let mut stale_retry = retry.clone();
19255        stale_retry
19256            .params
19257            .as_mut()
19258            .and_then(serde_json::Value::as_object_mut)
19259            .expect("retry parameters are an object")
19260            .insert(
19261                "requestState".to_owned(),
19262                serde_json::json!("stale-request-state"),
19263            );
19264        let stale_error = block_on(
19265            Arc::clone(&router).dispatch_stateless_owned_with_continuation_cancellation(
19266                origin_inbound.request_context(),
19267                stale_retry,
19268                origin_cancellation.clone(),
19269            ),
19270        )
19271        .expect_err("a stale state cannot consume the live continuation");
19272        assert_eq!(stale_error.code, McpErrorCode::InvalidParams);
19273        assert_eq!(calls.load(Ordering::SeqCst), 1);
19274        assert_eq!(router.mrtr_exchanges.active_len(), 1);
19275
19276        let completed = block_on(
19277            Arc::clone(&router).dispatch_stateless_owned_with_continuation_cancellation(
19278                origin_inbound.request_context(),
19279                retry,
19280                origin_cancellation,
19281            ),
19282        )
19283        .expect("the unchanged origin retry resumes exactly one continuation");
19284        assert_eq!(completed["resultType"], "complete");
19285        assert_eq!(calls.load(Ordering::SeqCst), 2);
19286        assert_eq!(router.mrtr_exchanges.active_len(), 0);
19287    }
19288
19289    #[test]
19290    fn modern_mrtr_state_only_retry_round_trips_only_when_input_responses_are_absent() {
19291        let initial_calls = Arc::new(AtomicUsize::new(0));
19292        let resumed_calls = Arc::new(AtomicUsize::new(0));
19293        let mut router = Router::new();
19294        router
19295            .add_tool(StateOnlyInputRequiredTool {
19296                initial_calls: Arc::clone(&initial_calls),
19297                resumed_calls: Arc::clone(&resumed_calls),
19298            })
19299            .expect("state-only tool registration succeeds");
19300
19301        let cx = Cx::for_testing();
19302        let connection = ModernConnection::new();
19303        let inbound = InboundRequestContext::with_modern_connection(
19304            cx,
19305            144,
19306            InboundRequestTransport::Memory,
19307            &connection,
19308        );
19309        let request_ctx = inbound.request_context();
19310        let metadata = serde_json::json!({
19311            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
19312            "io.modelcontextprotocol/clientCapabilities": {},
19313        });
19314        let initial = JsonRpcRequest::new(
19315            "tools/call",
19316            Some(serde_json::json!({
19317                "_meta": metadata.clone(),
19318                "name": "state-only-input-required-tool",
19319                "arguments": {},
19320            })),
19321            144_i64,
19322        );
19323        let input_required = router
19324            .dispatch_stateless(&request_ctx, &initial)
19325            .expect("state-only handler outcome is framework-bound");
19326        assert_eq!(input_required["resultType"], "input_required");
19327        assert!(
19328            input_required.get("inputRequests").is_none(),
19329            "the framework preserves state-only input_required without an empty request map"
19330        );
19331        let request_state = input_required["requestState"]
19332            .as_str()
19333            .expect("framework output includes opaque state")
19334            .to_owned();
19335
19336        let retry = JsonRpcRequest::new(
19337            "tools/call",
19338            Some(serde_json::json!({
19339                "_meta": metadata,
19340                "name": "state-only-input-required-tool",
19341                "arguments": {},
19342                "requestState": request_state,
19343            })),
19344            145_i64,
19345        );
19346        assert!(
19347            retry
19348                .params
19349                .as_ref()
19350                .and_then(serde_json::Value::as_object)
19351                .is_some_and(|params| !params.contains_key("inputResponses")),
19352            "the admitted retry keeps inputResponses absent"
19353        );
19354        let mut explicit_empty = retry.clone();
19355        explicit_empty
19356            .params
19357            .as_mut()
19358            .and_then(serde_json::Value::as_object_mut)
19359            .expect("retry parameters are an object")
19360            .insert("inputResponses".to_owned(), serde_json::json!({}));
19361        let explicit_empty_raw_params = serde_json::to_string(
19362            explicit_empty
19363                .params
19364                .as_ref()
19365                .expect("explicit-empty retry has parameters"),
19366        )
19367        .expect("explicit-empty retry parameters serialize");
19368        let empty_error = router
19369            .dispatch_stateless_with_raw_params(
19370                &request_ctx,
19371                &explicit_empty,
19372                Some(&explicit_empty_raw_params),
19373            )
19374            .expect_err("an explicit empty inputResponses map cannot impersonate an absent member");
19375        assert_eq!(empty_error.code, McpErrorCode::InvalidParams);
19376        assert_eq!(initial_calls.load(Ordering::SeqCst), 1);
19377        assert_eq!(resumed_calls.load(Ordering::SeqCst), 0);
19378
19379        let retry_raw_params = serde_json::to_string(
19380            retry
19381                .params
19382                .as_ref()
19383                .expect("absent-member retry has parameters"),
19384        )
19385        .expect("absent-member retry parameters serialize");
19386        let completed = router
19387            .dispatch_stateless_with_raw_params(&request_ctx, &retry, Some(&retry_raw_params))
19388            .expect("the unchanged absent-member retry resumes exactly once");
19389        assert_eq!(completed["resultType"], "complete");
19390        assert_eq!(completed["content"][0]["text"], "state-only resumed");
19391        assert_eq!(initial_calls.load(Ordering::SeqCst), 1);
19392        assert_eq!(resumed_calls.load(Ordering::SeqCst), 1);
19393    }
19394
19395    #[test]
19396    fn modern_mrtr_retries_resume_each_method_once_and_refuse_replay_or_kind_mismatch() {
19397        let tool_final_calls = Arc::new(AtomicUsize::new(0));
19398        let resource_final_calls = Arc::new(AtomicUsize::new(0));
19399        let prompt_final_calls = Arc::new(AtomicUsize::new(0));
19400        let mut router = Router::new();
19401        router
19402            .add_tool(InputRequiredTool {
19403                legacy_calls: Arc::new(AtomicUsize::new(0)),
19404                final_calls: Arc::clone(&tool_final_calls),
19405            })
19406            .expect("tool registration succeeds");
19407        router.add_resource(InputRequiredResource {
19408            legacy_calls: Arc::new(AtomicUsize::new(0)),
19409            final_calls: Arc::clone(&resource_final_calls),
19410        });
19411        router.add_prompt(InputRequiredPrompt {
19412            legacy_calls: Arc::new(AtomicUsize::new(0)),
19413            final_calls: Arc::clone(&prompt_final_calls),
19414        });
19415
19416        let cx = Cx::for_testing();
19417        let connection = ModernConnection::new();
19418        let inbound = InboundRequestContext::with_modern_connection(
19419            cx.clone(),
19420            145,
19421            InboundRequestTransport::Memory,
19422            &connection,
19423        );
19424        let request_ctx = inbound.request_context();
19425        let metadata = serde_json::json!({
19426            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
19427            "io.modelcontextprotocol/clientCapabilities": {},
19428        });
19429
19430        let tool_initial = JsonRpcRequest::new(
19431            "tools/call",
19432            Some(serde_json::json!({
19433                "_meta": metadata.clone(),
19434                "name": "input-required-tool",
19435                "arguments": {},
19436            })),
19437            145_i64,
19438        );
19439        let tool_initial_result = router
19440            .dispatch_stateless(&request_ctx, &tool_initial)
19441            .expect("normal final dispatch mints the tool request state");
19442        let tool_state = tool_initial_result["requestState"]
19443            .as_str()
19444            .expect("framework result carries opaque tool state")
19445            .to_owned();
19446        let tool_retry = JsonRpcRequest::new(
19447            "tools/call",
19448            Some(serde_json::json!({
19449                "_meta": metadata.clone(),
19450                "name": "input-required-tool",
19451                "arguments": {},
19452                "inputResponses": {"roots": router_roots_response_wire()},
19453                "requestState": tool_state,
19454            })),
19455            145_i64,
19456        );
19457        let raw_params_for = |request: &JsonRpcRequest| {
19458            serde_json::to_string(request.params.as_ref().expect("retry has parameters"))
19459                .expect("retry parameters serialize")
19460        };
19461        let mut kind_mismatch = tool_retry.clone();
19462        kind_mismatch
19463            .params
19464            .as_mut()
19465            .and_then(serde_json::Value::as_object_mut)
19466            .and_then(|params| params.get_mut("inputResponses"))
19467            .and_then(serde_json::Value::as_object_mut)
19468            .expect("tool retry contains an inputResponses object")
19469            .insert(
19470                "roots".to_owned(),
19471                serde_json::to_value(
19472                    MrtrInputResponse::sampling(
19473                        serde_json::from_value(serde_json::json!({
19474                            "content": {"type": "text", "text": "not roots"},
19475                            "role": "assistant",
19476                            "model": "test-model",
19477                        }))
19478                        .expect("final sampling response must decode"),
19479                    )
19480                    .expect("sampling response serializes"),
19481                )
19482                .expect("sampling response converts to a wire value"),
19483            );
19484        let kind_mismatch_raw_params = raw_params_for(&kind_mismatch);
19485        let kind_error = router
19486            .dispatch_stateless_with_raw_params(
19487                &request_ctx,
19488                &kind_mismatch,
19489                Some(&kind_mismatch_raw_params),
19490            )
19491            .expect_err("changing only the response kind is refused before handler invocation");
19492        assert_eq!(kind_error.code, McpErrorCode::InvalidParams);
19493        assert_eq!(
19494            tool_final_calls.load(Ordering::SeqCst),
19495            1,
19496            "a wrong-kind retry must leave the matching state unconsumed"
19497        );
19498
19499        let missing_responses = JsonRpcRequest::new(
19500            "tools/call",
19501            Some(serde_json::json!({
19502                "_meta": metadata.clone(),
19503                "name": "input-required-tool",
19504                "arguments": {},
19505                "inputResponses": {},
19506                "requestState": tool_state,
19507            })),
19508            145_i64,
19509        );
19510        let missing_raw_params = raw_params_for(&missing_responses);
19511        let missing_error = router
19512            .dispatch_stateless_with_raw_params(
19513                &request_ctx,
19514                &missing_responses,
19515                Some(&missing_raw_params),
19516            )
19517            .expect_err("a missing roots response cannot consume a tool continuation");
19518        assert_eq!(missing_error.code, McpErrorCode::InvalidParams);
19519        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
19520
19521        let roots_wire = serde_json::to_string(&router_roots_response_wire())
19522            .expect("roots response serializes for duplicate-key raw ingress");
19523        let duplicate_raw_params = format!(
19524            r#"{{"_meta":{},"name":"input-required-tool","arguments":{{}},"inputResponses":{{"roots":{roots_wire},"roots":{roots_wire}}},"requestState":{}}}"#,
19525            serde_json::to_string(&metadata).expect("metadata serializes"),
19526            serde_json::to_string(&tool_state).expect("opaque state serializes"),
19527        );
19528        let duplicate_retry = JsonRpcRequest::new(
19529            "tools/call",
19530            Some(
19531                serde_json::from_str(&duplicate_raw_params)
19532                    .expect("duplicate raw parameters materialize for source equality"),
19533            ),
19534            145_i64,
19535        );
19536        let duplicate_error = router
19537            .dispatch_stateless_with_raw_params(
19538                &request_ctx,
19539                &duplicate_retry,
19540                Some(&duplicate_raw_params),
19541            )
19542            .expect_err("duplicate raw response keys cannot collapse before MRTR admission");
19543        assert_eq!(duplicate_error.code, McpErrorCode::InvalidParams);
19544        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
19545
19546        let mut unknown_only = tool_retry.clone();
19547        *unknown_only
19548            .params
19549            .as_mut()
19550            .and_then(serde_json::Value::as_object_mut)
19551            .and_then(|params| params.get_mut("inputResponses"))
19552            .expect("tool retry contains inputResponses") = serde_json::json!({"inert": null});
19553        let unknown_only_raw_params = raw_params_for(&unknown_only);
19554        let unknown_error = router
19555            .dispatch_stateless_with_raw_params(
19556                &request_ctx,
19557                &unknown_only,
19558                Some(&unknown_only_raw_params),
19559            )
19560            .expect_err("unknown-only inputResponses cannot consume a tool continuation");
19561        assert_eq!(unknown_error.code, McpErrorCode::InvalidParams);
19562        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
19563
19564        let mut oversized_map = tool_retry.clone();
19565        let responses = oversized_map
19566            .params
19567            .as_mut()
19568            .and_then(serde_json::Value::as_object_mut)
19569            .and_then(|params| params.get_mut("inputResponses"))
19570            .and_then(serde_json::Value::as_object_mut)
19571            .expect("tool retry contains an inputResponses object");
19572        for index in 0..crate::bidirectional::DEFAULT_MAX_MRTR_INPUT_REQUESTS_PER_ROUND {
19573            responses.insert(format!("inert-{index}"), serde_json::Value::Null);
19574        }
19575        let oversized_error = router
19576            .dispatch_stateless(&request_ctx, &oversized_map)
19577            .expect_err("an oversized raw response map is refused before retry decoding");
19578        assert_eq!(oversized_error.code, McpErrorCode::InvalidParams);
19579        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
19580
19581        let mut tool_oversized_bytes = tool_retry.clone();
19582        tool_oversized_bytes
19583            .params
19584            .as_mut()
19585            .and_then(serde_json::Value::as_object_mut)
19586            .and_then(|params| params.get_mut("inputResponses"))
19587            .and_then(serde_json::Value::as_object_mut)
19588            .expect("tool retry contains an inputResponses object")
19589            .insert(
19590                "roots".to_owned(),
19591                serde_json::Value::String("x".repeat(MAX_MRTR_RAW_INPUT_RESPONSES_BYTES + 1)),
19592            );
19593        let tool_bytes_error = router
19594            .dispatch_stateless(&request_ctx, &tool_oversized_bytes)
19595            .expect_err("only an oversized tool response value is refused before retry decoding");
19596        assert_eq!(tool_bytes_error.code, McpErrorCode::InvalidParams);
19597        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
19598
19599        let mut target_mismatch = tool_retry.clone();
19600        target_mismatch
19601            .params
19602            .as_mut()
19603            .and_then(serde_json::Value::as_object_mut)
19604            .expect("tool retry parameters are an object")
19605            .insert("name".to_owned(), serde_json::json!("other-tool"));
19606        let target_error = router
19607            .dispatch_stateless(&request_ctx, &target_mismatch)
19608            .expect_err("changing only the target cannot consume a tool state");
19609        assert_eq!(target_error.code, McpErrorCode::InvalidParams);
19610        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
19611
19612        let other_connection = ModernConnection::new();
19613        let other_inbound = InboundRequestContext::with_modern_connection(
19614            cx.clone(),
19615            145,
19616            InboundRequestTransport::Memory,
19617            &other_connection,
19618        );
19619        let other_session_ctx = other_inbound.request_context();
19620        let session_error = router
19621            .dispatch_stateless(&other_session_ctx, &tool_retry)
19622            .expect_err("changing only the session cannot consume a tool state");
19623        assert_eq!(session_error.code, McpErrorCode::InvalidParams);
19624        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
19625
19626        let principal_ctx = inbound.request_context();
19627        assert!(principal_ctx.set_auth(fastmcp_core::AuthContext::with_subject("other-user")));
19628        let principal_error = router
19629            .dispatch_stateless(&principal_ctx, &tool_retry)
19630            .expect_err("changing only the principal cannot consume a tool state");
19631        assert_eq!(principal_error.code, McpErrorCode::InvalidParams);
19632        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
19633
19634        let tool_retry_raw_params = raw_params_for(&tool_retry);
19635        let tool_response = router
19636            .dispatch_stateless_with_raw_params(
19637                &request_ctx,
19638                &tool_retry,
19639                Some(&tool_retry_raw_params),
19640            )
19641            .expect("a framework-minted tool state resumes through the final handler");
19642        assert_eq!(tool_response["resultType"], "input_required");
19643        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 2);
19644
19645        let resource_initial = JsonRpcRequest::new(
19646            "resources/read",
19647            Some(serde_json::json!({
19648                "_meta": metadata.clone(),
19649                "uri": "file:///input-required-resource",
19650            })),
19651            146_i64,
19652        );
19653        let resource_initial_result = router
19654            .dispatch_stateless(&request_ctx, &resource_initial)
19655            .expect("normal final dispatch mints the resource request state");
19656        let resource_state = resource_initial_result["requestState"]
19657            .as_str()
19658            .expect("framework result carries opaque resource state")
19659            .to_owned();
19660        let resource_retry = JsonRpcRequest::new(
19661            "resources/read",
19662            Some(serde_json::json!({
19663                "_meta": metadata.clone(),
19664                "uri": "file:///input-required-resource",
19665                "inputResponses": {"roots": router_roots_response_wire()},
19666                "requestState": resource_state,
19667            })),
19668            146_i64,
19669        );
19670        let mut resource_unknown_only = resource_retry.clone();
19671        *resource_unknown_only
19672            .params
19673            .as_mut()
19674            .and_then(serde_json::Value::as_object_mut)
19675            .and_then(|params| params.get_mut("inputResponses"))
19676            .expect("resource retry contains inputResponses") = serde_json::json!({"inert": null});
19677        let resource_unknown_error = router
19678            .dispatch_stateless(&request_ctx, &resource_unknown_only)
19679            .expect_err("unknown-only inputResponses cannot consume a resource continuation");
19680        assert_eq!(resource_unknown_error.code, McpErrorCode::InvalidParams);
19681        assert_eq!(resource_final_calls.load(Ordering::SeqCst), 1);
19682
19683        let mut resource_nested_value = serde_json::Value::Null;
19684        for _ in 0..=MAX_MRTR_RAW_JSON_DEPTH {
19685            resource_nested_value = serde_json::Value::Array(vec![resource_nested_value]);
19686        }
19687        let mut resource_oversized_depth = resource_retry.clone();
19688        resource_oversized_depth
19689            .params
19690            .as_mut()
19691            .and_then(serde_json::Value::as_object_mut)
19692            .and_then(|params| params.get_mut("inputResponses"))
19693            .and_then(serde_json::Value::as_object_mut)
19694            .expect("resource retry contains an inputResponses object")
19695            .insert("roots".to_owned(), resource_nested_value);
19696        let resource_depth_error = router
19697            .dispatch_stateless(&request_ctx, &resource_oversized_depth)
19698            .expect_err("only excessive response nesting is refused before retry decoding");
19699        assert_eq!(resource_depth_error.code, McpErrorCode::InvalidParams);
19700        assert_eq!(resource_final_calls.load(Ordering::SeqCst), 1);
19701
19702        let resource_retry_raw_params = raw_params_for(&resource_retry);
19703        let resource_response = router
19704            .dispatch_stateless_with_raw_params(
19705                &request_ctx,
19706                &resource_retry,
19707                Some(&resource_retry_raw_params),
19708            )
19709            .expect("a framework-minted resource state resumes through the final handler");
19710        assert_eq!(resource_response["resultType"], "input_required");
19711        assert_eq!(resource_final_calls.load(Ordering::SeqCst), 2);
19712
19713        let prompt_initial = JsonRpcRequest::new(
19714            "prompts/get",
19715            Some(serde_json::json!({
19716                "_meta": metadata.clone(),
19717                "name": "input-required-prompt",
19718            })),
19719            147_i64,
19720        );
19721        let prompt_initial_result = router
19722            .dispatch_stateless(&request_ctx, &prompt_initial)
19723            .expect("normal final dispatch mints the prompt request state");
19724        let prompt_state = prompt_initial_result["requestState"]
19725            .as_str()
19726            .expect("framework result carries opaque prompt state")
19727            .to_owned();
19728        let prompt_retry = JsonRpcRequest::new(
19729            "prompts/get",
19730            Some(serde_json::json!({
19731                "_meta": metadata.clone(),
19732                "name": "input-required-prompt",
19733                "inputResponses": {"roots": router_roots_response_wire()},
19734                "requestState": prompt_state,
19735            })),
19736            147_i64,
19737        );
19738        let mut prompt_unknown_only = prompt_retry.clone();
19739        *prompt_unknown_only
19740            .params
19741            .as_mut()
19742            .and_then(serde_json::Value::as_object_mut)
19743            .and_then(|params| params.get_mut("inputResponses"))
19744            .expect("prompt retry contains inputResponses") = serde_json::json!({"inert": null});
19745        let prompt_unknown_error = router
19746            .dispatch_stateless(&request_ctx, &prompt_unknown_only)
19747            .expect_err("unknown-only inputResponses cannot consume a prompt continuation");
19748        assert_eq!(prompt_unknown_error.code, McpErrorCode::InvalidParams);
19749        assert_eq!(prompt_final_calls.load(Ordering::SeqCst), 1);
19750
19751        let mut prompt_oversized_values = prompt_retry.clone();
19752        prompt_oversized_values
19753            .params
19754            .as_mut()
19755            .and_then(serde_json::Value::as_object_mut)
19756            .and_then(|params| params.get_mut("inputResponses"))
19757            .and_then(serde_json::Value::as_object_mut)
19758            .expect("prompt retry contains an inputResponses object")
19759            .insert(
19760                "roots".to_owned(),
19761                serde_json::Value::Array(vec![serde_json::Value::Null; MAX_MRTR_RAW_JSON_VALUES]),
19762            );
19763        let prompt_values_error = router
19764            .dispatch_stateless(&request_ctx, &prompt_oversized_values)
19765            .expect_err(
19766                "only an oversized prompt response value set is refused before retry decoding",
19767            );
19768        assert_eq!(prompt_values_error.code, McpErrorCode::InvalidParams);
19769        assert_eq!(prompt_final_calls.load(Ordering::SeqCst), 1);
19770
19771        let prompt_retry_raw_params = raw_params_for(&prompt_retry);
19772        let prompt_response = router
19773            .dispatch_stateless_with_raw_params(
19774                &request_ctx,
19775                &prompt_retry,
19776                Some(&prompt_retry_raw_params),
19777            )
19778            .expect("a framework-minted prompt state resumes through the final handler");
19779        assert_eq!(prompt_response["resultType"], "input_required");
19780        assert_eq!(prompt_final_calls.load(Ordering::SeqCst), 2);
19781
19782        // Each retry receives the prior round's typed roots response through
19783        // the handler resume hook, then emits a distinct continuation for the
19784        // next JSON-RPC round. This exercises the public final dispatch path
19785        // across tools, resources, and prompts rather than only the registry.
19786        let mut tool_second_retry = tool_retry.clone();
19787        tool_second_retry
19788            .params
19789            .as_mut()
19790            .and_then(serde_json::Value::as_object_mut)
19791            .expect("tool retry parameters are an object")
19792            .insert(
19793                "requestState".to_owned(),
19794                tool_response["requestState"].clone(),
19795            );
19796        let tool_second_response = router
19797            .dispatch_stateless(&request_ctx, &tool_second_retry)
19798            .expect("a second public tools/call round reaches the resumed handler");
19799        assert_eq!(tool_second_response["resultType"], "input_required");
19800
19801        let mut resource_second_retry = resource_retry.clone();
19802        resource_second_retry
19803            .params
19804            .as_mut()
19805            .and_then(serde_json::Value::as_object_mut)
19806            .expect("resource retry parameters are an object")
19807            .insert(
19808                "requestState".to_owned(),
19809                resource_response["requestState"].clone(),
19810            );
19811        let resource_second_response = router
19812            .dispatch_stateless(&request_ctx, &resource_second_retry)
19813            .expect("a second public resources/read round reaches the resumed handler");
19814        assert_eq!(resource_second_response["resultType"], "input_required");
19815
19816        let mut prompt_second_retry = prompt_retry.clone();
19817        prompt_second_retry
19818            .params
19819            .as_mut()
19820            .and_then(serde_json::Value::as_object_mut)
19821            .expect("prompt retry parameters are an object")
19822            .insert(
19823                "requestState".to_owned(),
19824                prompt_response["requestState"].clone(),
19825            );
19826        let prompt_second_response = router
19827            .dispatch_stateless(&request_ctx, &prompt_second_retry)
19828            .expect("a second public prompts/get round reaches the resumed handler");
19829        assert_eq!(prompt_second_response["resultType"], "input_required");
19830        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 3);
19831        assert_eq!(resource_final_calls.load(Ordering::SeqCst), 3);
19832        assert_eq!(prompt_final_calls.load(Ordering::SeqCst), 3);
19833
19834        let replay = router
19835            .dispatch_stateless(&request_ctx, &tool_retry)
19836            .expect_err("replaying only the already consumed tool state is refused");
19837        assert_eq!(replay.code, McpErrorCode::InvalidParams);
19838        assert_eq!(
19839            tool_final_calls.load(Ordering::SeqCst),
19840            3,
19841            "replay must fail before the tool handler is invoked again"
19842        );
19843    }
19844
19845    #[test]
19846    fn modern_connection_disablements_block_final_handlers_before_mrtr_state_is_minted() {
19847        let tool_final_calls = Arc::new(AtomicUsize::new(0));
19848        let resource_final_calls = Arc::new(AtomicUsize::new(0));
19849        let prompt_final_calls = Arc::new(AtomicUsize::new(0));
19850        let mut router = Router::new();
19851        router
19852            .add_tool(InputRequiredTool {
19853                legacy_calls: Arc::new(AtomicUsize::new(0)),
19854                final_calls: Arc::clone(&tool_final_calls),
19855            })
19856            .expect("tool registration succeeds");
19857        router.add_resource(InputRequiredResource {
19858            legacy_calls: Arc::new(AtomicUsize::new(0)),
19859            final_calls: Arc::clone(&resource_final_calls),
19860        });
19861        router.add_prompt(InputRequiredPrompt {
19862            legacy_calls: Arc::new(AtomicUsize::new(0)),
19863            final_calls: Arc::clone(&prompt_final_calls),
19864        });
19865
19866        let metadata = serde_json::json!({
19867            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
19868            "io.modelcontextprotocol/clientCapabilities": {},
19869        });
19870        let tool_request = JsonRpcRequest::new(
19871            "tools/call",
19872            Some(serde_json::json!({
19873                "_meta": metadata.clone(),
19874                "name": "input-required-tool",
19875                "arguments": {},
19876            })),
19877            611_i64,
19878        );
19879        let resource_request = JsonRpcRequest::new(
19880            "resources/read",
19881            Some(serde_json::json!({
19882                "_meta": metadata.clone(),
19883                "uri": "file:///input-required-resource",
19884            })),
19885            612_i64,
19886        );
19887        let prompt_request = JsonRpcRequest::new(
19888            "prompts/get",
19889            Some(serde_json::json!({
19890                "_meta": metadata,
19891                "name": "input-required-prompt",
19892            })),
19893            613_i64,
19894        );
19895        let request_bytes = [
19896            serde_json::to_vec(&tool_request).expect("tool request serializes"),
19897            serde_json::to_vec(&resource_request).expect("resource request serializes"),
19898            serde_json::to_vec(&prompt_request).expect("prompt request serializes"),
19899        ];
19900
19901        let cx = Cx::for_testing();
19902        let allowed_connection = ModernConnection::new();
19903        let allowed_inbound = InboundRequestContext::with_modern_connection(
19904            cx.clone(),
19905            611,
19906            InboundRequestTransport::Stdio,
19907            &allowed_connection,
19908        );
19909        let allowed_ctx = allowed_inbound.request_context();
19910        let allowed_cancellation = allowed_inbound
19911            .mrtr_continuation_cancellation()
19912            .expect("modern connection supplies continuation ownership");
19913
19914        for request in [&tool_request, &resource_request, &prompt_request] {
19915            let result = router
19916                .dispatch_stateless_with_continuation_cancellation(
19917                    &allowed_ctx,
19918                    request,
19919                    &allowed_cancellation,
19920                )
19921                .expect("enabled final component reaches its handler");
19922            assert_eq!(result["resultType"], "input_required");
19923            assert!(
19924                result["requestState"].is_string(),
19925                "an enabled MRTR-capable handler mints framework-owned state"
19926            );
19927        }
19928        assert_eq!(tool_final_calls.load(Ordering::SeqCst), 1);
19929        assert_eq!(resource_final_calls.load(Ordering::SeqCst), 1);
19930        assert_eq!(prompt_final_calls.load(Ordering::SeqCst), 1);
19931
19932        let denied_connection = ModernConnection::new();
19933        let denied_inbound = InboundRequestContext::with_modern_connection(
19934            cx,
19935            611,
19936            InboundRequestTransport::Stdio,
19937            &denied_connection,
19938        );
19939        let denied_ctx = denied_inbound.request_context();
19940        let denied_cancellation = denied_inbound
19941            .mrtr_continuation_cancellation()
19942            .expect("modern connection supplies continuation ownership");
19943        assert!(denied_ctx.disable_tool("input-required-tool"));
19944        assert!(denied_ctx.disable_resource("file:///input-required-resource"));
19945        assert!(denied_ctx.disable_prompt("input-required-prompt"));
19946
19947        assert_eq!(
19948            serde_json::to_vec(&tool_request).expect("tool request remains serializable"),
19949            request_bytes[0],
19950            "connection state is the sole tool-request admission difference"
19951        );
19952        let tool_error = router
19953            .dispatch_stateless_with_continuation_cancellation(
19954                &denied_ctx,
19955                &tool_request,
19956                &denied_cancellation,
19957            )
19958            .expect_err("a disabled tool cannot mint final MRTR state");
19959        assert_eq!(tool_error.code, McpErrorCode::MethodNotFound);
19960
19961        assert_eq!(
19962            serde_json::to_vec(&resource_request).expect("resource request remains serializable"),
19963            request_bytes[1],
19964            "connection state is the sole resource-request admission difference"
19965        );
19966        let resource_error = router
19967            .dispatch_stateless_with_continuation_cancellation(
19968                &denied_ctx,
19969                &resource_request,
19970                &denied_cancellation,
19971            )
19972            .expect_err("a disabled resource cannot mint final MRTR state");
19973        assert_eq!(resource_error.code, McpErrorCode::ResourceNotFound);
19974
19975        assert_eq!(
19976            serde_json::to_vec(&prompt_request).expect("prompt request remains serializable"),
19977            request_bytes[2],
19978            "connection state is the sole prompt-request admission difference"
19979        );
19980        let prompt_error = router
19981            .dispatch_stateless_with_continuation_cancellation(
19982                &denied_ctx,
19983                &prompt_request,
19984                &denied_cancellation,
19985            )
19986            .expect_err("a disabled prompt cannot mint final MRTR state");
19987        assert_eq!(prompt_error.code, McpErrorCode::PromptNotFound);
19988
19989        assert_eq!(
19990            tool_final_calls.load(Ordering::SeqCst),
19991            1,
19992            "refused tool dispatch must not invoke the MRTR-capable handler"
19993        );
19994        assert_eq!(
19995            resource_final_calls.load(Ordering::SeqCst),
19996            1,
19997            "refused resource dispatch must not invoke the MRTR-capable handler"
19998        );
19999        assert_eq!(
20000            prompt_final_calls.load(Ordering::SeqCst),
20001            1,
20002            "refused prompt dispatch must not invoke the MRTR-capable handler"
20003        );
20004    }
20005
20006    #[test]
20007    fn modern_final_catalogs_ignore_connection_disablements_while_exact_legacy_lists_remain_stateful()
20008     {
20009        let template_uri = "file:///input-required-template/{id}";
20010        let mut router = Router::new();
20011        router
20012            .add_tool(NamedTool::new("connection-scoped-tool"))
20013            .expect("tool registration succeeds");
20014        router.add_resource(NamedResource::new("file:///connection-scoped-resource"));
20015        router
20016            .add_resource_template_with_behavior(
20017                ResourceTemplate {
20018                    uri_template: template_uri.to_owned(),
20019                    name: "connection-scoped-template".to_owned(),
20020                    description: None,
20021                    mime_type: None,
20022                    icon: None,
20023                    version: None,
20024                    tags: Vec::new(),
20025                },
20026                crate::DuplicateBehavior::Replace,
20027            )
20028            .expect("resource-template registration succeeds");
20029        router.add_prompt(NamedPrompt::new("connection-scoped-prompt"));
20030
20031        let cx = Cx::for_testing();
20032        let connection = ModernConnection::new();
20033        let inbound = InboundRequestContext::with_modern_connection(
20034            cx,
20035            614,
20036            InboundRequestTransport::Stdio,
20037            &connection,
20038        );
20039        let request_ctx = inbound.request_context();
20040        let metadata = serde_json::json!({
20041            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
20042            "io.modelcontextprotocol/clientCapabilities": {},
20043        });
20044        let list_request = |method, id| {
20045            JsonRpcRequest::new(
20046                method,
20047                Some(serde_json::json!({"_meta": metadata.clone()})),
20048                id,
20049            )
20050        };
20051
20052        let tools = router
20053            .dispatch_stateless(&request_ctx, &list_request("tools/list", 614_i64))
20054            .expect("enabled tool is discovered");
20055        let resources = router
20056            .dispatch_stateless(&request_ctx, &list_request("resources/list", 615_i64))
20057            .expect("enabled resource is discovered");
20058        let templates = router
20059            .dispatch_stateless(
20060                &request_ctx,
20061                &list_request("resources/templates/list", 616_i64),
20062            )
20063            .expect("enabled resource template is discovered");
20064        let prompts = router
20065            .dispatch_stateless(&request_ctx, &list_request("prompts/list", 617_i64))
20066            .expect("enabled prompt is discovered");
20067        assert_eq!(tools["tools"][0]["name"], "connection-scoped-tool");
20068        assert_eq!(
20069            resources["resources"][0]["uri"],
20070            "file:///connection-scoped-resource"
20071        );
20072        assert_eq!(
20073            templates["resourceTemplates"][0]["uriTemplate"],
20074            template_uri
20075        );
20076        assert_eq!(prompts["prompts"][0]["name"], "connection-scoped-prompt");
20077
20078        assert!(request_ctx.disable_tool("connection-scoped-tool"));
20079        assert!(request_ctx.disable_resource("file:///connection-scoped-resource"));
20080        assert!(request_ctx.disable_resource(template_uri));
20081        assert!(request_ctx.disable_prompt("connection-scoped-prompt"));
20082
20083        let discovered_tools = router
20084            .dispatch_stateless(&request_ctx, &list_request("tools/list", 618_i64))
20085            .expect("disabled tool remains in the immutable final catalog");
20086        let discovered_resources = router
20087            .dispatch_stateless(&request_ctx, &list_request("resources/list", 619_i64))
20088            .expect("disabled resource remains in the immutable final catalog");
20089        let discovered_templates = router
20090            .dispatch_stateless(
20091                &request_ctx,
20092                &list_request("resources/templates/list", 620_i64),
20093            )
20094            .expect("disabled template remains in the immutable final catalog");
20095        let discovered_prompts = router
20096            .dispatch_stateless(&request_ctx, &list_request("prompts/list", 621_i64))
20097            .expect("disabled prompt remains in the immutable final catalog");
20098        assert_eq!(
20099            discovered_tools["tools"][0]["name"],
20100            "connection-scoped-tool"
20101        );
20102        assert_eq!(
20103            discovered_resources["resources"][0]["uri"],
20104            "file:///connection-scoped-resource"
20105        );
20106        assert_eq!(
20107            discovered_templates["resourceTemplates"][0]["uriTemplate"],
20108            template_uri
20109        );
20110        assert_eq!(
20111            discovered_prompts["prompts"][0]["name"],
20112            "connection-scoped-prompt"
20113        );
20114
20115        let legacy_state = SessionState::new();
20116        let legacy_request_ctx =
20117            request_context(request_ctx.cx(), 622, Budget::INFINITE, &legacy_state);
20118        assert!(legacy_request_ctx.disable_tool("connection-scoped-tool"));
20119        assert!(legacy_request_ctx.disable_resource("file:///connection-scoped-resource"));
20120        assert!(legacy_request_ctx.disable_resource(template_uri));
20121        assert!(legacy_request_ctx.disable_prompt("connection-scoped-prompt"));
20122
20123        let legacy_tools = router
20124            .handle_tools_list(
20125                &legacy_request_ctx,
20126                ListToolsParams {
20127                    cursor: None,
20128                    include_tags: None,
20129                    exclude_tags: None,
20130                },
20131                Some(&legacy_state),
20132            )
20133            .expect("exact legacy tools/list remains session-stateful");
20134        let legacy_resources = router
20135            .handle_resources_list(
20136                &legacy_request_ctx,
20137                ListResourcesParams {
20138                    cursor: None,
20139                    include_tags: None,
20140                    exclude_tags: None,
20141                },
20142                Some(&legacy_state),
20143            )
20144            .expect("exact legacy resources/list remains session-stateful");
20145        let legacy_templates = router
20146            .handle_resource_templates_list(
20147                &legacy_request_ctx,
20148                ListResourceTemplatesParams {
20149                    cursor: None,
20150                    include_tags: None,
20151                    exclude_tags: None,
20152                },
20153                Some(&legacy_state),
20154            )
20155            .expect("exact legacy resources/templates/list remains session-stateful");
20156        let legacy_prompts = router
20157            .handle_prompts_list(
20158                &legacy_request_ctx,
20159                ListPromptsParams {
20160                    cursor: None,
20161                    include_tags: None,
20162                    exclude_tags: None,
20163                },
20164                Some(&legacy_state),
20165            )
20166            .expect("exact legacy prompts/list remains session-stateful");
20167        assert!(legacy_tools.tools.is_empty());
20168        assert!(legacy_resources.resources.is_empty());
20169        assert!(legacy_templates.resource_templates.is_empty());
20170        assert!(legacy_prompts.prompts.is_empty());
20171
20172        let later_inbound = InboundRequestContext::with_modern_connection(
20173            request_ctx.cx().clone(),
20174            630,
20175            InboundRequestTransport::Stdio,
20176            &connection,
20177        );
20178        let later_ctx = later_inbound.request_context();
20179        let read_error = router
20180            .dispatch_stateless(
20181                &later_ctx,
20182                &JsonRpcRequest::new(
20183                    "resources/read",
20184                    Some(serde_json::json!({
20185                        "_meta": metadata,
20186                        "uri": "file:///connection-scoped-resource",
20187                    })),
20188                    631_i64,
20189                ),
20190            )
20191            .expect_err(
20192                "a later inbound on the same modern connection must refuse the disabled resource",
20193            );
20194        assert_eq!(read_error.code, McpErrorCode::ResourceNotFound);
20195        assert!(
20196            read_error.message.contains("disabled"),
20197            "the refused resource read must keep the session-disabled message: {read_error:?}"
20198        );
20199        let prompt_error = router
20200            .dispatch_stateless(
20201                &later_ctx,
20202                &JsonRpcRequest::new(
20203                    "prompts/get",
20204                    Some(serde_json::json!({
20205                        "_meta": metadata,
20206                        "name": "connection-scoped-prompt",
20207                    })),
20208                    632_i64,
20209                ),
20210            )
20211            .expect_err(
20212                "a later inbound on the same modern connection must refuse the disabled prompt",
20213            );
20214        assert_eq!(prompt_error.code, McpErrorCode::PromptNotFound);
20215        assert!(
20216            prompt_error.message.contains("disabled"),
20217            "the refused prompt get must keep the session-disabled message: {prompt_error:?}"
20218        );
20219        let tool_error = router
20220            .dispatch_stateless(
20221                &later_ctx,
20222                &JsonRpcRequest::new(
20223                    "tools/call",
20224                    Some(serde_json::json!({
20225                        "_meta": metadata,
20226                        "name": "connection-scoped-tool",
20227                        "arguments": {},
20228                    })),
20229                    633_i64,
20230                ),
20231            )
20232            .expect_err(
20233                "a later inbound on the same modern connection must refuse the disabled tool",
20234            );
20235        assert_eq!(tool_error.code, McpErrorCode::MethodNotFound);
20236        assert!(
20237            tool_error.message.contains("disabled"),
20238            "the refused tool call must keep the session-disabled message: {tool_error:?}"
20239        );
20240    }
20241
20242    #[test]
20243    fn final_catalog_cursor_survives_a_changed_session_state() {
20244        let mut router = Router::new();
20245        router.set_list_page_size(Some(1));
20246        router
20247            .add_tool(NamedTool::new("first-connection-cursor-tool"))
20248            .expect("first tool registration succeeds");
20249        router
20250            .add_tool(NamedTool::new("second-connection-cursor-tool"))
20251            .expect("second tool registration succeeds");
20252
20253        let cx = Cx::for_testing();
20254        let state = SessionState::new();
20255        let request_ctx = request_context(&cx, 622, Budget::INFINITE, &state);
20256        let initial = final_tools_list_request(None, None, None, 622_i64);
20257        let initial_bytes = serde_json::to_vec(&initial).expect("initial request serializes");
20258        let first_page = router
20259            .dispatch_stateless(&request_ctx, &initial)
20260            .expect("the initial final catalog page is admitted");
20261        let cursor = first_page["nextCursor"]
20262            .as_str()
20263            .expect("the first page has a continuation")
20264            .to_owned();
20265
20266        assert!(request_ctx.disable_tool("first-connection-cursor-tool"));
20267        assert_eq!(
20268            serde_json::to_vec(&initial).expect("initial request remains serializable"),
20269            initial_bytes,
20270            "connection state cannot alter the request or its catalog continuation"
20271        );
20272        let continued = router
20273            .dispatch_stateless(
20274                &request_ctx,
20275                &final_tools_list_request(Some(&cursor), None, None, 623_i64),
20276            )
20277            .expect("a disabled-component change cannot invalidate an immutable final cursor");
20278        assert_eq!(
20279            continued["tools"][0]["name"],
20280            "second-connection-cursor-tool"
20281        );
20282        assert!(continued.get("nextCursor").is_none());
20283
20284        let refreshed = router
20285            .dispatch_stateless(
20286                &request_ctx,
20287                &final_tools_list_request(None, None, None, 624_i64),
20288            )
20289            .expect("a cursor-free request keeps the immutable final catalog");
20290        assert_eq!(
20291            refreshed["tools"][0]["name"],
20292            "first-connection-cursor-tool"
20293        );
20294    }
20295
20296    #[test]
20297    fn final_catalog_cursor_continues_across_modern_connection_partitions() {
20298        let mut router = Router::new();
20299        router.set_list_page_size(Some(1));
20300        router
20301            .add_tool(NamedTool::new("first-cross-connection-cursor-tool"))
20302            .expect("first tool registration succeeds");
20303        router
20304            .add_tool(NamedTool::new("second-cross-connection-cursor-tool"))
20305            .expect("second tool registration succeeds");
20306
20307        let cx = Cx::for_testing();
20308        let origin_connection = ModernConnection::new();
20309        let origin_inbound = InboundRequestContext::with_modern_connection(
20310            cx.clone(),
20311            625,
20312            InboundRequestTransport::Stdio,
20313            &origin_connection,
20314        );
20315        let origin_context = origin_inbound.request_context();
20316        let continuation_connection = ModernConnection::new();
20317        let continuation_inbound = InboundRequestContext::with_modern_connection(
20318            cx,
20319            626,
20320            InboundRequestTransport::Stdio,
20321            &continuation_connection,
20322        );
20323        let continuation_context = continuation_inbound.request_context();
20324        let origin_partition = origin_context
20325            .session_cache_partition()
20326            .expect("a modern origin context has a durable partition")
20327            .0;
20328        let continuation_partition = continuation_context
20329            .session_cache_partition()
20330            .expect("a modern continuation context has a durable partition")
20331            .0;
20332        assert_ne!(
20333            origin_partition, continuation_partition,
20334            "distinct modern connections have distinct durable partitions"
20335        );
20336
20337        let first_page = router
20338            .dispatch_stateless(
20339                &origin_context,
20340                &final_tools_list_request(None, None, None, 625_i64),
20341            )
20342            .expect("the origin connection receives the first final catalog page");
20343        let cursor = first_page["nextCursor"]
20344            .as_str()
20345            .expect("the first page has a continuation")
20346            .to_owned();
20347
20348        let continued = router
20349            .dispatch_stateless(
20350                &continuation_context,
20351                &final_tools_list_request(Some(&cursor), None, None, 626_i64),
20352            )
20353            .expect("a final catalog cursor is not bound to its origin connection partition");
20354        assert_eq!(
20355            continued["tools"][0]["name"],
20356            "second-cross-connection-cursor-tool"
20357        );
20358        assert!(continued.get("nextCursor").is_none());
20359    }
20360
20361    #[test]
20362    fn modern_connection_disconnect_is_the_only_changed_retry_dimension_and_cancels_state() {
20363        let final_calls = Arc::new(AtomicUsize::new(0));
20364        let mut router = Router::new();
20365        router
20366            .add_tool(InputRequiredTool {
20367                legacy_calls: Arc::new(AtomicUsize::new(0)),
20368                final_calls: Arc::clone(&final_calls),
20369            })
20370            .expect("tool registration succeeds");
20371
20372        let cx = Cx::for_testing();
20373        let connection = ModernConnection::new();
20374        let inbound = InboundRequestContext::with_modern_connection(
20375            cx.clone(),
20376            301,
20377            InboundRequestTransport::Stdio,
20378            &connection,
20379        );
20380        let request_ctx = inbound.request_context();
20381        let continuation_cancellation = inbound
20382            .mrtr_continuation_cancellation()
20383            .expect("a modern connection supplies continuation ownership");
20384        let metadata = serde_json::json!({
20385            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
20386            "io.modelcontextprotocol/clientCapabilities": {},
20387        });
20388        let initial = JsonRpcRequest::new(
20389            "tools/call",
20390            Some(serde_json::json!({
20391                "_meta": metadata.clone(),
20392                "name": "input-required-tool",
20393                "arguments": {},
20394            })),
20395            301_i64,
20396        );
20397        let initial_result = router
20398            .dispatch_stateless_with_continuation_cancellation(
20399                &request_ctx,
20400                &initial,
20401                &continuation_cancellation,
20402            )
20403            .expect("the connected request mints a continuation");
20404        let retry = JsonRpcRequest::new(
20405            "tools/call",
20406            Some(serde_json::json!({
20407                "_meta": metadata,
20408                "name": "input-required-tool",
20409                "arguments": {},
20410                "inputResponses": {"roots": router_roots_response_wire()},
20411                "requestState": initial_result["requestState"].clone(),
20412            })),
20413            302_i64,
20414        );
20415        let retry_before_disconnect = serde_json::to_vec(&retry).expect("retry serializes");
20416
20417        // The request, opaque state, durable partition, and response map all
20418        // remain unchanged. Peer disconnect is the sole changed dimension.
20419        connection.disconnect();
20420        let error = router
20421            .dispatch_stateless_with_continuation_cancellation(
20422                &request_ctx,
20423                &retry,
20424                &continuation_cancellation,
20425            )
20426            .expect_err("disconnect must cancel the retained continuation");
20427        assert_eq!(error.code, McpErrorCode::RequestCancelled);
20428        assert_eq!(
20429            serde_json::to_vec(&retry).expect("retry serializes"),
20430            retry_before_disconnect,
20431            "disconnect cancellation cannot mutate client retry state"
20432        );
20433        assert_eq!(
20434            final_calls.load(Ordering::SeqCst),
20435            1,
20436            "a disconnected continuation cannot reach the resumed handler"
20437        );
20438    }
20439
20440    #[cfg(feature = "tasks")]
20441    #[test]
20442    fn task_capable_input_required_retry_does_not_require_tasks_capability() {
20443        let final_calls = Arc::new(AtomicUsize::new(0));
20444        let mut router = Router::new();
20445        router
20446            .add_tool(TaskCapableInputRequiredTool {
20447                final_calls: Arc::clone(&final_calls),
20448            })
20449            .expect("task-capable input-required tool registration succeeds");
20450        let cx = Cx::for_testing();
20451        let connection = ModernConnection::new();
20452        let inbound = InboundRequestContext::with_modern_connection(
20453            cx,
20454            148,
20455            InboundRequestTransport::Memory,
20456            &connection,
20457        );
20458        let request_ctx = inbound.request_context();
20459        let metadata_without_tasks = serde_json::json!({
20460            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
20461            "io.modelcontextprotocol/clientCapabilities": {},
20462        });
20463
20464        let initial = JsonRpcRequest::new(
20465            "tools/call",
20466            Some(serde_json::json!({
20467                "_meta": metadata_without_tasks.clone(),
20468                "name": "task-capable-input-required-tool",
20469                "arguments": {},
20470            })),
20471            148_i64,
20472        );
20473        let initial_result = router
20474            .dispatch_stateless(&request_ctx, &initial)
20475            .expect("a task-capable tool may return input_required without Tasks negotiation");
20476        let request_state = initial_result["requestState"]
20477            .as_str()
20478            .expect("framework result carries opaque task-capable state")
20479            .to_owned();
20480        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
20481
20482        let retry = JsonRpcRequest::new(
20483            "tools/call",
20484            Some(serde_json::json!({
20485                "_meta": metadata_without_tasks,
20486                "name": "task-capable-input-required-tool",
20487                "arguments": {},
20488                "inputResponses": {"roots": router_roots_response_wire()},
20489                "requestState": request_state,
20490            })),
20491            149_i64,
20492        );
20493        let resumed = router
20494            .dispatch_stateless(&request_ctx, &retry)
20495            .expect("a task-capable input-required retry remains an ordinary MRTR operation");
20496        assert_eq!(resumed["resultType"], "input_required");
20497        assert_eq!(final_calls.load(Ordering::SeqCst), 2);
20498    }
20499
20500    #[test]
20501    fn final_input_required_does_not_fallback_without_negotiation_metadata() {
20502        let legacy_calls = Arc::new(AtomicUsize::new(0));
20503        let final_calls = Arc::new(AtomicUsize::new(0));
20504        let mut router = Router::new();
20505        router
20506            .add_tool(InputRequiredTool {
20507                legacy_calls: Arc::clone(&legacy_calls),
20508                final_calls: Arc::clone(&final_calls),
20509            })
20510            .expect("tool registration succeeds");
20511        let cx = Cx::for_testing();
20512        let connection = ModernConnection::new();
20513        let inbound = InboundRequestContext::with_modern_connection(
20514            cx,
20515            144,
20516            InboundRequestTransport::Memory,
20517            &connection,
20518        );
20519        let request_ctx = inbound.request_context();
20520        let baseline = JsonRpcRequest::new(
20521            "tools/call",
20522            Some(serde_json::json!({
20523                "_meta": {
20524                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
20525                    "io.modelcontextprotocol/clientCapabilities": {},
20526                },
20527                "name": "input-required-tool",
20528                "arguments": {},
20529            })),
20530            144_i64,
20531        );
20532        let mut planted = baseline.clone();
20533        planted
20534            .params
20535            .as_mut()
20536            .and_then(serde_json::Value::as_object_mut)
20537            .expect("tools/call parameters are an object")
20538            .remove("_meta");
20539
20540        assert_eq!(baseline.method, planted.method);
20541        assert_eq!(baseline.id, planted.id);
20542        assert_eq!(
20543            baseline
20544                .params
20545                .as_ref()
20546                .and_then(|params| params.get("name")),
20547            planted
20548                .params
20549                .as_ref()
20550                .and_then(|params| params.get("name")),
20551            "negotiation metadata is the sole planted dimension"
20552        );
20553        let catalog_before = serde_json::to_vec(&router.tools()).expect("catalog serializes");
20554        let planted_before = serde_json::to_vec(&planted).expect("request serializes");
20555
20556        let accepted = router
20557            .dispatch_stateless(&request_ctx, &baseline)
20558            .expect("negotiated final request is accepted");
20559        assert_eq!(accepted["resultType"], "input_required");
20560        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
20561        assert_eq!(legacy_calls.load(Ordering::SeqCst), 0);
20562
20563        let error = router
20564            .dispatch_stateless(&request_ctx, &planted)
20565            .expect_err("one-field no-negotiation request is rejected instead of falling back");
20566        assert_eq!(error.code, McpErrorCode::InvalidParams);
20567        assert_eq!(
20568            serde_json::to_vec(&planted).expect("rejected request serializes"),
20569            planted_before,
20570            "rejection cannot mutate caller-owned no-negotiation parameters"
20571        );
20572        assert_eq!(
20573            serde_json::to_vec(&router.tools()).expect("catalog serializes"),
20574            catalog_before,
20575            "rejection cannot mutate the installed handler catalog"
20576        );
20577        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
20578        assert_eq!(legacy_calls.load(Ordering::SeqCst), 0);
20579        let retried = router
20580            .dispatch_stateless(&request_ctx, &baseline)
20581            .expect("the negotiated baseline remains accepted after rejection");
20582        assert_eq!(retried["resultType"], "input_required");
20583        assert_ne!(
20584            retried["requestState"], accepted["requestState"],
20585            "every framework-issued MRTR continuation has fresh opaque state"
20586        );
20587        assert_eq!(final_calls.load(Ordering::SeqCst), 2);
20588        assert_eq!(legacy_calls.load(Ordering::SeqCst), 0);
20589    }
20590
20591    #[test]
20592    fn final_prompts_get_dispatches_direct_request_owned_handler() {
20593        let final_calls = Arc::new(AtomicUsize::new(0));
20594        let mut router = Router::new();
20595        router.add_prompt(DirectFinalPrompt {
20596            final_calls: Arc::clone(&final_calls),
20597        });
20598        let cx = Cx::for_testing();
20599        let state = SessionState::new();
20600        let request_ctx = request_context(&cx, 96, Budget::INFINITE, &state);
20601        let request = direct_final_prompt_request(96);
20602        let typed_request = CoreRequest::decode(
20603            ProtocolEra::Modern2026,
20604            "prompts/get",
20605            request.params.as_ref(),
20606        )
20607        .expect("final prompts/get request decodes through the public core surface");
20608
20609        let response = router
20610            .dispatch_stateless(&request_ctx, &request)
20611            .expect("final prompts/get reaches the direct final handler");
20612
20613        assert_eq!(
20614            response.get("resultType"),
20615            Some(&serde_json::json!("complete"))
20616        );
20617        assert_eq!(
20618            response["description"],
20619            serde_json::json!("direct final prompt description")
20620        );
20621        assert_eq!(response["messages"][0]["content"]["type"], "audio");
20622        assert_eq!(
20623            response["messages"][0]["content"]["_meta"]["com.example/direct-prompt"]["source"],
20624            "final-handler"
20625        );
20626        assert_eq!(
20627            response["messages"][0]["content"]["com.example/direct-field"],
20628            true
20629        );
20630        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
20631
20632        let wire = serde_json::to_string(&response).expect("final prompt response serializes");
20633        let CoreResult::Final(FinalCoreResult::PromptsGet { result, .. }) = typed_request
20634            .decode_result(&wire)
20635            .expect("final prompts/get result decodes through the public core surface")
20636        else {
20637            panic!("prompts/get selects the exact final result");
20638        };
20639        assert_eq!(
20640            result.payload.description.as_deref(),
20641            Some("direct final prompt description")
20642        );
20643        assert!(matches!(
20644            result.payload.messages.as_slice(),
20645            [FinalPromptMessage {
20646                content: ContentBlock::Audio { data, mime_type, .. },
20647                ..
20648            }] if data == "aGVsbG8=" && mime_type == "audio/mpeg"
20649        ));
20650    }
20651
20652    #[test]
20653    fn public_final_resource_uri_policy_admits_client_direct_https_and_rejects_only_policy_change_without_mutation()
20654     {
20655        let client_direct = HttpsCatalogResource {
20656            client_direct_https: true,
20657        };
20658        let server_mediated = HttpsCatalogResource {
20659            client_direct_https: false,
20660        };
20661        assert_eq!(
20662            client_direct.definition(),
20663            server_mediated.definition(),
20664            "the URI-use policy is the sole registration difference"
20665        );
20666
20667        let cx = Cx::for_testing();
20668        let state = SessionState::new();
20669        let request_ctx = request_context(&cx, 971, Budget::INFINITE, &state);
20670        let final_metadata = serde_json::json!({
20671            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
20672            "io.modelcontextprotocol/clientCapabilities": {},
20673        });
20674
20675        let mut accepted_router = Router::new();
20676        accepted_router
20677            .add_resource_with_behavior(client_direct, crate::DuplicateBehavior::Replace)
20678            .expect("an explicitly client-direct HTTPS catalog resource is admitted");
20679        let accepted = accepted_router
20680            .dispatch_stateless(
20681                &request_ctx,
20682                &JsonRpcRequest::new(
20683                    "resources/list",
20684                    Some(serde_json::json!({"_meta": final_metadata.clone()})),
20685                    971_i64,
20686                ),
20687            )
20688            .expect("public final resource listing succeeds");
20689        assert_eq!(
20690            accepted["resources"][0]["uri"],
20691            "https://client.example.test/catalog.txt"
20692        );
20693        let direct_read = accepted_router
20694            .dispatch_stateless(
20695                &request_ctx,
20696                &JsonRpcRequest::new(
20697                    "resources/read",
20698                    Some(serde_json::json!({
20699                        "_meta": final_metadata.clone(),
20700                        "uri": "https://client.example.test/catalog.txt",
20701                    })),
20702                    972_i64,
20703                ),
20704            )
20705            .expect_err("client-direct HTTPS catalog entries are not MCP-read identities");
20706        assert_eq!(direct_read.code, McpErrorCode::InvalidParams);
20707
20708        let mut rejected_router = Router::new();
20709        rejected_router.add_resource(NamedResource::new("mcp://uri-policy/unchanged"));
20710        let catalog_before =
20711            serde_json::to_vec(&rejected_router.resources()).expect("existing catalog serializes");
20712        let count_before = rejected_router.resources_count();
20713        let error = rejected_router
20714            .add_resource_with_behavior(server_mediated, crate::DuplicateBehavior::Replace)
20715            .expect_err("changing only to server-mediated policy rejects HTTPS registration");
20716        assert_eq!(error.code, McpErrorCode::InvalidParams);
20717        assert_eq!(
20718            rejected_router.resources_count(),
20719            count_before,
20720            "rejected final admission cannot add or replace a resource handler"
20721        );
20722        assert_eq!(
20723            serde_json::to_vec(&rejected_router.resources()).expect("catalog remains serializable"),
20724            catalog_before,
20725            "rejected URI-use admission leaves the prior catalog unchanged"
20726        );
20727        let unchanged = rejected_router
20728            .dispatch_stateless(
20729                &request_ctx,
20730                &JsonRpcRequest::new(
20731                    "resources/list",
20732                    Some(serde_json::json!({"_meta": final_metadata})),
20733                    972_i64,
20734                ),
20735            )
20736            .expect("rejection leaves public final dispatch usable");
20737        assert_eq!(unchanged["resources"].as_array().map(Vec::len), Some(1));
20738        assert_eq!(
20739            unchanged["resources"][0]["uri"],
20740            "mcp://uri-policy/unchanged"
20741        );
20742    }
20743
20744    #[test]
20745    fn resource_uri_use_policy_leaves_exact_2024_resource_registration_unchanged() {
20746        let mut router = Router::new();
20747        router
20748            .add_legacy_resource_with_behavior(
20749                HttpsCatalogResource {
20750                    client_direct_https: false,
20751                },
20752                crate::DuplicateBehavior::Replace,
20753            )
20754            .expect("exact-2024 registration does not consult final URI-use admission");
20755
20756        let cx = Cx::for_testing();
20757        let request_ctx = McpContext::new(cx, 976);
20758        let listed = router
20759            .handle_resources_list(&request_ctx, ListResourcesParams::default(), None)
20760            .expect("exact-2024 listing remains available");
20761        assert_eq!(listed.resources.len(), 1);
20762        assert_eq!(
20763            listed.resources[0].uri,
20764            "https://client.example.test/catalog.txt"
20765        );
20766    }
20767
20768    #[test]
20769    fn public_final_uri_policy_rechecks_prompt_and_mrtr_resource_emissions() {
20770        let initial_calls = Arc::new(AtomicUsize::new(0));
20771        let resumed_calls = Arc::new(AtomicUsize::new(0));
20772        let mut router = Router::new();
20773        router.add_prompt(ClientDirectHttpsPrompt);
20774        router.add_resource(MrtrHttpsEmbeddedResource {
20775            initial_calls: Arc::clone(&initial_calls),
20776            resumed_calls: Arc::clone(&resumed_calls),
20777        });
20778
20779        let cx = Cx::for_testing();
20780        let state = SessionState::new();
20781        let request_ctx = request_context(&cx, 973, Budget::INFINITE, &state);
20782        let metadata = serde_json::json!({
20783            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
20784            "io.modelcontextprotocol/clientCapabilities": {},
20785        });
20786
20787        let prompt = router
20788            .dispatch_stateless(
20789                &request_ctx,
20790                &JsonRpcRequest::new(
20791                    "prompts/get",
20792                    Some(serde_json::json!({
20793                        "_meta": metadata.clone(),
20794                        "name": "client-direct-https-prompt",
20795                    })),
20796                    973_i64,
20797                ),
20798            )
20799            .expect("a client-direct HTTPS resource link is emitted from the public prompt path");
20800        assert_eq!(
20801            prompt["messages"][0]["content"]["uri"],
20802            "https://client.example.test/prompt-link"
20803        );
20804
20805        let initial = router
20806            .dispatch_stateless(
20807                &request_ctx,
20808                &JsonRpcRequest::new(
20809                    "resources/read",
20810                    Some(serde_json::json!({
20811                        "_meta": metadata.clone(),
20812                        "uri": "mcp://uri-policy/mrtr",
20813                    })),
20814                    974_i64,
20815                ),
20816            )
20817            .expect("initial public resource request mints MRTR state");
20818        assert_eq!(initial["resultType"], "input_required");
20819        let request_state = initial["requestState"]
20820            .as_str()
20821            .expect("framework minted MRTR state")
20822            .to_owned();
20823        let catalog_before = serde_json::to_vec(&router.resources())
20824            .expect("catalog serializes before the resumed rejection");
20825        let retry = JsonRpcRequest::new(
20826            "resources/read",
20827            Some(serde_json::json!({
20828                "_meta": metadata,
20829                "uri": "mcp://uri-policy/mrtr",
20830                "inputResponses": {"roots": router_roots_response_wire()},
20831                "requestState": request_state,
20832            })),
20833            975_i64,
20834        );
20835        let error = router.dispatch_stateless(&request_ctx, &retry).expect_err(
20836            "an MRTR-resumed HTTPS embedded resource remains server-mediated and is refused",
20837        );
20838        assert_eq!(error.code, McpErrorCode::InternalError);
20839        assert_eq!(initial_calls.load(Ordering::SeqCst), 1);
20840        assert_eq!(resumed_calls.load(Ordering::SeqCst), 1);
20841        assert_eq!(
20842            serde_json::to_vec(&router.resources()).expect("catalog remains serializable"),
20843            catalog_before,
20844            "rejected dynamic MRTR output cannot mutate registered resource state"
20845        );
20846    }
20847
20848    #[test]
20849    fn final_prompt_arguments_are_validated_before_handler_and_legacy_is_unchanged() {
20850        let final_calls = Arc::new(AtomicUsize::new(0));
20851        let legacy_calls = Arc::new(AtomicUsize::new(0));
20852        let mut router = Router::new();
20853        router.add_prompt(PromptArgumentBoundary {
20854            final_calls: Arc::clone(&final_calls),
20855            legacy_calls: Arc::clone(&legacy_calls),
20856        });
20857        let cx = Cx::for_testing();
20858        let state = SessionState::new();
20859        let request_ctx = request_context(&cx, 155, Budget::INFINITE, &state);
20860        let baseline = final_prompt_get_request(
20861            "prompt-argument-boundary",
20862            serde_json::json!({"topic": "release"}),
20863            155_i64,
20864        );
20865        let mut missing_required = baseline.clone();
20866        let removed_topic = missing_required
20867            .params
20868            .as_mut()
20869            .and_then(serde_json::Value::as_object_mut)
20870            .and_then(|params| params.get_mut("arguments"))
20871            .and_then(serde_json::Value::as_object_mut)
20872            .expect("prompt arguments are an object")
20873            .remove("topic");
20874        assert_eq!(removed_topic, Some(serde_json::json!("release")));
20875
20876        assert_eq!(baseline.method, missing_required.method);
20877        assert_eq!(baseline.id, missing_required.id);
20878        assert_eq!(
20879            baseline
20880                .params
20881                .as_ref()
20882                .and_then(|params| params.get("_meta")),
20883            missing_required
20884                .params
20885                .as_ref()
20886                .and_then(|params| params.get("_meta")),
20887            "the required argument is the sole planted dimension"
20888        );
20889        assert_eq!(
20890            baseline
20891                .params
20892                .as_ref()
20893                .and_then(|params| params.get("name")),
20894            missing_required
20895                .params
20896                .as_ref()
20897                .and_then(|params| params.get("name")),
20898            "the required argument is the sole planted dimension"
20899        );
20900
20901        let accepted = router
20902            .dispatch_stateless(&request_ctx, &baseline)
20903            .expect("the complete required-argument request is accepted");
20904        assert_eq!(accepted["resultType"], "complete");
20905        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
20906        assert_eq!(legacy_calls.load(Ordering::SeqCst), 0);
20907
20908        let missing_error = router
20909            .dispatch_stateless(&request_ctx, &missing_required)
20910            .expect_err("removing only a required prompt argument is rejected");
20911        assert_eq!(missing_error.code, McpErrorCode::InvalidParams);
20912        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
20913        assert_eq!(legacy_calls.load(Ordering::SeqCst), 0);
20914
20915        let unknown_error = router
20916            .dispatch_stateless(
20917                &request_ctx,
20918                &final_prompt_get_request("unknown-prompt", serde_json::json!({}), 156_i64),
20919            )
20920            .expect_err("an unknown final prompt is an invalid-params protocol error");
20921        assert_eq!(unknown_error.code, McpErrorCode::InvalidParams);
20922        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
20923
20924        let legacy = router
20925            .handle_prompts_get(
20926                &request_ctx,
20927                GetPromptParams {
20928                    name: "prompt-argument-boundary".to_owned(),
20929                    arguments: None,
20930                    meta: None,
20931                },
20932                state,
20933                None,
20934                None,
20935            )
20936            .expect("the exact legacy prompt path retains its existing argument behavior");
20937        let legacy_wire = serde_json::to_value(&legacy).expect("legacy prompt result serializes");
20938        assert_eq!(
20939            legacy_wire["messages"][0]["content"]["text"],
20940            "legacy prompt-argument-boundary result"
20941        );
20942        assert_eq!(legacy_calls.load(Ordering::SeqCst), 1);
20943    }
20944
20945    #[test]
20946    fn final_prompt_argument_snapshot_is_immutable_after_registration() {
20947        let expose_admitted_argument = Arc::new(AtomicBool::new(true));
20948        let final_calls = Arc::new(AtomicUsize::new(0));
20949        let mut router = Router::new();
20950        router.add_prompt(MutablePromptDefinition {
20951            expose_admitted_argument: Arc::clone(&expose_admitted_argument),
20952            final_calls: Arc::clone(&final_calls),
20953        });
20954        expose_admitted_argument.store(false, Ordering::SeqCst);
20955
20956        let cx = Cx::for_testing();
20957        let state = SessionState::new();
20958        let request_ctx = request_context(&cx, 156, Budget::INFINITE, &state);
20959        let accepted = final_prompt_get_request(
20960            "mutable-prompt-definition",
20961            serde_json::json!({"topic": "release"}),
20962            156_i64,
20963        );
20964        let mut missing_required = accepted.clone();
20965        let removed = missing_required
20966            .params
20967            .as_mut()
20968            .and_then(serde_json::Value::as_object_mut)
20969            .and_then(|params| params.get_mut("arguments"))
20970            .and_then(serde_json::Value::as_object_mut)
20971            .expect("prompt arguments are an object")
20972            .remove("topic");
20973        assert_eq!(removed, Some(serde_json::json!("release")));
20974        assert_eq!(accepted.method, missing_required.method);
20975        assert_eq!(accepted.id, missing_required.id);
20976        assert_eq!(
20977            accepted
20978                .params
20979                .as_ref()
20980                .and_then(|params| params.get("_meta")),
20981            missing_required
20982                .params
20983                .as_ref()
20984                .and_then(|params| params.get("_meta")),
20985            "the required argument is the sole planted dimension"
20986        );
20987        assert_eq!(
20988            accepted
20989                .params
20990                .as_ref()
20991                .and_then(|params| params.get("name")),
20992            missing_required
20993                .params
20994                .as_ref()
20995                .and_then(|params| params.get("name")),
20996            "the required argument is the sole planted dimension"
20997        );
20998
20999        let catalog = router
21000            .dispatch_stateless(
21001                &request_ctx,
21002                &JsonRpcRequest::new(
21003                    "prompts/list",
21004                    Some(serde_json::json!({
21005                        "_meta": {
21006                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
21007                            "io.modelcontextprotocol/clientCapabilities": {},
21008                        },
21009                    })),
21010                    157_i64,
21011                ),
21012            )
21013            .expect("the final prompt catalog retains its admitted argument metadata");
21014        assert_eq!(catalog["prompts"][0]["arguments"][0]["name"], "topic");
21015        assert_eq!(catalog["prompts"][0]["arguments"][0]["required"], true);
21016
21017        let response = router
21018            .dispatch_stateless(&request_ctx, &accepted)
21019            .expect("the admitted final argument remains accepted after the legacy hook changes");
21020        assert_eq!(response["resultType"], "complete");
21021        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
21022
21023        let error = router
21024            .dispatch_stateless(&request_ctx, &missing_required)
21025            .expect_err("removing only the admitted required argument is rejected");
21026        assert_eq!(error.code, McpErrorCode::InvalidParams);
21027        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
21028    }
21029
21030    #[test]
21031    fn final_prompts_get_rejects_one_field_incompatible_result_type() {
21032        let final_calls = Arc::new(AtomicUsize::new(0));
21033        let mut router = Router::new();
21034        router.add_prompt(DirectFinalPrompt {
21035            final_calls: Arc::clone(&final_calls),
21036        });
21037        let cx = Cx::for_testing();
21038        let state = SessionState::new();
21039        let request_ctx = request_context(&cx, 97, Budget::INFINITE, &state);
21040        let request = direct_final_prompt_request(97);
21041        let typed_request = CoreRequest::decode(
21042            ProtocolEra::Modern2026,
21043            "prompts/get",
21044            request.params.as_ref(),
21045        )
21046        .expect("final prompts/get request decodes through the public core surface");
21047        let accepted = router
21048            .dispatch_stateless(&request_ctx, &request)
21049            .expect("the direct final prompt response is accepted");
21050        let accepted_wire = serde_json::to_string(&accepted).expect("accepted result serializes");
21051        let mut incompatible = accepted.clone();
21052        incompatible["resultType"] = serde_json::json!("input_required");
21053
21054        let mut accepted_without_type = accepted.clone();
21055        let mut incompatible_without_type = incompatible.clone();
21056        let accepted_type = accepted_without_type
21057            .as_object_mut()
21058            .and_then(|object| object.remove("resultType"));
21059        let incompatible_type = incompatible_without_type
21060            .as_object_mut()
21061            .and_then(|object| object.remove("resultType"));
21062        assert_eq!(accepted_type, Some(serde_json::json!("complete")));
21063        assert_eq!(incompatible_type, Some(serde_json::json!("input_required")));
21064        assert_eq!(
21065            incompatible_without_type, accepted_without_type,
21066            "resultType is the sole incompatible result dimension"
21067        );
21068
21069        let incompatible_wire =
21070            serde_json::to_string(&incompatible).expect("incompatible result serializes");
21071        assert!(matches!(
21072            typed_request.decode_result(&incompatible_wire),
21073            Err(fastmcp_protocol::CoreDispatchError::ResultCodec(_))
21074        ));
21075        assert_eq!(
21076            serde_json::to_string(&accepted).expect("accepted result remains serializable"),
21077            accepted_wire,
21078            "rejecting the one-field incompatible result cannot mutate the accepted response"
21079        );
21080        assert_eq!(
21081            final_calls.load(Ordering::SeqCst),
21082            1,
21083            "local result admission cannot invoke or mutate the direct prompt handler"
21084        );
21085        let reaccepted = typed_request
21086            .decode_result(&accepted_wire)
21087            .expect("the original direct final result remains accepted");
21088        assert_eq!(
21089            serde_json::from_str::<serde_json::Value>(
21090                &reaccepted.encode().expect("reaccepted result encodes"),
21091            )
21092            .expect("reaccepted result is JSON"),
21093            accepted,
21094            "the incompatible result cannot alter the accepted final prompt contract"
21095        );
21096    }
21097
21098    #[test]
21099    fn final_resources_read_dispatches_direct_handler_without_legacy_projection() {
21100        let legacy_calls = Arc::new(AtomicUsize::new(0));
21101        let final_calls = Arc::new(AtomicUsize::new(0));
21102        let mut router = Router::new();
21103        router.add_resource(DirectFinalResource {
21104            legacy_calls: Arc::clone(&legacy_calls),
21105            final_calls: Arc::clone(&final_calls),
21106        });
21107        let cx = Cx::for_testing();
21108        let state = SessionState::new();
21109        let request_ctx = request_context(&cx, 98, Budget::INFINITE, &state);
21110
21111        let legacy = router
21112            .handle_resources_read(
21113                &request_ctx,
21114                &ReadResourceParams {
21115                    uri: "file:///direct-final-resource".to_owned(),
21116                    meta: None,
21117                },
21118                state.clone(),
21119                None,
21120                None,
21121            )
21122            .expect("legacy resource requests retain their exact handler path");
21123        let [LegacyResourceContent::Text { text, .. }] = legacy.contents.as_slice() else {
21124            panic!("legacy resource result retains its exact text variant");
21125        };
21126        assert_eq!(text, "legacy resource result");
21127        assert_eq!(legacy_calls.load(Ordering::SeqCst), 1);
21128        assert_eq!(final_calls.load(Ordering::SeqCst), 0);
21129
21130        let request = direct_final_resource_request(98);
21131        let typed_request = CoreRequest::decode(
21132            ProtocolEra::Modern2026,
21133            "resources/read",
21134            request.params.as_ref(),
21135        )
21136        .expect("final resources/read request decodes through the public core surface");
21137        let response = router
21138            .dispatch_stateless(&request_ctx, &request)
21139            .expect("final resources/read reaches the direct final handler");
21140
21141        assert_eq!(response["resultType"], "complete");
21142        assert_eq!(response["ttlMs"], 321);
21143        assert_eq!(response["cacheScope"], "public");
21144        assert_eq!(
21145            response["contents"][0]["text"],
21146            "direct final resource result"
21147        );
21148        assert_eq!(response["contents"][0]["mimeType"], "text/markdown");
21149        assert_eq!(
21150            response["contents"][0]["_meta"]["com.example/direct-resource"]["source"],
21151            "final-handler"
21152        );
21153        assert_eq!(response["contents"][0]["com.example/direct-field"], true);
21154        assert_eq!(legacy_calls.load(Ordering::SeqCst), 1);
21155        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
21156
21157        let wire = serde_json::to_string(&response).expect("final resource response serializes");
21158        let CoreResult::Final(FinalCoreResult::ResourcesRead { result, .. }) = typed_request
21159            .decode_result(&wire)
21160            .expect("final resource result decodes through the public core surface")
21161        else {
21162            panic!("resources/read selects the exact final result");
21163        };
21164        assert_eq!(result.payload.ttl_ms.as_str(), "321");
21165        assert_eq!(result.payload.cache_scope, CacheScope::Public);
21166        assert!(matches!(
21167            result.payload.contents.as_slice(),
21168            [EmbeddedResourceContents::Text { text, mime_type, .. }]
21169                if text == "direct final resource result"
21170                    && mime_type.as_deref() == Some("text/markdown")
21171        ));
21172    }
21173
21174    #[test]
21175    fn resource_read_cache_hint_provenance_controls_router_policy_not_wire_value() {
21176        let cx = Cx::for_testing();
21177        let state = SessionState::new();
21178        let request_ctx = request_context(&cx, 981, Budget::INFINITE, &state);
21179        let request = JsonRpcRequest::new(
21180            "resources/read",
21181            Some(serde_json::json!({
21182                "_meta": {
21183                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
21184                    "io.modelcontextprotocol/clientCapabilities": {},
21185                },
21186                "uri": "file:///sentinel-hint-resource",
21187            })),
21188            981_i64,
21189        );
21190
21191        for (provenance, expected_ttl, expected_scope) in [
21192            (
21193                FinalResourceReadCacheHintProvenance::Explicit,
21194                DEFAULT_FINAL_RESOURCE_TTL_MS,
21195                "private",
21196            ),
21197            (
21198                FinalResourceReadCacheHintProvenance::RouterPolicy,
21199                23,
21200                "public",
21201            ),
21202        ] {
21203            let mut router = Router::new();
21204            router.set_final_cache_hint_policy(
21205                CacheTtl::milliseconds(17),
21206                CacheTtl::milliseconds(23),
21207                CacheScope::Public,
21208            );
21209            router.add_resource(SentinelHintResource { provenance });
21210
21211            let response = router
21212                .dispatch_stateless(&request_ctx, &request)
21213                .expect("final resource result dispatches");
21214            assert_eq!(response["ttlMs"], expected_ttl);
21215            assert_eq!(response["cacheScope"], expected_scope);
21216        }
21217    }
21218
21219    #[test]
21220    fn final_unknown_resource_is_invalid_params_with_exact_uri_and_legacy_is_unchanged() {
21221        let legacy_calls = Arc::new(AtomicUsize::new(0));
21222        let final_calls = Arc::new(AtomicUsize::new(0));
21223        let mut router = Router::new();
21224        router.add_resource(DirectFinalResource {
21225            legacy_calls: Arc::clone(&legacy_calls),
21226            final_calls: Arc::clone(&final_calls),
21227        });
21228        let cx = Cx::for_testing();
21229        let state = SessionState::new();
21230        let request_ctx = request_context(&cx, 157, Budget::INFINITE, &state);
21231        let baseline = direct_final_resource_request(157);
21232        let mut unknown = baseline.clone();
21233        let replaced_uri = unknown
21234            .params
21235            .as_mut()
21236            .and_then(serde_json::Value::as_object_mut)
21237            .expect("resource parameters are an object")
21238            .insert(
21239                "uri".to_owned(),
21240                serde_json::json!("file:///unknown-final-resource"),
21241            );
21242        assert_eq!(
21243            replaced_uri,
21244            Some(serde_json::json!("file:///direct-final-resource"))
21245        );
21246
21247        assert_eq!(baseline.method, unknown.method);
21248        assert_eq!(baseline.id, unknown.id);
21249        assert_eq!(
21250            baseline
21251                .params
21252                .as_ref()
21253                .and_then(|params| params.get("_meta")),
21254            unknown
21255                .params
21256                .as_ref()
21257                .and_then(|params| params.get("_meta")),
21258            "the resource URI is the sole planted dimension"
21259        );
21260
21261        let accepted = router
21262            .dispatch_stateless(&request_ctx, &baseline)
21263            .expect("the registered final resource is accepted");
21264        assert_eq!(accepted["resultType"], "complete");
21265        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
21266        assert_eq!(legacy_calls.load(Ordering::SeqCst), 0);
21267
21268        let error = router
21269            .dispatch_stateless(&request_ctx, &unknown)
21270            .expect_err("changing only the URI to an unknown resource is rejected");
21271        assert_eq!(error.code, McpErrorCode::InvalidParams);
21272        assert_eq!(error.message, "Resource not found");
21273        assert_eq!(
21274            error.data,
21275            Some(serde_json::json!({"uri": "file:///unknown-final-resource"}))
21276        );
21277        assert_eq!(final_calls.load(Ordering::SeqCst), 1);
21278        assert_eq!(legacy_calls.load(Ordering::SeqCst), 0);
21279
21280        let legacy_error = router
21281            .handle_resources_read(
21282                &request_ctx,
21283                &ReadResourceParams {
21284                    uri: "file:///unknown-final-resource".to_owned(),
21285                    meta: None,
21286                },
21287                state,
21288                None,
21289                None,
21290            )
21291            .expect_err("the exact legacy missing-resource error remains unchanged");
21292        assert_eq!(legacy_error.code, McpErrorCode::ResourceNotFound);
21293        assert_eq!(
21294            legacy_error.message,
21295            "Resource not found: file:///unknown-final-resource"
21296        );
21297    }
21298
21299    #[test]
21300    fn final_resources_read_rejects_one_field_incompatible_result_type() {
21301        let legacy_calls = Arc::new(AtomicUsize::new(0));
21302        let final_calls = Arc::new(AtomicUsize::new(0));
21303        let mut router = Router::new();
21304        router.add_resource(DirectFinalResource {
21305            legacy_calls,
21306            final_calls: Arc::clone(&final_calls),
21307        });
21308        let cx = Cx::for_testing();
21309        let state = SessionState::new();
21310        let request_ctx = request_context(&cx, 99, Budget::INFINITE, &state);
21311        let request = direct_final_resource_request(99);
21312        let typed_request = CoreRequest::decode(
21313            ProtocolEra::Modern2026,
21314            "resources/read",
21315            request.params.as_ref(),
21316        )
21317        .expect("final resources/read request decodes through the public core surface");
21318        let accepted = router
21319            .dispatch_stateless(&request_ctx, &request)
21320            .expect("the direct final resource response is accepted");
21321        let accepted_wire = serde_json::to_string(&accepted).expect("accepted result serializes");
21322        let mut incompatible = accepted.clone();
21323        incompatible["resultType"] = serde_json::json!("input_required");
21324
21325        let mut accepted_without_type = accepted.clone();
21326        let mut incompatible_without_type = incompatible.clone();
21327        let accepted_type = accepted_without_type
21328            .as_object_mut()
21329            .and_then(|object| object.remove("resultType"));
21330        let incompatible_type = incompatible_without_type
21331            .as_object_mut()
21332            .and_then(|object| object.remove("resultType"));
21333        assert_eq!(accepted_type, Some(serde_json::json!("complete")));
21334        assert_eq!(incompatible_type, Some(serde_json::json!("input_required")));
21335        assert_eq!(
21336            incompatible_without_type, accepted_without_type,
21337            "resultType is the sole incompatible result dimension"
21338        );
21339
21340        let incompatible_wire =
21341            serde_json::to_string(&incompatible).expect("incompatible result serializes");
21342        assert!(matches!(
21343            typed_request.decode_result(&incompatible_wire),
21344            Err(fastmcp_protocol::CoreDispatchError::ResultCodec(_))
21345        ));
21346        assert_eq!(
21347            serde_json::to_string(&accepted).expect("accepted result remains serializable"),
21348            accepted_wire,
21349            "rejecting the one-field incompatible result cannot mutate the accepted response"
21350        );
21351        assert_eq!(
21352            final_calls.load(Ordering::SeqCst),
21353            1,
21354            "local result admission cannot invoke or mutate the direct resource handler"
21355        );
21356        let reaccepted = typed_request
21357            .decode_result(&accepted_wire)
21358            .expect("the original direct final result remains accepted");
21359        assert_eq!(
21360            serde_json::from_str::<serde_json::Value>(
21361                &reaccepted.encode().expect("reaccepted result encodes"),
21362            )
21363            .expect("reaccepted result is JSON"),
21364            accepted,
21365            "the incompatible result cannot alter the accepted final resource contract"
21366        );
21367    }
21368
21369    #[test]
21370    fn final_catalog_preserves_resource_template_and_prompt_fields() {
21371        let resource_metadata = OpenMetadata::try_from_entries([(
21372            "com.example/resource".to_owned(),
21373            serde_json::json!({"source": "resource-handler"}),
21374        )])
21375        .expect("resource metadata is valid");
21376        let template_metadata = OpenMetadata::try_from_entries([(
21377            "com.example/template".to_owned(),
21378            serde_json::json!({"source": "template-handler"}),
21379        )])
21380        .expect("template metadata is valid");
21381        let prompt_metadata = OpenMetadata::try_from_entries([(
21382            "com.example/prompt".to_owned(),
21383            serde_json::json!({"source": "prompt-handler"}),
21384        )])
21385        .expect("prompt metadata is valid");
21386        let resource_annotations = Annotations {
21387            audience: None,
21388            priority: Some(0.25),
21389            last_modified: Some("2026-08-08T00:00:00Z".to_owned()),
21390            additional: BTreeMap::new(),
21391        };
21392        let template_annotations = Annotations {
21393            audience: None,
21394            priority: Some(0.75),
21395            last_modified: Some("2026-08-08T00:00:01Z".to_owned()),
21396            additional: BTreeMap::new(),
21397        };
21398        let resource_icon = RawIcon::try_with_details(
21399            "https://example.test/resource.png",
21400            Some("image/png".to_owned()),
21401            Some(vec!["32x32".to_owned()]),
21402            None,
21403        )
21404        .expect("resource icon is valid");
21405        let template_icon = RawIcon::try_with_details(
21406            "https://example.test/template.png",
21407            Some("image/png".to_owned()),
21408            Some(vec!["48x48".to_owned()]),
21409            None,
21410        )
21411        .expect("template icon is valid");
21412        let prompt_icon = RawIcon::try_with_details(
21413            "https://example.test/prompt.png",
21414            Some("image/png".to_owned()),
21415            Some(vec!["64x64".to_owned()]),
21416            None,
21417        )
21418        .expect("prompt icon is valid");
21419
21420        let mut router = Router::new();
21421        router.add_resource(FinalCatalogResource {
21422            metadata: resource_metadata,
21423            icons: vec![resource_icon],
21424            annotations: resource_annotations,
21425        });
21426        router.add_resource(FinalCatalogResourceTemplate {
21427            metadata: template_metadata,
21428            icons: vec![template_icon],
21429            annotations: template_annotations,
21430        });
21431        router.add_prompt(FinalCatalogPrompt {
21432            metadata: prompt_metadata,
21433            icons: vec![prompt_icon],
21434        });
21435
21436        let cx = Cx::for_testing();
21437        let state = SessionState::new();
21438        let request_ctx = request_context(&cx, 96, Budget::INFINITE, &state);
21439        let final_metadata = serde_json::json!({
21440            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
21441            "io.modelcontextprotocol/clientCapabilities": {},
21442        });
21443
21444        let resources = router
21445            .dispatch_stateless(
21446                &request_ctx,
21447                &JsonRpcRequest::new(
21448                    "resources/list",
21449                    Some(serde_json::json!({"_meta": final_metadata.clone()})),
21450                    96_i64,
21451                ),
21452            )
21453            .expect("final resource catalog is encoded");
21454        assert_eq!(resources["resources"][0]["title"], "Final Catalog Resource");
21455        assert_eq!(
21456            resources["resources"][0]["icons"][0]["src"],
21457            "https://example.test/resource.png"
21458        );
21459        assert_eq!(resources["resources"][0]["annotations"]["priority"], 0.25);
21460        assert_eq!(
21461            resources["resources"][0]["_meta"]["com.example/resource"]["source"],
21462            "resource-handler"
21463        );
21464
21465        let templates = router
21466            .dispatch_stateless(
21467                &request_ctx,
21468                &JsonRpcRequest::new(
21469                    "resources/templates/list",
21470                    Some(serde_json::json!({"_meta": final_metadata.clone()})),
21471                    97_i64,
21472                ),
21473            )
21474            .expect("final resource-template catalog is encoded");
21475        assert_eq!(
21476            templates["resourceTemplates"][0]["title"],
21477            "Final Catalog Template"
21478        );
21479        assert_eq!(
21480            templates["resourceTemplates"][0]["icons"][0]["src"],
21481            "https://example.test/template.png"
21482        );
21483        assert_eq!(
21484            templates["resourceTemplates"][0]["annotations"]["priority"],
21485            0.75
21486        );
21487        assert_eq!(
21488            templates["resourceTemplates"][0]["_meta"]["com.example/template"]["source"],
21489            "template-handler"
21490        );
21491
21492        let prompts = router
21493            .dispatch_stateless(
21494                &request_ctx,
21495                &JsonRpcRequest::new(
21496                    "prompts/list",
21497                    Some(serde_json::json!({"_meta": final_metadata})),
21498                    98_i64,
21499                ),
21500            )
21501            .expect("final prompt catalog is encoded");
21502        assert_eq!(prompts["prompts"][0]["title"], "Final Catalog Prompt");
21503        assert_eq!(
21504            prompts["prompts"][0]["icons"][0]["src"],
21505            "https://example.test/prompt.png"
21506        );
21507        assert_eq!(
21508            prompts["prompts"][0]["_meta"]["com.example/prompt"]["source"],
21509            "prompt-handler"
21510        );
21511        assert_eq!(prompts["prompts"][0]["arguments"][0]["required"], false);
21512    }
21513
21514    #[test]
21515    fn final_resource_catalog_missing_metadata_is_non_mutating() {
21516        let metadata = OpenMetadata::try_from_entries([(
21517            "com.example/resource".to_owned(),
21518            serde_json::json!({"source": "resource-handler"}),
21519        )])
21520        .expect("resource metadata is valid");
21521        let icon = RawIcon::try_with_details(
21522            "https://example.test/resource.png",
21523            Some("image/png".to_owned()),
21524            None,
21525            None,
21526        )
21527        .expect("resource icon is valid");
21528        let mut router = Router::new();
21529        router.add_resource(FinalCatalogResource {
21530            metadata,
21531            icons: vec![icon],
21532            annotations: Annotations::default(),
21533        });
21534        let cx = Cx::for_testing();
21535        let state = SessionState::new();
21536        let request_ctx = request_context(&cx, 99, Budget::INFINITE, &state);
21537        let baseline = JsonRpcRequest::new(
21538            "resources/list",
21539            Some(serde_json::json!({
21540                "_meta": {
21541                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
21542                    "io.modelcontextprotocol/clientCapabilities": {},
21543                },
21544            })),
21545            99_i64,
21546        );
21547        let mut planted = baseline.clone();
21548        planted
21549            .params
21550            .as_mut()
21551            .and_then(serde_json::Value::as_object_mut)
21552            .expect("final resource-list parameters are an object")
21553            .remove("_meta");
21554
21555        assert_eq!(baseline.method, planted.method);
21556        assert_eq!(baseline.id, planted.id);
21557        assert_eq!(
21558            baseline
21559                .params
21560                .as_ref()
21561                .and_then(serde_json::Value::as_object)
21562                .map(serde_json::Map::len),
21563            Some(1),
21564            "the final metadata object is the sole baseline parameter"
21565        );
21566        assert_eq!(
21567            planted
21568                .params
21569                .as_ref()
21570                .and_then(serde_json::Value::as_object)
21571                .map(serde_json::Map::len),
21572            Some(0),
21573            "the planted request differs only by final metadata removal"
21574        );
21575        let catalog_before = serde_json::to_vec(&router.resources()).expect("catalog serializes");
21576        let planted_before = serde_json::to_vec(&planted).expect("request serializes");
21577        let baseline_result = router
21578            .dispatch_stateless(&request_ctx, &baseline)
21579            .expect("final baseline is accepted");
21580        assert_eq!(
21581            baseline_result["resources"][0]["title"],
21582            "Final Catalog Resource"
21583        );
21584
21585        let error = router
21586            .dispatch_stateless(&request_ctx, &planted)
21587            .expect_err("only final request metadata is refused");
21588        assert_eq!(error.code, McpErrorCode::InvalidParams);
21589        assert_eq!(
21590            serde_json::to_vec(&planted).expect("rejected request serializes"),
21591            planted_before,
21592            "the one-field rejection cannot mutate caller-owned input"
21593        );
21594        assert_eq!(
21595            serde_json::to_vec(&router.resources()).expect("catalog serializes"),
21596            catalog_before,
21597            "the one-field rejection cannot mutate the resource catalog"
21598        );
21599        assert_eq!(
21600            router
21601                .dispatch_stateless(&request_ctx, &baseline)
21602                .expect("the baseline remains accepted after rejection"),
21603            baseline_result,
21604            "the one-field rejection cannot alter final field preservation"
21605        );
21606    }
21607
21608    #[test]
21609    fn core_request_decode_result_round_trips_final_catalog_and_read_cache_hints() {
21610        let metadata = OpenMetadata::try_from_entries([(
21611            "com.example/catalog".to_owned(),
21612            serde_json::json!({"source": "handler"}),
21613        )])
21614        .expect("valid final catalog metadata");
21615        let icon = RawIcon::try_with_details(
21616            "https://example.test/tool.png",
21617            Some("image/png".to_owned()),
21618            Some(vec!["48x48".to_owned()]),
21619            None,
21620        )
21621        .expect("valid final icon");
21622        let alternate_icon = RawIcon::try_with_details(
21623            "https://example.test/tool-dark.svg",
21624            Some("image/svg+xml".to_owned()),
21625            Some(vec!["any".to_owned()]),
21626            Some(fastmcp_protocol::common_types::IconTheme::Dark),
21627        )
21628        .expect("valid alternate final icon");
21629        let mut router = Router::new();
21630        let list_ttl: CacheTtl = serde_json::from_str("922337203685477580812345678901234567890")
21631            .expect("an arbitrary-width final list TTL is valid");
21632        let resource_read_ttl: CacheTtl =
21633            serde_json::from_str("184467440737095516160000000000000000000")
21634                .expect("an arbitrary-width final resource-read TTL is valid");
21635        router.set_final_cache_hint_policy(
21636            list_ttl.clone(),
21637            resource_read_ttl.clone(),
21638            CacheScope::Private,
21639        );
21640        router
21641            .add_tool(FinalCatalogTool {
21642                metadata,
21643                icons: vec![icon, alternate_icon],
21644            })
21645            .expect("final catalog tool registration succeeds");
21646        router.add_resource(NamedResource::new("file:///catalog-resource"));
21647        let cx = Cx::for_testing();
21648        let state = SessionState::new();
21649        let request_ctx = request_context(&cx, 93, Budget::INFINITE, &state);
21650
21651        let legacy = router
21652            .handle_tools_list(&request_ctx, ListToolsParams::default(), None)
21653            .expect("legacy catalog remains available");
21654        let legacy_wire = serde_json::to_value(&legacy).expect("legacy catalog serializes");
21655        assert!(legacy_wire.get("ttlMs").is_none());
21656        assert!(legacy_wire.get("cacheScope").is_none());
21657        assert!(legacy_wire["tools"][0].get("icon").is_some());
21658        assert!(legacy_wire["tools"][0].get("version").is_some());
21659        assert!(legacy_wire["tools"][0].get("tags").is_some());
21660
21661        let final_list_params = serde_json::json!({
21662            "_meta": {
21663                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
21664                "io.modelcontextprotocol/clientCapabilities": {},
21665            },
21666        });
21667        let final_list_request = CoreRequest::decode(
21668            ProtocolEra::Modern2026,
21669            "tools/list",
21670            Some(&final_list_params),
21671        )
21672        .expect("final catalog request decodes through the public core surface");
21673        let modern_list = router
21674            .dispatch_stateless(
21675                &request_ctx,
21676                &JsonRpcRequest::new("tools/list", Some(final_list_params), 93_i64),
21677            )
21678            .expect("final catalog is projected through the exact model");
21679        assert_eq!(modern_list["resultType"], "complete");
21680        assert_eq!(modern_list["ttlMs"].to_string(), list_ttl.as_str());
21681        assert_eq!(modern_list["cacheScope"], "private");
21682        assert_eq!(modern_list["tools"][0]["title"], "Exact Final Catalog Tool");
21683        assert_eq!(
21684            modern_list["tools"][0]["annotations"]["title"],
21685            "Exact annotation title"
21686        );
21687        assert_eq!(
21688            modern_list["tools"][0]["icons"].as_array().map(Vec::len),
21689            Some(2)
21690        );
21691        assert_eq!(
21692            modern_list["tools"][0]["icons"][0]["sizes"],
21693            serde_json::json!(["48x48"])
21694        );
21695        assert_eq!(
21696            modern_list["tools"][0]["_meta"]["com.example/catalog"]["source"],
21697            "handler"
21698        );
21699        assert_eq!(modern_list["tools"][0]["outputSchema"]["type"], "object");
21700        assert!(modern_list["tools"][0].get("icon").is_none());
21701        assert!(modern_list["tools"][0].get("version").is_none());
21702        assert!(modern_list["tools"][0].get("tags").is_none());
21703        let modern_list_wire =
21704            serde_json::to_string(&modern_list).expect("final catalog response serializes");
21705        let CoreResult::Final(FinalCoreResult::ToolsList { result, .. }) = final_list_request
21706            .decode_result(&modern_list_wire)
21707            .expect("final catalog response decodes through the public core surface")
21708        else {
21709            panic!("tools/list selects the exact final catalog result");
21710        };
21711        assert_eq!(result.payload.ttl_ms.as_str(), list_ttl.as_str());
21712        assert_eq!(result.payload.cache_scope, CacheScope::Private);
21713        let final_tool = result
21714            .payload
21715            .tools
21716            .first()
21717            .expect("final catalog contains the registered tool");
21718        assert_eq!(
21719            final_tool.title.as_deref(),
21720            Some("Exact Final Catalog Tool")
21721        );
21722        assert_eq!(
21723            final_tool
21724                .annotations
21725                .as_ref()
21726                .and_then(|annotations| annotations.title.as_deref()),
21727            Some("Exact annotation title")
21728        );
21729        assert_eq!(final_tool.icons.as_ref().map(Vec::len), Some(2));
21730        assert_eq!(
21731            final_tool
21732                .icons
21733                .as_ref()
21734                .and_then(|icons| icons.first())
21735                .map(|icon| icon.src.as_str()),
21736            Some("https://example.test/tool.png")
21737        );
21738        assert_eq!(
21739            final_tool
21740                .meta
21741                .as_ref()
21742                .and_then(|metadata| metadata.get("com.example/catalog"))
21743                .and_then(|value| value.get("source"))
21744                .and_then(serde_json::Value::as_str),
21745            Some("handler")
21746        );
21747        assert_eq!(
21748            final_tool
21749                .output_schema
21750                .as_ref()
21751                .and_then(|schema| schema.get("type"))
21752                .and_then(serde_json::Value::as_str),
21753            Some("object")
21754        );
21755
21756        let final_read_params = serde_json::json!({
21757            "_meta": {
21758                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
21759                "io.modelcontextprotocol/clientCapabilities": {},
21760            },
21761            "uri": "file:///catalog-resource",
21762        });
21763        let final_read_request = CoreRequest::decode(
21764            ProtocolEra::Modern2026,
21765            "resources/read",
21766            Some(&final_read_params),
21767        )
21768        .expect("final resource-read request decodes through the public core surface");
21769        let modern_read = router
21770            .dispatch_stateless(
21771                &request_ctx,
21772                &JsonRpcRequest::new("resources/read", Some(final_read_params), 94_i64),
21773            )
21774            .expect("final resource content is projected through the final model");
21775        assert_eq!(modern_read["ttlMs"].to_string(), resource_read_ttl.as_str());
21776        assert_eq!(modern_read["cacheScope"], "private");
21777        assert_eq!(
21778            modern_read["contents"][0]["uri"],
21779            "file:///catalog-resource"
21780        );
21781        assert_eq!(modern_read["contents"][0]["text"], "content");
21782        let modern_read_wire =
21783            serde_json::to_string(&modern_read).expect("final resource-read response serializes");
21784        let CoreResult::Final(FinalCoreResult::ResourcesRead { result, .. }) = final_read_request
21785            .decode_result(&modern_read_wire)
21786            .expect("final resource-read response decodes through the public core surface")
21787        else {
21788            panic!("resources/read selects the exact final read result");
21789        };
21790        assert_eq!(result.payload.ttl_ms.as_str(), resource_read_ttl.as_str());
21791        assert_eq!(result.payload.cache_scope, CacheScope::Private);
21792        assert!(matches!(
21793            result.payload.contents.as_slice(),
21794            [EmbeddedResourceContents::Text { text, .. }] if text == "content"
21795        ));
21796    }
21797
21798    #[test]
21799    fn final_catalog_missing_metadata_is_non_mutating() {
21800        let mut router = Router::new();
21801        router
21802            .add_tool(NamedTool::new("metadata-guarded-tool"))
21803            .expect("tool registration succeeds");
21804        let cx = Cx::for_testing();
21805        let state = SessionState::new();
21806        let request_ctx = request_context(&cx, 95, Budget::INFINITE, &state);
21807        let baseline = JsonRpcRequest::new(
21808            "tools/list",
21809            Some(serde_json::json!({
21810                "_meta": {
21811                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
21812                    "io.modelcontextprotocol/clientCapabilities": {},
21813                },
21814            })),
21815            95_i64,
21816        );
21817        let mut planted = baseline.clone();
21818        planted
21819            .params
21820            .as_mut()
21821            .and_then(serde_json::Value::as_object_mut)
21822            .expect("final list parameters are an object")
21823            .remove("_meta");
21824        assert_eq!(baseline.method, planted.method);
21825        assert_eq!(baseline.id, planted.id);
21826        let catalog_before = serde_json::to_vec(&router.tools()).expect("catalog serializes");
21827        let planted_before = serde_json::to_vec(&planted).expect("request serializes");
21828
21829        let baseline_result = router
21830            .dispatch_stateless(&request_ctx, &baseline)
21831            .expect("the final baseline is accepted");
21832        let error = router
21833            .dispatch_stateless(&request_ctx, &planted)
21834            .expect_err("only missing final metadata is refused");
21835        assert_eq!(error.code, McpErrorCode::InvalidParams);
21836        assert_eq!(
21837            serde_json::to_vec(&planted).expect("rejected request serializes"),
21838            planted_before,
21839            "the rejected one-field request remains unchanged"
21840        );
21841        assert_eq!(
21842            serde_json::to_vec(&router.tools()).expect("catalog serializes"),
21843            catalog_before,
21844            "the rejected one-field request cannot mutate the catalog"
21845        );
21846        assert_eq!(
21847            router
21848                .dispatch_stateless(&request_ctx, &baseline)
21849                .expect("the unchanged final baseline remains accepted"),
21850            baseline_result,
21851            "the rejection cannot alter final cache hints or catalog projection"
21852        );
21853    }
21854
21855    #[test]
21856    fn srv_04_modern_owned_dispatch_runs_requests_concurrently() {
21857        let started = Arc::new(AtomicUsize::new(0));
21858        let completed = Arc::new(Mutex::new(Vec::new()));
21859        let mut router = Router::new();
21860        router
21861            .add_tool(ConcurrentModernTool::new(
21862                Arc::clone(&started),
21863                Arc::clone(&completed),
21864            ))
21865            .expect("tool registration succeeds");
21866        let router = Arc::new(router);
21867        let runtime = RuntimeBuilder::current_thread()
21868            .build()
21869            .expect("the test runtime is available");
21870        let runtime_handle = runtime.handle();
21871        let mut first = spawn_owned_modern_request(
21872            &runtime_handle,
21873            Arc::clone(&router),
21874            401,
21875            "modern-one",
21876            "one",
21877            None,
21878        );
21879        let mut second =
21880            spawn_owned_modern_request(&runtime_handle, router, 402, "modern-two", "two", None);
21881
21882        let (first, second) = runtime.block_on(async {
21883            let cx = Cx::current().expect("block_on installs an observer context");
21884            let first = first
21885                .recv(&cx)
21886                .await
21887                .expect("the first owner reports a terminal result");
21888            let second = second
21889                .recv(&cx)
21890                .await
21891                .expect("the second owner reports a terminal result");
21892            (first, second)
21893        });
21894        let first = first.expect("the first modern request completes");
21895        let second = second.expect("the second modern request completes");
21896
21897        assert_eq!(started.load(Ordering::SeqCst), 2);
21898        assert_eq!(
21899            first.get("resultType"),
21900            Some(&serde_json::json!("complete"))
21901        );
21902        assert_eq!(
21903            second.get("resultType"),
21904            Some(&serde_json::json!("complete"))
21905        );
21906        let mut completed = completed
21907            .lock()
21908            .expect("completion probe lock is not poisoned")
21909            .clone();
21910        completed.sort();
21911        assert_eq!(completed, vec!["one".to_string(), "two".to_string()]);
21912    }
21913
21914    #[test]
21915    fn srv_04_modern_owned_cancellation_does_not_change_sibling() {
21916        let started = Arc::new(AtomicUsize::new(0));
21917        let completed = Arc::new(Mutex::new(Vec::new()));
21918        let mut router = Router::new();
21919        router
21920            .add_tool(ConcurrentModernTool::new(
21921                Arc::clone(&started),
21922                Arc::clone(&completed),
21923            ))
21924            .expect("tool registration succeeds");
21925        let router = Arc::new(router);
21926        let runtime = RuntimeBuilder::current_thread()
21927            .build()
21928            .expect("the test runtime is available");
21929        let runtime_handle = runtime.handle();
21930        let (cancel_control_sender, mut cancel_control) = oneshot::channel();
21931        let mut cancelled = spawn_owned_modern_request(
21932            &runtime_handle,
21933            Arc::clone(&router),
21934            403,
21935            "modern-cancelled",
21936            "cancelled",
21937            Some(cancel_control_sender),
21938        );
21939        let mut sibling = spawn_owned_modern_request(
21940            &runtime_handle,
21941            router,
21942            404,
21943            "modern-sibling",
21944            "sibling",
21945            None,
21946        );
21947
21948        let (cancelled, sibling) = runtime.block_on(async {
21949            let observer_cx = Cx::current().expect("block_on installs an observer context");
21950            let cancelled_cx = cancel_control
21951                .recv(&observer_cx)
21952                .await
21953                .expect("the request owner exposes its cancellation context");
21954            // Bounded so a dispatch rejection fails the test loudly instead of
21955            // spinning this observer loop forever.
21956            let admission_deadline = std::time::Instant::now() + Duration::from_secs(10);
21957            while started.load(Ordering::SeqCst) < 2 {
21958                assert!(
21959                    std::time::Instant::now() < admission_deadline,
21960                    "both owned modern requests must start before the admission deadline"
21961                );
21962                yield_once().await;
21963            }
21964            cancelled_cx.cancel_with(CancelKind::User, Some("test single-request cancellation"));
21965            let cancelled = cancelled
21966                .recv(&observer_cx)
21967                .await
21968                .expect("the cancelled owner reports a terminal result");
21969            let sibling = sibling
21970                .recv(&observer_cx)
21971                .await
21972                .expect("the sibling owner reports a terminal result");
21973            (cancelled, sibling)
21974        });
21975
21976        let cancelled = cancelled.expect_err("only the selected request is cancelled");
21977        assert_eq!(cancelled.code, McpErrorCode::RequestCancelled);
21978        let sibling = sibling.expect("the sibling completes despite peer cancellation");
21979        assert_eq!(
21980            sibling.get("resultType"),
21981            Some(&serde_json::json!("complete"))
21982        );
21983        assert_eq!(
21984            completed
21985                .lock()
21986                .expect("completion probe lock is not poisoned")
21987                .as_slice(),
21988            ["sibling"],
21989            "cancelling one request cannot add, remove, or alter sibling completion"
21990        );
21991    }
21992
21993    #[test]
21994    fn mount_result_with_warning_and_no_error_is_successful() {
21995        let result = MountResult {
21996            tools: 0,
21997            resources: 0,
21998            resource_templates: 0,
21999            prompts: 0,
22000            warnings: vec!["something".to_string()],
22001            errors: vec![],
22002        };
22003        assert!(result.is_success());
22004        assert!(!result.has_components());
22005    }
22006
22007    struct AppsLinkedTool;
22008
22009    impl ToolHandler for AppsLinkedTool {
22010        fn definition(&self) -> Tool {
22011            Tool {
22012                name: "apps-linked-tool".to_owned(),
22013                description: None,
22014                input_schema: serde_json::json!({"type": "object"}),
22015                output_schema: None,
22016                icon: None,
22017                version: None,
22018                tags: Vec::new(),
22019                annotations: None,
22020            }
22021        }
22022
22023        fn final_definition(&self) -> Option<FinalTool> {
22024            let metadata = fastmcp_protocol::McpAppsToolMetadata::try_new(
22025                Some(
22026                    AbsoluteUri::parse("ui://weather/dashboard").expect("fixed Apps URI is valid"),
22027                ),
22028                Some(vec![fastmcp_protocol::McpAppsToolVisibility::App]),
22029            )
22030            .expect("fixed Apps metadata is valid")
22031            .to_open_metadata()
22032            .expect("fixed Apps metadata serializes");
22033            Some(FinalTool {
22034                name: "apps-linked-tool".to_owned(),
22035                title: None,
22036                description: None,
22037                input_schema: serde_json::json!({"type": "object"}),
22038                output_schema: None,
22039                annotations: None,
22040                icons: None,
22041                meta: Some(metadata),
22042            })
22043        }
22044
22045        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
22046            Ok(Vec::new())
22047        }
22048    }
22049
22050    struct AppsBoundResource {
22051        mime_type: &'static str,
22052    }
22053
22054    impl ResourceHandler for AppsBoundResource {
22055        fn definition(&self) -> Resource {
22056            Resource {
22057                uri: "ui://weather/dashboard".to_owned(),
22058                name: "weather-dashboard".to_owned(),
22059                description: None,
22060                mime_type: Some(self.mime_type.to_owned()),
22061                icon: None,
22062                version: None,
22063                tags: Vec::new(),
22064            }
22065        }
22066
22067        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
22068            Ok(Vec::new())
22069        }
22070    }
22071
22072    #[test]
22073    fn apps_tool_registration_requires_a_registered_html_ui_resource() {
22074        let mut router = Router::new();
22075        router
22076            .add_mcp_apps_ui_resource_with_behavior(
22077                AppsBoundResource {
22078                    mime_type: fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE,
22079                },
22080                crate::DuplicateBehavior::Error,
22081            )
22082            .expect("matching Apps HTML resource registers first");
22083        router
22084            .add_mcp_apps_tool_with_behavior(AppsLinkedTool, crate::DuplicateBehavior::Error)
22085            .expect("tool binding to the registered Apps HTML resource is admitted");
22086        assert!(router.tools().is_empty(), "Apps tools are final-only");
22087        assert!(
22088            router
22089                .resolve_tool_for_era("apps-linked-tool", Some(ProtocolEra::Legacy2024),)
22090                .is_none()
22091        );
22092        assert!(
22093            router
22094                .resolve_tool_for_era("apps-linked-tool", Some(ProtocolEra::Modern2026),)
22095                .is_some()
22096        );
22097    }
22098
22099    #[test]
22100    fn apps_tool_registration_rejects_only_a_non_html_ui_resource_without_mutation() {
22101        let mut router = Router::new();
22102        router
22103            .add_final_resource_with_behavior(
22104                AppsBoundResource {
22105                    mime_type: "text/plain",
22106                },
22107                crate::DuplicateBehavior::Error,
22108            )
22109            .expect("the one-field MIME variant remains a valid final resource");
22110
22111        let error = router
22112            .add_mcp_apps_tool_with_behavior(AppsLinkedTool, crate::DuplicateBehavior::Error)
22113            .expect_err("only replacing the Apps HTML MIME type rejects the linked tool");
22114        assert_eq!(error.code, McpErrorCode::InvalidRequest);
22115        assert!(
22116            router.tools().is_empty(),
22117            "rejected Apps linkage cannot add the tool to the legacy or final catalog"
22118        );
22119    }
22120
22121    #[test]
22122    fn generic_registration_rejects_apps_ui_resources_and_tools_without_mutation() {
22123        let mut router = Router::new();
22124        let resource_error = router
22125            .add_resource_with_behavior(
22126                AppsBoundResource {
22127                    mime_type: fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE,
22128                },
22129                crate::DuplicateBehavior::Error,
22130            )
22131            .expect_err("generic registration must not expose an Apps View to exact 2024");
22132        assert_eq!(resource_error.code, McpErrorCode::InvalidRequest);
22133        assert!(router.resources().is_empty());
22134
22135        router
22136            .add_mcp_apps_ui_resource_with_behavior(
22137                AppsBoundResource {
22138                    mime_type: fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE,
22139                },
22140                crate::DuplicateBehavior::Error,
22141            )
22142            .expect("the negotiated final-only Apps resource registers");
22143        let tool_error = router
22144            .add_tool_with_behavior(AppsLinkedTool, crate::DuplicateBehavior::Error)
22145            .expect_err("generic registration must not publish Apps metadata without opt-in");
22146        assert_eq!(tool_error.code, McpErrorCode::InvalidRequest);
22147        assert!(router.tools().is_empty());
22148    }
22149
22150    #[test]
22151    fn apps_binding_mounts_reject_dangling_tools_replacements_and_prefixes_atomically() {
22152        let mut source = Router::new();
22153        source
22154            .add_mcp_apps_ui_resource_with_behavior(
22155                AppsBoundResource {
22156                    mime_type: fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE,
22157                },
22158                crate::DuplicateBehavior::Error,
22159            )
22160            .expect("source Apps resource registers");
22161        source
22162            .add_mcp_apps_tool_with_behavior(AppsLinkedTool, crate::DuplicateBehavior::Error)
22163            .expect("source Apps tool registers");
22164
22165        let mut tools_only_destination = Router::new();
22166        let tools_only = tools_only_destination.mount_tools(source, None);
22167        assert!(!tools_only.is_success());
22168        assert!(tools_only_destination.tools().is_empty());
22169
22170        let mut destination = Router::new();
22171        destination
22172            .add_mcp_apps_ui_resource_with_behavior(
22173                AppsBoundResource {
22174                    mime_type: fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE,
22175                },
22176                crate::DuplicateBehavior::Error,
22177            )
22178            .expect("destination Apps resource registers");
22179        destination
22180            .add_mcp_apps_tool_with_behavior(AppsLinkedTool, crate::DuplicateBehavior::Error)
22181            .expect("destination Apps tool registers");
22182
22183        let mut incompatible_resource = Router::new();
22184        incompatible_resource
22185            .add_final_resource_with_behavior(
22186                AppsBoundResource {
22187                    mime_type: "text/plain",
22188                },
22189                crate::DuplicateBehavior::Error,
22190            )
22191            .expect("non-Apps final resource is independently admissible");
22192        let replacement = destination.mount_resources_with_behavior(
22193            incompatible_resource,
22194            None,
22195            crate::DuplicateBehavior::Replace,
22196        );
22197        assert!(!replacement.is_success());
22198        assert_eq!(
22199            destination
22200                .final_resources
22201                .get("ui://weather/dashboard")
22202                .expect("rejected replacement retains the HTML resource")
22203                .definition
22204                .mime_type
22205                .as_deref(),
22206            Some(fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE)
22207        );
22208
22209        let mut prefixed_source = Router::new();
22210        prefixed_source
22211            .add_mcp_apps_ui_resource_with_behavior(
22212                AppsBoundResource {
22213                    mime_type: fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE,
22214                },
22215                crate::DuplicateBehavior::Error,
22216            )
22217            .expect("prefixed source Apps resource registers");
22218        prefixed_source
22219            .add_mcp_apps_tool_with_behavior(AppsLinkedTool, crate::DuplicateBehavior::Error)
22220            .expect("prefixed source Apps tool registers");
22221        let mut prefixed_destination = Router::new();
22222        let prefixed = prefixed_destination.mount(prefixed_source, Some("peer"));
22223        assert!(!prefixed.is_success());
22224        assert!(prefixed_destination.tools().is_empty());
22225        assert!(prefixed_destination.resources().is_empty());
22226    }
22227}