pmcp/server/mod.rs
1//! MCP server implementation.
2
3#[cfg(not(target_arch = "wasm32"))]
4use crate::error::{Error, Result};
5#[cfg(not(target_arch = "wasm32"))]
6use crate::shared::TransportMessage;
7#[cfg(not(target_arch = "wasm32"))]
8use crate::types::{
9 CallToolRequest, CallToolResult, ClientCapabilities, ClientRequest, GetPromptRequest,
10 Implementation, InitializeResult, JSONRPCResponse, ListPromptsRequest, ListPromptsResult,
11 ListResourceTemplatesRequest, ListResourceTemplatesResult, ListResourcesRequest,
12 ListResourcesResult, ListToolsRequest, ListToolsResult, Notification, ProtocolVersion,
13 ReadResourceRequest, Request, RequestId, ServerCapabilities, ServerNotification, ToolInfo,
14};
15#[cfg(not(target_arch = "wasm32"))]
16use async_trait::async_trait;
17#[cfg(not(target_arch = "wasm32"))]
18use serde_json::Value;
19#[cfg(not(target_arch = "wasm32"))]
20use std::collections::HashMap;
21#[cfg(not(target_arch = "wasm32"))]
22use std::collections::HashSet;
23#[cfg(not(target_arch = "wasm32"))]
24use std::sync::Arc;
25
26#[cfg(not(target_arch = "wasm32"))]
27use crate::runtime::RwLock;
28#[cfg(not(target_arch = "wasm32"))]
29use tokio::sync::mpsc;
30// Scrubs the by-value `[u8; 32]` / `Vec<[u8; 32]>` setter parameters on
31// `ServerBuilder` after their contents move into the zeroizing fields (D-113-P,
32// copy 2 of 3). `zeroize` is only compiled in under `streamable-http`, so the
33// import carries the same gate as the fields it serves.
34#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
35use zeroize::Zeroize;
36
37// Core modules (currently native-only due to dependencies)
38#[cfg(not(target_arch = "wasm32"))]
39pub mod adapters;
40#[cfg(not(target_arch = "wasm32"))]
41pub mod builder;
42#[cfg(not(target_arch = "wasm32"))]
43// Dead by CONFIGURATION, not disuse: the dispatch paths that call into this
44// module are gated behind the transport features, so a `default-features = false`
45// build (as `pmcp-tasks` does) and a wasm32 build both compile the module with no
46// callers. Scoped so genuine dead code is still caught in a normal build.
47#[cfg_attr(
48 any(target_arch = "wasm32", not(feature = "streamable-http")),
49 allow(dead_code)
50)]
51pub mod core;
52pub mod limits;
53
54// Native-only modules (require tokio, threading, etc.)
55#[cfg(not(target_arch = "wasm32"))]
56pub mod auth;
57#[cfg(not(target_arch = "wasm32"))]
58pub mod batch;
59/// Builder-scoped middleware executor for workflow registration.
60#[cfg(not(target_arch = "wasm32"))]
61pub mod builder_middleware_executor;
62#[cfg(not(target_arch = "wasm32"))]
63pub mod cancellation;
64/// Dynamic resource provider system for pattern-based resource routing.
65#[cfg(not(target_arch = "wasm32"))]
66pub mod dynamic_resources;
67#[cfg(not(target_arch = "wasm32"))]
68pub mod http_middleware;
69/// Middleware executor abstraction for consistent tool execution.
70#[cfg(not(target_arch = "wasm32"))]
71pub mod middleware_executor;
72// Warn-only emit-time validation of `structuredContent` against a declared
73// `outputSchema` (no-op unless the `validation` feature is enabled).
74//
75// Deliberately NOT gated by target: the module compiles everywhere so dispatcher
76// call sites stay plain one-liners. The second `#[cfg]` widens the module's
77// visibility for the `fuzzing` feature ONLY, so
78// `fuzz/fuzz_targets/fuzz_schema_draft_pin.rs` can reach
79// `output_validation::fuzz_support` without any item becoming part of the
80// shipped public API (`fuzzing` is in neither `default` nor `full`, so
81// `cargo public-api` never sees it). This is verbatim the shape
82// `server::request_state` and `server::task_dispatch` already use.
83#[cfg(not(feature = "fuzzing"))]
84pub(crate) mod output_validation;
85/// Warn-only emit-time validation of `structuredContent` against a declared
86/// `outputSchema` (no-op unless the `validation` feature is enabled).
87#[cfg(feature = "fuzzing")]
88pub mod output_validation;
89/// Concrete `PeerHandle` implementation delegating to the
90/// `ServerRequestDispatcher`.
91#[cfg(not(target_arch = "wasm32"))]
92pub(crate) mod peer_impl;
93#[cfg(not(target_arch = "wasm32"))]
94pub mod preset;
95/// Progress reporting support for long-running operations.
96#[cfg(not(target_arch = "wasm32"))]
97pub mod progress;
98// Server-owned `requestState` AEAD continuation tokens (Phase 113, HTTP-02).
99//
100// D-14 locks MRTR AEAD to native + `streamable-http`: `ring` is only enabled by
101// that feature and the wasm server (`WasmServerCore`) gets no MRTR this phase.
102// The second `#[cfg]` widens the module's visibility for the `fuzzing` feature
103// ONLY, so `fuzz/fuzz_targets/fuzz_request_state.rs` can reach
104// `request_state::fuzz_support` without any item becoming part of the shipped
105// public API (`fuzzing` is in neither `default` nor `full`).
106#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
107#[cfg(not(feature = "fuzzing"))]
108pub(crate) mod request_state;
109/// Server-owned `requestState` AEAD continuation tokens (Phase 113, HTTP-02).
110#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
111#[cfg(feature = "fuzzing")]
112pub mod request_state;
113/// Outbound server-to-client request dispatcher with response correlation.
114#[cfg(not(target_arch = "wasm32"))]
115pub(crate) mod server_request_dispatcher;
116/// Simple prompt implementations with metadata support.
117#[cfg(not(target_arch = "wasm32"))]
118pub mod simple_prompt;
119/// Simple resource implementations with builder pattern support.
120#[cfg(not(target_arch = "wasm32"))]
121pub mod simple_resources;
122/// Simple tool implementations with schema support.
123#[cfg(not(target_arch = "wasm32"))]
124pub mod simple_tool;
125// Shared task-lifecycle dispatch unit used by both Server and ServerCore.
126//
127// The second `#[cfg]` widens the module's visibility for the `fuzzing` feature
128// ONLY, so `fuzz/fuzz_targets/fuzz_tasks_update.rs` can reach
129// `task_dispatch::fuzz_support` without any item becoming part of the shipped
130// public API (`fuzzing` is in neither `default` nor `full`, so `cargo public-api`
131// never sees it). This is verbatim the shape `server::request_state` already uses
132// for `fuzz_request_state`, so the crate has ONE convention for a fuzz seam
133// rather than two.
134#[cfg(not(target_arch = "wasm32"))]
135#[cfg(not(feature = "fuzzing"))]
136// Dead by CONFIGURATION, not disuse: the dispatch paths that call into this
137// module are gated behind the transport features, so a `default-features = false`
138// build (as `pmcp-tasks` does) and a wasm32 build both compile the module with no
139// callers. Scoped so genuine dead code is still caught in a normal build.
140#[cfg_attr(
141 any(target_arch = "wasm32", not(feature = "streamable-http")),
142 allow(dead_code)
143)]
144pub(crate) mod task_dispatch;
145/// Shared task-lifecycle dispatch unit used by both Server and ServerCore.
146#[cfg(not(target_arch = "wasm32"))]
147#[cfg(feature = "fuzzing")]
148pub mod task_dispatch;
149/// SDK-level task store trait and in-memory implementation.
150#[cfg(not(target_arch = "wasm32"))]
151pub mod task_store;
152/// Task routing trait for MCP Tasks integration.
153#[cfg(not(target_arch = "wasm32"))]
154pub mod tasks;
155/// Tool middleware for cross-cutting concerns in tool execution.
156#[cfg(not(target_arch = "wasm32"))]
157pub mod tool_middleware;
158
159/// Observability infrastructure for tracing, metrics, and logging.
160#[cfg(not(target_arch = "wasm32"))]
161pub mod observability;
162/// Workflow-based prompt system with type-safe handles and ergonomic builders.
163#[cfg(not(target_arch = "wasm32"))]
164pub mod workflow;
165
166/// State extractor for `#[mcp_tool]` shared state injection.
167#[cfg(not(target_arch = "wasm32"))]
168pub mod state;
169
170/// Typed tool implementations with automatic schema generation.
171#[cfg(not(target_arch = "wasm32"))]
172pub mod typed_tool;
173
174/// Typed prompt implementations with automatic argument schema generation.
175#[cfg(not(target_arch = "wasm32"))]
176pub mod typed_prompt;
177
178/// UI resource implementations for MCP Apps Extension (SEP-1865).
179#[cfg(not(target_arch = "wasm32"))]
180pub mod ui;
181
182/// MCP Apps Extension - Interactive UI support for multiple MCP hosts.
183///
184/// Provides adapters for `ChatGPT` Apps, MCP Apps (SEP-1865), and MCP-UI.
185#[cfg(all(not(target_arch = "wasm32"), feature = "mcp-apps"))]
186pub mod mcp_apps;
187
188/// Agent Skills (SEP-2640) — [`skills::Skill`] / [`skills::SkillReference`] /
189/// [`skills::Skills`] plus a dual-surface `PromptHandler` fallback.
190///
191/// Gated on `feature = "skills"` AND `not(target_arch = "wasm32")`: the
192/// module's contents consume [`ResourceHandler`] and [`PromptHandler`],
193/// which are themselves non-wasm-only.
194#[cfg(all(feature = "skills", not(target_arch = "wasm32")))]
195pub mod skills;
196
197/// Re-export the public Skills DX types so callers can `use
198/// pmcp::server::{Skill, SkillReference, Skills}` without descending
199/// into the `skills::` submodule path. The canonical path remains
200/// `pmcp::server::skills::*`.
201#[cfg(all(feature = "skills", not(target_arch = "wasm32")))]
202pub use skills::{Skill, SkillReference, Skills};
203
204/// Validation helpers for typed tools.
205#[cfg(not(target_arch = "wasm32"))]
206pub mod validation;
207
208/// Schema utilities for normalizing and inlining JSON schemas.
209#[cfg(feature = "schema-generation")]
210pub mod schema_utils;
211
212/// Standard error codes for validation with client elicitation support.
213#[cfg(not(target_arch = "wasm32"))]
214pub mod error_codes;
215
216/// Cross-platform path validation with security constraints.
217#[cfg(not(target_arch = "wasm32"))]
218pub mod path_validation;
219
220/// WASM-compatible typed tools with automatic schema generation.
221#[cfg(target_arch = "wasm32")]
222pub mod wasm_typed_tool;
223
224/// wasm32 stand-in for the native cancellation module.
225///
226/// wasm32 has no cancellation support, so this exposes only a zero-field
227/// [`cancellation::RequestHandlerExtra`] so handler signatures stay identical
228/// across targets.
229#[cfg(target_arch = "wasm32")]
230pub mod cancellation {
231 /// Stub for WASM - no cancellation support
232 #[derive(Debug, Clone, Default)]
233 pub struct RequestHandlerExtra;
234}
235/// Axum Router convenience function for secure MCP server hosting.
236#[cfg(feature = "streamable-http")]
237pub mod axum_router;
238#[cfg(not(target_arch = "wasm32"))]
239pub mod dynamic;
240#[cfg(not(target_arch = "wasm32"))]
241pub mod elicitation;
242#[cfg(not(target_arch = "wasm32"))]
243pub mod notification_debouncer;
244#[cfg(all(not(target_arch = "wasm32"), feature = "resource-watcher"))]
245pub mod resource_watcher;
246#[cfg(not(target_arch = "wasm32"))]
247pub mod roots;
248#[cfg(all(not(target_arch = "wasm32"), feature = "streamable-http"))]
249pub mod streamable_http_server;
250#[cfg(not(target_arch = "wasm32"))]
251// Dead by CONFIGURATION, not disuse: the dispatch paths that call into this
252// module are gated behind the transport features, so a `default-features = false`
253// build (as `pmcp-tasks` does) and a wasm32 build both compile the module with no
254// callers. Scoped so genuine dead code is still caught in a normal build.
255#[cfg_attr(
256 any(target_arch = "wasm32", not(feature = "streamable-http")),
257 allow(dead_code)
258)]
259pub mod subscriptions;
260/// Tower middleware layers for MCP HTTP security (DNS rebinding, security headers).
261#[cfg(feature = "streamable-http")]
262pub mod tower_layers;
263#[cfg(not(target_arch = "wasm32"))]
264pub mod transport;
265
266// WASM-specific modules and types
267#[cfg(target_arch = "wasm32")]
268pub mod wasi_adapter;
269#[cfg(target_arch = "wasm32")]
270pub mod wasm_core;
271#[cfg(target_arch = "wasm32")]
272pub mod wasm_server;
273#[cfg(all(test, target_arch = "wasm32"))]
274mod wasm_server_tests;
275
276// WASM-compatible protocol handler trait
277#[cfg(target_arch = "wasm32")]
278pub use wasi_protocol::ProtocolHandler;
279
280#[cfg(target_arch = "wasm32")]
281mod wasi_protocol {
282 use crate::error::Result;
283 use crate::types::{JSONRPCResponse, Notification, Request, RequestId};
284 use async_trait::async_trait;
285
286 /// Protocol-agnostic request handler trait for WASM.
287 ///
288 /// This is a simplified version of the ProtocolHandler trait that
289 /// doesn't depend on native-only types like handlers and managers.
290 #[async_trait(?Send)]
291 pub trait ProtocolHandler {
292 /// Handle a single request and return a response.
293 async fn handle_request(&self, id: RequestId, request: Request) -> JSONRPCResponse;
294
295 /// Handle a notification (no response expected).
296 async fn handle_notification(&self, notification: Notification) -> Result<()>;
297 }
298}
299
300#[cfg(test)]
301mod adapter_tests;
302#[cfg(test)]
303mod core_tests;
304#[cfg(all(test, not(target_arch = "wasm32")))]
305mod task_dispatch_tests;
306
307/// Output of a [`ToolHandler`] call: either a plain value the server wraps into a
308/// `CallToolResult`, or a fully-formed `CallToolResult` the handler owns end-to-end.
309///
310/// A handler that implements only [`ToolHandler::handle`] (the common case) never
311/// constructs this enum — the default [`ToolHandler::handle_output`] wraps the
312/// returned value in [`ToolOutput::Payload`], preserving today's behavior exactly.
313///
314/// This is a *control* enum consumed inside the server dispatch tail, NOT a wire
315/// type — it deliberately does not derive `Serialize`/`Deserialize`.
316///
317/// Marked `#[non_exhaustive]`: additional output modes may be added in a
318/// backwards-compatible way, so downstream `match`es must include a wildcard arm.
319#[cfg(not(target_arch = "wasm32"))]
320#[non_exhaustive]
321#[derive(Debug)]
322pub enum ToolOutput {
323 /// A plain value. The server runs it through the existing tail: response
324 /// middleware (redaction/sanitization), the Phase 102 task create-path gate,
325 /// and text-wrap / widget enrichment — byte-identical to returning a `Value`
326 /// from [`ToolHandler::handle`] today.
327 Payload(serde_json::Value),
328
329 /// A fully-formed `CallToolResult` the handler owns.
330 ///
331 /// # ⚠️ BYPASS WARNING — this variant is sent to the wire VERBATIM
332 ///
333 /// The contained [`CallToolResult`] is
334 /// serialized and returned to the client **exactly as provided**. It
335 /// **BYPASSES**:
336 /// - **response middleware** — redaction, sanitization, and audit hooks
337 /// (`ToolMiddleware::on_response`) DO NOT run for this variant;
338 /// - **text-wrapping** — `content` is not synthesized from a stringified value;
339 /// - **widget enrichment** — `structured_content` / `_meta` are not injected.
340 ///
341 /// The handler is therefore responsible for its OWN redaction and
342 /// sanitization of both `content` and `_meta`, at the same trust level as
343 /// returning a raw `Value` today. This is a deliberate, user-approved design
344 /// choice (D-04a): a handler that needs to own the full envelope (e.g. to set
345 /// `_meta[relatedTask]`) opts into owning its security posture too.
346 ///
347 /// What is **NOT** bypassed: **request** middleware
348 /// (`ToolMiddleware::on_request`) still runs before the handler executes, and
349 /// handler errors still route through the normal error path. Only the
350 /// successful `Result` arm skips response middleware.
351 ///
352 /// See the phase migration guide for how to move a hand-written handler onto
353 /// this variant safely.
354 Result(crate::types::CallToolResult),
355}
356
357/// Handler for tool execution.
358#[cfg(not(target_arch = "wasm32"))]
359#[async_trait]
360pub trait ToolHandler: Send + Sync {
361 /// Handle a tool call with the given arguments.
362 async fn handle(&self, args: Value, extra: cancellation::RequestHandlerExtra) -> Result<Value>;
363
364 /// Get tool metadata including description and schema.
365 /// Returns None to use default empty metadata.
366 fn metadata(&self) -> Option<crate::types::ToolInfo> {
367 None
368 }
369
370 /// Produce the tool's [`ToolOutput`] for a call.
371 ///
372 /// The DEFAULT delegates to [`ToolHandler::handle`] and wraps the returned
373 /// value in [`ToolOutput::Payload`], so existing handlers (which implement
374 /// only `handle`) keep their exact current behavior — response middleware,
375 /// the create-path gate, and text-wrap all apply unchanged.
376 ///
377 /// Override this to return [`ToolOutput::Result`] and own the full
378 /// `CallToolResult` envelope. Read the [`ToolOutput::Result`] docs FIRST —
379 /// that path bypasses response middleware and you own your own redaction.
380 async fn handle_output(
381 &self,
382 args: Value,
383 extra: cancellation::RequestHandlerExtra,
384 ) -> Result<ToolOutput> {
385 self.handle(args, extra).await.map(ToolOutput::Payload)
386 }
387}
388
389/// Handler for prompt generation.
390#[cfg(not(target_arch = "wasm32"))]
391#[async_trait]
392pub trait PromptHandler: Send + Sync {
393 /// Generate a prompt with the given arguments.
394 async fn handle(
395 &self,
396 args: HashMap<String, String>,
397 extra: cancellation::RequestHandlerExtra,
398 ) -> Result<crate::types::GetPromptResult>;
399
400 /// Get prompt metadata including description and arguments schema.
401 /// Returns None to use default empty metadata.
402 fn metadata(&self) -> Option<crate::types::PromptInfo> {
403 None
404 }
405}
406
407/// Handler for resource access.
408#[cfg(not(target_arch = "wasm32"))]
409#[async_trait]
410pub trait ResourceHandler: Send + Sync {
411 /// Read a resource at the given URI.
412 async fn read(
413 &self,
414 uri: &str,
415 extra: cancellation::RequestHandlerExtra,
416 ) -> Result<crate::types::ReadResourceResult>;
417
418 /// List available resources.
419 async fn list(
420 &self,
421 _cursor: Option<String>,
422 extra: cancellation::RequestHandlerExtra,
423 ) -> Result<crate::types::ListResourcesResult>;
424}
425
426/// Handler for message sampling (LLM operations).
427#[cfg(not(target_arch = "wasm32"))]
428#[async_trait]
429pub trait SamplingHandler: Send + Sync {
430 /// Create a message using the language model.
431 async fn create_message(
432 &self,
433 params: crate::types::CreateMessageParams,
434 extra: cancellation::RequestHandlerExtra,
435 ) -> Result<crate::types::CreateMessageResult>;
436}
437
438/// MCP server implementation.
439///
440/// # Examples
441///
442/// ```rust,no_run
443/// use pmcp::{Server, ServerCapabilities, ToolHandler};
444/// use async_trait::async_trait;
445/// use serde_json::Value;
446///
447/// struct MyTool;
448///
449/// #[async_trait]
450/// impl ToolHandler for MyTool {
451/// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
452/// Ok(serde_json::json!({"result": "success"}))
453/// }
454/// }
455///
456/// # async fn example() -> pmcp::Result<()> {
457/// let server = Server::builder()
458/// .name("my-server")
459/// .version("1.0.0")
460/// .tool("my-tool", MyTool)
461/// .build()?;
462///
463/// server.run_stdio().await?;
464/// # Ok(())
465/// # }
466/// ```
467#[cfg(not(target_arch = "wasm32"))]
468#[allow(dead_code)]
469pub struct Server {
470 info: Implementation,
471 capabilities: ServerCapabilities,
472 tools: HashMap<String, Arc<dyn ToolHandler>>,
473 tool_infos: HashMap<String, ToolInfo>,
474 /// Cached URI-to-tool-meta index for widget resource `_meta` propagation.
475 uri_to_tool_meta: HashMap<String, serde_json::Map<String, serde_json::Value>>,
476 prompts: HashMap<String, Arc<dyn PromptHandler>>,
477 resources: Option<Arc<dyn ResourceHandler>>,
478 /// Completion provider backing `completion/complete` (Phase 118.1-04,
479 /// CONF-05). Mirrors `ServerCore`'s field of the same name so BOTH native
480 /// dispatchers consult the same registered seam through the same shared
481 /// unit. `None` still answers the spec shape with an empty `values` array.
482 completions: Option<Arc<dyn crate::types::completable::CompletionProviderTrait>>,
483 sampling: Option<Arc<dyn SamplingHandler>>,
484 client_capabilities: Arc<RwLock<Option<ClientCapabilities>>>,
485 initialized: Arc<RwLock<bool>>,
486 /// Channel for sending notifications
487 notification_tx: Option<mpsc::Sender<Notification>>,
488 /// Cancellation manager for request cancellation
489 cancellation_manager: cancellation::CancellationManager,
490 /// Roots manager for directory/URI registration
491 roots_manager: Arc<RwLock<roots::RootsManager>>,
492 /// Subscription manager for resource subscriptions (v1 `resources/subscribe`)
493 subscription_manager: Arc<RwLock<subscriptions::SubscriptionManager>>,
494 /// The v2 `subscriptions/listen` stream registry (Phase 113, HTTP-04).
495 ///
496 /// Shared by the streamable-HTTP transport (which REGISTERS a stream) and
497 /// [`send_notification`](Self::send_notification) (which FANS OUT to it), so
498 /// a change notification emitted through the server's real notification path
499 /// reaches every live listen stream whose agreed filter covers it. Empty —
500 /// and therefore a no-op — on any server that never served a v2 listen
501 /// request, which is every v1 server.
502 listen_registry: Arc<subscriptions::ListenRegistry>,
503 /// Elicitation manager for user input requests
504 elicitation_manager: Option<Arc<elicitation::ElicitationManager>>,
505 /// Outbound server-to-client request dispatcher with response correlation.
506 /// Wired in `Server::run`; `None` outside the run lifecycle.
507 #[allow(clippy::struct_field_names)]
508 server_request_dispatcher: Option<Arc<server_request_dispatcher::ServerRequestDispatcher>>,
509 /// Cached peer handle built alongside the dispatcher so dispatch sites
510 /// clone the Arc rather than allocating a new `DispatchPeerHandle` per
511 /// request. `None` outside the run lifecycle.
512 peer_handle: Option<Arc<dyn crate::shared::peer::PeerHandle>>,
513 /// Authentication provider for validating requests
514 auth_provider: Option<Arc<dyn auth::AuthProvider>>,
515 /// Tool authorizer for fine-grained access control
516 tool_authorizer: Option<Arc<dyn auth::ToolAuthorizer>>,
517 /// Tool middleware chain for cross-cutting concerns in tool execution
518 #[cfg(not(target_arch = "wasm32"))]
519 tool_middleware_chain: Arc<RwLock<tool_middleware::ToolMiddlewareChain>>,
520 /// HTTP middleware chain for `StreamableHttpServer` (configured via `ServerBuilder`)
521 #[cfg(feature = "streamable-http")]
522 http_middleware: Option<Arc<http_middleware::ServerHttpMiddlewareChain>>,
523 /// Legacy experimental task router backend (fall-through path). Mirrors
524 /// `ServerCore`'s field; presence backs the `tasks/*` endpoints over the
525 /// router. Both backends feed the shared `task_dispatch` unit.
526 #[cfg(not(target_arch = "wasm32"))]
527 task_router: Option<Arc<dyn crate::server::tasks::TaskRouter>>,
528 /// Standard task store backend (polling path). Mirrors `ServerCore`'s field;
529 /// presence flips the `tasks` capability on at `build()` and backs the
530 /// `tasks/*` endpoints + create-path via the shared `task_dispatch` unit.
531 #[cfg(not(target_arch = "wasm32"))]
532 task_store: Option<Arc<dyn crate::server::task_store::TaskStore>>,
533 /// Per-tool TOUT-02 double-wrap tripwire opt-out set (D-08). A tool named
534 /// here has the tripwire suppressed at the Payload wrap site. Threaded from
535 /// `ServerBuilder::suppress_double_wrap_check`; `ServerCore` carries an
536 /// IDENTICAL set so both dispatchers consult the same suppression rule.
537 #[cfg(not(target_arch = "wasm32"))]
538 suppress_double_wrap: HashSet<String>,
539 /// Configured protocol-version accept-list (Phase 112, VERS-01/02). Mirrors
540 /// `ServerCore`'s field so this high-level `Server` dispatch site resolves the
541 /// per-request [`ProtocolContext`](crate::types::protocol::ProtocolContext)
542 /// through the SAME shared resolver. Default is v1-only (excludes
543 /// `2026-07-28`) — an un-opted-in server is byte-for-byte unchanged.
544 supported_protocol_versions: Vec<ProtocolVersion>,
545 /// The server-owned `requestState` codec (Phase 113, HTTP-02), resolved
546 /// EXACTLY ONCE at [`ServerBuilder::build`] time. `None` for a server that
547 /// did not opt into v2 — such a server reads no MRTR env var and pays
548 /// nothing (D-04). Deliberately an instance field, never a process-global:
549 /// see [`request_state`] for why.
550 #[cfg(feature = "streamable-http")]
551 request_state_codec: Option<Arc<request_state::RequestStateCodec>>,
552}
553
554#[cfg(not(target_arch = "wasm32"))]
555impl std::fmt::Debug for Server {
556 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557 f.debug_struct("Server")
558 .field("info", &self.info)
559 .field("capabilities", &self.capabilities)
560 .field("tools", &self.tools.keys().collect::<Vec<_>>())
561 .field("prompts", &self.prompts.keys().collect::<Vec<_>>())
562 .field("resources", &self.resources.is_some())
563 .field("sampling", &self.sampling.is_some())
564 .field("initialized", &self.initialized)
565 .finish()
566 }
567}
568
569// Every accessor in this block is read by the transport dispatch (the v2 envelope
570// and the `subscriptions/listen` + `tasks/update` paths). A transport-less build
571// compiles the block with no readers, which is a property of the feature set, not
572// of the code.
573#[cfg_attr(not(feature = "streamable-http"), allow(dead_code))]
574#[cfg(not(target_arch = "wasm32"))]
575impl Server {
576 /// The server-owned `requestState` codec, or `None` when this server did not
577 /// opt into the v2 (`2026-07-28`) era.
578 ///
579 /// Resolved once at build time; the production consumers
580 /// (`core::mrtr_ingest`'s `verify` and `core::mrtr_egress`'s `mint`) borrow
581 /// it from server state rather than reaching for a process-global.
582 #[cfg(feature = "streamable-http")]
583 pub(crate) fn request_state_codec(&self) -> Option<&request_state::RequestStateCodec> {
584 self.request_state_codec.as_deref()
585 }
586
587 /// The server's already-computed capabilities.
588 ///
589 /// A READ-ONLY borrow — the SAME value
590 /// [`handle_discover`](Self::handle_discover) projects onto the wire. The
591 /// `subscriptions/listen` gate reads it through
592 /// [`advertises_subscriptions`](crate::types::subscriptions::advertises_subscriptions)
593 /// so the advertisement and the implementation cannot drift (HTTP-04).
594 pub(crate) fn capabilities(&self) -> &ServerCapabilities {
595 &self.capabilities
596 }
597
598 /// The server's own [`Implementation`] identity.
599 ///
600 /// Borrowed by the v2 response envelope so a `subscriptions/listen` terminal
601 /// result carries the same `io.modelcontextprotocol/serverInfo` every other
602 /// v2 result carries.
603 pub(crate) fn info(&self) -> &Implementation {
604 &self.info
605 }
606
607 /// Check if a tool exists
608 pub fn has_tool(&self, name: &str) -> bool {
609 self.tools.contains_key(name)
610 }
611
612 /// Check if a prompt exists
613 pub fn has_prompt(&self, name: &str) -> bool {
614 self.prompts.contains_key(name)
615 }
616
617 /// Get a prompt handler by name.
618 ///
619 /// Returns a borrowed reference to the registered prompt handler `Arc`,
620 /// or `None` if no prompt with that name has been registered. Callers
621 /// who need ownership can `Arc::clone(...)` the returned reference.
622 ///
623 /// # Handler-level testing pattern
624 ///
625 /// This accessor is the public API surface for the handler-level
626 /// integration testing pattern documented in the testing chapter of
627 /// the PMCP book: build a `Server`, retrieve the handler by name,
628 /// invoke `.handle(...).await` directly with a synthetic
629 /// `RequestHandlerExtra`. It exercises handler logic in isolation
630 /// without spinning up a transport.
631 ///
632 /// # What this pattern skips
633 ///
634 /// This pattern exercises handler logic only. The JSONRPC dispatch
635 /// path (`Server::handle_request`) is bypassed, so `auth_provider`,
636 /// `tool_authorizer`, and `tool_middleware` are **not** invoked. For
637 /// full-pipeline tests that exercise the security pipeline, drive a
638 /// real transport (stdio or streamable-http) with a `pmcp::Client`.
639 ///
640 /// # Examples
641 ///
642 /// ```rust
643 /// use std::collections::HashMap;
644 /// use std::sync::Arc;
645 /// use async_trait::async_trait;
646 /// use pmcp::{PromptHandler, Server};
647 /// use pmcp::types::{GetPromptResult, PromptMessage, Content};
648 /// use pmcp::types::content::Role;
649 ///
650 /// struct GreetingPrompt;
651 ///
652 /// #[async_trait]
653 /// impl PromptHandler for GreetingPrompt {
654 /// async fn handle(
655 /// &self,
656 /// args: HashMap<String, String>,
657 /// _extra: pmcp::RequestHandlerExtra,
658 /// ) -> pmcp::Result<GetPromptResult> {
659 /// let who = args.get("name").cloned().unwrap_or_else(|| "world".to_string());
660 /// Ok(GetPromptResult::new(
661 /// vec![PromptMessage::new(Role::User, Content::text(format!("Hello, {}!", who)))],
662 /// Some("Greeting prompt".to_string()),
663 /// ))
664 /// }
665 /// }
666 ///
667 /// # async fn example() -> pmcp::Result<()> {
668 /// let server = Server::builder()
669 /// .name("demo")
670 /// .version("0.1")
671 /// .prompt_arc("greet", Arc::new(GreetingPrompt))
672 /// .build()?;
673 ///
674 /// let handler = server.get_prompt("greet").expect("registered above");
675 /// let mut args = HashMap::new();
676 /// args.insert("name".to_string(), "claude".to_string());
677 /// let result = handler
678 /// .handle(args, pmcp::RequestHandlerExtra::default())
679 /// .await?;
680 /// assert_eq!(result.messages.len(), 1);
681 /// # Ok(())
682 /// # }
683 /// ```
684 pub fn get_prompt(&self, name: &str) -> Option<&Arc<dyn PromptHandler>> {
685 self.prompts.get(name)
686 }
687
688 /// Get a tool handler by name.
689 ///
690 /// Returns a borrowed reference to the registered tool handler `Arc`,
691 /// or `None` if no tool with that name has been registered. Callers
692 /// who need ownership can `Arc::clone(...)` the returned reference.
693 ///
694 /// # Handler-level testing pattern
695 ///
696 /// This accessor is the public API surface for the handler-level
697 /// integration testing pattern: build a `Server`, retrieve the
698 /// handler by name, invoke `.handle(...).await` directly with a
699 /// synthetic `RequestHandlerExtra`. It exercises handler logic in
700 /// isolation without spinning up a transport, which is the primary
701 /// shape downstream toolkit authors use to assert on a built
702 /// `pmcp::Server`'s registered handlers.
703 ///
704 /// # What this pattern skips
705 ///
706 /// This pattern exercises handler logic only. The JSONRPC dispatch
707 /// path (`Server::handle_request`) is bypassed, so `auth_provider`,
708 /// `tool_authorizer`, and `tool_middleware` are **not** invoked. For
709 /// full-pipeline tests that exercise the security pipeline, drive a
710 /// real transport (stdio or streamable-http) with a `pmcp::Client`.
711 ///
712 /// # Examples
713 ///
714 /// ```rust
715 /// use std::sync::Arc;
716 /// use async_trait::async_trait;
717 /// use pmcp::{Server, ToolHandler};
718 /// use serde_json::Value;
719 ///
720 /// struct EchoTool;
721 ///
722 /// #[async_trait]
723 /// impl ToolHandler for EchoTool {
724 /// async fn handle(
725 /// &self,
726 /// args: Value,
727 /// _extra: pmcp::RequestHandlerExtra,
728 /// ) -> pmcp::Result<Value> {
729 /// Ok(serde_json::json!({ "echoed": args }))
730 /// }
731 /// }
732 ///
733 /// # async fn example() -> pmcp::Result<()> {
734 /// let server = Server::builder()
735 /// .name("demo")
736 /// .version("0.1")
737 /// .tool_arc("echo", Arc::new(EchoTool))
738 /// .build()?;
739 ///
740 /// let handler = server.get_tool("echo").expect("registered above");
741 /// let result = handler
742 /// .handle(serde_json::json!({"msg": "hi"}), pmcp::RequestHandlerExtra::default())
743 /// .await?;
744 /// assert_eq!(result, serde_json::json!({"echoed": {"msg": "hi"}}));
745 /// # Ok(())
746 /// # }
747 /// ```
748 pub fn get_tool(&self, name: &str) -> Option<&Arc<dyn ToolHandler>> {
749 self.tools.get(name)
750 }
751
752 /// Get the HTTP middleware chain configured via `ServerBuilder`.
753 ///
754 /// Returns the HTTP middleware chain that was set using
755 /// `ServerBuilder::with_http_middleware()`. This can be used when
756 /// creating a `StreamableHttpServer`.
757 ///
758 /// # Examples
759 ///
760 /// ```rust,no_run
761 /// # #[cfg(feature = "streamable-http")]
762 /// # {
763 /// use pmcp::Server;
764 /// use pmcp::server::streamable_http_server::StreamableHttpServerConfig;
765 ///
766 /// # async fn example() -> pmcp::Result<()> {
767 /// let server = Server::builder()
768 /// .name("my-server")
769 /// .version("1.0.0")
770 /// // ... with_http_middleware() called here
771 /// .build()?;
772 ///
773 /// let config = StreamableHttpServerConfig {
774 /// http_middleware: server.http_middleware(),
775 /// ..Default::default()
776 /// };
777 /// # Ok(())
778 /// # }
779 /// # }
780 /// ```
781 #[cfg(feature = "streamable-http")]
782 pub fn http_middleware(&self) -> Option<Arc<http_middleware::ServerHttpMiddlewareChain>> {
783 self.http_middleware.clone()
784 }
785
786 /// Get the authentication provider configured via `ServerBuilder`.
787 ///
788 /// Returns the authentication provider that was set using
789 /// `ServerBuilder::auth_provider()`. This can be used by transport
790 /// layers to validate incoming requests and extract auth context.
791 pub fn get_auth_provider(&self) -> Option<Arc<dyn auth::AuthProvider>> {
792 self.auth_provider.clone()
793 }
794
795 /// Build tool and resource registries for workflow expansion.
796 ///
797 /// Creates `HashMap` registries that can be used to build an `ExpansionContext`
798 /// for converting workflow prompts to protocol types. The registries are
799 /// automatically populated from all registered tools and resources.
800 ///
801 /// Returns a tuple of (`tools_map`, `resources_map`) that can be used with
802 /// `ExpansionContext`.
803 ///
804 /// # Examples
805 ///
806 /// ```rust,ignore
807 /// use pmcp::Server;
808 /// use pmcp::server::workflow::{InternalPromptMessage, ToolHandle, PromptContent, conversion::ExpansionContext};
809 /// use pmcp::types::Role;
810 ///
811 /// # async fn example() -> pmcp::Result<()> {
812 /// let server = Server::builder()
813 /// .name("example-server")
814 /// .version("1.0.0")
815 /// .build()?;
816 ///
817 /// // Build registries from registered tools/resources
818 /// let (tools, resources) = server.build_expansion_registries();
819 ///
820 /// // Create expansion context
821 /// let ctx = ExpansionContext {
822 /// tools: &tools,
823 /// resources: &resources,
824 /// };
825 ///
826 /// // Use it to convert workflow prompts to protocol types
827 /// let msg = InternalPromptMessage::new(
828 /// Role::System,
829 /// PromptContent::ToolHandle(ToolHandle::new("my_tool"))
830 /// );
831 /// let protocol_msg = msg.to_protocol(&ctx)?;
832 /// # Ok(())
833 /// # }
834 /// ```
835 pub fn build_expansion_registries(
836 &self,
837 ) -> (
838 HashMap<Arc<str>, workflow::conversion::ToolInfo>,
839 HashMap<Arc<str>, workflow::conversion::ResourceInfo>,
840 ) {
841 use std::collections::HashMap;
842
843 // Build tools map from registered tool handlers
844 let mut tools_map = HashMap::new();
845 for (name, handler) in &self.tools {
846 if let Some(metadata) = handler.metadata() {
847 tools_map.insert(
848 Arc::from(name.as_str()),
849 workflow::conversion::ToolInfo {
850 name: metadata.name,
851 description: metadata.description.unwrap_or_default(),
852 input_schema: metadata.input_schema,
853 },
854 );
855 }
856 }
857
858 // Build resources map (currently empty - resources don't have metadata())
859 // This could be enhanced in the future when resources have better metadata
860 let resources_map = HashMap::new();
861
862 (tools_map, resources_map)
863 }
864
865 /// Send a notification.
866 ///
867 /// Sends a notification to the connected client. Notifications are one-way
868 /// messages that don't expect a response.
869 ///
870 /// # Arguments
871 ///
872 /// * `notification` - The server notification to send
873 ///
874 /// # Examples
875 ///
876 /// ```rust,no_run
877 /// use pmcp::{Server, ServerNotification, ProgressNotification, ProgressToken};
878 ///
879 /// # async fn example() -> pmcp::Result<()> {
880 /// let server = Server::builder()
881 /// .name("example-server")
882 /// .version("1.0.0")
883 /// .build()?;
884 ///
885 /// // Send a progress notification
886 /// let progress = ProgressNotification::new(
887 /// ProgressToken::String("task-123".to_string()),
888 /// 50.0,
889 /// Some("Processing...".to_string()),
890 /// );
891 ///
892 /// server.send_notification(ServerNotification::Progress(progress)).await;
893 /// # Ok(())
894 /// # }
895 /// ```
896 pub async fn send_notification(&self, notification: ServerNotification) {
897 // HTTP-04: fan out to every live v2 `subscriptions/listen` stream FIRST,
898 // then take the existing v1 transport path unchanged. The registry is
899 // empty on any server that never served a listen request, so this is a
900 // map lookup on a v1 server and no wire byte changes there.
901 self.listen_registry.fan_out(¬ification);
902 if let Some(tx) = &self.notification_tx {
903 let _ = tx.send(Notification::Server(notification)).await;
904 }
905 }
906
907 /// Gracefully close every open `subscriptions/listen` stream (HTTP-04).
908 ///
909 /// Call this from a shutdown handler: each stream receives its
910 /// [`SubscriptionsListenResult`](crate::types::subscriptions::SubscriptionsListenResult)
911 /// as the JSON-RPC response and is then ended. This is the ONLY one of the
912 /// three closure triggers that can send a terminal result — a client
913 /// disconnect cannot (the peer is gone) and the buffer-overflow policy cannot
914 /// (the buffer is full); both simply end the stream.
915 ///
916 /// # Examples
917 ///
918 /// ```rust,no_run
919 /// # async fn example() -> pmcp::Result<()> {
920 /// let server = pmcp::Server::builder()
921 /// .name("example-server")
922 /// .version("1.0.0")
923 /// .build()?;
924 ///
925 /// // ... on shutdown:
926 /// server.close_subscription_streams();
927 /// # Ok(())
928 /// # }
929 /// ```
930 pub fn close_subscription_streams(&self) {
931 self.listen_registry.close_all();
932 }
933
934 /// The v2 `subscriptions/listen` registry this server fans notifications out
935 /// to.
936 ///
937 /// The streamable-HTTP transport clones this `Arc` to register a stream; the
938 /// `Arc` is cloned under the server lock and the lock is released
939 /// immediately, so a held-open stream never holds the server mutex.
940 pub(crate) fn listen_registry(&self) -> &Arc<subscriptions::ListenRegistry> {
941 &self.listen_registry
942 }
943
944 /// Get client capabilities.
945 ///
946 /// Returns the capabilities that the client declared during initialization.
947 /// This can be used to check if the client supports specific features.
948 ///
949 /// # Examples
950 ///
951 /// ```rust,no_run
952 /// use pmcp::Server;
953 ///
954 /// # async fn example() -> pmcp::Result<()> {
955 /// let server = Server::builder()
956 /// .name("example-server")
957 /// .version("1.0.0")
958 /// .build()?;
959 ///
960 /// // Check client capabilities after initialization
961 /// if let Some(capabilities) = server.get_client_capabilities().await {
962 /// if capabilities.sampling.is_some() {
963 /// println!("Client supports LLM sampling requests");
964 /// }
965 /// if capabilities.elicitation.is_some() {
966 /// println!("Client supports user input requests");
967 /// }
968 /// }
969 /// # Ok(())
970 /// # }
971 /// ```
972 ///
973 /// # Returns
974 ///
975 /// - `Some(ClientCapabilities)` if the client has been initialized
976 /// - `None` if the client hasn't initialized yet
977 pub async fn get_client_capabilities(&self) -> Option<ClientCapabilities> {
978 self.client_capabilities.read().await.clone()
979 }
980
981 /// Check if the server is initialized.
982 ///
983 /// Returns true if the initialization handshake with a client has completed.
984 /// The server must be initialized before it can process most requests.
985 ///
986 /// # Examples
987 ///
988 /// ```rust,no_run
989 /// use pmcp::Server;
990 ///
991 /// # async fn example() -> pmcp::Result<()> {
992 /// let server = Server::builder()
993 /// .name("example-server")
994 /// .version("1.0.0")
995 /// .build()?;
996 ///
997 /// if server.is_initialized().await {
998 /// println!("Server is ready to handle requests");
999 /// } else {
1000 /// println!("Waiting for client initialization");
1001 /// }
1002 /// # Ok(())
1003 /// # }
1004 /// ```
1005 pub async fn is_initialized(&self) -> bool {
1006 *self.initialized.read().await
1007 }
1008 /// Create a new server builder.
1009 ///
1010 /// Returns a `ServerBuilder` for configuring and constructing a new MCP server.
1011 /// The builder pattern allows you to set server information, capabilities,
1012 /// and register handlers before building the final server instance.
1013 ///
1014 /// # Examples
1015 ///
1016 /// ```rust,no_run
1017 /// use pmcp::{Server, ToolHandler};
1018 /// use async_trait::async_trait;
1019 /// use serde_json::Value;
1020 ///
1021 /// struct HelloTool;
1022 ///
1023 /// #[async_trait]
1024 /// impl ToolHandler for HelloTool {
1025 /// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
1026 /// Ok(serde_json::json!({"message": "Hello, World!"}))
1027 /// }
1028 /// }
1029 ///
1030 /// # async fn example() -> pmcp::Result<()> {
1031 /// let server = Server::builder()
1032 /// .name("greeting-server")
1033 /// .version("1.0.0")
1034 /// .tool("hello", HelloTool{})
1035 /// .build()?;
1036 /// # Ok(())
1037 /// # }
1038 /// ```
1039 pub fn builder() -> ServerBuilder {
1040 ServerBuilder::new()
1041 }
1042
1043 /// Run the server with stdio transport.
1044 ///
1045 /// Starts the server using stdin/stdout for communication.
1046 /// This is the standard way to run MCP servers as they communicate
1047 /// via JSON-RPC over stdio.
1048 ///
1049 /// # Examples
1050 ///
1051 /// ```rust,no_run
1052 /// use pmcp::{Server, ToolHandler};
1053 /// use async_trait::async_trait;
1054 /// use serde_json::Value;
1055 ///
1056 /// struct EchoTool;
1057 ///
1058 /// #[async_trait]
1059 /// impl ToolHandler for EchoTool {
1060 /// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
1061 /// Ok(args) // Echo the input
1062 /// }
1063 /// }
1064 ///
1065 /// # async fn example() -> pmcp::Result<()> {
1066 /// let server = Server::builder()
1067 /// .name("echo-server")
1068 /// .version("1.0.0")
1069 /// .tool("echo", EchoTool{})
1070 /// .build()?;
1071 ///
1072 /// // This will run indefinitely, handling client requests
1073 /// server.run_stdio().await?;
1074 /// # Ok(())
1075 /// # }
1076 /// ```
1077 ///
1078 /// # Security: task isolation
1079 ///
1080 /// Stdio (and any transport that does not resolve a per-request
1081 /// `AuthContext`) carries no authenticated principal, so every `tasks/*`
1082 /// request on a [`task_store`](ServerBuilder::task_store)-backed server is
1083 /// owned by the single `"local"` owner — there is NO per-user task isolation.
1084 /// This is correct for single-user CLI use; for multi-tenant deployments use
1085 /// an HTTP transport whose auth layer populates the OAuth subject.
1086 ///
1087 /// # Errors
1088 ///
1089 /// Returns an error if:
1090 /// - The stdio transport fails to initialize
1091 /// - Communication with the client fails
1092 /// - The server encounters an unrecoverable error
1093 pub async fn run_stdio(self) -> Result<()> {
1094 let transport = crate::shared::StdioTransport::new();
1095 self.run(transport).await
1096 }
1097
1098 /// Run the server with a custom transport.
1099 ///
1100 /// Starts the server using a custom transport implementation.
1101 /// This allows for different communication mechanisms beyond stdio,
1102 /// such as TCP sockets, `WebSockets`, or other protocols.
1103 ///
1104 /// # Arguments
1105 ///
1106 /// * `transport` - The transport implementation to use for communication
1107 ///
1108 /// # Examples
1109 ///
1110 /// ```rust,no_run
1111 /// use pmcp::{Server, StdioTransport, ToolHandler};
1112 /// use async_trait::async_trait;
1113 /// use serde_json::Value;
1114 ///
1115 /// struct CalculatorTool;
1116 ///
1117 /// #[async_trait]
1118 /// impl ToolHandler for CalculatorTool {
1119 /// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
1120 /// let a = args["a"].as_f64().unwrap_or(0.0);
1121 /// let b = args["b"].as_f64().unwrap_or(0.0);
1122 /// Ok(serde_json::json!({"result": a + b}))
1123 /// }
1124 /// }
1125 ///
1126 /// # async fn example() -> pmcp::Result<()> {
1127 /// let server = Server::builder()
1128 /// .name("calculator-server")
1129 /// .version("1.0.0")
1130 /// .tool("add", CalculatorTool{})
1131 /// .build()?;
1132 ///
1133 /// let transport = StdioTransport::new();
1134 /// server.run(transport).await?;
1135 /// # Ok(())
1136 /// # }
1137 /// ```
1138 ///
1139 /// # Security: task isolation
1140 ///
1141 /// Per-user `tasks/*` isolation requires the transport to resolve a per-request
1142 /// `AuthContext` carrying the OAuth subject. Transports without one (stdio and
1143 /// the like) own every task under the single `"local"` owner — no per-user
1144 /// isolation. Use an authenticating HTTP transport for multi-tenant task servers.
1145 ///
1146 /// # Errors
1147 ///
1148 /// Returns an error if:
1149 /// - The transport fails to initialize or operate
1150 /// - Communication with the client fails
1151 /// - The server encounters an unrecoverable error
1152 pub async fn run<T: crate::shared::Transport + 'static>(mut self, transport: T) -> Result<()> {
1153 let (notification_tx, notification_rx) = mpsc::channel(100);
1154 self.notification_tx = Some(notification_tx);
1155
1156 // Hook cancellation manager to send notifications via the same channel,
1157 // through the SINGLE `notification_tx`-to-sink conversion
1158 // ([`Server::notification_tx_sink`]). This site used to carry its own
1159 // `try_send` closure — a third copy of the same three lines, and a third
1160 // chance to disagree about the send discipline.
1161 if let Some(sender) = self.notification_tx_sink() {
1162 self.cancellation_manager.set_notification_sender(sender);
1163 }
1164
1165 // Outbound server-to-client request channel + dispatcher. Drain task
1166 // wraps each `(correlation_id, ServerRequest)` as
1167 // `TransportMessage::Request` and forwards to the transport;
1168 // `handle_transport_message` routes responses back through
1169 // `dispatcher.handle_response`.
1170 let (outbound_tx, outbound_rx) =
1171 mpsc::channel::<(String, crate::types::ServerRequest)>(100);
1172 let dispatcher = Arc::new(
1173 server_request_dispatcher::ServerRequestDispatcher::new_with_channel(outbound_tx),
1174 );
1175 let peer: Arc<dyn crate::shared::peer::PeerHandle> = Arc::new(
1176 crate::server::peer_impl::DispatchPeerHandle::new(dispatcher.clone()),
1177 );
1178 self.peer_handle = Some(peer);
1179 self.server_request_dispatcher = Some(dispatcher);
1180
1181 let server = Arc::new(self);
1182
1183 // Transport Actor design (Phase 108, D-01/D-02): the transport is OWNED
1184 // by exactly one actor task and is NEVER wrapped in a shared
1185 // `Arc<RwLock<T>>`. ALL outbound frames (responses, server-requests,
1186 // notifications) funnel through a single UNBOUNDED `send_tx`; inbound
1187 // requests go to a SINGLE sequential worker via an UNBOUNDED
1188 // `request_tx`. The receive/drain path therefore never blocks on request
1189 // execution, request-queue capacity, or a transport write-lock, so an
1190 // in-tool `peer.sample()` / `.list_roots()` round-trip cannot deadlock
1191 // the loop. Request handling stays serialized (one worker) — zero
1192 // behavior change for existing single-request servers.
1193 let (send_tx, send_rx) = mpsc::unbounded_channel::<TransportMessage>();
1194 let (request_tx, request_rx) = mpsc::unbounded_channel::<(RequestId, Request)>();
1195
1196 Self::spawn_notification_handler(send_tx.clone(), notification_rx);
1197 server_request_dispatcher::spawn_server_request_drain(send_tx.clone(), outbound_rx);
1198 Self::spawn_request_worker(server.clone(), request_rx, send_tx.clone());
1199 let actor = tokio::spawn(Self::run_transport_actor(
1200 server.clone(),
1201 transport,
1202 send_rx,
1203 request_tx,
1204 ));
1205
1206 // Return once the actor task ends (transport closed / all senders gone).
1207 Self::run_main_loop(actor).await
1208 }
1209
1210 /// Attach a peer handle to `extra`, preferring the REQUEST-SCOPED one.
1211 ///
1212 /// No-op on wasm32, and — when neither source is present — outside the
1213 /// `run()` lifecycle, exactly as before.
1214 ///
1215 /// # Precedence: the request-scoped transport handle wins (T-118.1-11-04)
1216 ///
1217 /// Two sources can supply a peer and they are NOT equivalent:
1218 ///
1219 /// 1. `self.peer_handle` — a SINGLE field on `Server`, set once by
1220 /// [`Server::run`] for the in-process actor loop. One transport, one
1221 /// client, so one handle says everything there is to say.
1222 /// 2. the [`TransportBackchannel`](crate::types::protocol::context::TransportBackchannel)
1223 /// riding THIS request's `ProtocolContext`, attached by the
1224 /// `StreamableHTTP` transport at the one site that knows which session the
1225 /// request arrived on.
1226 ///
1227 /// On a MULTIPLEXED transport (1) cannot express "the session that issued
1228 /// this request": a handle set there is shared by every concurrent session,
1229 /// so one client's `sampling/createMessage` would be delivered to whichever
1230 /// session the global handle happened to be bound to — the T-113-07
1231 /// misbinding class. (2) is constructed per request and bound to the
1232 /// originating session, so it is always the more specific answer and is
1233 /// therefore read FIRST.
1234 ///
1235 /// The in-process path is untouched: `Server::run` attaches no backchannel,
1236 /// so the `self.peer_handle` fallback below is what runs there.
1237 ///
1238 /// # Ordering
1239 ///
1240 /// Every dispatch site calls this AFTER its `tool_authorizer` check, so an
1241 /// unauthorized caller returns before a handler body ever runs and therefore
1242 /// never sees `extra.peer()` — the invariant stated at `src/shared/peer.rs`.
1243 ///
1244 /// Delegates to [`crate::server::core::attach_request_peer`], the ONE unit
1245 /// `ServerCore::attach_peer` also calls — the precedence rule is defined
1246 /// once and merely invoked here (twin-site parity).
1247 ///
1248 /// # It attaches the LOG SINK too (Phase 118.2, CONF-10)
1249 ///
1250 /// The name is kept for its call-site history, but this is now the single
1251 /// post-authorization site where ALL of a request's server-to-client
1252 /// capability handles are attached: the peer, the log sink, and the resolved
1253 /// log level. They share one site DELIBERATELY — a second method a future
1254 /// dispatch site had to remember to call is exactly the drift the shared
1255 /// units exist to prevent. The `ServerCore` twin does the same, in the same
1256 /// order, through the same two units.
1257 #[inline]
1258 fn attach_peer(
1259 &self,
1260 extra: crate::server::cancellation::RequestHandlerExtra,
1261 ) -> crate::server::cancellation::RequestHandlerExtra {
1262 #[cfg(not(target_arch = "wasm32"))]
1263 {
1264 let extra = crate::server::core::attach_request_peer(extra, self.peer_handle.as_ref());
1265 // The fallback is passed as a THUNK: `attach_request_log_sink` prefers
1266 // this request's `TransportBackchannel` sink and never reads it on any
1267 // HTTP-served request, so building it eagerly allocated one
1268 // `Arc<dyn Fn(..)>` per dispatch only to drop it.
1269 crate::server::core::attach_request_log_sink(extra, || self.notification_tx_sink())
1270 }
1271 #[cfg(target_arch = "wasm32")]
1272 {
1273 extra
1274 }
1275 }
1276
1277 /// The server-wide notification channel expressed as a sink, or `None` when
1278 /// this server has no channel.
1279 ///
1280 /// The ONE place in the crate that turns `self.notification_tx` into an
1281 /// `Arc<dyn Fn(Notification) + Send + Sync>`. Two consumers read it — the
1282 /// progress-reporter path via [`Server::progress_notification_sink`] and the
1283 /// log-sink path via [`Server::attach_peer`] — and a second `try_send`
1284 /// closure would be a second chance to disagree about the send discipline.
1285 ///
1286 /// Both consumers call it only once they have decided they need it: the
1287 /// `Arc` allocation happens on the branch that uses the value, never
1288 /// speculatively ahead of the request-scoped sink that outranks it.
1289 ///
1290 /// `try_send` and a discarded result, deliberately: this is a bounded
1291 /// channel, and a full channel must never block or fail a handler. The cost
1292 /// is that a saturated channel drops records silently, which is why
1293 /// `RequestHandlerExtra::log`'s `Ok(())` is documented as NOT being delivery
1294 /// acknowledgement.
1295 ///
1296 /// It is `None` on every HTTP-served server, because `StreamableHttpServer`
1297 /// never calls [`Server::run`] — which is exactly why the request-scoped
1298 /// `TransportBackchannel` sink has to win over it.
1299 #[inline]
1300 fn notification_tx_sink(&self) -> Option<Arc<dyn Fn(Notification) + Send + Sync>> {
1301 let tx = self.notification_tx.as_ref()?.clone();
1302 Some(Arc::new(move |notification| {
1303 let _ = tx.try_send(notification);
1304 }))
1305 }
1306
1307 /// The one-way notification sink this request's progress reporter emits
1308 /// through, or `None` when the request has no vehicle at all.
1309 ///
1310 /// # Precedence mirrors [`Server::attach_peer`], for the same reason
1311 ///
1312 /// 1. the `TransportBackchannel`'s `notification_sink` on THIS request's
1313 /// `ProtocolContext` — session-bound, supplied by the `StreamableHTTP`
1314 /// transport at the one site that knows which session the request arrived
1315 /// on;
1316 /// 2. `self.notification_tx`, the server-wide channel assigned by
1317 /// [`Server::run`] and by nothing else.
1318 ///
1319 /// The second is `None` on every HTTP-served server, because
1320 /// `StreamableHttpServer` never calls `Server::run()`. That is precisely why
1321 /// `extra.report_progress(..)` was silently inert over HTTP before phase
1322 /// 118.1: `RequestHandlerExtra::report_progress` returns `Ok(())` when the
1323 /// reporter is `None`, so the gap produced no error anywhere.
1324 ///
1325 /// The sink is handed through with NO adapter — the transport chose its type
1326 /// to be `ServerProgressReporter::new`'s second parameter verbatim.
1327 #[inline]
1328 fn progress_notification_sink(
1329 &self,
1330 #[cfg_attr(target_arch = "wasm32", allow(unused_variables))] protocol_context: Option<
1331 &crate::types::protocol::ProtocolContext,
1332 >,
1333 ) -> Option<Arc<dyn Fn(Notification) + Send + Sync>> {
1334 #[cfg(not(target_arch = "wasm32"))]
1335 if let Some(sink) = protocol_context
1336 .and_then(crate::types::protocol::ProtocolContext::transport_backchannel)
1337 .and_then(crate::types::protocol::context::TransportBackchannel::notification_sink)
1338 {
1339 return Some(Arc::clone(sink));
1340 }
1341 // The single `notification_tx`-to-sink conversion lives in
1342 // `notification_tx_sink`; the log-sink path reads the same one.
1343 self.notification_tx_sink()
1344 }
1345
1346 /// Build this request's progress reporter, or `None` when the client asked
1347 /// for no progress or the request carries no notification vehicle.
1348 ///
1349 /// The `progress_token` lookup is unchanged: a request with no
1350 /// `params._meta.progressToken` still gets no reporter, so a handler that
1351 /// calls `extra.report_progress(..)` anyway stays silent. Only the SENDER
1352 /// resolution moved — see [`Server::progress_notification_sink`].
1353 ///
1354 /// ONE construction site for all three dispatchers (tools, prompts,
1355 /// resources): they had three byte-identical copies, and a fourth would have
1356 /// been the one that kept reading `self.notification_tx` alone.
1357 #[inline]
1358 fn progress_reporter_for(
1359 &self,
1360 meta: Option<&crate::types::protocol::RequestMeta>,
1361 protocol_context: Option<&crate::types::protocol::ProtocolContext>,
1362 ) -> Option<Arc<dyn crate::server::progress::ProgressReporter>> {
1363 // DELIBERATE, and NOT to be "unified" with the log path: Phase 118.2
1364 // D-07 removed the progress-token gate from the LOG sink only. Progress
1365 // stays opt-in — a client that sent no `progressToken` has nothing to
1366 // correlate progress notifications with — while `notifications/message`
1367 // is unconditional. See `core::attach_request_log_sink`.
1368 let token = meta.and_then(|meta| meta.progress_token.as_ref())?;
1369 let sink = self.progress_notification_sink(protocol_context)?;
1370 let reporter = crate::server::progress::ServerProgressReporter::new(token.clone(), sink);
1371 Some(Arc::new(reporter) as Arc<dyn crate::server::progress::ProgressReporter>)
1372 }
1373
1374 /// Spawn the outgoing-notification forwarder.
1375 ///
1376 /// Forwards each queued [`Notification`] onto the transport actor's
1377 /// unbounded `send_tx` as a [`TransportMessage::Notification`]. It no longer
1378 /// touches the transport directly — all outbound framing is serialized by
1379 /// the single actor task, so notifications can never starve on a transport
1380 /// write-lock held across an in-flight `receive()`.
1381 fn spawn_notification_handler(
1382 send_tx: mpsc::UnboundedSender<TransportMessage>,
1383 mut notification_rx: mpsc::Receiver<Notification>,
1384 ) {
1385 tokio::spawn(async move {
1386 while let Some(notification) = notification_rx.recv().await {
1387 if send_tx
1388 .send(TransportMessage::Notification(notification))
1389 .is_err()
1390 {
1391 // Actor gone — nothing left to forward to.
1392 break;
1393 }
1394 }
1395 });
1396 }
1397
1398 /// Spawn the single sequential request worker.
1399 ///
1400 /// Consumes inbound requests one at a time and forwards each response back
1401 /// through the actor's `send_tx`. Exactly ONE worker runs per server, so
1402 /// request handling stays serialized exactly as before this refactor. The
1403 /// actor merely decouples receiving from handling: a handler that awaits a
1404 /// peer round-trip no longer blocks the receive path, so the correlated
1405 /// client response can be read and routed while the handler is parked.
1406 fn spawn_request_worker(
1407 server: Arc<Self>,
1408 mut request_rx: mpsc::UnboundedReceiver<(RequestId, Request)>,
1409 send_tx: mpsc::UnboundedSender<TransportMessage>,
1410 ) {
1411 tokio::spawn(async move {
1412 while let Some((id, request)) = request_rx.recv().await {
1413 let response = server.handle_request(id, request, None).await;
1414 if send_tx.send(TransportMessage::Response(response)).is_err() {
1415 // Actor gone — stop draining.
1416 break;
1417 }
1418 }
1419 });
1420 }
1421
1422 /// Own the transport and interleave receive + send from a single task.
1423 ///
1424 /// The actor `select!`s over (a) the outbound `send_rx` and (b)
1425 /// `transport.receive()`. Inbound frames are routed by kind: a `Response`
1426 /// resolves the awaiting dispatcher IMMEDIATELY (unblocking an in-tool
1427 /// `peer.sample()`), a `Request` is queued to the sequential worker, and a
1428 /// `Notification` is handled inline. Routing NEVER touches the transport.
1429 ///
1430 /// When the send branch wins, the in-flight `receive()` future is DROPPED,
1431 /// so [`crate::shared::Transport::receive`] MUST be cancel-safe (see the
1432 /// trait's `# Cancellation` contract) — the stock `StdioTransport` persists
1433 /// its partial line across calls for exactly this reason. The real
1434 /// `transport.send(..)` runs AFTER the `select!` block so the receive
1435 /// future's mutable borrow is released first; this also keeps the borrow
1436 /// checker satisfied for the single `&mut self` transport object.
1437 async fn run_transport_actor<T: crate::shared::Transport + 'static>(
1438 server: Arc<Self>,
1439 mut transport: T,
1440 mut send_rx: mpsc::UnboundedReceiver<TransportMessage>,
1441 request_tx: mpsc::UnboundedSender<(RequestId, Request)>,
1442 ) {
1443 loop {
1444 let mut outbound: Option<TransportMessage> = None;
1445 tokio::select! {
1446 biased;
1447 maybe_frame = send_rx.recv() => {
1448 match maybe_frame {
1449 Some(frame) => outbound = Some(frame),
1450 // All senders dropped — shut the actor down.
1451 None => break,
1452 }
1453 },
1454 received = transport.receive() => {
1455 match received {
1456 Ok(message) => {
1457 if Self::route_inbound_message(&server, &request_tx, message)
1458 .await
1459 .is_break()
1460 {
1461 break;
1462 }
1463 },
1464 Err(e) => {
1465 Self::log_error(&format!("Transport receive error: {}", e)).await;
1466 break;
1467 },
1468 }
1469 },
1470 }
1471 if let Some(frame) = outbound {
1472 if let Err(e) = transport.send(frame).await {
1473 Self::log_error(&format!("Transport send error: {}", e)).await;
1474 break;
1475 }
1476 }
1477 }
1478 }
1479
1480 /// Route one inbound transport frame. Never touches the transport, so it is
1481 /// safe to call from inside the actor's `select!` receive arm. Returns
1482 /// [`std::ops::ControlFlow::Break`] only when the request worker is gone.
1483 async fn route_inbound_message(
1484 server: &Arc<Self>,
1485 request_tx: &mpsc::UnboundedSender<(RequestId, Request)>,
1486 message: TransportMessage,
1487 ) -> std::ops::ControlFlow<()> {
1488 match message {
1489 TransportMessage::Request { id, request } => {
1490 // Unbounded queue: the receive branch MUST NOT await capacity
1491 // (the anti-deadlock invariant). A send error means the worker
1492 // is gone, so the actor should stop.
1493 if request_tx.send((id, request)).is_err() {
1494 return std::ops::ControlFlow::Break(());
1495 }
1496 },
1497 TransportMessage::Response(response) => {
1498 Self::route_response(server, response).await;
1499 },
1500 TransportMessage::Notification(notification) => {
1501 Self::route_notification(server, notification).await;
1502 },
1503 }
1504 std::ops::ControlFlow::Continue(())
1505 }
1506
1507 /// Route a correlated client response through the dispatcher so a pending
1508 /// in-tool peer dispatch resolves. Behavior is lifted verbatim from the
1509 /// former `handle_transport_message` response arm — only the caller changed.
1510 async fn route_response(server: &Arc<Self>, response: JSONRPCResponse) {
1511 let Some(dispatcher) = &server.server_request_dispatcher else {
1512 Self::log_warning("Server received response but no dispatcher configured").await;
1513 return;
1514 };
1515 let correlation_id = response.id.to_string();
1516 let payload = match &response.payload {
1517 crate::types::jsonrpc::ResponsePayload::Result(value) => value.clone(),
1518 crate::types::jsonrpc::ResponsePayload::Error(err) => {
1519 // Represent errors as a JSON object so callers can distinguish —
1520 // dispatch() returns the Value as-is.
1521 serde_json::to_value(err).unwrap_or(Value::Null)
1522 },
1523 };
1524 if let Err(e) = dispatcher.handle_response(&correlation_id, payload).await {
1525 Self::log_warning(&format!(
1526 "Failed to route response {}: {}",
1527 correlation_id, e
1528 ))
1529 .await;
1530 }
1531 }
1532
1533 /// Handle an inbound notification (client cancellation). A failed
1534 /// cancellation lookup is logged rather than tearing down the actor loop.
1535 async fn route_notification(server: &Arc<Self>, notification: Notification) {
1536 if let Notification::Client(crate::types::ClientNotification::Cancelled(params)) =
1537 ¬ification
1538 {
1539 let request_id = params.request_id.to_string();
1540 if let Err(e) = server
1541 .cancellation_manager
1542 .cancel_request_silent(request_id)
1543 .await
1544 {
1545 Self::log_warning(&format!("Failed to process cancellation: {}", e)).await;
1546 }
1547 }
1548 Self::log_debug("Server received notification").await;
1549 }
1550
1551 /// Log an error message.
1552 async fn log_error(message: &str) {
1553 crate::log(crate::types::LogLevel::Error, message, None).await;
1554 }
1555
1556 /// Log a warning message.
1557 async fn log_warning(message: &str) {
1558 crate::log(crate::types::LogLevel::Warning, message, None).await;
1559 }
1560
1561 /// Log a debug message.
1562 async fn log_debug(message: &str) {
1563 crate::log(crate::types::LogLevel::Debug, message, None).await;
1564 }
1565
1566 /// Await the transport actor task; `run()` returns once it ends (transport
1567 /// closed or all outbound senders dropped). This is the shutdown join point.
1568 async fn run_main_loop(actor: tokio::task::JoinHandle<()>) -> Result<()> {
1569 if let Err(e) = actor.await {
1570 Self::log_error(&format!("Transport actor task ended abnormally: {}", e)).await;
1571 }
1572 Ok(())
1573 }
1574
1575 /// Resolve the per-request [`ProtocolContext`](crate::types::protocol::ProtocolContext)
1576 /// ONCE at this dispatch site's ingress via the SAME shared resolver
1577 /// `ServerCore` uses — the twin wiring (Pitfall 3). `Ok(None)` for a
1578 /// non-opted-in server (zero era-detection, D-04).
1579 ///
1580 /// `pub(crate)` so the streamable-HTTP layer (Plan 06) can resolve ONCE for
1581 /// its header gate and thread the SAME value into
1582 /// [`handle_request_with_context`](Self::handle_request_with_context) — the
1583 /// HTTP layer CONSUMES the resolved era, it never runs a second resolver
1584 /// (D-11 / Pitfall 2).
1585 /// The server's configured protocol-version accept-list.
1586 ///
1587 /// `pub(crate)` so the streamable-HTTP layer can put it in an
1588 /// `UNSUPPORTED_PROTOCOL_VERSION` (-32022) rejection's
1589 /// `error.data.supported` — the spec requires the rejection to tell the
1590 /// client which versions it COULD have asked for, so it can pick a mutually
1591 /// supported one instead of probing.
1592 pub(crate) fn supported_protocol_versions(&self) -> &[ProtocolVersion] {
1593 &self.supported_protocol_versions
1594 }
1595
1596 pub(crate) fn resolve_ingress_protocol_context(
1597 &self,
1598 request: &Request,
1599 ) -> std::result::Result<
1600 Option<crate::types::protocol::ProtocolContext>,
1601 crate::types::protocol::context::ProtocolNegotiationError,
1602 > {
1603 crate::server::core::resolve_ingress_protocol_context(
1604 &self.supported_protocol_versions,
1605 request,
1606 )
1607 }
1608
1609 /// Resolve the per-request `ProtocolContext` from a request's RAW
1610 /// `params._meta` value.
1611 ///
1612 /// # This is the era resolver the streamable-HTTP transport uses, for EVERY method
1613 ///
1614 /// Introduced in Phase 112 for the `server/discover` ingress (which has no
1615 /// parsed [`Request`] to read a typed field from), and generalized in Phase
1616 /// 113 plan 04 to every method (finding D-113-B).
1617 ///
1618 /// The typed
1619 /// [`resolve_ingress_protocol_context`](Self::resolve_ingress_protocol_context)
1620 /// can only see the three request structs that carry a `_meta` FIELD, so a
1621 /// stateless v2 `tools/list` — which has no handshake and therefore no other
1622 /// era channel — could not be expressed at all. Widening those `pub` structs
1623 /// would have been a MAJOR semver break (`cargo semver-checks`
1624 /// `constructible_struct_adds_field`), and the v2.5 milestone is scoped
1625 /// additive; reading the raw body needs no public API change and covers every
1626 /// method, so the HTTP transport routes ALL era detection through here.
1627 ///
1628 /// Mirrors the same non-opted-in short-circuit (D-04): a server that has NOT
1629 /// opted into v2 returns `Ok(None)` WITHOUT inspecting `_meta` at all, so the
1630 /// v1 request path runs zero era detection.
1631 pub(crate) fn resolve_raw_meta_protocol_context(
1632 &self,
1633 raw_meta: Option<&serde_json::Value>,
1634 ) -> std::result::Result<
1635 Option<crate::types::protocol::ProtocolContext>,
1636 crate::types::protocol::context::ProtocolNegotiationError,
1637 > {
1638 if !crate::types::protocol::context::is_v2_opted_in(&self.supported_protocol_versions) {
1639 return Ok(None);
1640 }
1641 crate::types::protocol::context::resolve_protocol_context(
1642 &self.supported_protocol_versions,
1643 raw_meta,
1644 )
1645 }
1646
1647 /// Handle the v2 `server/discover` request (Phase 112, VERS-04, D-09/D-10).
1648 ///
1649 /// The production discover caller: the streamable-HTTP transport classifies a
1650 /// `server/discover` POST as `HttpIngress::Discover` and, at the per-path
1651 /// response-assembly step, calls this THIN delegate. It projects the server's
1652 /// already-computed capabilities (incl. the `extensions` map) read-only via
1653 /// the ONE shared [`build_discover_response`](crate::server::core::build_discover_response)
1654 /// free fn — one projection/one envelope path, no duplicate capability type,
1655 /// no `is_initialized` mutation. The era gate inside the free fn yields the v2
1656 /// projection for an `Era::V2` context and `-32601` for v1 / non-opted-in.
1657 pub(crate) fn handle_discover(
1658 &self,
1659 id: RequestId,
1660 protocol_context: Option<&crate::types::protocol::ProtocolContext>,
1661 ) -> JSONRPCResponse {
1662 crate::server::core::build_discover_response(
1663 id,
1664 // The SINGLE accept-list source (G-7): the same slice
1665 // `negotiation_error_to_gate_reject` puts in an
1666 // `UNSUPPORTED_PROTOCOL_VERSION` rejection's `error.data.supported`
1667 // becomes the result's `supportedVersions`. There is no second list
1668 // to drift from.
1669 crate::server::core::DiscoverSource::new(
1670 &self.capabilities,
1671 self.supported_protocol_versions(),
1672 ),
1673 &self.info,
1674 protocol_context,
1675 )
1676 }
1677
1678 /// Handle the v2 `tasks/update` request (Phase 114 plan 13, TASK-02).
1679 ///
1680 /// The production `tasks/update` caller, and a THIN delegate exactly like
1681 /// [`handle_discover`](Self::handle_discover) beside it: the streamable-HTTP
1682 /// transport classifies a `tasks/update` POST as `HttpIngress::TasksUpdate`
1683 /// and, at the per-path response-assembly step, calls this. It constructs the
1684 /// SHARED [`TaskDispatch`](crate::server::task_dispatch::TaskDispatch) over
1685 /// this server's own backends — the same borrow-struct
1686 /// [`handle_client_request`](Self::handle_client_request) builds for the four
1687 /// `ClientRequest` tasks methods — and hands off.
1688 ///
1689 /// **It defines no gate of its own.** The era gate, the backend gate, the
1690 /// `-32021` client-declaration gate, the `-32003` identity table, the `-32602`
1691 /// params check, the four `inputResponses` bounds and the kind-directed decode
1692 /// all live in
1693 /// [`TaskDispatch::route_tasks_update`](crate::server::task_dispatch::TaskDispatch::route_tasks_update),
1694 /// in that order. This function's entire job is to pass the ALREADY-RESOLVED
1695 /// `auth_context` and `ProtocolContext` through unchanged, which is also what
1696 /// keeps it from ever re-reading `params._meta` for a second answer.
1697 ///
1698 /// `params` are the RAW value the classifier carried. Nothing between the wire
1699 /// and the router deserializes them, so a malformed body becomes a structured
1700 /// `-32602` AFTER the gates rather than a parse error before them — and the
1701 /// `inputResponses` map reaches the route UNDECODED, which is what lets the
1702 /// route bound it and then type it against the kinds the SERVER recorded
1703 /// rather than against whichever overlapping shape happened to fit (D-113-O).
1704 ///
1705 /// `async` since plan 114-14: the delivery reads the task record and writes
1706 /// the responses.
1707 ///
1708 /// # The v2 result envelope is injected HERE, for the same reason `server/discover`'s is
1709 ///
1710 /// `tasks/update` rides the crate-private internal-request route, so it does
1711 /// NOT pass through `process_client_request`, which is where every
1712 /// `ClientRequest` result gets its `resultType` + `_meta.serverInfo`. Left
1713 /// alone, the `UpdateTaskResult` acknowledgement would reach the wire as a
1714 /// bare `{}` — and the extension says its `resultType` field MUST be
1715 /// `"complete"`. `build_discover_response` solved the identical problem the
1716 /// identical way (Phase 112), so the internal route has ONE shape rather than
1717 /// two.
1718 ///
1719 /// [`ReservedFieldOwner::None`](crate::server::core::ReservedFieldOwner) is
1720 /// named explicitly and is correct: the acknowledgement is EMPTY, so this
1721 /// route mints no reserved result field at all — no `inputRequests` (that is
1722 /// `tasks/get`'s, plan 114-11) and no `requestState` (the tasks surface has no
1723 /// continuation token, D-17). A future change that made this ack non-empty
1724 /// would have to state its own owner here rather than inherit one.
1725 ///
1726 /// The call is a no-op on v1 and for every ERROR payload, so all seven of the
1727 /// route's refusals are byte-unchanged by it.
1728 #[cfg(not(target_arch = "wasm32"))]
1729 pub(crate) async fn handle_tasks_update(
1730 &self,
1731 id: RequestId,
1732 params: &serde_json::Value,
1733 auth_context: Option<&auth::AuthContext>,
1734 protocol_context: Option<&crate::types::protocol::ProtocolContext>,
1735 ) -> JSONRPCResponse {
1736 let mut response = self
1737 .task_dispatch()
1738 .route_tasks_update(id, params, auth_context, protocol_context)
1739 .await;
1740 crate::server::core::inject_v2_result_envelope(
1741 &mut response,
1742 protocol_context,
1743 &self.info,
1744 crate::server::core::ResponseDisposition::Complete,
1745 crate::server::core::ReservedFieldOwner::None,
1746 // The `tasks/update` acknowledgement is an `UpdateTaskResult`, which
1747 // does NOT extend `CacheableResult` — the tasks surface carries no
1748 // caching hint at all in the 2026-07-28 schema (D-07), and the
1749 // `ttlMs` that DOES live on `TaskV2` is a task LIFETIME, a different
1750 // concept in a different module (D-10). So this route gains neither
1751 // key, on either era.
1752 crate::types::caching::Cacheable::No,
1753 );
1754 response
1755 }
1756
1757 async fn handle_request(
1758 &self,
1759 id: RequestId,
1760 request: Request,
1761 auth_context: Option<auth::AuthContext>,
1762 ) -> JSONRPCResponse {
1763 // Resolve the per-request ProtocolContext ONCE at ingress (opted-in
1764 // only — D-04), through the single shared resolver, and thread it into
1765 // dispatch. Never re-derived downstream (D-11).
1766 let protocol_context = match self.resolve_ingress_protocol_context(&request) {
1767 Ok(ctx) => ctx,
1768 Err(negotiation_error) => {
1769 let (code, message) =
1770 crate::server::core::negotiation_error_to_rejection(&negotiation_error);
1771 return JSONRPCResponse {
1772 jsonrpc: "2.0".to_string(),
1773 id,
1774 payload: crate::types::jsonrpc::ResponsePayload::Error(
1775 crate::types::jsonrpc::JSONRPCError {
1776 code,
1777 message,
1778 data: None,
1779 },
1780 ),
1781 };
1782 },
1783 };
1784 self.handle_request_with_context(id, request, auth_context, protocol_context)
1785 .await
1786 }
1787
1788 /// Dispatch a request with an ALREADY-RESOLVED `ProtocolContext` threaded in.
1789 ///
1790 /// This is the pass-through seam Plan 06 relies on: the streamable-HTTP layer
1791 /// resolves the `ProtocolContext` ONCE (via
1792 /// [`resolve_ingress_protocol_context`](Self::resolve_ingress_protocol_context))
1793 /// for its header gate, then passes that SAME value here so dispatch does NOT
1794 /// re-resolve `_meta` — one authoritative era per request (D-11 / Pitfall 2).
1795 /// [`handle_request`](Self::handle_request) is the thin wrapper that resolves
1796 /// then calls this.
1797 pub(crate) async fn handle_request_with_context(
1798 &self,
1799 id: RequestId,
1800 request: Request,
1801 auth_context: Option<auth::AuthContext>,
1802 protocol_context: Option<crate::types::protocol::ProtocolContext>,
1803 ) -> JSONRPCResponse {
1804 // MRTR ingress (Plan 113-06, HTTP-03) — the SAME shared helper
1805 // `ServerCore` calls (twin-site parity; this site never defines its
1806 // own). Verifies a presented `requestState` against the live principal
1807 // and originating request, then folds the D-15 verdict into the context
1808 // threaded into dispatch. Inert on v1 / non-opted-in / non-eligible
1809 // requests, so the legacy path is byte-for-byte unchanged.
1810 #[cfg(feature = "streamable-http")]
1811 let (mrtr, protocol_context) = match crate::server::core::MrtrRound::begin(
1812 &request,
1813 protocol_context,
1814 auth_context.as_ref().map(|ctx| ctx.subject.as_str()),
1815 self.auth_provider.is_some(),
1816 self.request_state_codec(),
1817 ) {
1818 Ok(resolved) => resolved,
1819 // The single-source envelope builder, rather than a hand-written
1820 // `JSONRPCResponse` literal re-spelling `"2.0"` and `data: None`.
1821 Err((code, message)) => {
1822 return crate::server::task_dispatch::error_response(id, code, message)
1823 },
1824 };
1825
1826 // Capture the cacheability claim BEFORE the `match` below: arm 1 binds
1827 // `ref boxed_req` but arm 2 MOVES `boxed_req`, so `request` is gone by
1828 // the time the injection at the bottom of this function runs. Twin of
1829 // the `ServerCore` capture — and it CALLS the shared classifier in
1830 // `core.rs` rather than defining a second table, which is the twin-site
1831 // parity rule this file follows everywhere else.
1832 let cacheable = crate::server::core::request_is_cacheable(&request);
1833
1834 // G-9 / CONF-08 (Phase 118.1-08): fold the v1 `initialize` handshake's
1835 // advertised capabilities into the context threaded into DISPATCH — the
1836 // SAME shared unit `ServerCore` calls, never a second copy (twin-site
1837 // parity). The fold owns the lock, so the guard that keeps v2 traffic
1838 // off it (T-118.1-08-02) lives there too rather than being re-spelled
1839 // here; the read guard drops inside, before the `Initialize` arm below
1840 // takes the WRITE lock a few lines down. The EGRESS keeps the UNFOLDED
1841 // `protocol_context`: the fold is a handler-visibility concern, not a
1842 // wire-shape one.
1843 let dispatch_context = crate::server::core::fold_v1_handshake_capabilities(
1844 protocol_context.clone(),
1845 &self.client_capabilities,
1846 &self.supported_protocol_versions,
1847 )
1848 .await;
1849
1850 // The SECOND envelope claimant (Phase 114 plan 11), twin of the
1851 // `ServerCore` site: the `tasks/*` routes and the `tools/call` create
1852 // path state their own `resultType` and reserved-field ownership from the
1853 // site that writes them. `NONE` for every other dispatch.
1854 let mut dispatch_claim = crate::server::core::DispatchEnvelopeClaim::NONE;
1855 let mut response = match request {
1856 Request::Client(ref boxed_req)
1857 if matches!(**boxed_req, ClientRequest::Initialize(_)) =>
1858 {
1859 let ClientRequest::Initialize(init_req) = boxed_req.as_ref() else {
1860 unreachable!("Pattern matched for Initialize");
1861 };
1862 // Store client capabilities
1863 *self.client_capabilities.write().await = Some(init_req.capabilities.clone());
1864 *self.initialized.write().await = true;
1865
1866 let negotiated_version =
1867 crate::negotiate_protocol_version(&init_req.protocol_version);
1868
1869 let result = InitializeResult {
1870 protocol_version: ProtocolVersion(negotiated_version.to_string()),
1871 // Twin-site parity (114-05, D-02): the SAME shared v1
1872 // projection `ServerCore::handle_initialize` uses — this
1873 // site never defines its own. Without it the build-time
1874 // tasks-extension entry, which is the v2 negotiation home,
1875 // leaks onto the v1 `initialize` wire of every tasks server.
1876 capabilities: crate::server::core::project_capabilities_for_v1(
1877 &self.capabilities,
1878 ),
1879 server_info: self.info.clone(),
1880 instructions: None,
1881 };
1882 JSONRPCResponse {
1883 jsonrpc: "2.0".to_string(),
1884 id: id.clone(),
1885 payload: crate::types::jsonrpc::ResponsePayload::Result(
1886 serde_json::to_value(result).unwrap(),
1887 ),
1888 }
1889 },
1890 // `Box::pin`: the MRTR ingress/egress locals (Plan 113-06) push this
1891 // dispatch future past clippy's `large_futures` threshold. Boxing the
1892 // inner future keeps every CALLER of this method small without
1893 // changing behavior — the same treatment the two POST entrypoints and
1894 // the discover assembly already get.
1895 Request::Client(boxed_req) => {
1896 Box::pin(self.handle_client_request(
1897 id,
1898 *boxed_req,
1899 auth_context,
1900 dispatch_context,
1901 &mut dispatch_claim,
1902 ))
1903 .await
1904 },
1905 Request::Server(_) => JSONRPCResponse {
1906 jsonrpc: "2.0".to_string(),
1907 id,
1908 payload: crate::types::jsonrpc::ResponsePayload::Error(
1909 crate::types::jsonrpc::JSONRPCError {
1910 code: crate::types::protocol::error_codes::METHOD_NOT_FOUND,
1911 message: "Server requests not supported by server".to_string(),
1912 data: None,
1913 },
1914 ),
1915 },
1916 };
1917
1918 // Twin-site MRTR egress (Plan 113-06): the SAME shared helper `ServerCore`
1919 // calls. Converts a handler's "I need more input" signal into an
1920 // `input_required` result carrying a freshly minted `requestState`, and
1921 // STRIPS the pmcp-internal signal key on every other path.
1922 #[cfg(feature = "streamable-http")]
1923 let (disposition, reserved_field_owner) = mrtr.finish(
1924 &mut response,
1925 protocol_context.as_ref(),
1926 self.request_state_codec(),
1927 );
1928 #[cfg(not(feature = "streamable-http"))]
1929 let (disposition, reserved_field_owner) = {
1930 // No `mrtr_egress` on this build — strip the reserved signal key
1931 // here so it cannot reach the wire (see `core::scrub_mrtr_signal`).
1932 crate::server::core::scrub_mrtr_signal(&mut response);
1933 (
1934 crate::server::core::ResponseDisposition::Complete,
1935 crate::server::core::ReservedFieldOwner::None,
1936 )
1937 };
1938
1939 // Twin-site v2 envelope injection (VERS-07 / D-07 / D-08) plus the
1940 // caching-hint projection (SCHM-03): the ONE shared helper in `core.rs`
1941 // — the envelope half is v2-only, object-results-only, collision-safe,
1942 // so v1 / non-opted-in responses stay byte-identical; the caching half
1943 // runs on both eras, ensuring on v2 and STRIPPING on v1 (D-11). The
1944 // reserved-field owner comes from the egress that minted the fields,
1945 // never from the disposition (Phase 114 plan 10) — folded with the
1946 // dispatch's own claim through the SAME named rule `ServerCore` uses
1947 // (Phase 114 plan 11).
1948 let claim = dispatch_claim.or_egress(disposition, reserved_field_owner);
1949 crate::server::core::inject_v2_result_envelope(
1950 &mut response,
1951 protocol_context.as_ref(),
1952 &self.info,
1953 claim.disposition,
1954 claim.owner,
1955 cacheable,
1956 );
1957 response
1958 }
1959
1960 async fn handle_client_request(
1961 &self,
1962 id: RequestId,
1963 request: ClientRequest,
1964 auth_context: Option<auth::AuthContext>,
1965 protocol_context: Option<crate::types::protocol::ProtocolContext>,
1966 dispatch_claim: &mut crate::server::core::DispatchEnvelopeClaim,
1967 ) -> JSONRPCResponse {
1968 // ADAPTER (a) — tasks/* dispatch at the post-auth assembly layer.
1969 //
1970 // The four `tasks/*` variants are served by the SHARED `task_dispatch`
1971 // unit (the SAME `route_tasks_endpoint` `ServerCore` uses), returning a
1972 // full `JSONRPCResponse` DIRECTLY — no `JSONRPCResponse -> Result<Value>`
1973 // round-trip and no double-wrap, so the FROZEN `-32002` pending code
1974 // survives unchanged (T-102-07).
1975 //
1976 // SECURITY (T-102-04): this interception sits DOWNSTREAM of auth
1977 // resolution — `auth_context` is the already-resolved context the
1978 // transport layer passed into `handle_request`, threaded here unchanged.
1979 // The tasks/* path is therefore subject to the SAME auth as every other
1980 // request; owner-scoping inside `route_tasks_endpoint` derives the owner
1981 // from this `AuthContext` ONLY (never client params), enforcing
1982 // cross-owner isolation (T-102-05).
1983 #[cfg(not(target_arch = "wasm32"))]
1984 if matches!(
1985 request,
1986 ClientRequest::TasksGet(_)
1987 | ClientRequest::TasksResult(_)
1988 | ClientRequest::TasksList(_)
1989 | ClientRequest::TasksCancel(_)
1990 ) {
1991 let (response, claim) = self
1992 .task_dispatch()
1993 .route_tasks_endpoint(
1994 id,
1995 &request,
1996 auth_context.as_ref(),
1997 // The context resolved ONCE at transport ingress, CONSUMED
1998 // here. Its `era` is read by the `tasks/result` pending
1999 // refusal, so that a v2 request cannot elicit the
2000 // spec-prohibited `-32002` (Finding 11;
2001 // `task_dispatch::is_v1_task_era`), and by the two v2
2002 // retirement gates for `tasks/list` / `tasks/result`
2003 // (TASK-03; `task_dispatch::tasks_list_serves_on_era`); its
2004 // `client_capabilities` are read by the v2
2005 // extension-declaration gate (TASK-05). Passing the whole
2006 // context is what keeps this dispatcher from ever re-reading
2007 // `params._meta` for a second answer. Every gate lives in
2008 // `task_dispatch`, never here.
2009 protocol_context.as_ref(),
2010 )
2011 .await;
2012 // The claim travels WITH the response: a v2 `tasks/get` on an
2013 // `input_required` task owns the top-level `inputRequests` the
2014 // reserved-field registry would otherwise strip (114-10 row 23).
2015 *dispatch_claim = claim;
2016 return response;
2017 }
2018
2019 // ADAPTER (b) — `logging/setLevel`, era-branched (Phase 118.2-08, D-13).
2020 //
2021 // Intercepted HERE, above `process_client_request`, for the same
2022 // structural reason ADAPTER (a) above intercepts `tasks/*`: this
2023 // method's v2 answer is a JSON-RPC ERROR with a SPECIFIC code, and
2024 // `Self::create_response` flattens EVERY `Err` returned by
2025 // `process_client_request` to `-32603 INTERNAL_ERROR`. A `-32601`
2026 // therefore cannot travel through that function's
2027 // `Result<serde_json::Value>` return type at all — the same
2028 // `JSONRPCResponse -> Result<Value>` round-trip the `tasks/*` adapter
2029 // exists to avoid. Making `create_response` code-aware instead would
2030 // silently change the wire code of every other handler that returns a
2031 // `Error::Protocol`, which is not this plan's change to make.
2032 //
2033 // The ANSWER itself is not computed here: it comes from the single
2034 // shared unit in `server/core.rs`, the very same one `ServerCore`'s
2035 // dispatch arm calls. One era branch, two roots — that is D-13.
2036 if matches!(request, ClientRequest::SetLoggingLevel { .. }) {
2037 return crate::server::core::set_logging_level_response(
2038 id,
2039 protocol_context.as_ref().map(|ctx| ctx.era),
2040 );
2041 }
2042
2043 let result = self
2044 .process_client_request(
2045 id.clone(),
2046 request,
2047 auth_context,
2048 protocol_context,
2049 dispatch_claim,
2050 )
2051 .await;
2052 Self::create_response(id, result)
2053 }
2054
2055 /// Process a client request and return the result.
2056 ///
2057 /// `dispatch_claim` is the out-param the `tools/call` create path writes its
2058 /// v2 envelope claim into (Phase 114 plan 11); every other arm leaves it as
2059 /// the caller set it.
2060 async fn process_client_request(
2061 &self,
2062 request_id: RequestId,
2063 request: ClientRequest,
2064 auth_context: Option<auth::AuthContext>,
2065 protocol_context: Option<crate::types::protocol::ProtocolContext>,
2066 dispatch_claim: &mut crate::server::core::DispatchEnvelopeClaim,
2067 ) -> Result<serde_json::Value> {
2068 match request {
2069 ClientRequest::Initialize(_) => {
2070 // Already handled above
2071 unreachable!("Initialize should be handled separately")
2072 },
2073 ClientRequest::ListTools(req) => self.handle_list_tools(req),
2074 ClientRequest::CallTool(req) => {
2075 self.handle_call_tool(
2076 request_id,
2077 req,
2078 auth_context,
2079 protocol_context,
2080 dispatch_claim,
2081 )
2082 .await
2083 },
2084 ClientRequest::ListPrompts(req) => self.handle_list_prompts(req),
2085 ClientRequest::GetPrompt(req) => {
2086 self.handle_get_prompt(request_id, req, auth_context, protocol_context)
2087 .await
2088 },
2089 ClientRequest::ListResources(req) => {
2090 self.handle_list_resources(request_id, req, auth_context, protocol_context)
2091 .await
2092 },
2093 ClientRequest::ReadResource(req) => {
2094 self.handle_read_resource(request_id, req, auth_context, protocol_context)
2095 .await
2096 },
2097 ClientRequest::ListResourceTemplates(req) => {
2098 Self::handle_list_resource_templates(self, req)
2099 },
2100 // `completion/complete` (Phase 118.1-04, CONF-05 / G-4) — its OWN
2101 // arm, no longer inside the catch-all below. The shared unit is
2102 // DEFINED in `server/core.rs` and merely CALLED here, per the
2103 // twin-site parity rule stated at `src/server/core.rs`'s MRTR
2104 // section: `mod.rs` calls these helpers, it never defines its own.
2105 ClientRequest::Complete(req) => {
2106 crate::server::core::complete_completion(self.completions.as_ref(), &req)
2107 .await
2108 .and_then(|result| serde_json::to_value(result).map_err(Into::into))
2109 },
2110 // `logging/setLevel` (Phase 118.2-08, CONF-10 / D-13) — its OWN
2111 // arm, no longer inside the residual below, and answering from the
2112 // SAME shared unit in `server/core.rs` that `ServerCore`'s dispatch
2113 // arm calls.
2114 //
2115 // Reached only when a caller drives this function DIRECTLY. On the
2116 // production path `handle_client_request`'s ADAPTER (b) has already
2117 // answered — it must, because the v2 half of the era branch is a
2118 // `-32601` and `Self::create_response` flattens every `Err` from
2119 // this function to `-32603`. The v1 half is the whole of what this
2120 // arm can express, and it is spelled by calling the shared unit
2121 // rather than by re-typing `json!({})`, so a future change to the
2122 // measured shape (Pitfall 8) lands in exactly one place.
2123 ClientRequest::SetLoggingLevel { level: _ } => {
2124 Ok(crate::server::core::set_logging_level_v1_result())
2125 },
2126 // RESIDUAL, recorded rather than silently unified (Phase 118.1-04,
2127 // RESEARCH Open Question 4): these THREE methods STILL diverge
2128 // between the two native dispatchers. Here they answer
2129 // `json!({})`; `ServerCore`'s `_ =>` arm
2130 // (`src/server/core.rs`, the arm immediately after the
2131 // `ClientRequest::SetLoggingLevel` one) answers `-32601 Method not
2132 // supported`. Only this dispatcher is on the HTTP path, so only
2133 // this side is measured by the official conformance suite. G-5
2134 // (`resources/subscribe`, `resources/unsubscribe`,
2135 // `logging/setLevel`, `ping` retirement on v2) is the requirement
2136 // that owns them; unifying them here would smuggle a behaviour
2137 // change in behind a conformance fix.
2138 //
2139 // HISTORY — a residual is recorded, never silently unified, and
2140 // never silently SHRUNK either: `logging/setLevel` was the FOURTH
2141 // method on this arm and left it in Phase 118.2-08 under D-13,
2142 // because the official suite measures that method and the two roots
2143 // disagreed about it. `ping` in particular must stay here: it
2144 // already carries a recorded 118.1 v2 behaviour change (HTTP 404 /
2145 // `-32601` at the transport gate) and a second, differently-shaped
2146 // retirement at this layer would be a new divergence, not a fix.
2147 ClientRequest::Subscribe(_) | ClientRequest::Unsubscribe(_) | ClientRequest::Ping => {
2148 Ok(serde_json::json!({}))
2149 },
2150 ClientRequest::CreateMessage(req) => {
2151 self.handle_create_message(request_id, *req, protocol_context)
2152 .await
2153 },
2154 // Note: Elicitation responses are now handled as the response to
2155 // ServerRequest::ElicitationCreate in the JSON-RPC response flow,
2156 // not as a separate client request variant.
2157 // Task requests (experimental MCP Tasks). On non-wasm these are
2158 // intercepted UPSTREAM in `handle_client_request` (adapter (a)) and
2159 // served by the shared `task_dispatch` unit, so they never reach
2160 // here. This arm is the wasm32 fall-through (the task lifecycle is
2161 // non-wasm-gated): the endpoints are genuinely unsupported there.
2162 ClientRequest::TasksGet(_)
2163 | ClientRequest::TasksResult(_)
2164 | ClientRequest::TasksList(_)
2165 | ClientRequest::TasksCancel(_) => Err(crate::Error::protocol(
2166 crate::ErrorCode::METHOD_NOT_FOUND,
2167 "Tasks not supported on this build",
2168 )),
2169 }
2170 }
2171
2172 /// Borrow this server's task backends as a [`TaskDispatch`] for the shared
2173 /// task-lifecycle unit. Single construction point so the `tasks/*` and
2174 /// create-path call sites don't re-inline the struct literal.
2175 #[cfg(not(target_arch = "wasm32"))]
2176 fn task_dispatch(&self) -> crate::server::task_dispatch::TaskDispatch<'_> {
2177 crate::server::task_dispatch::TaskDispatch {
2178 task_store: &self.task_store,
2179 task_router: &self.task_router,
2180 // The EXISTING public accessor, not a new field and not a widened
2181 // one — the same read `listen_server_view` makes for
2182 // `subscriptions/listen` (D-113-N), now feeding the SAME identity
2183 // table for `tasks/*` (TASK-05).
2184 has_auth_provider: self.get_auth_provider().is_some(),
2185 }
2186 }
2187
2188 /// Create a JSON-RPC response from a result.
2189 fn create_response(id: RequestId, result: Result<serde_json::Value>) -> JSONRPCResponse {
2190 match result {
2191 Ok(value) => JSONRPCResponse {
2192 jsonrpc: "2.0".to_string(),
2193 id,
2194 payload: crate::types::jsonrpc::ResponsePayload::Result(value),
2195 },
2196 Err(e) => JSONRPCResponse {
2197 jsonrpc: "2.0".to_string(),
2198 id,
2199 payload: crate::types::jsonrpc::ResponsePayload::Error(
2200 crate::types::jsonrpc::JSONRPCError {
2201 code: crate::types::protocol::error_codes::INTERNAL_ERROR,
2202 message: e.to_string(),
2203 data: None,
2204 },
2205 ),
2206 },
2207 }
2208 }
2209
2210 fn handle_list_tools(&self, _req: ListToolsRequest) -> Result<Value> {
2211 let tools: Vec<ToolInfo> = self.tool_infos.values().cloned().collect();
2212
2213 Ok(serde_json::to_value(ListToolsResult {
2214 tools,
2215 next_cursor: None,
2216 ttl_ms: None,
2217 cache_scope: None,
2218 })?)
2219 }
2220
2221 async fn handle_call_tool(
2222 &self,
2223 request_id: RequestId,
2224 req: CallToolRequest,
2225 auth_context: Option<auth::AuthContext>,
2226 protocol_context: Option<crate::types::protocol::ProtocolContext>,
2227 dispatch_claim: &mut crate::server::core::DispatchEnvelopeClaim,
2228 ) -> Result<Value> {
2229 let handler = self
2230 .tools
2231 .get(&req.name)
2232 .ok_or_else(|| Error::not_found(format!("Tool '{}' not found", req.name)))?;
2233
2234 // Capture the create-path inputs BEFORE `req` is partially moved
2235 // (arguments are consumed by the middleware/handler below). The
2236 // create-path gate reads:
2237 // - the ERA's create trigger — `req.task` on v1, the client's
2238 // per-request tasks-extension declaration on v2 (plan 114-12), and
2239 // - the tool's declared `TaskSupport` (from the cached `tool_infos`).
2240 // The SHARED `maybe_build_task_created` enforces the FULL gate
2241 // internally — we pass these RAW facts, never a pre-filtered precondition.
2242 // `CreateTrigger::resolve` is the ONE place the era picks a trigger, so
2243 // this dispatcher cannot implement a trigger `ServerCore` misses.
2244 #[cfg(not(target_arch = "wasm32"))]
2245 let create_trigger = crate::server::task_dispatch::CreateTrigger::resolve(
2246 protocol_context.as_ref().map(|ctx| ctx.era),
2247 req.task.is_some(),
2248 protocol_context.as_ref(),
2249 );
2250 #[cfg(not(target_arch = "wasm32"))]
2251 let tool_task_support = self
2252 .tool_infos
2253 .get(&req.name)
2254 .and_then(|info| info.execution.as_ref())
2255 .and_then(|exec| exec.task_support);
2256 #[cfg(not(target_arch = "wasm32"))]
2257 let create_path_id = request_id.clone();
2258
2259 let request_id_str = request_id.to_string();
2260 let cancellation_token = self
2261 .cancellation_manager
2262 .create_token(request_id_str.clone())
2263 .await;
2264
2265 // Auth context now comes from the transport layer
2266 // Validate authentication if auth provider is configured
2267 let validated_auth_context = if let Some(auth_provider) = &self.auth_provider {
2268 // If auth_context was provided by transport, use it; otherwise validate
2269 if auth_context.is_some() {
2270 auth_context
2271 } else {
2272 // Fallback: try to validate without headers (for backward compatibility)
2273 auth_provider.validate_request(None).await?
2274 }
2275 } else {
2276 auth_context // No auth provider, just use what was provided
2277 };
2278
2279 // Check tool authorization if tool authorizer is configured
2280 if let (Some(auth_ctx), Some(authorizer)) = (&validated_auth_context, &self.tool_authorizer)
2281 {
2282 if !authorizer.can_access_tool(auth_ctx, &req.name).await? {
2283 return Err(Error::protocol(
2284 crate::error::ErrorCode::AUTHENTICATION_REQUIRED,
2285 format!("Access denied for tool '{}'", req.name),
2286 ));
2287 }
2288 }
2289
2290 // The request-scoped progress reporter — the channel
2291 // `extra.report_progress(..)` actually reads. Resolved BEFORE
2292 // `protocol_context` is moved into `extra` below, because the transport's
2293 // session-bound sink rides on it (Phase 118.1 plan 11).
2294 #[allow(clippy::used_underscore_binding)] // _meta is part of MCP protocol spec
2295 let progress_reporter =
2296 self.progress_reporter_for(req._meta.as_ref(), protocol_context.as_ref());
2297
2298 // Clone the validated auth context for the create-path owner resolution
2299 // (the original is moved into `extra` below). This guarantees the
2300 // create-path scopes the minted task to the SAME owner the tool ran as.
2301 #[cfg(not(target_arch = "wasm32"))]
2302 let create_path_auth = validated_auth_context.clone();
2303
2304 // Capture the ALREADY-RESOLVED era before `protocol_context` is moved into
2305 // `extra` below, so the create-path owner binding reads the SAME ingress
2306 // value the handler does and never re-parses `params._meta` (Phase 112).
2307 // The twin of the `ServerCore` capture on its own `CallTool` arm.
2308 #[cfg(not(target_arch = "wasm32"))]
2309 let create_path_era = protocol_context.as_ref().map(|ctx| ctx.era);
2310
2311 // Same capture-before-move reason: the emit-time outputSchema validator
2312 // is era-branched (Phase 115 D-01) and `protocol_context` is moved into
2313 // `extra` below. UN-cfg'd — unlike `create_path_era` — because the
2314 // validation call site compiles on wasm32 too. The twin of the
2315 // `ServerCore` capture on its own `CallTool` arm.
2316 let validation_era = protocol_context.as_ref().map(|ctx| ctx.era);
2317
2318 // Propagate the request's `_meta` object (raw JSON incl. namespaced
2319 // `other` keys) so handlers can read it via `extra.request_meta` in the
2320 // high-level `Server` path too (ServerCore already wires this at core.rs).
2321 #[allow(clippy::used_underscore_binding)] // _meta is part of MCP protocol spec
2322 let request_meta_value = crate::server::core::request_meta_to_value(req._meta.as_ref());
2323
2324 let mut extra = self.attach_peer(
2325 crate::server::cancellation::RequestHandlerExtra::new(
2326 request_id.to_string(),
2327 cancellation_token,
2328 )
2329 .with_auth_context(validated_auth_context)
2330 .with_progress_reporter(progress_reporter)
2331 // Surface whether the client requested task augmentation so handlers
2332 // can branch on `extra.is_task_request()` in the high-level `Server`
2333 // path too (ServerCore already wires this at core.rs). Additive: the
2334 // dispatcher's own task-creation decision still reads `req.task`.
2335 .with_task_request(req.task.clone())
2336 .with_request_meta(request_meta_value)
2337 // Thread the once-at-ingress resolved protocol context (Phase 112) —
2338 // the twin of the ServerCore wiring so handlers read the SAME
2339 // era/identity on both dispatch sites.
2340 .with_protocol_context(protocol_context),
2341 );
2342
2343 // D-03.3 (TOUT-01): clone the interior-mutable result-`_meta` slot BEFORE
2344 // `extra` is moved into `handle_output`, so any `extra.set_result_meta(..)`
2345 // the handler performs can be drained back onto the Payload-path result.
2346 #[cfg(not(target_arch = "wasm32"))]
2347 let result_meta_handle = extra.result_meta_handle();
2348
2349 // Execute tool with middleware (native-only)
2350 #[cfg(not(target_arch = "wasm32"))]
2351 let dispatch_output = {
2352 // Create tool context for middleware
2353 let context = tool_middleware::ToolContext::new(&req.name, &request_id_str);
2354
2355 // Clone arguments for middleware processing
2356 let mut args = req.arguments;
2357
2358 // Process request through tool middleware chain.
2359 // Middleware rejection short-circuits tool execution. REQUEST
2360 // middleware runs BEFORE the handler for EVERY tool, regardless of the
2361 // ToolOutput variant it will return (kept here so it fires on both the
2362 // Payload and the verbatim Result path).
2363 self.tool_middleware_chain
2364 .read()
2365 .await
2366 .process_request(&req.name, &mut args, &mut extra, &context)
2367 .await?;
2368
2369 // Execute the tool. `handle_output` returns `Result<ToolOutput>`; the
2370 // SHARED `resolve_tool_output` (D-05) is the SINGLE place that decides
2371 // Payload-vs-Result and encodes the response-middleware-bypass rule, so
2372 // this dispatcher and `ServerCore` can never drift on it.
2373 let output = handler.handle_output(args, extra).await;
2374 let mut resolved = task_dispatch::resolve_tool_output(output);
2375
2376 // Why: `ToolOutput::Result` deliberately BYPASSES RESPONSE middleware
2377 // (D-04 + D-04a — USER-APPROVED and LOCKED: "keep the bypass, harden
2378 // it"). The handler owns the full envelope, including its own
2379 // redaction/sanitization, at the same trust level as returning a raw
2380 // Value today. RESPONSE middleware (redaction/sanitization/audit) +
2381 // `handle_tool_error` therefore run ONLY for the Payload/error arm
2382 // below; REQUEST middleware already fired above for EVERY tool, and a
2383 // handler `Err(_)` still routes through `handle_tool_error` via this
2384 // arm (the bypass is scoped to the successful `Verbatim` arm only).
2385 if let task_dispatch::DispatchOutput::Middleware(ref mut result) = resolved {
2386 // Process response through tool middleware chain
2387 if let Err(e) = self
2388 .tool_middleware_chain
2389 .read()
2390 .await
2391 .process_response(&req.name, result, &context)
2392 .await
2393 {
2394 // Log error but continue with original result
2395 tracing::warn!("Tool response middleware processing failed: {}", e);
2396 }
2397
2398 // If tool execution failed, call handle_tool_error
2399 if let Err(ref e) = result {
2400 self.tool_middleware_chain
2401 .read()
2402 .await
2403 .handle_tool_error(&req.name, e, &context)
2404 .await;
2405 }
2406 }
2407
2408 resolved
2409 };
2410
2411 // On WASM, execute tool directly without middleware
2412 #[cfg(target_arch = "wasm32")]
2413 let result = handler.handle(req.arguments, extra).await;
2414
2415 // Token cleanup is unconditional (success or failure) and does not
2416 // touch the outcome, so it runs once before the value/error split —
2417 // preserving cleanup parity for the verbatim `ToolOutput::Result` arm.
2418 self.cancellation_manager
2419 .remove_token(&request_id_str)
2420 .await;
2421 let result = match dispatch_output {
2422 // VERBATIM (D-04 + D-04a): the handler owns the full `CallToolResult`
2423 // envelope — emit it to the wire as-is. RESPONSE middleware, the
2424 // create-path gate, text-wrap, and widget enrichment are ALL bypassed
2425 // (mirrors the `ToolRejected` verbatim early-return below, which also
2426 // returns after the unconditional token cleanup).
2427 //
2428 // D-06 (Phase 118.1) RECLASSIFIES exactly one clause of D-04a: the
2429 // bypass covers the response PIPELINE, not the handler's own
2430 // `extra.set_result_meta(..)`. Those keys come from the same handler
2431 // that authored this envelope, at the same trust level, so draining
2432 // them here merges a handler's two `_meta` sources rather than
2433 // reintroducing server-side rewriting. Handler-key-wins precedence,
2434 // never a whole-map replace. Twin of the `ServerCore` arm.
2435 task_dispatch::DispatchOutput::Verbatim(call_result) => {
2436 #[cfg(not(target_arch = "wasm32"))]
2437 let call_result = {
2438 let mut call_result = call_result;
2439 if let Some(handler_meta) = result_meta_handle.take_result_meta() {
2440 crate::server::cancellation::merge_result_meta(
2441 &mut call_result,
2442 handler_meta,
2443 );
2444 }
2445 call_result
2446 };
2447 return Ok(serde_json::to_value(call_result)?);
2448 },
2449 task_dispatch::DispatchOutput::Middleware(result) => match result {
2450 Ok(v) => v,
2451 // `Error::ToolRejected` is an APPLICATION-level rejection (e.g.
2452 // Code Mode policy: a SELECT missing its LIMIT), not a protocol
2453 // fault. Map it to a successful `CallToolResult { isError: true }`
2454 // (message → content, details → structuredContent) so the model
2455 // reads the reason and retries with corrected input, instead of
2456 // `?`-propagating a JSON-RPC error that reads as a server crash.
2457 // All other errors keep propagating as protocol errors.
2458 Err(Error::ToolRejected { message, details }) => {
2459 return Ok(serde_json::to_value(CallToolResult::rejected(
2460 message, details,
2461 ))?);
2462 },
2463 Err(e) => return Err(e),
2464 },
2465 };
2466
2467 // CREATE-PATH (Phase 102, HTASK-02; era-aware trigger from plan 114-12):
2468 // a `tools/call` whose era trigger fired over the high-level `Server`
2469 // mints a store task and returns a `CreateTaskResult` envelope. The
2470 // SHARED `maybe_build_task_created` gate is the SINGLE source of truth:
2471 // it returns `Some` ONLY when the era's trigger fired (v1: the `task`
2472 // field; v2: the client's tasks-extension declaration) AND a store
2473 // backend exists AND the tool's `TaskSupport ∈ {Required, Optional}` AND
2474 // the produced value is task-shaped (`taskId` + `status`); otherwise
2475 // `None` (fall through to a normal `CallToolResult`, no leakage — incl.
2476 // `Forbidden`/`None`).
2477 //
2478 // The store mints the canonical id (D-STORE-MINTS-ID); the tool's
2479 // fabricated `taskId` is never trusted on the wire. We pass the RAW
2480 // facts (`create_trigger`, `tool_task_support`) — the gate enforces the
2481 // complete precondition internally. The gate returns a full
2482 // `JSONRPCResponse`; we decompose it back into this fn's `Result<Value>`
2483 // contract (the caller re-wraps with the SAME request id via
2484 // `create_response`, so the id is preserved and `-32603` store errors
2485 // surface as JSON-RPC errors).
2486 #[cfg(not(target_arch = "wasm32"))]
2487 {
2488 if let Some((response, claim)) = self
2489 .task_dispatch()
2490 .maybe_build_task_created(
2491 create_path_id,
2492 &result,
2493 tool_task_support,
2494 create_trigger,
2495 create_path_auth.as_ref(),
2496 create_path_era,
2497 )
2498 .await
2499 {
2500 // On v2 this is the ONE response in the whole surface that earns
2501 // `resultType: "task"`; the claim is what carries that fact past
2502 // the `Result<Value>` contract this fn is bound to.
2503 *dispatch_claim = claim;
2504 return match response.payload {
2505 crate::types::jsonrpc::ResponsePayload::Result(value) => Ok(value),
2506 // The create-path only emits `-32603` store errors here; the
2507 // caller's `create_response` re-wraps `Err` as `-32603`, so
2508 // the code is preserved. Surface the store's message.
2509 crate::types::jsonrpc::ResponsePayload::Error(err) => {
2510 Err(crate::Error::Protocol {
2511 code: crate::error::ErrorCode(err.code),
2512 message: err.message,
2513 data: err.data,
2514 })
2515 },
2516 };
2517 }
2518 }
2519
2520 // TOUT-02 double-wrap tripwire: BEFORE stringifying `result` into text
2521 // content, WARN (+ debug_assert in debug/CI) if it structurally resembles
2522 // an already-built `CallToolResult` — the silent double-wrap bug. Honors
2523 // the per-tool `suppress_double_wrap_check` opt-out (D-08). Non-wasm only
2524 // (the `task_dispatch` unit is non-wasm, matching the create-path above).
2525 #[cfg(not(target_arch = "wasm32"))]
2526 task_dispatch::double_wrap_tripwire(
2527 &req.name,
2528 &result,
2529 self.suppress_double_wrap.contains(req.name.as_str()),
2530 );
2531
2532 // Build CallToolResult, adding structured_content for widget tools and
2533 // for tools with a declared outputSchema (MCP spec: a tool that
2534 // declares an outputSchema SHOULD return structuredContent conforming
2535 // to it). The text voice always carries the serialized value so
2536 // text-only clients keep working.
2537 let text = result.to_string();
2538 let mut call_result = CallToolResult::new(vec![crate::types::Content::text(text)]);
2539
2540 if let Some(info) = self.tool_infos.get(&req.name) {
2541 // A declared outputSchema means structuredContent is emitted below
2542 // (via widget enrichment or the schema bridge) — validate the value
2543 // against it regardless of which branch does the emitting.
2544 if let Some(schema) = &info.output_schema {
2545 output_validation::warn_on_schema_mismatch(
2546 &req.name,
2547 schema,
2548 &result,
2549 validation_era,
2550 );
2551 }
2552 if info.widget_meta().is_some() {
2553 call_result = call_result.with_widget_enrichment(info, result);
2554 } else if info.output_schema.is_some() {
2555 call_result = call_result.with_structured_content(result);
2556 }
2557 }
2558
2559 // D-03.3: drain any handler-set result `_meta` (via extra.set_result_meta)
2560 // and merge it onto the Payload-built envelope with handler-key-wins
2561 // precedence (unrelated widget/native keys preserved). The verbatim
2562 // `ToolOutput::Result` arm above returns earlier and still owns its
2563 // content, its redaction and its bypass of the response pipeline — but
2564 // since D-06 (Phase 118.1) it performs this SAME drain against its own
2565 // envelope before returning, so `set_result_meta` is no longer silently
2566 // dropped there. By the time control reaches this line the slot has
2567 // therefore only ever been filled by a Payload-path handler.
2568 #[cfg(not(target_arch = "wasm32"))]
2569 if let Some(handler_meta) = result_meta_handle.take_result_meta() {
2570 crate::server::cancellation::merge_result_meta(&mut call_result, handler_meta);
2571 }
2572
2573 Ok(serde_json::to_value(call_result)?)
2574 }
2575
2576 fn handle_list_prompts(&self, _req: ListPromptsRequest) -> Result<Value> {
2577 let prompts = self
2578 .prompts
2579 .iter()
2580 .map(|(name, handler)| {
2581 // Use prompt metadata if provided, otherwise use defaults
2582 if let Some(mut info) = handler.metadata() {
2583 // Ensure the name matches the registered name
2584 info.name.clone_from(name);
2585 info
2586 } else {
2587 crate::types::PromptInfo::new(name)
2588 }
2589 })
2590 .collect::<Vec<_>>();
2591
2592 Ok(serde_json::to_value(ListPromptsResult {
2593 prompts,
2594 next_cursor: None,
2595 ttl_ms: None,
2596 cache_scope: None,
2597 })?)
2598 }
2599
2600 async fn handle_get_prompt(
2601 &self,
2602 request_id: RequestId,
2603 req: GetPromptRequest,
2604 auth_context: Option<auth::AuthContext>,
2605 protocol_context: Option<crate::types::protocol::ProtocolContext>,
2606 ) -> Result<Value> {
2607 let handler = self
2608 .prompts
2609 .get(&req.name)
2610 .ok_or_else(|| Error::not_found(format!("Prompt '{}' not found", req.name)))?;
2611
2612 let request_id_str = request_id.to_string();
2613 let cancellation_token = self
2614 .cancellation_manager
2615 .create_token(request_id_str.clone())
2616 .await;
2617
2618 // The request-scoped progress reporter — the SAME resolution the
2619 // tools/call dispatcher makes, so a prompt handler over v1 HTTP emits on
2620 // the session stream too (Phase 118.1 plan 11).
2621 #[allow(clippy::used_underscore_binding)] // _meta is part of MCP protocol spec
2622 let progress_reporter =
2623 self.progress_reporter_for(req._meta.as_ref(), protocol_context.as_ref());
2624
2625 // Propagate the request `_meta` (raw JSON) and the once-at-ingress
2626 // resolved protocol context so prompt handlers read
2627 // era/client_info/trace_context via `extra` on the high-level `Server`
2628 // path too — the twin of the ServerCore wiring (Phase 112, mirrors the
2629 // handle_call_tool twin).
2630 #[allow(clippy::used_underscore_binding)] // _meta is part of MCP protocol spec
2631 let request_meta_value = crate::server::core::request_meta_to_value(req._meta.as_ref());
2632
2633 let extra = self.attach_peer(
2634 crate::server::cancellation::RequestHandlerExtra::new(
2635 request_id_str.clone(),
2636 cancellation_token,
2637 )
2638 .with_auth_context(auth_context)
2639 .with_progress_reporter(progress_reporter)
2640 .with_request_meta(request_meta_value)
2641 .with_protocol_context(protocol_context),
2642 );
2643 let result = match handler.handle(req.arguments, extra).await {
2644 Ok(v) => {
2645 self.cancellation_manager
2646 .remove_token(&request_id_str)
2647 .await;
2648 Ok(v)
2649 },
2650 Err(e) => {
2651 self.cancellation_manager
2652 .remove_token(&request_id_str)
2653 .await;
2654 Err(e)
2655 },
2656 }?;
2657 Ok(serde_json::to_value(result)?)
2658 }
2659
2660 async fn handle_list_resources(
2661 &self,
2662 request_id: RequestId,
2663 req: ListResourcesRequest,
2664 auth_context: Option<auth::AuthContext>,
2665 // THREADED, not resolved here (Phase 118.1-08, G-9) — the twin of the
2666 // `ServerCore` site. `ListResourcesRequest` carries no `_meta`, so the
2667 // context can only arrive from the caller.
2668 protocol_context: Option<crate::types::protocol::ProtocolContext>,
2669 ) -> Result<Value> {
2670 if let Some(handler) = &self.resources {
2671 let request_id_str = request_id.to_string();
2672 let cancellation_token = self
2673 .cancellation_manager
2674 .create_token(request_id_str.clone())
2675 .await;
2676 let extra = self.attach_peer(
2677 crate::server::cancellation::RequestHandlerExtra::new(
2678 request_id_str.clone(),
2679 cancellation_token,
2680 )
2681 .with_auth_context(auth_context)
2682 .with_protocol_context(protocol_context),
2683 );
2684 let mut result = match handler.list(req.cursor, extra).await {
2685 Ok(v) => {
2686 self.cancellation_manager
2687 .remove_token(&request_id_str)
2688 .await;
2689 Ok(v)
2690 },
2691 Err(e) => {
2692 self.cancellation_manager
2693 .remove_token(&request_id_str)
2694 .await;
2695 Err(e)
2696 },
2697 }?;
2698 // Enrich ResourceInfo with tool _meta for widget resources
2699 if !self.uri_to_tool_meta.is_empty() {
2700 for resource in &mut result.resources {
2701 if let Some(tool_meta) = self.uri_to_tool_meta.get(&resource.uri) {
2702 let meta = resource.meta.get_or_insert_with(serde_json::Map::new);
2703 crate::types::ui::deep_merge(meta, tool_meta.clone());
2704 }
2705 }
2706 }
2707 Ok(serde_json::to_value(result)?)
2708 } else {
2709 Ok(serde_json::to_value(ListResourcesResult {
2710 resources: vec![],
2711 next_cursor: None,
2712 ttl_ms: None,
2713 cache_scope: None,
2714 })?)
2715 }
2716 }
2717
2718 async fn handle_read_resource(
2719 &self,
2720 request_id: RequestId,
2721 req: ReadResourceRequest,
2722 auth_context: Option<auth::AuthContext>,
2723 protocol_context: Option<crate::types::protocol::ProtocolContext>,
2724 ) -> Result<Value> {
2725 let handler = self
2726 .resources
2727 .as_ref()
2728 .ok_or_else(|| Error::not_found("No resource handler configured".to_string()))?;
2729
2730 let request_id_str = request_id.to_string();
2731 let cancellation_token = self
2732 .cancellation_manager
2733 .create_token(request_id_str.clone())
2734 .await;
2735
2736 // The request-scoped progress reporter — the SAME resolution the
2737 // tools/call dispatcher makes, so a resource read over v1 HTTP emits on
2738 // the session stream too (Phase 118.1 plan 11).
2739 #[allow(clippy::used_underscore_binding)] // _meta is part of MCP protocol spec
2740 let progress_reporter =
2741 self.progress_reporter_for(req._meta.as_ref(), protocol_context.as_ref());
2742
2743 // Propagate the request `_meta` (raw JSON) and the once-at-ingress
2744 // resolved protocol context so resource handlers read
2745 // era/client_info/trace_context via `extra` on the high-level `Server`
2746 // path too — the twin of the ServerCore wiring (Phase 112).
2747 #[allow(clippy::used_underscore_binding)] // _meta is part of MCP protocol spec
2748 let request_meta_value = crate::server::core::request_meta_to_value(req._meta.as_ref());
2749
2750 let extra = self.attach_peer(
2751 crate::server::cancellation::RequestHandlerExtra::new(
2752 request_id_str.clone(),
2753 cancellation_token,
2754 )
2755 .with_auth_context(auth_context)
2756 .with_progress_reporter(progress_reporter)
2757 .with_request_meta(request_meta_value)
2758 .with_protocol_context(protocol_context),
2759 );
2760 let mut result = match handler.read(&req.uri, extra).await {
2761 Ok(v) => {
2762 self.cancellation_manager
2763 .remove_token(&request_id_str)
2764 .await;
2765 Ok(v)
2766 },
2767 Err(e) => {
2768 self.cancellation_manager
2769 .remove_token(&request_id_str)
2770 .await;
2771 Err(e)
2772 },
2773 }?;
2774 // Merge tool descriptor keys into content _meta for widget resources
2775 if !self.uri_to_tool_meta.is_empty() {
2776 for content in &mut result.contents {
2777 if let crate::types::Content::Resource { uri, meta, .. } = content {
2778 if let Some(tool_meta) = self.uri_to_tool_meta.get(uri.as_str()) {
2779 let content_meta = meta.get_or_insert_with(serde_json::Map::new);
2780 crate::types::ui::deep_merge(content_meta, tool_meta.clone());
2781 }
2782 }
2783 }
2784 }
2785 Ok(serde_json::to_value(result)?)
2786 }
2787
2788 #[allow(clippy::unused_self)]
2789 fn handle_list_resource_templates(&self, _req: ListResourceTemplatesRequest) -> Result<Value> {
2790 Ok(serde_json::to_value(ListResourceTemplatesResult {
2791 resource_templates: vec![],
2792 next_cursor: None,
2793 ttl_ms: None,
2794 cache_scope: None,
2795 })?)
2796 }
2797
2798 async fn handle_create_message(
2799 &self,
2800 request_id: RequestId,
2801 req: crate::types::CreateMessageParams,
2802 // THREADED, not resolved here (Phase 118.1-08, G-9). This arm serves an
2803 // INBOUND `sampling/createMessage` — a `ClientRequest` variant, so a
2804 // client handshake absolutely does have meaning here and the site is
2805 // THREAD-THEN-FOLD, not a NO-OP. (The server-to-client direction is a
2806 // `ServerRequest` handled by the peer dispatcher, which builds no
2807 // `RequestHandlerExtra` at all.) `CreateMessageParams` carries no
2808 // `_meta`, so the context can only arrive from the caller.
2809 protocol_context: Option<crate::types::protocol::ProtocolContext>,
2810 ) -> Result<Value> {
2811 let handler = self
2812 .sampling
2813 .as_ref()
2814 .ok_or_else(|| Error::not_found("No sampling handler configured".to_string()))?;
2815
2816 let request_id_str = request_id.to_string();
2817 let cancellation_token = self
2818 .cancellation_manager
2819 .create_token(request_id_str.clone())
2820 .await;
2821 let extra = self.attach_peer(
2822 crate::server::cancellation::RequestHandlerExtra::new(
2823 request_id_str.clone(),
2824 cancellation_token,
2825 )
2826 .with_protocol_context(protocol_context),
2827 );
2828 let result = match handler.create_message(req, extra).await {
2829 Ok(v) => {
2830 self.cancellation_manager
2831 .remove_token(&request_id_str)
2832 .await;
2833 Ok(v)
2834 },
2835 Err(e) => {
2836 self.cancellation_manager
2837 .remove_token(&request_id_str)
2838 .await;
2839 Err(e)
2840 },
2841 }?;
2842 Ok(serde_json::to_value(result)?)
2843 }
2844
2845 /// Register a root directory or URI that the server has access to.
2846 ///
2847 /// This method allows the server to announce to clients that it has
2848 /// access to specific file system roots or URIs. This is useful for
2849 /// resource handlers that need to expose filesystem access or other
2850 /// URI-based resources.
2851 ///
2852 /// # Arguments
2853 ///
2854 /// * `uri` - The root URI to register (e.g., `file:///home/user/project`)
2855 /// * `name` - Optional human-readable name for the root
2856 ///
2857 /// # Returns
2858 ///
2859 /// An unregister function that can be called to remove the root registration.
2860 ///
2861 /// # Examples
2862 ///
2863 /// ```rust,no_run
2864 /// use pmcp::Server;
2865 ///
2866 /// # async fn example() -> pmcp::Result<()> {
2867 /// let server = Server::builder()
2868 /// .name("file-server")
2869 /// .version("1.0.0")
2870 /// .build()?;
2871 ///
2872 /// // Register a project root
2873 /// let unregister = server.register_root(
2874 /// "file:///home/user/project",
2875 /// Some("My Project".to_string())
2876 /// ).await?;
2877 ///
2878 /// // Later, unregister the root
2879 /// unregister();
2880 /// # Ok(())
2881 /// # }
2882 /// ```
2883 pub async fn register_root(
2884 &self,
2885 uri: impl Into<String>,
2886 name: Option<String>,
2887 ) -> Result<impl FnOnce() + Send + 'static> {
2888 let mut roots_manager = self.roots_manager.write().await;
2889 if let Some(tx) = &self.notification_tx {
2890 roots_manager.set_notification_sender({
2891 let tx = tx.clone();
2892 move |server_notification| {
2893 let _ = tx.try_send(Notification::Server(server_notification));
2894 }
2895 });
2896 }
2897 roots_manager.register_root(uri.into(), name).await
2898 }
2899
2900 /// Get the list of registered roots.
2901 ///
2902 /// Returns a list of all currently registered root URIs and their
2903 /// associated names. Roots are directories or URIs that the server
2904 /// has announced access to.
2905 ///
2906 /// # Returns
2907 ///
2908 /// A vector of `Root` objects containing URI and optional name.
2909 ///
2910 /// # Examples
2911 ///
2912 /// ```rust,no_run
2913 /// use pmcp::Server;
2914 ///
2915 /// # async fn example() -> pmcp::Result<()> {
2916 /// let server = Server::builder()
2917 /// .name("file-server")
2918 /// .version("1.0.0")
2919 /// .build()?;
2920 ///
2921 /// // Register some roots
2922 /// server.register_root("file:///home/user/project1", Some("Project 1".to_string())).await?;
2923 /// server.register_root("file:///home/user/project2", None).await?;
2924 ///
2925 /// // Get the list of roots
2926 /// let roots = server.get_roots().await;
2927 /// println!("Registered {} roots", roots.len());
2928 /// # Ok(())
2929 /// # }
2930 /// ```
2931 pub async fn get_roots(&self) -> Vec<roots::Root> {
2932 let roots_manager = self.roots_manager.read().await;
2933 roots_manager.get_roots().await
2934 }
2935
2936 /// Subscribe a client to resource updates.
2937 ///
2938 /// This method allows the server to track which clients are interested
2939 /// in updates to specific resources. When a resource changes, the server
2940 /// can notify all subscribed clients.
2941 ///
2942 /// # Arguments
2943 ///
2944 /// * `uri` - The resource URI to subscribe to
2945 /// * `client_id` - Identifier for the subscribing client
2946 ///
2947 /// # Examples
2948 ///
2949 /// ```rust,no_run
2950 /// use pmcp::Server;
2951 ///
2952 /// # async fn example() -> pmcp::Result<()> {
2953 /// let server = Server::builder()
2954 /// .name("file-server")
2955 /// .version("1.0.0")
2956 /// .build()?;
2957 ///
2958 /// // Subscribe client to resource updates
2959 /// server.subscribe_resource(
2960 /// "file:///project/file.txt".to_string(),
2961 /// "client-123".to_string()
2962 /// ).await?;
2963 /// # Ok(())
2964 /// # }
2965 /// ```
2966 pub async fn subscribe_resource(&self, uri: String, client_id: String) -> Result<()> {
2967 if uri.is_empty() || client_id.is_empty() {
2968 return Err(Error::invalid_params("URI and client_id must not be empty"));
2969 }
2970
2971 let mut subscription_manager = self.subscription_manager.write().await;
2972 if let Some(tx) = &self.notification_tx {
2973 subscription_manager.set_notification_sender({
2974 let tx = tx.clone();
2975 move |notification| {
2976 let _ = tx.try_send(Notification::Server(notification));
2977 }
2978 });
2979 }
2980
2981 subscription_manager.subscribe(uri, client_id).await
2982 }
2983
2984 /// Cancel a request that is currently being processed.
2985 ///
2986 /// This method allows the server to cancel ongoing requests, which is
2987 /// useful for implementing request timeouts or client-requested cancellations.
2988 ///
2989 /// # Arguments
2990 ///
2991 /// * `request_id` - The ID of the request to cancel
2992 /// * `reason` - Optional reason for cancellation
2993 ///
2994 /// # Examples
2995 ///
2996 /// ```rust,no_run
2997 /// use pmcp::Server;
2998 ///
2999 /// # async fn example() -> pmcp::Result<()> {
3000 /// let server = Server::builder()
3001 /// .name("cancel-server")
3002 /// .version("1.0.0")
3003 /// .build()?;
3004 ///
3005 /// // Cancel a request
3006 /// server.cancel_request(
3007 /// "request-123".to_string(),
3008 /// Some("User requested cancellation".to_string())
3009 /// ).await?;
3010 /// # Ok(())
3011 /// # }
3012 /// ```
3013 pub async fn cancel_request(&self, request_id: String, reason: Option<String>) -> Result<()> {
3014 if request_id.is_empty() {
3015 return Err(Error::invalid_params("Request ID must not be empty"));
3016 }
3017
3018 self.cancellation_manager
3019 .cancel_request(request_id, reason)
3020 .await
3021 }
3022
3023 /// Unsubscribe a client from resource updates.
3024 ///
3025 /// This method removes a client's subscription to a specific resource,
3026 /// so they will no longer receive notifications when that resource changes.
3027 ///
3028 /// # Arguments
3029 ///
3030 /// * `uri` - The resource URI to unsubscribe from
3031 /// * `client_id` - Identifier for the client to unsubscribe
3032 ///
3033 /// # Examples
3034 ///
3035 /// ```rust,no_run
3036 /// use pmcp::Server;
3037 ///
3038 /// # async fn example() -> pmcp::Result<()> {
3039 /// let server = Server::builder()
3040 /// .name("file-server")
3041 /// .version("1.0.0")
3042 /// .build()?;
3043 ///
3044 /// // Unsubscribe client from resource updates
3045 /// server.unsubscribe_resource(
3046 /// "file:///project/file.txt".to_string(),
3047 /// "client-123".to_string()
3048 /// ).await?;
3049 /// # Ok(())
3050 /// # }
3051 /// ```
3052 pub async fn unsubscribe_resource(&self, uri: String, client_id: String) -> Result<()> {
3053 if uri.is_empty() || client_id.is_empty() {
3054 return Err(Error::invalid_params("URI and client_id must not be empty"));
3055 }
3056
3057 let subscription_manager = self.subscription_manager.read().await;
3058 subscription_manager.unsubscribe(uri, client_id).await
3059 }
3060
3061 /// Notify subscribers that a resource has been updated.
3062 ///
3063 /// # Arguments
3064 ///
3065 /// * `uri` - The URI of the resource that was updated
3066 ///
3067 /// # Returns
3068 ///
3069 /// The number of subscribers that were notified.
3070 pub async fn notify_resource_updated(&self, uri: String) -> Result<usize> {
3071 let mut subscription_manager = self.subscription_manager.write().await;
3072 if let Some(tx) = &self.notification_tx {
3073 subscription_manager.set_notification_sender({
3074 let tx = tx.clone();
3075 move |notification| {
3076 let _ = tx.try_send(Notification::Server(notification));
3077 }
3078 });
3079 }
3080 subscription_manager.notify_resource_updated(uri).await
3081 }
3082}
3083
3084/// Trait for types annotated with `#[mcp_server]`.
3085///
3086/// Generated by the `#[mcp_server]` proc macro. Provides bulk registration of
3087/// tools and prompts via `register()`. Users should call `.mcp_server(instance)`
3088/// on the builder instead of implementing this trait manually.
3089///
3090/// # Examples
3091///
3092/// ```rust,ignore
3093/// use pmcp::ServerBuilder;
3094///
3095/// #[mcp_server]
3096/// impl MyServer {
3097/// #[mcp_tool(description = "Query data")]
3098/// async fn query(&self, args: QueryArgs) -> Result<Value> { /* ... */ }
3099///
3100/// #[mcp_prompt(description = "Generate query")]
3101/// async fn query_prompt(&self, args: PromptArgs) -> Result<GetPromptResult> { /* ... */ }
3102/// }
3103///
3104/// let server = MyServer { db };
3105/// let builder = ServerBuilder::new()
3106/// .mcp_server(server);
3107/// ```
3108#[cfg(not(target_arch = "wasm32"))]
3109pub trait McpServer {
3110 /// Register all tools and prompts from this server on the builder.
3111 fn register(self, builder: ServerBuilder) -> ServerBuilder;
3112}
3113
3114/// Builder for creating servers.
3115#[cfg(not(target_arch = "wasm32"))]
3116pub struct ServerBuilder {
3117 name: Option<String>,
3118 version: Option<String>,
3119 capabilities: ServerCapabilities,
3120 tools: HashMap<String, Arc<dyn ToolHandler>>,
3121 prompts: HashMap<String, Arc<dyn PromptHandler>>,
3122 resources: Option<Arc<dyn ResourceHandler>>,
3123 /// Completion provider backing `completion/complete` (Phase 118.1-04,
3124 /// CONF-05), set via [`Self::completions`]. The twin of
3125 /// `ServerCoreBuilder`'s slot of the same name: a provider registered
3126 /// through EITHER builder family reaches its own dispatcher.
3127 completions: Option<Arc<dyn crate::types::completable::CompletionProviderTrait>>,
3128 sampling: Option<Arc<dyn SamplingHandler>>,
3129 /// Cancellation manager for request cancellation
3130 cancellation_manager: cancellation::CancellationManager,
3131 /// Roots manager for directory/URI registration
3132 roots_manager: roots::RootsManager,
3133 /// Authentication provider for validating requests
3134 auth_provider: Option<Arc<dyn auth::AuthProvider>>,
3135 /// Tool authorizer for fine-grained access control
3136 tool_authorizer: Option<Arc<dyn auth::ToolAuthorizer>>,
3137 /// Tool protection requirements to be applied at build time
3138 tool_protections: HashMap<String, Vec<String>>,
3139 /// Tool middleware chain for cross-cutting concerns
3140 #[cfg(not(target_arch = "wasm32"))]
3141 tool_middlewares: Vec<Arc<dyn tool_middleware::ToolMiddleware>>,
3142 /// HTTP middleware chain for `StreamableHttpServer`
3143 #[cfg(feature = "streamable-http")]
3144 http_middleware: Option<Arc<http_middleware::ServerHttpMiddlewareChain>>,
3145 /// Host layers for MCP Apps metadata enrichment (e.g., `ChatGPT`)
3146 #[cfg(feature = "mcp-apps")]
3147 host_layers: Vec<crate::types::mcp_apps::HostType>,
3148 /// Optional website URL for the server implementation (MCP 2025-11-25)
3149 website_url: Option<String>,
3150 /// Optional icons for the server implementation (MCP 2025-11-25)
3151 icons: Option<Vec<crate::types::protocol::IconInfo>>,
3152 /// Accumulated SEP-2640 Agent Skills. The registry is finalized into a
3153 /// single `SkillsHandler` exactly once at `.build()` time so chained
3154 /// `.skill(...)` / `.skills(...)` calls never produce nested wrappers.
3155 #[cfg(feature = "skills")]
3156 pending_skills: Option<skills::Skills>,
3157 /// Legacy experimental task router backend (set via [`Self::with_task_store`]).
3158 #[cfg(not(target_arch = "wasm32"))]
3159 task_router: Option<Arc<dyn crate::server::tasks::TaskRouter>>,
3160 /// Standard task store backend (set via [`Self::task_store`]). Presence
3161 /// auto-advertises the `tasks` capability at `build()`.
3162 #[cfg(not(target_arch = "wasm32"))]
3163 task_store: Option<Arc<dyn crate::server::task_store::TaskStore>>,
3164 /// Tool names opting out of the TOUT-02 double-wrap tripwire (D-08), set via
3165 /// [`Self::suppress_double_wrap_check`]. Carried into the built `Server` and
3166 /// consulted at the Payload wrap site.
3167 #[cfg(not(target_arch = "wasm32"))]
3168 suppress_double_wrap: HashSet<String>,
3169 /// Configured protocol-version accept-list (Phase 112, VERS-01/02). Defaults
3170 /// to the v1-only legacy set (excludes `2026-07-28`); overridden via
3171 /// [`Self::with_supported_protocol_versions`].
3172 supported_protocol_versions: Vec<ProtocolVersion>,
3173 /// Explicit `requestState` minting key (Phase 113, HTTP-02), set via
3174 /// [`Self::with_request_state_key`]. When present it overrides
3175 /// `PMCP_REQUEST_STATE_KEY` entirely.
3176 ///
3177 /// Copy 1 of 3 (D-113-P): held as a
3178 /// [`SecretKey`](crate::server::request_state::SecretKey), never as bare
3179 /// `[u8; 32]`, so the destructor rides on the value and scrubs on drop —
3180 /// including on every early-`?` path out of [`Self::build`]. Reverting this
3181 /// to bare bytes is caught at COMPILE time by
3182 /// `server_builder_request_state_key_field_is_the_zeroizing_type`.
3183 #[cfg(feature = "streamable-http")]
3184 request_state_key: Option<request_state::SecretKey>,
3185 /// Rotated-out `requestState` keys accepted for VERIFICATION only, set via
3186 /// [`Self::with_request_state_previous_keys`].
3187 ///
3188 /// Copy 1 of 3 (D-113-P), the rotated-out half: each element scrubs itself
3189 /// when the `Vec` drops.
3190 #[cfg(feature = "streamable-http")]
3191 request_state_previous_keys: Vec<request_state::SecretKey>,
3192 /// Explicit continuation lifetime, set via [`Self::with_request_state_ttl`].
3193 /// Beats both the 300-second default and `PMCP_REQUEST_STATE_TTL_SECS` (D-05).
3194 #[cfg(feature = "streamable-http")]
3195 request_state_ttl: Option<std::time::Duration>,
3196}
3197
3198#[cfg(not(target_arch = "wasm32"))]
3199impl std::fmt::Debug for ServerBuilder {
3200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3201 f.debug_struct("ServerBuilder")
3202 .field("name", &self.name)
3203 .field("version", &self.version)
3204 .field("capabilities", &self.capabilities)
3205 .field("tools", &self.tools.keys().collect::<Vec<_>>())
3206 .field("prompts", &self.prompts.keys().collect::<Vec<_>>())
3207 .field("resources", &self.resources.is_some())
3208 .field("sampling", &self.sampling.is_some())
3209 .finish()
3210 }
3211}
3212
3213#[cfg(not(target_arch = "wasm32"))]
3214impl ServerBuilder {
3215 /// Create a new server builder.
3216 ///
3217 /// Creates a new `ServerBuilder` with default capabilities and no handlers.
3218 /// Use the builder methods to configure the server before calling `build()`.
3219 ///
3220 /// # Examples
3221 ///
3222 /// ```rust,no_run
3223 /// use pmcp::ServerBuilder;
3224 ///
3225 /// let builder = ServerBuilder::new();
3226 /// ```
3227 ///
3228 /// This is equivalent to using the default implementation:
3229 ///
3230 /// ```rust,no_run
3231 /// use pmcp::ServerBuilder;
3232 ///
3233 /// let builder = ServerBuilder::default();
3234 /// ```
3235 pub fn new() -> Self {
3236 Self {
3237 name: None,
3238 version: None,
3239 capabilities: ServerCapabilities::default(),
3240 tools: HashMap::new(),
3241 prompts: HashMap::new(),
3242 resources: None,
3243 completions: None,
3244 sampling: None,
3245 cancellation_manager: cancellation::CancellationManager::new(),
3246 roots_manager: roots::RootsManager::new(),
3247 auth_provider: None,
3248 tool_authorizer: None,
3249 tool_protections: HashMap::new(),
3250 #[cfg(not(target_arch = "wasm32"))]
3251 tool_middlewares: Vec::new(),
3252 #[cfg(feature = "streamable-http")]
3253 http_middleware: None,
3254 #[cfg(feature = "mcp-apps")]
3255 host_layers: Vec::new(),
3256 website_url: None,
3257 icons: None,
3258 #[cfg(feature = "skills")]
3259 pending_skills: None,
3260 #[cfg(not(target_arch = "wasm32"))]
3261 task_router: None,
3262 #[cfg(not(target_arch = "wasm32"))]
3263 task_store: None,
3264 #[cfg(not(target_arch = "wasm32"))]
3265 suppress_double_wrap: HashSet::new(),
3266 supported_protocol_versions: crate::types::protocol::context::default_accept_list(),
3267 #[cfg(feature = "streamable-http")]
3268 request_state_key: None,
3269 #[cfg(feature = "streamable-http")]
3270 request_state_previous_keys: Vec::new(),
3271 #[cfg(feature = "streamable-http")]
3272 request_state_ttl: None,
3273 }
3274 }
3275
3276 /// Configure the shared `requestState` minting key (Phase 113, HTTP-02, D-03).
3277 ///
3278 /// With no call, the key is resolved from `PMCP_REQUEST_STATE_KEY`; when that
3279 /// variable is unset the server generates a per-process key and WARNs at build
3280 /// time (D-04). Calling this overrides the environment entirely, which is what
3281 /// makes deterministic integration tests and multiple differently-configured
3282 /// servers in one process possible.
3283 ///
3284 /// The key must be shared byte-for-byte by every instance behind a load
3285 /// balancer that should be able to resume each other's multi-round-trip
3286 /// requests.
3287 ///
3288 /// Has no effect on a server that did not opt into the v2 (`2026-07-28`) era.
3289 ///
3290 /// The parameter type is deliberately still `[u8; 32]`: the SDK owns the
3291 /// copy it takes, not the caller's (D-113-P, T-113-121).
3292 #[cfg(feature = "streamable-http")]
3293 #[must_use]
3294 pub fn with_request_state_key(mut self, mut key: [u8; 32]) -> Self {
3295 // Closes copy 1 of 3 (D-113-P): the FIELD now scrubs on drop.
3296 self.request_state_key = Some(request_state::SecretKey::new(key));
3297 // Closes copy 2 of 3 (D-113-P): this by-value parameter's OWN stack
3298 // slot. `[u8; 32]` is `Copy`, so the line above copied out of it and
3299 // left the caller's key bytes sitting here.
3300 key.zeroize();
3301 self
3302 }
3303
3304 /// Accept rotated-out `requestState` keys for VERIFICATION only.
3305 ///
3306 /// Tokens minted under a listed key still verify, but new tokens are always
3307 /// minted under the current key — so a rotation does not strand in-flight
3308 /// continuations. With no call, only the current key is accepted.
3309 ///
3310 /// Has no effect on a server that did not opt into the v2 (`2026-07-28`) era.
3311 #[cfg(feature = "streamable-http")]
3312 #[must_use]
3313 pub fn with_request_state_previous_keys(mut self, mut keys: Vec<[u8; 32]>) -> Self {
3314 // Closes copy 1 of 3 (D-113-P), rotated-out half.
3315 self.request_state_previous_keys = keys
3316 .iter()
3317 .copied()
3318 .map(request_state::SecretKey::new)
3319 .collect();
3320 // Closes copy 2 of 3 (D-113-P): the by-value `Vec`'s own heap buffer,
3321 // which the copy above read out of and would otherwise return to the
3322 // allocator holding every rotated-out key in the clear. `Vec::zeroize`
3323 // scrubs the initialized elements AND the spare capacity.
3324 keys.zeroize();
3325 self
3326 }
3327
3328 /// Configure the `requestState` continuation lifetime (D-05).
3329 ///
3330 /// With no call, the lifetime is `PMCP_REQUEST_STATE_TTL_SECS` if parseable,
3331 /// else 300 seconds. A builder value beats both.
3332 ///
3333 /// Has no effect on a server that did not opt into the v2 (`2026-07-28`) era.
3334 #[cfg(feature = "streamable-http")]
3335 #[must_use]
3336 pub fn with_request_state_ttl(mut self, ttl: std::time::Duration) -> Self {
3337 self.request_state_ttl = Some(ttl);
3338 self
3339 }
3340
3341 /// Opt into a protocol-version accept-list (Phase 112, VERS-01/02; D-02/D-04).
3342 ///
3343 /// The high-level `Server` twin of
3344 /// [`ServerCoreBuilder::with_supported_protocol_versions`](crate::server::builder::ServerCoreBuilder::with_supported_protocol_versions).
3345 /// With no call, the server is v1-only and behaves exactly as today. An empty
3346 /// accept-list falls back to the v1-only default (never all-reject).
3347 #[must_use]
3348 pub fn with_supported_protocol_versions(
3349 mut self,
3350 versions: impl IntoIterator<Item = ProtocolVersion>,
3351 ) -> Self {
3352 self.supported_protocol_versions =
3353 crate::types::protocol::context::normalize_accept_list(versions);
3354 self
3355 }
3356
3357 /// Set the server name.
3358 ///
3359 /// The server name identifies this MCP server implementation.
3360 /// This is required and will be sent to clients during initialization.
3361 ///
3362 /// # Arguments
3363 ///
3364 /// * `name` - The name of the server
3365 ///
3366 /// # Examples
3367 ///
3368 /// ```rust,no_run
3369 /// use pmcp::Server;
3370 ///
3371 /// let server = Server::builder()
3372 /// .name("file-manager")
3373 /// .version("1.0.0")
3374 /// .build()?;
3375 /// # Ok::<(), pmcp::Error>(())
3376 /// ```
3377 pub fn name(mut self, name: impl Into<String>) -> Self {
3378 self.name = Some(name.into());
3379 self
3380 }
3381
3382 /// Set the server version.
3383 ///
3384 /// The server version identifies this specific version of the MCP server.
3385 /// This is required and will be sent to clients during initialization.
3386 ///
3387 /// # Arguments
3388 ///
3389 /// * `version` - The version string (e.g., "1.0.0", "2.1.3-beta")
3390 ///
3391 /// # Examples
3392 ///
3393 /// ```rust,no_run
3394 /// use pmcp::Server;
3395 ///
3396 /// let server = Server::builder()
3397 /// .name("data-processor")
3398 /// .version("2.1.0")
3399 /// .build()?;
3400 /// # Ok::<(), pmcp::Error>(())
3401 /// ```
3402 pub fn version(mut self, version: impl Into<String>) -> Self {
3403 self.version = Some(version.into());
3404 self
3405 }
3406
3407 /// Set the website URL for the server implementation (MCP 2025-11-25).
3408 pub fn website_url(mut self, url: impl Into<String>) -> Self {
3409 self.website_url = Some(url.into());
3410 self
3411 }
3412
3413 /// Set icons for the server implementation (MCP 2025-11-25).
3414 pub fn with_icons(mut self, icons: Vec<crate::types::protocol::IconInfo>) -> Self {
3415 self.icons = Some(icons);
3416 self
3417 }
3418
3419 /// Set server capabilities.
3420 ///
3421 /// Configures the capabilities that this server supports.
3422 /// Capabilities inform clients about which MCP features are available.
3423 ///
3424 /// # Arguments
3425 ///
3426 /// * `capabilities` - The server capabilities to advertise
3427 ///
3428 /// # Examples
3429 ///
3430 /// ```rust,no_run
3431 /// use pmcp::{Server, ServerCapabilities, ToolCapabilities};
3432 ///
3433 /// let mut capabilities = ServerCapabilities::default();
3434 /// capabilities.tools = Some(ToolCapabilities {
3435 /// list_changed: Some(true),
3436 /// });
3437 ///
3438 /// let server = Server::builder()
3439 /// .name("advanced-server")
3440 /// .version("1.0.0")
3441 /// .capabilities(capabilities)
3442 /// .build()?;
3443 /// # Ok::<(), pmcp::Error>(())
3444 /// ```
3445 pub fn capabilities(mut self, capabilities: ServerCapabilities) -> Self {
3446 self.capabilities = capabilities;
3447 self
3448 }
3449
3450 /// Add a tool handler.
3451 ///
3452 /// Registers a tool that clients can call via the tools/call method.
3453 /// Tools are the primary way servers provide functionality to clients.
3454 ///
3455 /// # Arguments
3456 ///
3457 /// * `name` - The name of the tool (used by clients to call it)
3458 /// * `handler` - The handler implementation for this tool
3459 ///
3460 /// # Examples
3461 ///
3462 /// ```rust,no_run
3463 /// use pmcp::{Server, ToolHandler};
3464 /// use async_trait::async_trait;
3465 /// use serde_json::Value;
3466 ///
3467 /// struct FileListTool;
3468 ///
3469 /// #[async_trait]
3470 /// impl ToolHandler for FileListTool {
3471 /// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
3472 /// let path = args["path"].as_str().unwrap_or(".");
3473 /// // List files in path...
3474 /// Ok(serde_json::json!({"files": ["file1.txt", "file2.txt"]}))
3475 /// }
3476 /// }
3477 ///
3478 /// let server = Server::builder()
3479 /// .name("file-server")
3480 /// .version("1.0.0")
3481 /// .tool("list_files", FileListTool{})
3482 /// .build()?;
3483 /// # Ok::<(), pmcp::Error>(())
3484 /// ```
3485 pub fn tool(mut self, name: impl Into<String>, handler: impl ToolHandler + 'static) -> Self {
3486 self.tools.insert(name.into(), Arc::new(handler));
3487
3488 // Update capabilities to include tools
3489 // Use Some(false) instead of None to ensure the field serializes properly
3490 if self.capabilities.tools.is_none() {
3491 self.capabilities.tools = Some(crate::types::ToolCapabilities {
3492 list_changed: Some(false),
3493 });
3494 }
3495
3496 self
3497 }
3498
3499 /// Add a tool handler with an Arc.
3500 ///
3501 /// This variant lets the caller share the handler `Arc` between the
3502 /// builder and an external in-process handler map (e.g., a downstream
3503 /// toolkit's handler registry) without writing a delegating wrapper
3504 /// shim. Behavior is otherwise identical to [`Self::tool`]: the first
3505 /// registration auto-enables `capabilities.tools`.
3506 pub fn tool_arc(mut self, name: impl Into<String>, handler: Arc<dyn ToolHandler>) -> Self {
3507 let name = name.into();
3508 self.tools.insert(name, handler);
3509
3510 // Update capabilities to include tools
3511 // Use Some(false) instead of None to ensure the field serializes properly
3512 if self.capabilities.tools.is_none() {
3513 self.capabilities.tools = Some(crate::types::ToolCapabilities {
3514 list_changed: Some(false),
3515 });
3516 }
3517
3518 self
3519 }
3520
3521 /// Register all tools and prompts from an `#[mcp_server]` annotated type.
3522 ///
3523 /// This is the ergonomic counterpart to individually registering tools and
3524 /// prompts. The server instance provides shared state via `&self` to all
3525 /// tool and prompt methods.
3526 ///
3527 /// # Examples
3528 ///
3529 /// ```rust,ignore
3530 /// use pmcp::ServerBuilder;
3531 ///
3532 /// #[mcp_server]
3533 /// impl MyServer {
3534 /// #[mcp_tool(description = "Query data")]
3535 /// async fn query(&self, args: QueryArgs) -> Result<Value> { /* ... */ }
3536 ///
3537 /// #[mcp_prompt(description = "Generate query")]
3538 /// async fn query_prompt(&self, args: PromptArgs) -> Result<GetPromptResult> { /* ... */ }
3539 /// }
3540 ///
3541 /// let server = MyServer { db };
3542 /// let builder = ServerBuilder::new()
3543 /// .name("my-server")
3544 /// .mcp_server(server);
3545 /// ```
3546 pub fn mcp_server<T: McpServer>(self, server: T) -> Self {
3547 server.register(self)
3548 }
3549
3550 /// Add a type-safe tool handler with automatic schema generation.
3551 ///
3552 /// This method provides first-class support for creating tools with:
3553 /// - Automatic JSON schema generation from Rust types
3554 /// - Compile-time type safety
3555 /// - Runtime validation
3556 /// - Field descriptions from doc comments
3557 ///
3558 /// # Example
3559 /// ```no_run
3560 /// # #[cfg(feature = "schema-generation")]
3561 /// # {
3562 /// use pmcp::ServerBuilder;
3563 /// use schemars::JsonSchema;
3564 /// use serde::{Deserialize, Serialize};
3565 ///
3566 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
3567 /// struct EchoArgs {
3568 /// /// The message to echo
3569 /// message: String,
3570 /// /// Optional prefix
3571 /// prefix: Option<String>,
3572 /// }
3573 ///
3574 /// # #[tokio::main]
3575 /// # async fn main() -> Result<(), pmcp::Error> {
3576 /// let server = ServerBuilder::new()
3577 /// .name("example")
3578 /// .tool_typed("echo", |args: EchoArgs, _| {
3579 /// Box::pin(async move {
3580 /// let message = match args.prefix {
3581 /// Some(p) => format!("{}: {}", p, args.message),
3582 /// None => args.message,
3583 /// };
3584 /// Ok(serde_json::json!({ "message": message }))
3585 /// })
3586 /// })
3587 /// .build();
3588 /// # Ok::<(), pmcp::Error>(())
3589 /// # }
3590 /// # }
3591 /// ```
3592 #[cfg(feature = "schema-generation")]
3593 pub fn tool_typed<T, F, Fut>(mut self, name: impl Into<String>, handler: F) -> Self
3594 where
3595 T: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
3596 F: Fn(T, crate::RequestHandlerExtra) -> Fut + Send + Sync + 'static,
3597 Fut: std::future::Future<Output = crate::Result<serde_json::Value>> + Send + 'static,
3598 {
3599 use crate::server::typed_tool::TypedTool;
3600 use std::pin::Pin;
3601
3602 let name_str = name.into();
3603
3604 // Wrap the handler to return Pin<Box<dyn Future>>
3605 let wrapped_handler = move |args: T,
3606 extra: crate::RequestHandlerExtra|
3607 -> Pin<
3608 Box<dyn std::future::Future<Output = crate::Result<serde_json::Value>> + Send>,
3609 > { Box::pin(handler(args, extra)) };
3610
3611 let tool = TypedTool::new(name_str.clone(), wrapped_handler);
3612 self.tools.insert(name_str, Arc::new(tool));
3613
3614 // Update capabilities to include tools
3615 if self.capabilities.tools.is_none() {
3616 self.capabilities.tools = Some(crate::types::ToolCapabilities {
3617 list_changed: Some(false),
3618 });
3619 }
3620
3621 self
3622 }
3623
3624 /// Add a type-safe tool handler with automatic schema generation and description.
3625 ///
3626 /// This is a convenience overload that allows setting a description directly
3627 /// without needing to chain `.with_description()`.
3628 ///
3629 /// # Example
3630 /// ```no_run
3631 /// # #[cfg(feature = "schema-generation")]
3632 /// # {
3633 /// use pmcp::ServerBuilder;
3634 /// use schemars::JsonSchema;
3635 /// use serde::{Deserialize, Serialize};
3636 ///
3637 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
3638 /// struct EchoArgs {
3639 /// /// The message to echo
3640 /// message: String,
3641 /// /// Optional prefix
3642 /// prefix: Option<String>,
3643 /// }
3644 ///
3645 /// let server = ServerBuilder::new()
3646 /// .name("example")
3647 /// .tool_typed_with_description(
3648 /// "echo",
3649 /// "Echoes back a message with an optional prefix",
3650 /// |args: EchoArgs, _| {
3651 /// Box::pin(async move {
3652 /// let message = match args.prefix {
3653 /// Some(p) => format!("{}: {}", p, args.message),
3654 /// None => args.message,
3655 /// };
3656 /// Ok(serde_json::json!({ "message": message }))
3657 /// })
3658 /// }
3659 /// );
3660 /// # }
3661 /// ```
3662 #[cfg(feature = "schema-generation")]
3663 pub fn tool_typed_with_description<T, F, Fut>(
3664 mut self,
3665 name: impl Into<String>,
3666 description: impl Into<String>,
3667 handler: F,
3668 ) -> Self
3669 where
3670 T: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
3671 F: Fn(T, crate::RequestHandlerExtra) -> Fut + Send + Sync + 'static,
3672 Fut: std::future::Future<Output = crate::Result<serde_json::Value>> + Send + 'static,
3673 {
3674 use crate::server::typed_tool::TypedTool;
3675 use std::pin::Pin;
3676
3677 let name_str = name.into();
3678
3679 // Wrap the handler to return Pin<Box<dyn Future>>
3680 let wrapped_handler = move |args: T,
3681 extra: crate::RequestHandlerExtra|
3682 -> Pin<
3683 Box<dyn std::future::Future<Output = crate::Result<serde_json::Value>> + Send>,
3684 > { Box::pin(handler(args, extra)) };
3685
3686 let tool = TypedTool::new(name_str.clone(), wrapped_handler).with_description(description);
3687 self.tools.insert(name_str, Arc::new(tool));
3688
3689 // Update capabilities to include tools
3690 if self.capabilities.tools.is_none() {
3691 self.capabilities.tools = Some(crate::types::ToolCapabilities {
3692 list_changed: Some(false),
3693 });
3694 }
3695
3696 self
3697 }
3698
3699 /// Add a synchronous type-safe tool handler with automatic schema generation.
3700 ///
3701 /// Similar to `tool_typed` but for synchronous handlers.
3702 ///
3703 /// # Example
3704 /// ```no_run
3705 /// # #[cfg(feature = "schema-generation")]
3706 /// # {
3707 /// use pmcp::ServerBuilder;
3708 /// use schemars::JsonSchema;
3709 /// use serde::{Deserialize, Serialize};
3710 ///
3711 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
3712 /// struct MathArgs {
3713 /// /// First number
3714 /// a: f64,
3715 /// /// Second number
3716 /// b: f64,
3717 /// /// Operation to perform
3718 /// op: String,
3719 /// }
3720 ///
3721 /// # fn main() -> Result<(), pmcp::Error> {
3722 /// let server = ServerBuilder::new()
3723 /// .name("example")
3724 /// .tool_typed_sync("calculator", |args: MathArgs, _| {
3725 /// let result = match args.op.as_str() {
3726 /// "add" => args.a + args.b,
3727 /// "subtract" => args.a - args.b,
3728 /// "multiply" => args.a * args.b,
3729 /// "divide" => args.a / args.b,
3730 /// _ => return Err(pmcp::Error::Validation("Unknown operation".into())),
3731 /// };
3732 /// Ok(serde_json::json!({ "result": result }))
3733 /// })
3734 /// .build();
3735 /// # Ok::<(), pmcp::Error>(())
3736 /// # }
3737 /// # }
3738 /// ```
3739 #[cfg(feature = "schema-generation")]
3740 pub fn tool_typed_sync<T, F>(mut self, name: impl Into<String>, handler: F) -> Self
3741 where
3742 T: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
3743 F: Fn(T, crate::RequestHandlerExtra) -> crate::Result<serde_json::Value>
3744 + Send
3745 + Sync
3746 + 'static,
3747 {
3748 use crate::server::typed_tool::TypedSyncTool;
3749 let name_str = name.into();
3750 let tool = TypedSyncTool::new(name_str.clone(), handler);
3751 self.tools.insert(name_str, Arc::new(tool));
3752
3753 // Update capabilities to include tools
3754 if self.capabilities.tools.is_none() {
3755 self.capabilities.tools = Some(crate::types::ToolCapabilities {
3756 list_changed: Some(false),
3757 });
3758 }
3759
3760 self
3761 }
3762
3763 /// Add a synchronous type-safe tool handler with automatic schema generation and description.
3764 ///
3765 /// This is a convenience overload that allows setting a description directly
3766 /// without needing to chain `.with_description()`.
3767 ///
3768 /// # Example
3769 /// ```no_run
3770 /// # #[cfg(feature = "schema-generation")]
3771 /// # {
3772 /// use pmcp::ServerBuilder;
3773 /// use schemars::JsonSchema;
3774 /// use serde::{Deserialize, Serialize};
3775 ///
3776 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
3777 /// struct MathArgs {
3778 /// /// First number
3779 /// a: f64,
3780 /// /// Second number
3781 /// b: f64,
3782 /// /// Operation to perform
3783 /// op: String,
3784 /// }
3785 ///
3786 /// let server = ServerBuilder::new()
3787 /// .name("example")
3788 /// .tool_typed_sync_with_description(
3789 /// "calculator",
3790 /// "Performs synchronous mathematical operations",
3791 /// |args: MathArgs, _| {
3792 /// let result = match args.op.as_str() {
3793 /// "add" => args.a + args.b,
3794 /// "subtract" => args.a - args.b,
3795 /// "multiply" => args.a * args.b,
3796 /// "divide" => args.a / args.b,
3797 /// _ => return Err(pmcp::Error::Validation("Unknown operation".into())),
3798 /// };
3799 /// Ok(serde_json::json!({ "result": result }))
3800 /// }
3801 /// );
3802 /// # }
3803 /// ```
3804 #[cfg(feature = "schema-generation")]
3805 pub fn tool_typed_sync_with_description<T, F>(
3806 mut self,
3807 name: impl Into<String>,
3808 description: impl Into<String>,
3809 handler: F,
3810 ) -> Self
3811 where
3812 T: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
3813 F: Fn(T, crate::RequestHandlerExtra) -> crate::Result<serde_json::Value>
3814 + Send
3815 + Sync
3816 + 'static,
3817 {
3818 use crate::server::typed_tool::TypedSyncTool;
3819 let name_str = name.into();
3820 let tool = TypedSyncTool::new(name_str.clone(), handler).with_description(description);
3821 self.tools.insert(name_str, Arc::new(tool));
3822
3823 // Update capabilities to include tools
3824 if self.capabilities.tools.is_none() {
3825 self.capabilities.tools = Some(crate::types::ToolCapabilities {
3826 list_changed: Some(false),
3827 });
3828 }
3829
3830 self
3831 }
3832
3833 /// Add a type-safe tool handler with both input and output typing.
3834 ///
3835 /// This method provides full type safety for both input and output types,
3836 /// which is useful for testing, documentation, and API contracts.
3837 /// Note that output schemas are not part of the MCP protocol but can be
3838 /// valuable for development and integration testing.
3839 ///
3840 /// # Type Parameters
3841 ///
3842 /// * `TIn` - Input type that implements `JsonSchema`, `Deserialize`, `Send`, `Sync`
3843 /// * `TOut` - Output type that implements `JsonSchema`, `Serialize`, `Send`, `Sync`
3844 ///
3845 /// # Example
3846 /// ```no_run
3847 /// # #[cfg(feature = "schema-generation")]
3848 /// # {
3849 /// use pmcp::{ServerBuilder, TypedToolWithOutput};
3850 /// use schemars::JsonSchema;
3851 /// use serde::{Deserialize, Serialize};
3852 ///
3853 /// #[derive(JsonSchema, Deserialize)]
3854 /// struct MathInput { a: f64, b: f64, op: String }
3855 ///
3856 /// #[derive(JsonSchema, Serialize)]
3857 /// struct MathOutput { result: f64, operation: String }
3858 ///
3859 /// let server = ServerBuilder::new()
3860 /// .name("example")
3861 /// .tool_typed_with_output::<MathInput, MathOutput>("math", |args, _| {
3862 /// Box::pin(async move {
3863 /// let result = match args.op.as_str() {
3864 /// "add" => args.a + args.b,
3865 /// "subtract" => args.a - args.b,
3866 /// _ => return Err(pmcp::Error::Validation("Unknown operation".into())),
3867 /// };
3868 /// Ok(MathOutput {
3869 /// result,
3870 /// operation: args.op,
3871 /// })
3872 /// })
3873 /// });
3874 /// # }
3875 /// ```
3876 #[cfg(feature = "schema-generation")]
3877 pub fn tool_typed_with_output<TIn, TOut>(
3878 mut self,
3879 name: impl Into<String>,
3880 handler: impl Fn(
3881 TIn,
3882 crate::RequestHandlerExtra,
3883 )
3884 -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<TOut>> + Send>>
3885 + Send
3886 + Sync
3887 + 'static,
3888 ) -> Self
3889 where
3890 TIn: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
3891 TOut: serde::Serialize + schemars::JsonSchema + Send + Sync + 'static,
3892 {
3893 use crate::server::typed_tool::TypedToolWithOutput;
3894
3895 let name_str = name.into();
3896 let tool = TypedToolWithOutput::new(name_str.clone(), handler);
3897 self.tools.insert(name_str, Arc::new(tool));
3898
3899 // Update capabilities to include tools
3900 if self.capabilities.tools.is_none() {
3901 self.capabilities.tools = Some(crate::types::ToolCapabilities {
3902 list_changed: Some(false),
3903 });
3904 }
3905
3906 self
3907 }
3908
3909 /// Register a tool whose async closure returns a full [`CallToolResult`] the
3910 /// handler owns end-to-end, emitted to the wire **VERBATIM**.
3911 ///
3912 /// This mirrors [`tool_typed_with_output`](Self::tool_typed_with_output) but
3913 /// fixes the return type to
3914 /// [`CallToolResult`], so a handler can attach
3915 /// task augmentation (`CallToolResult::with_related_task(...)`), custom
3916 /// `_meta`, structured content, or an error envelope in ONE call — no
3917 /// hand-written [`ToolHandler`] `impl` required. The input
3918 /// arg type `TIn` deserializes from the tool arguments exactly as with
3919 /// [`tool_typed`](Self::tool_typed).
3920 ///
3921 /// # ⚠️ BYPASS WARNING — the returned result is sent to the wire VERBATIM
3922 ///
3923 /// The closure's [`CallToolResult`] is routed
3924 /// through [`ToolOutput::Result`] and
3925 /// therefore **BYPASSES response middleware** — redaction, sanitization, and
3926 /// audit hooks (`ToolMiddleware::on_response`) DO NOT run — as well as
3927 /// text-wrapping and widget enrichment. The handler owns its OWN redaction
3928 /// and sanitization of both `content` and `_meta`, at the same trust level as
3929 /// returning a raw `Value` today (D-04a). **Request** middleware still runs
3930 /// before the handler, and handler errors still route through the normal
3931 /// error path.
3932 ///
3933 /// To advertise a human-readable tool description in `tools/list`, use
3934 /// [`tool_with_result_and_description`](Self::tool_with_result_and_description).
3935 ///
3936 /// # Example
3937 /// ```no_run
3938 /// # #[cfg(feature = "schema-generation")]
3939 /// # {
3940 /// use pmcp::ServerBuilder;
3941 /// use pmcp::types::CallToolResult;
3942 /// use pmcp::types::tasks::TaskMetadata;
3943 /// use pmcp::types::Content;
3944 /// use schemars::JsonSchema;
3945 /// use serde::Deserialize;
3946 ///
3947 /// #[derive(JsonSchema, Deserialize)]
3948 /// struct RunArgs { job: String }
3949 ///
3950 /// let server = ServerBuilder::new()
3951 /// .name("example")
3952 /// .tool_with_result("start_job", |args: RunArgs, _extra| {
3953 /// Box::pin(async move {
3954 /// Ok(CallToolResult::new(vec![Content::text(
3955 /// format!("started {}", args.job),
3956 /// )])
3957 /// .with_related_task(TaskMetadata::new("t1")))
3958 /// })
3959 /// })
3960 /// .build();
3961 /// # }
3962 /// ```
3963 #[cfg(feature = "schema-generation")]
3964 pub fn tool_with_result<TIn>(
3965 mut self,
3966 name: impl Into<String>,
3967 handler: impl Fn(
3968 TIn,
3969 crate::RequestHandlerExtra,
3970 ) -> std::pin::Pin<
3971 Box<
3972 dyn std::future::Future<Output = crate::Result<crate::types::CallToolResult>>
3973 + Send,
3974 >,
3975 > + Send
3976 + Sync
3977 + 'static,
3978 ) -> Self
3979 where
3980 TIn: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
3981 {
3982 use crate::server::typed_tool::TypedToolWithResult;
3983
3984 let name_str = name.into();
3985 let tool = TypedToolWithResult::new(name_str.clone(), handler);
3986 self.tools.insert(name_str, Arc::new(tool));
3987
3988 // Update capabilities to include tools
3989 if self.capabilities.tools.is_none() {
3990 self.capabilities.tools = Some(crate::types::ToolCapabilities {
3991 list_changed: Some(false),
3992 });
3993 }
3994
3995 self
3996 }
3997
3998 /// [`tool_with_result`](Self::tool_with_result) WITH a human-readable
3999 /// description advertised in `tools/list`.
4000 ///
4001 /// Identical to [`tool_with_result`](Self::tool_with_result) — including
4002 /// the **BYPASS WARNING** documented there (the returned
4003 /// [`CallToolResult`] goes to the wire
4004 /// VERBATIM and skips response middleware) — but also sets the tool
4005 /// description, mirroring
4006 /// [`tool_typed_with_description`](Self::tool_typed_with_description).
4007 /// A description materially improves LLM tool selection; prefer this
4008 /// overload over the description-less
4009 /// [`tool_with_result`](Self::tool_with_result).
4010 ///
4011 /// # Example
4012 /// ```no_run
4013 /// # #[cfg(feature = "schema-generation")]
4014 /// # {
4015 /// use pmcp::ServerBuilder;
4016 /// use pmcp::types::CallToolResult;
4017 /// use pmcp::types::tasks::TaskMetadata;
4018 /// use pmcp::types::Content;
4019 /// use schemars::JsonSchema;
4020 /// use serde::Deserialize;
4021 ///
4022 /// #[derive(JsonSchema, Deserialize)]
4023 /// struct RunArgs { job: String }
4024 ///
4025 /// let server = ServerBuilder::new()
4026 /// .name("example")
4027 /// .tool_with_result_and_description(
4028 /// "start_job",
4029 /// "Start a background export job and return its task handle",
4030 /// |args: RunArgs, _extra| {
4031 /// Box::pin(async move {
4032 /// Ok(CallToolResult::new(vec![Content::text(
4033 /// format!("started {}", args.job),
4034 /// )])
4035 /// .with_related_task(TaskMetadata::new("t1")))
4036 /// })
4037 /// },
4038 /// )
4039 /// .build();
4040 /// # }
4041 /// ```
4042 #[cfg(feature = "schema-generation")]
4043 pub fn tool_with_result_and_description<TIn>(
4044 mut self,
4045 name: impl Into<String>,
4046 description: impl Into<String>,
4047 handler: impl Fn(
4048 TIn,
4049 crate::RequestHandlerExtra,
4050 ) -> std::pin::Pin<
4051 Box<
4052 dyn std::future::Future<Output = crate::Result<crate::types::CallToolResult>>
4053 + Send,
4054 >,
4055 > + Send
4056 + Sync
4057 + 'static,
4058 ) -> Self
4059 where
4060 TIn: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
4061 {
4062 use crate::server::typed_tool::TypedToolWithResult;
4063
4064 let name_str = name.into();
4065 let tool =
4066 TypedToolWithResult::new(name_str.clone(), handler).with_description(description);
4067 self.tools.insert(name_str, Arc::new(tool));
4068
4069 // Update capabilities to include tools
4070 if self.capabilities.tools.is_none() {
4071 self.capabilities.tools = Some(crate::types::ToolCapabilities {
4072 list_changed: Some(false),
4073 });
4074 }
4075
4076 self
4077 }
4078
4079 /// Add a type-safe tool handler with both input and output typing and description.
4080 ///
4081 /// This is a convenience overload that allows setting a description directly
4082 /// without needing to chain `.with_description()`.
4083 ///
4084 /// # Example
4085 /// ```no_run
4086 /// # #[cfg(feature = "schema-generation")]
4087 /// # {
4088 /// use pmcp::ServerBuilder;
4089 /// use schemars::JsonSchema;
4090 /// use serde::{Deserialize, Serialize};
4091 ///
4092 /// #[derive(JsonSchema, Deserialize)]
4093 /// struct MathInput { a: f64, b: f64, op: String }
4094 ///
4095 /// #[derive(JsonSchema, Serialize)]
4096 /// struct MathOutput { result: f64, operation: String }
4097 ///
4098 /// let server = ServerBuilder::new()
4099 /// .name("example")
4100 /// .tool_typed_with_output_and_description::<MathInput, MathOutput>(
4101 /// "math",
4102 /// "Performs basic mathematical operations on two numbers",
4103 /// |args, _| {
4104 /// Box::pin(async move {
4105 /// let result = match args.op.as_str() {
4106 /// "add" => args.a + args.b,
4107 /// "subtract" => args.a - args.b,
4108 /// _ => return Err(pmcp::Error::Validation("Unknown operation".into())),
4109 /// };
4110 /// Ok(MathOutput { result, operation: args.op })
4111 /// })
4112 /// }
4113 /// );
4114 /// # }
4115 /// ```
4116 #[cfg(feature = "schema-generation")]
4117 pub fn tool_typed_with_output_and_description<TIn, TOut>(
4118 mut self,
4119 name: impl Into<String>,
4120 description: impl Into<String>,
4121 handler: impl Fn(
4122 TIn,
4123 crate::RequestHandlerExtra,
4124 )
4125 -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<TOut>> + Send>>
4126 + Send
4127 + Sync
4128 + 'static,
4129 ) -> Self
4130 where
4131 TIn: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
4132 TOut: serde::Serialize + schemars::JsonSchema + Send + Sync + 'static,
4133 {
4134 use crate::server::typed_tool::TypedToolWithOutput;
4135
4136 let name_str = name.into();
4137 let tool =
4138 TypedToolWithOutput::new(name_str.clone(), handler).with_description(description);
4139 self.tools.insert(name_str, Arc::new(tool));
4140
4141 // Update capabilities to include tools
4142 if self.capabilities.tools.is_none() {
4143 self.capabilities.tools = Some(crate::types::ToolCapabilities {
4144 list_changed: Some(false),
4145 });
4146 }
4147
4148 self
4149 }
4150
4151 /// Add a prompt handler.
4152 ///
4153 /// Registers a prompt that clients can retrieve via the prompts/get method.
4154 /// Prompts provide templates that clients can use for various tasks.
4155 ///
4156 /// # Arguments
4157 ///
4158 /// * `name` - The name of the prompt (used by clients to retrieve it)
4159 /// * `handler` - The handler implementation for this prompt
4160 ///
4161 /// # Examples
4162 ///
4163 /// ```rust,no_run
4164 /// use pmcp::{Server, PromptHandler, GetPromptResult, PromptMessage, Content};
4165 /// use async_trait::async_trait;
4166 /// use std::collections::HashMap;
4167 ///
4168 /// struct CodeReviewPrompt;
4169 ///
4170 /// #[async_trait]
4171 /// impl PromptHandler for CodeReviewPrompt {
4172 /// async fn handle(&self, args: HashMap<String, String>, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<GetPromptResult> {
4173 /// let language = args.get("language").map(|s| s.as_str()).unwrap_or("unknown");
4174 /// Ok(GetPromptResult::new(
4175 /// vec![PromptMessage::user(pmcp::Content::text(format!(
4176 /// "Please review this {} code:",
4177 /// language
4178 /// )))],
4179 /// Some(format!("Code review prompt for {}", language)),
4180 /// ))
4181 /// }
4182 /// }
4183 ///
4184 /// let server = Server::builder()
4185 /// .name("code-server")
4186 /// .version("1.0.0")
4187 /// .prompt("code_review", CodeReviewPrompt{})
4188 /// .build()?;
4189 /// # Ok::<(), pmcp::Error>(())
4190 /// ```
4191 pub fn prompt(
4192 mut self,
4193 name: impl Into<String>,
4194 handler: impl PromptHandler + 'static,
4195 ) -> Self {
4196 self.prompts.insert(name.into(), Arc::new(handler));
4197
4198 // Update capabilities to include prompts
4199 // Use Some(false) instead of None to ensure the field serializes properly
4200 if self.capabilities.prompts.is_none() {
4201 self.capabilities.prompts = Some(crate::types::PromptCapabilities {
4202 list_changed: Some(false),
4203 });
4204 }
4205
4206 self
4207 }
4208
4209 /// Add a prompt handler with an Arc.
4210 ///
4211 /// This variant lets the caller share the handler `Arc` between the
4212 /// builder and an external in-process handler map (e.g., a downstream
4213 /// toolkit's handler registry) without writing a delegating wrapper
4214 /// shim. Behavior is otherwise identical to [`Self::prompt`]: the first
4215 /// registration auto-enables `capabilities.prompts`.
4216 pub fn prompt_arc(mut self, name: impl Into<String>, handler: Arc<dyn PromptHandler>) -> Self {
4217 let name = name.into();
4218 self.prompts.insert(name, handler);
4219
4220 // Update capabilities to include prompts
4221 // Use Some(false) instead of None to ensure the field serializes properly
4222 if self.capabilities.prompts.is_none() {
4223 self.capabilities.prompts = Some(crate::types::PromptCapabilities {
4224 list_changed: Some(false),
4225 });
4226 }
4227
4228 self
4229 }
4230
4231 /// Register a workflow-based prompt with automatic validation.
4232 ///
4233 /// This method validates the workflow before registration and converts it
4234 /// to a prompt handler. The workflow's instructions become the prompt messages,
4235 /// and the workflow's arguments become the prompt arguments.
4236 ///
4237 /// # Arguments
4238 ///
4239 /// * `workflow` - The workflow definition to register as a prompt
4240 ///
4241 /// # Errors
4242 ///
4243 /// Returns an error if the workflow validation fails (e.g., undefined bindings,
4244 /// undefined prompt arguments, etc.).
4245 ///
4246 /// # Examples
4247 ///
4248 /// ```rust,no_run
4249 /// use pmcp::{Server, ServerBuilder};
4250 /// use pmcp::server::workflow::{SequentialWorkflow, InternalPromptMessage};
4251 /// use pmcp::types::Role;
4252 ///
4253 /// # fn main() -> pmcp::Result<()> {
4254 /// let workflow = SequentialWorkflow::new(
4255 /// "code_review_workflow",
4256 /// "Review code with multiple steps"
4257 /// )
4258 /// .argument("code", "Code to review", true)
4259 /// .instruction(InternalPromptMessage::new(
4260 /// Role::System,
4261 /// "You are a code reviewer. Review the provided code carefully."
4262 /// ));
4263 ///
4264 /// let server = Server::builder()
4265 /// .name("code-server")
4266 /// .version("1.0.0")
4267 /// .prompt_workflow(workflow)?
4268 /// .build()?;
4269 /// # Ok(())
4270 /// # }
4271 /// ```
4272 pub fn prompt_workflow(mut self, workflow: workflow::SequentialWorkflow) -> Result<Self> {
4273 // Validate the workflow before registration
4274 workflow
4275 .validate()
4276 .map_err(|e| Error::Validation(format!("Workflow validation failed: {}", e)))?;
4277
4278 // Build tool and resource registries from currently registered handlers
4279 // Note: This captures the current state of registered tools/resources
4280 let mut tools = std::collections::HashMap::new();
4281 for (name, handler) in &self.tools {
4282 if let Some(metadata) = handler.metadata() {
4283 tools.insert(
4284 Arc::from(name.as_str()),
4285 workflow::conversion::ToolInfo {
4286 name: metadata.name,
4287 description: metadata.description.unwrap_or_default(),
4288 input_schema: metadata.input_schema,
4289 },
4290 );
4291 }
4292 }
4293
4294 // Build tool handlers map for workflow execution
4295 // Clone Arc references for shared ownership
4296 let mut tool_handlers: std::collections::HashMap<Arc<str>, Arc<dyn ToolHandler>> =
4297 std::collections::HashMap::new();
4298 for (name, handler) in &self.tools {
4299 tool_handlers.insert(Arc::from(name.as_str()), Arc::clone(handler));
4300 }
4301
4302 // Get the workflow name before moving it
4303 let name = workflow.name().to_string();
4304
4305 // Create workflow prompt handler with tool execution and resource fetching capability
4306 // Note: Workflow prompts in ServerBuilder do not currently execute tool middleware.
4307 // For middleware support in workflow tool execution, use ServerCoreBuilder.
4308 let handler = workflow::WorkflowPromptHandler::new(
4309 workflow,
4310 tools,
4311 tool_handlers,
4312 self.resources.clone(),
4313 );
4314
4315 // Register as a prompt
4316 self.prompts.insert(name, Arc::new(handler));
4317
4318 // Update capabilities to include prompts
4319 // This ensures prompts/list returns the workflow prompts
4320 if self.capabilities.prompts.is_none() {
4321 self.capabilities.prompts = Some(crate::types::PromptCapabilities {
4322 list_changed: Some(false),
4323 });
4324 }
4325
4326 Ok(self)
4327 }
4328
4329 /// Set the resource handler.
4330 ///
4331 /// Registers a resource handler that provides access to server resources.
4332 /// Resources allow clients to read files, configurations, or other data.
4333 ///
4334 /// # Arguments
4335 ///
4336 /// * `handler` - The resource handler implementation
4337 ///
4338 /// # Examples
4339 ///
4340 /// ```rust,no_run
4341 /// use pmcp::{Server, ResourceHandler, ReadResourceResult, ListResourcesResult, ResourceInfo};
4342 /// use async_trait::async_trait;
4343 ///
4344 /// struct FileResourceHandler;
4345 ///
4346 /// #[async_trait]
4347 /// impl ResourceHandler for FileResourceHandler {
4348 /// async fn read(&self, uri: &str, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<ReadResourceResult> {
4349 /// // Read file content...
4350 /// Ok(ReadResourceResult::new(vec![pmcp::Content::text("File content here")]))
4351 /// }
4352 ///
4353 /// async fn list(&self, _cursor: Option<String>, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<ListResourcesResult> {
4354 /// Ok(ListResourcesResult::new(vec![
4355 /// pmcp::ResourceInfo::new("file://example.txt", "example.txt")
4356 /// .with_description("Example file")
4357 /// .with_mime_type("text/plain"),
4358 /// ]))
4359 /// }
4360 /// }
4361 ///
4362 /// let server = Server::builder()
4363 /// .name("file-server")
4364 /// .version("1.0.0")
4365 /// .resources(FileResourceHandler{})
4366 /// .build()?;
4367 /// # Ok::<(), pmcp::Error>(())
4368 /// ```
4369 pub fn resources(mut self, handler: impl ResourceHandler + 'static) -> Self {
4370 self.resources = Some(Arc::new(handler));
4371
4372 // Update capabilities to include resources
4373 // Use Some(false) instead of None to ensure fields serialize properly
4374 if self.capabilities.resources.is_none() {
4375 self.capabilities.resources = Some(crate::types::ResourceCapabilities {
4376 subscribe: Some(false),
4377 list_changed: Some(false),
4378 });
4379 }
4380
4381 self
4382 }
4383
4384 /// Set the resource handler with an Arc.
4385 ///
4386 /// This variant lets the caller share the handler `Arc` between the
4387 /// builder and an external in-process handler map without writing a
4388 /// delegating wrapper. Behavior is otherwise identical to
4389 /// [`Self::resources`]: the first registration auto-enables
4390 /// `capabilities.resources`.
4391 pub fn resources_arc(mut self, handler: Arc<dyn ResourceHandler>) -> Self {
4392 self.resources = Some(handler);
4393
4394 // Update capabilities to include resources
4395 // Use Some(false) instead of None to ensure fields serialize properly
4396 if self.capabilities.resources.is_none() {
4397 self.capabilities.resources = Some(crate::types::ResourceCapabilities {
4398 subscribe: Some(false),
4399 list_changed: Some(false),
4400 });
4401 }
4402
4403 self
4404 }
4405
4406 /// Set the completion provider backing `completion/complete`.
4407 ///
4408 /// The twin of
4409 /// [`ServerCoreBuilder::completions`](crate::server::builder::ServerCoreBuilder::completions) —
4410 /// same name, same signature, same single-provider shape — so a provider
4411 /// registered through EITHER builder family reaches its own dispatcher. A
4412 /// slot on one family with the dispatch arm on the other's server would be
4413 /// an unreachable seam that still answered the spec shape, which is exactly
4414 /// the false green this pair exists to prevent.
4415 ///
4416 /// A SINGLE, server-wide provider (the [`Self::resources`] shape, not the
4417 /// name-keyed [`Self::prompt`] shape): the spec routes every
4418 /// `completion/complete` to one seam and passes the `ref` as data. The
4419 /// reference reaches the provider through
4420 /// [`CompletionRequest::context`](crate::types::completable::CompletionRequest::context)
4421 /// under the key `ref/prompt` or `ref/resource`.
4422 ///
4423 /// Registering a provider auto-advertises `capabilities.completions`.
4424 /// Not registering one is NOT an error: `completion/complete` still answers
4425 /// `{"completion": {"values": []}}`.
4426 ///
4427 /// # Examples
4428 ///
4429 /// ```rust
4430 /// use pmcp::Server;
4431 /// use pmcp::types::completable::StaticCompletionProvider;
4432 ///
4433 /// let server = Server::builder()
4434 /// .name("completion-server")
4435 /// .version("1.0.0")
4436 /// .completions(StaticCompletionProvider::from_strings(vec![
4437 /// "alpha".to_string(),
4438 /// "beta".to_string(),
4439 /// ]))
4440 /// .build()?;
4441 /// # Ok::<(), pmcp::Error>(())
4442 /// ```
4443 #[must_use]
4444 pub fn completions(
4445 self,
4446 provider: impl crate::types::completable::CompletionProviderTrait + 'static,
4447 ) -> Self {
4448 self.completions_arc(Arc::new(provider))
4449 }
4450
4451 /// Set the completion provider with an Arc.
4452 ///
4453 /// This variant lets the caller share the provider `Arc` with something
4454 /// outside the builder. Behavior is otherwise identical to
4455 /// [`Self::completions`].
4456 #[must_use]
4457 pub fn completions_arc(
4458 mut self,
4459 provider: Arc<dyn crate::types::completable::CompletionProviderTrait>,
4460 ) -> Self {
4461 self.completions = Some(provider);
4462
4463 // Update capabilities to include completions.
4464 // Use Some(default) instead of None to ensure the field serializes.
4465 if self.capabilities.completions.is_none() {
4466 self.capabilities.completions = Some(crate::types::CompletionCapabilities::default());
4467 }
4468
4469 self
4470 }
4471
4472 /// Register a single SEP-2640 Agent Skill.
4473 ///
4474 /// Convenience over [`Self::skills`] for the single-skill case. The skill
4475 /// is accumulated and finalized into a `SkillsHandler` exactly once at
4476 /// [`Self::build`] time, then composed with any `.resources(...)`
4477 /// handler set on this builder.
4478 ///
4479 /// # Panics
4480 ///
4481 /// Panics at `.build()` time if multiple registered skills resolve to
4482 /// the same `skill://` URI. Use [`Self::try_skills`] with a pre-built
4483 /// [`skills::Skills`] registry to surface duplicates as a `Result`.
4484 ///
4485 /// # Examples
4486 ///
4487 /// ```rust,no_run
4488 /// # #[cfg(feature = "skills")] {
4489 /// use pmcp::{Server, server::skills::Skill};
4490 ///
4491 /// # fn example() -> pmcp::Result<()> {
4492 /// let server = Server::builder()
4493 /// .name("my-server")
4494 /// .version("1.0.0")
4495 /// .skill(Skill::new("hello", "# Hello skill"))
4496 /// .build()?;
4497 /// # Ok(())
4498 /// # }
4499 /// # }
4500 /// ```
4501 #[cfg(feature = "skills")]
4502 #[must_use]
4503 pub fn skill(self, skill: skills::Skill) -> Self {
4504 self.skills(skills::Skills::new().add(skill))
4505 }
4506
4507 /// Register a registry of SEP-2640 Agent Skills.
4508 ///
4509 /// Merges into any prior accumulated skills (a previous `.skill(...)` or
4510 /// `.skills(...)` call). The accumulated registry is finalized into a
4511 /// single `SkillsHandler` exactly once at [`Self::build`] time, then
4512 /// composed at most once with any `.resources(...)` handler.
4513 ///
4514 /// # Panics
4515 ///
4516 /// Panics at `.build()` if two registered skills resolve to the same
4517 /// `skill://` URI. Use [`Self::try_skills`] for fallible registration.
4518 #[cfg(feature = "skills")]
4519 #[must_use]
4520 pub fn skills(mut self, skills_registry: skills::Skills) -> Self {
4521 let merged = match self.pending_skills.take() {
4522 Some(prior) => prior.merge(skills_registry),
4523 None => skills_registry,
4524 };
4525 self.pending_skills = Some(merged);
4526 skills::set_skills_capabilities(&mut self.capabilities);
4527 self
4528 }
4529
4530 /// Fallible variant of [`Self::skills`] — returns `Err` immediately if
4531 /// the merged registry would contain duplicate URIs. Useful for
4532 /// runtime-dynamic registration where panicking is unacceptable.
4533 ///
4534 /// # Errors
4535 ///
4536 /// Returns `Err(pmcp::Error::Validation)` if the merged registry would
4537 /// produce duplicate `skill://` URIs.
4538 #[cfg(feature = "skills")]
4539 pub fn try_skills(mut self, skills_registry: skills::Skills) -> Result<Self> {
4540 let merged = match self.pending_skills.take() {
4541 Some(prior) => prior.merge(skills_registry),
4542 None => skills_registry,
4543 };
4544 // Probe by cloning + into_handler; discard the handler. The real
4545 // construction happens in `.build()` once everything is settled.
4546 merged.clone().into_handler()?;
4547 self.pending_skills = Some(merged);
4548 skills::set_skills_capabilities(&mut self.capabilities);
4549 Ok(self)
4550 }
4551
4552 /// Register a skill AND a parallel prompt that returns the same content.
4553 ///
4554 /// The dual-surface bootstrap: both surfaces are derived from one
4555 /// [`skills::Skill`] value so they cannot drift. The byte-equality
4556 /// between surfaces is asserted by the skills integration test.
4557 #[cfg(feature = "skills")]
4558 #[must_use]
4559 pub fn bootstrap_skill_and_prompt(
4560 self,
4561 skill: skills::Skill,
4562 prompt_name: impl Into<String>,
4563 ) -> Self {
4564 let prompt_handler = skills::SkillPromptHandler::new(skill.clone());
4565 self.skill(skill).prompt(prompt_name, prompt_handler)
4566 }
4567
4568 /// Set the sampling handler.
4569 ///
4570 /// Registers a sampling handler that provides LLM functionality.
4571 /// This allows the server to act as a language model provider.
4572 ///
4573 /// # Arguments
4574 ///
4575 /// * `handler` - The sampling handler implementation
4576 ///
4577 /// # Examples
4578 ///
4579 /// ```rust,no_run
4580 /// use pmcp::{Server, SamplingHandler, CreateMessageParams, CreateMessageResult};
4581 /// use async_trait::async_trait;
4582 ///
4583 /// struct MockLLM;
4584 ///
4585 /// #[async_trait]
4586 /// impl SamplingHandler for MockLLM {
4587 /// async fn create_message(&self, params: CreateMessageParams, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<CreateMessageResult> {
4588 /// // Process the messages and generate a response
4589 /// Ok(CreateMessageResult::new(pmcp::Content::text("Generated response"), "mock-llm-v1")
4590 /// .with_usage(pmcp::TokenUsage::new(10, 5, 15))
4591 /// .with_stop_reason("end_of_text"))
4592 /// }
4593 /// }
4594 ///
4595 /// let server = Server::builder()
4596 /// .name("llm-server")
4597 /// .version("1.0.0")
4598 /// .sampling(MockLLM{})
4599 /// .build()?;
4600 /// # Ok::<(), pmcp::Error>(())
4601 /// ```
4602 pub fn sampling(mut self, handler: impl SamplingHandler + 'static) -> Self {
4603 self.sampling = Some(Arc::new(handler));
4604 // Enable sampling capability
4605 self.capabilities.sampling = Some(crate::types::SamplingCapabilities::default());
4606 self
4607 }
4608
4609 /// Set the sampling handler with an Arc.
4610 ///
4611 /// This variant lets the caller share the handler `Arc` between the
4612 /// builder and an external in-process handler map without writing a
4613 /// delegating wrapper. Uses the donor's `if is_none` capability
4614 /// auto-enable so an explicit prior `.capabilities(custom)` is not
4615 /// clobbered by a later `_arc` registration.
4616 pub fn sampling_arc(mut self, handler: Arc<dyn SamplingHandler>) -> Self {
4617 self.sampling = Some(handler);
4618
4619 // Update capabilities to include sampling
4620 if self.capabilities.sampling.is_none() {
4621 self.capabilities.sampling = Some(crate::types::SamplingCapabilities::default());
4622 }
4623
4624 self
4625 }
4626
4627 /// Build the server.
4628 ///
4629 /// Constructs the final Server instance from the configured builder.
4630 /// This validates that required fields (name and version) are set.
4631 ///
4632 /// # Examples
4633 ///
4634 /// ```rust,no_run
4635 /// use pmcp::{Server, ToolHandler};
4636 /// use async_trait::async_trait;
4637 /// use serde_json::Value;
4638 ///
4639 /// struct PingTool;
4640 ///
4641 /// #[async_trait]
4642 /// impl ToolHandler for PingTool {
4643 /// async fn handle(&self, _args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
4644 /// Ok(serde_json::json!({"response": "pong"}))
4645 /// }
4646 /// }
4647 ///
4648 /// let server = Server::builder()
4649 /// .name("ping-server")
4650 /// .version("1.0.0")
4651 /// .tool("ping", PingTool{})
4652 /// .build()?;
4653 ///
4654 /// // Server is now ready to run
4655 /// // server.run_stdio().await?;
4656 /// # Ok::<(), pmcp::Error>(())
4657 /// ```
4658 /// Set the authentication provider.
4659 ///
4660 /// Configures an authentication provider that will validate incoming requests.
4661 /// When set, the server will use this provider to authenticate requests before
4662 /// processing them.
4663 ///
4664 /// # Arguments
4665 ///
4666 /// * `provider` - The authentication provider implementation
4667 ///
4668 /// # Examples
4669 ///
4670 /// ```rust,no_run
4671 /// use pmcp::{Server, auth::ProxyProvider};
4672 ///
4673 /// let auth_provider = ProxyProvider::with_upstream("https://oauth.example.com");
4674 ///
4675 /// let server = Server::builder()
4676 /// .name("secure-server")
4677 /// .version("1.0.0")
4678 /// .auth_provider(auth_provider)
4679 /// .build()?;
4680 /// # Ok::<(), pmcp::Error>(())
4681 /// ```
4682 pub fn auth_provider(mut self, provider: impl auth::AuthProvider + 'static) -> Self {
4683 self.auth_provider = Some(Arc::new(provider));
4684 self
4685 }
4686
4687 /// Set the authentication provider with an Arc.
4688 ///
4689 /// This variant lets the caller share the provider `Arc` between the
4690 /// builder and an external in-process registry without writing a
4691 /// delegating wrapper. Behavior is otherwise identical to
4692 /// [`Self::auth_provider`].
4693 pub fn auth_provider_arc(mut self, provider: Arc<dyn auth::AuthProvider>) -> Self {
4694 self.auth_provider = Some(provider);
4695 self
4696 }
4697
4698 /// Set the tool authorizer.
4699 ///
4700 /// Configures a tool authorizer for fine-grained access control.
4701 /// The authorizer determines which tools authenticated users can access
4702 /// based on their authentication context.
4703 ///
4704 /// # Arguments
4705 ///
4706 /// * `authorizer` - The tool authorization implementation
4707 ///
4708 /// # Examples
4709 ///
4710 /// ```rust,no_run
4711 /// use pmcp::{Server, auth::ScopeBasedAuthorizer};
4712 ///
4713 /// let authorizer = ScopeBasedAuthorizer::new()
4714 /// .require_scopes("sensitive_tool", vec!["admin".to_string()])
4715 /// .default_scopes(vec!["read".to_string()]);
4716 ///
4717 /// let server = Server::builder()
4718 /// .name("secure-server")
4719 /// .version("1.0.0")
4720 /// .tool_authorizer(authorizer)
4721 /// .build()?;
4722 /// # Ok::<(), pmcp::Error>(())
4723 /// ```
4724 pub fn tool_authorizer(mut self, authorizer: impl auth::ToolAuthorizer + 'static) -> Self {
4725 if !self.tool_protections.is_empty() {
4726 // Log a warning - custom authorizer supersedes protect_tool() configurations
4727 tracing::warn!(
4728 target: "mcp.auth",
4729 "Setting a custom tool_authorizer clears any previous protect_tool() configurations"
4730 );
4731 self.tool_protections.clear();
4732 }
4733 self.tool_authorizer = Some(Arc::new(authorizer));
4734 self
4735 }
4736
4737 /// Set the tool authorizer with an Arc.
4738 ///
4739 /// This variant lets the caller share the authorizer `Arc` between
4740 /// the builder and an external in-process registry without writing a
4741 /// delegating wrapper. Mirrors [`Self::tool_authorizer`]'s
4742 /// protection-clearing semantics: if any prior `protect_tool()`
4743 /// configurations exist, they are cleared and a `tracing::warn!` is
4744 /// emitted under target `"mcp.auth"`, since a custom authorizer
4745 /// supersedes scope-based tool protections.
4746 pub fn tool_authorizer_arc(mut self, authorizer: Arc<dyn auth::ToolAuthorizer>) -> Self {
4747 if !self.tool_protections.is_empty() {
4748 // Log a warning - custom authorizer supersedes protect_tool() configurations
4749 tracing::warn!(
4750 target: "mcp.auth",
4751 "Setting a custom tool_authorizer clears any previous protect_tool() configurations"
4752 );
4753 self.tool_protections.clear();
4754 }
4755 self.tool_authorizer = Some(authorizer);
4756 self
4757 }
4758
4759 /// Protect a specific tool with required scopes.
4760 ///
4761 /// This is a convenience method that creates or updates a scope-based authorizer
4762 /// to require specific scopes for accessing the named tool.
4763 ///
4764 /// # Arguments
4765 ///
4766 /// * `tool_name` - The name of the tool to protect
4767 /// * `scopes` - The required scopes for accessing this tool
4768 ///
4769 /// # Examples
4770 ///
4771 /// ```rust,no_run
4772 /// use pmcp::Server;
4773 ///
4774 /// let server = Server::builder()
4775 /// .name("secure-server")
4776 /// .version("1.0.0")
4777 /// .protect_tool("delete_data", vec!["admin".to_string(), "write".to_string()])
4778 /// .protect_tool("read_data", vec!["read".to_string()])
4779 /// .build()?;
4780 /// # Ok::<(), pmcp::Error>(())
4781 /// ```
4782 pub fn protect_tool(mut self, tool_name: impl Into<String>, scopes: Vec<String>) -> Self {
4783 // Store the tool protection requirements to be applied at build time
4784 self.tool_protections.insert(tool_name.into(), scopes);
4785 self
4786 }
4787
4788 /// Add tool middleware for cross-cutting concerns.
4789 ///
4790 /// Tool middleware allows you to inject cross-cutting concerns into tool execution,
4791 /// such as OAuth token injection, logging, metrics, or request transformation.
4792 /// Middleware is executed in the order it's added, both for request processing
4793 /// (before tool execution) and response processing (after tool execution).
4794 ///
4795 /// This method brings middleware support to the high-level `ServerBuilder` API,
4796 /// enabling developers to use both typed tool registration AND middleware without
4797 /// dropping down to the lower-level `ServerCoreBuilder` API.
4798 ///
4799 /// # Arguments
4800 ///
4801 /// * `middleware` - The middleware implementation to add to the chain
4802 ///
4803 /// # Examples
4804 ///
4805 /// ## OAuth Token Injection Middleware
4806 ///
4807 /// ```rust,no_run
4808 /// use pmcp::server::tool_middleware::{ToolMiddleware, ToolContext};
4809 /// use pmcp::server::cancellation::RequestHandlerExtra;
4810 /// use pmcp::Server;
4811 /// use std::sync::Arc;
4812 /// use async_trait::async_trait;
4813 /// use serde_json::Value;
4814 ///
4815 /// struct OAuthInjectionMiddleware;
4816 ///
4817 /// #[async_trait]
4818 /// impl ToolMiddleware for OAuthInjectionMiddleware {
4819 /// async fn on_request(
4820 /// &self,
4821 /// _tool_name: &str,
4822 /// _args: &mut Value,
4823 /// extra: &mut RequestHandlerExtra,
4824 /// _context: &ToolContext,
4825 /// ) -> pmcp::Result<()> {
4826 /// // Extract OAuth token from auth_context and inject into metadata
4827 /// if let Some(auth_ctx) = extra.auth_context() {
4828 /// if let Some(token) = &auth_ctx.token {
4829 /// extra.set_metadata("oauth_token".to_string(), token.clone());
4830 /// }
4831 /// }
4832 /// Ok(())
4833 /// }
4834 /// }
4835 ///
4836 /// let server = Server::builder()
4837 /// .name("oauth-server")
4838 /// .version("1.0.0")
4839 /// .tool_middleware(Arc::new(OAuthInjectionMiddleware))
4840 /// .build()?;
4841 /// # Ok::<(), pmcp::Error>(())
4842 /// ```
4843 ///
4844 /// ## Combining with Typed Tools
4845 ///
4846 /// ```rust,no_run
4847 /// # #[cfg(feature = "schema-generation")]
4848 /// # {
4849 /// use pmcp::Server;
4850 /// use schemars::JsonSchema;
4851 /// use serde::{Deserialize, Serialize};
4852 ///
4853 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
4854 /// struct ListGamesArgs {
4855 /// filter: Option<String>,
4856 /// }
4857 ///
4858 /// let server = Server::builder()
4859 /// .name("game-server")
4860 /// .version("1.0.0")
4861 /// .tool_typed_with_description(
4862 /// "list_games",
4863 /// "List all available games",
4864 /// |args: ListGamesArgs, extra| {
4865 /// Box::pin(async move {
4866 /// // Access OAuth token injected by middleware
4867 /// let _token = extra.get_metadata("oauth_token");
4868 /// Ok(serde_json::json!({"games": []}))
4869 /// })
4870 /// }
4871 /// )
4872 /// // .tool_middleware(Arc::new(oauth_middleware)) // Works with typed tools!
4873 /// .build()?;
4874 /// # }
4875 /// # Ok::<(), pmcp::Error>(())
4876 /// ```
4877 ///
4878 /// # Middleware Execution Order
4879 ///
4880 /// Multiple middleware are executed in FIFO order for requests and FIFO for responses:
4881 ///
4882 /// ```text
4883 /// Request: Middleware1 → Middleware2 → Tool Handler
4884 /// Response: Tool Handler → Middleware1 → Middleware2
4885 /// ```
4886 #[cfg(not(target_arch = "wasm32"))]
4887 pub fn tool_middleware(mut self, middleware: Arc<dyn tool_middleware::ToolMiddleware>) -> Self {
4888 self.tool_middlewares.push(middleware);
4889 self
4890 }
4891
4892 /// Enable observability for this server.
4893 ///
4894 /// This adds observability middleware that provides:
4895 /// - Distributed tracing with trace/span IDs
4896 /// - Request/response event logging
4897 /// - Metrics emission (duration, count, errors)
4898 ///
4899 /// The backend is automatically selected based on the configuration:
4900 /// - "console" - Pretty or JSON output to stdout (development)
4901 /// - "cloudwatch" - AWS `CloudWatch` EMF format (production)
4902 /// - "null" - Discards all events (testing)
4903 ///
4904 /// # Examples
4905 ///
4906 /// ```rust,no_run
4907 /// use pmcp::Server;
4908 /// use pmcp::server::observability::ObservabilityConfig;
4909 ///
4910 /// // Development: console output with pretty printing
4911 /// let server = Server::builder()
4912 /// .name("my-server")
4913 /// .version("1.0.0")
4914 /// .with_observability(ObservabilityConfig::development())
4915 /// .build()?;
4916 ///
4917 /// // Production: CloudWatch with EMF metrics
4918 /// let server = Server::builder()
4919 /// .name("my-server")
4920 /// .version("1.0.0")
4921 /// .with_observability(ObservabilityConfig::production())
4922 /// .build()?;
4923 ///
4924 /// // Auto-detect environment (Lambda vs local)
4925 /// let config = if std::env::var("AWS_LAMBDA_FUNCTION_NAME").is_ok() {
4926 /// ObservabilityConfig::production()
4927 /// } else {
4928 /// ObservabilityConfig::development()
4929 /// };
4930 /// let server = Server::builder()
4931 /// .name("my-server")
4932 /// .version("1.0.0")
4933 /// .with_observability(config)
4934 /// .build()?;
4935 /// # Ok::<(), pmcp::Error>(())
4936 /// ```
4937 #[cfg(not(target_arch = "wasm32"))]
4938 pub fn with_observability(mut self, config: observability::ObservabilityConfig) -> Self {
4939 if !config.enabled {
4940 return self;
4941 }
4942
4943 // Create backend based on configuration
4944 let backend: Arc<dyn observability::ObservabilityBackend> = match config.backend.as_str() {
4945 "cloudwatch" => Arc::new(observability::CloudWatchBackend::new(
4946 config.cloudwatch.clone(),
4947 )),
4948 "null" => Arc::new(observability::NullBackend),
4949 _ => Arc::new(observability::ConsoleBackend::new(config.console.pretty)),
4950 };
4951
4952 // Get server name for middleware (use placeholder if not yet set)
4953 let server_name = self.name.clone().unwrap_or_else(|| "unknown".to_string());
4954
4955 // Create and add the observability middleware
4956 let middleware =
4957 observability::McpObservabilityMiddleware::new(server_name, config, backend);
4958 self.tool_middlewares.push(Arc::new(middleware));
4959
4960 self
4961 }
4962
4963 /// Enable observability with a custom backend.
4964 ///
4965 /// Use this when you need a custom backend implementation (e.g., Datadog, custom metrics).
4966 ///
4967 /// # Examples
4968 ///
4969 /// ```rust,ignore
4970 /// use pmcp::Server;
4971 /// use pmcp::server::observability::{ObservabilityConfig, ObservabilityBackend};
4972 /// use std::sync::Arc;
4973 ///
4974 /// struct MyCustomBackend;
4975 ///
4976 /// #[async_trait]
4977 /// impl ObservabilityBackend for MyCustomBackend {
4978 /// // ... custom implementation
4979 /// }
4980 ///
4981 /// let server = Server::builder()
4982 /// .name("my-server")
4983 /// .version("1.0.0")
4984 /// .with_observability_backend(
4985 /// ObservabilityConfig::development(),
4986 /// Arc::new(MyCustomBackend),
4987 /// )
4988 /// .build()?;
4989 /// ```
4990 #[cfg(not(target_arch = "wasm32"))]
4991 pub fn with_observability_backend(
4992 mut self,
4993 config: observability::ObservabilityConfig,
4994 backend: Arc<dyn observability::ObservabilityBackend>,
4995 ) -> Self {
4996 if !config.enabled {
4997 return self;
4998 }
4999
5000 // Get server name for middleware (use placeholder if not yet set)
5001 let server_name = self.name.clone().unwrap_or_else(|| "unknown".to_string());
5002
5003 // Create and add the observability middleware
5004 let middleware =
5005 observability::McpObservabilityMiddleware::new(server_name, config, backend);
5006 self.tool_middlewares.push(Arc::new(middleware));
5007
5008 self
5009 }
5010
5011 /// Add a description to a tool (Note: Limited support).
5012 ///
5013 /// **Important**: Due to the immutable design of tool handlers, this method
5014 /// cannot retroactively add descriptions to already-registered tools.
5015 ///
5016 /// **Recommended**: Use the `*_with_description` variants instead:
5017 /// - `.tool_typed_with_description()`
5018 /// - `.tool_typed_sync_with_description()`
5019 /// - `.tool_typed_with_output_and_description()`
5020 ///
5021 /// This method is provided for API completeness but will log warnings
5022 /// when used, encouraging migration to the preferred approaches.
5023 ///
5024 /// # Preferred Examples
5025 ///
5026 /// ```rust,no_run
5027 /// # #[cfg(feature = "schema-generation")]
5028 /// # {
5029 /// use pmcp::ServerBuilder;
5030 /// use schemars::JsonSchema;
5031 /// use serde::{Deserialize, Serialize};
5032 ///
5033 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
5034 /// struct MathArgs { a: f64, b: f64 }
5035 ///
5036 /// // Preferred: Use the direct description variants
5037 /// let server = ServerBuilder::new()
5038 /// .name("example")
5039 /// .tool_typed_with_description(
5040 /// "add",
5041 /// "Adds two numbers together",
5042 /// |args: MathArgs, _| {
5043 /// Box::pin(async move {
5044 /// Ok(serde_json::json!({ "result": args.a + args.b }))
5045 /// })
5046 /// }
5047 /// )
5048 /// .build();
5049 /// # }
5050 /// ```
5051 #[deprecated(
5052 since = "1.6.0",
5053 note = "Use tool_typed_with_description() and similar variants instead"
5054 )]
5055 pub fn with_tool_description(
5056 self,
5057 tool_name: impl Into<String>,
5058 description: impl Into<String>,
5059 ) -> Self {
5060 let tool_name = tool_name.into();
5061 let _description = description.into();
5062
5063 tracing::warn!(
5064 "with_tool_description('{}') called but cannot modify immutable tools. \
5065 Use tool_typed_with_description() variants instead.",
5066 tool_name
5067 );
5068
5069 self
5070 }
5071
5072 /// Configure HTTP middleware chain for `StreamableHttpServer`.
5073 ///
5074 /// This is a convenience method that stores the HTTP middleware chain
5075 /// so it can be retrieved later when creating a `StreamableHttpServer`.
5076 ///
5077 /// # Arguments
5078 ///
5079 /// * `middleware` - The HTTP middleware chain
5080 ///
5081 /// # Examples
5082 ///
5083 /// ```rust,no_run
5084 /// # #[cfg(feature = "streamable-http")]
5085 /// # fn example() -> Result<(), pmcp::Error> {
5086 /// use pmcp::Server;
5087 /// use pmcp::server::http_middleware::{ServerHttpLoggingMiddleware, ServerHttpMiddlewareChain};
5088 /// use std::sync::Arc;
5089 ///
5090 /// let mut http_chain = ServerHttpMiddlewareChain::new();
5091 /// http_chain.add(Arc::new(ServerHttpLoggingMiddleware::new()));
5092 ///
5093 /// let server = Server::builder()
5094 /// .name("my-server")
5095 /// .version("1.0.0")
5096 /// .with_http_middleware(Arc::new(http_chain))
5097 /// .build()?;
5098 ///
5099 /// // Later when creating StreamableHttpServer:
5100 /// // let config = StreamableHttpServerConfig {
5101 /// // http_middleware: server.http_middleware(),
5102 /// // ..Default::default()
5103 /// // };
5104 /// # Ok(())
5105 /// # }
5106 /// ```
5107 #[cfg(feature = "streamable-http")]
5108 pub fn with_http_middleware(
5109 mut self,
5110 middleware: Arc<http_middleware::ServerHttpMiddlewareChain>,
5111 ) -> Self {
5112 self.http_middleware = Some(middleware);
5113 self
5114 }
5115
5116 /// Add a host layer for MCP Apps metadata enrichment.
5117 ///
5118 /// Host layers enrich tool `_meta` at build time with host-specific keys.
5119 /// For example, `HostType::ChatGpt` adds `openai/outputTemplate` and
5120 /// `openai/widgetAccessible` derived from the standard `ui.resourceUri`.
5121 ///
5122 /// This is opt-in — standard MCP Apps hosts (Claude Desktop, etc.) work
5123 /// without any host layer. Duplicates are ignored.
5124 #[cfg(feature = "mcp-apps")]
5125 pub fn with_host_layer(mut self, host: crate::types::mcp_apps::HostType) -> Self {
5126 if !self.host_layers.contains(&host) {
5127 self.host_layers.push(host);
5128 }
5129 self
5130 }
5131
5132 /// Build the server.
5133 ///
5134 /// Constructs the final Server instance from the configured builder.
5135 /// This validates that required fields (name and version) are set.
5136 ///
5137 /// # Errors
5138 ///
5139 /// Register a [`TaskStore`](crate::server::task_store::TaskStore) for MCP
5140 /// Tasks on the high-level HTTP-facing `Server` (RECOMMENDED tools-as-Tasks
5141 /// path).
5142 ///
5143 /// This is the recommended, all-typed path for exposing a tool as an async
5144 /// MCP Task over the `Server` / `StreamableHttpServer` path: pair a
5145 /// task-capable [`TypedTool`](crate::server::typed_tool::TypedTool) (marked
5146 /// [`with_task_support(TaskSupport::Required)`](crate::types::ToolExecution::with_task_support))
5147 /// with a store here, and the SDK serves `tasks/*` typed from the store —
5148 /// you never hand-write `tasks/*` wire JSON, and the store mints the task id.
5149 /// For the legacy experimental router path, use [`Self::with_task_store`]
5150 /// (which takes a [`TaskRouter`](crate::server::tasks::TaskRouter), NOT a
5151 /// `TaskStore`).
5152 ///
5153 /// When a task store is registered, the server:
5154 /// - **Auto-advertises** `ServerCapabilities.tasks` (with list and cancel
5155 /// support) in `initialize` — the mere presence of a store flips the
5156 /// capability on, unless an explicit `tasks` capability was already
5157 /// configured (additive-only; an explicit value is preserved verbatim).
5158 /// - Handles the `tasks/*` surface via the store. The method set is
5159 /// ERA-DEPENDENT (Phase 114): v1 (2025-11-25) serves `tasks/get`,
5160 /// `tasks/result`, `tasks/list` and `tasks/cancel`; v2 (2026-07-28)
5161 /// serves `tasks/get`, `tasks/update` and `tasks/cancel`, and answers
5162 /// `-32601` for the two retired methods
5163 /// - Resolves task owner from auth context. **v1** falls back through OAuth
5164 /// subject → client ID → session ID; **v2** has no session to fall back
5165 /// to and binds fail-closed on an auth-configured server (TASK-05, D-07)
5166 ///
5167 /// A tool declaring
5168 /// [`TaskSupport::Required`](crate::types::tools::TaskSupport::Required)
5169 /// with NO store (or router) makes [`Self::build`] return an `Err`, rather
5170 /// than advertising a hollow `tasks` capability whose endpoints cannot work.
5171 ///
5172 /// # Examples
5173 ///
5174 /// ```no_run
5175 /// use std::sync::Arc;
5176 /// use pmcp::Server;
5177 /// use pmcp::server::task_store::{InMemoryTaskStore, TaskStore};
5178 /// use pmcp::server::typed_tool::TypedTool;
5179 /// use pmcp::types::{TaskSupport, ToolExecution};
5180 ///
5181 /// # fn build() -> pmcp::Result<()> {
5182 /// let task_tool = TypedTool::new_with_schema(
5183 /// "summarize",
5184 /// serde_json::json!({ "type": "object" }),
5185 /// |_args: serde_json::Value, _extra| {
5186 /// Box::pin(async { Ok(serde_json::json!({ "status": "completed" })) })
5187 /// },
5188 /// )
5189 /// .with_description("Summarize asynchronously as an MCP Task")
5190 /// .with_execution(ToolExecution::new().with_task_support(TaskSupport::Required));
5191 ///
5192 /// let store = Arc::new(InMemoryTaskStore::new()) as Arc<dyn TaskStore>;
5193 /// let server = Server::builder()
5194 /// .name("my-server")
5195 /// .version("1.0.0")
5196 /// .tool("summarize", task_tool)
5197 /// .task_store(store) // presence of a store auto-advertises the `tasks` capability
5198 /// .build()?;
5199 /// # let _ = server;
5200 /// # Ok(())
5201 /// # }
5202 /// ```
5203 #[cfg(not(target_arch = "wasm32"))]
5204 pub fn task_store(mut self, store: Arc<dyn crate::server::task_store::TaskStore>) -> Self {
5205 // Capability advertisement is centralized in `build()` (see
5206 // `task_dispatch::apply_tasks_capability_rule`). Registering a store
5207 // records the backend; it does NOT itself set `capabilities.tasks`, so
5208 // an explicitly-configured capability is never clobbered (additive-only,
5209 // per D-CAPABILITY-ENDPOINT-BACKED).
5210 self.task_store = Some(store);
5211 self
5212 }
5213
5214 /// Register a legacy experimental
5215 /// [`TaskRouter`](crate::server::tasks::TaskRouter) for MCP Tasks on the
5216 /// high-level `Server`.
5217 ///
5218 /// NAMING NOTE: despite the `with_task_store` name, this setter accepts a
5219 /// **[`TaskRouter`](crate::server::tasks::TaskRouter)** (the legacy,
5220 /// experimental router-backed path), NOT a
5221 /// [`TaskStore`](crate::server::task_store::TaskStore). The setter for an
5222 /// actual `TaskStore` (the RECOMMENDED polling path) is
5223 /// [`Self::task_store`]. This carried-over naming mirrors
5224 /// `ServerCoreBuilder::with_task_store`; the API is additive-only, so the
5225 /// confusing pair is documented here rather than renamed.
5226 ///
5227 /// Registering a router auto-configures the `experimental.tasks` capability
5228 /// from the router's `task_capabilities()`.
5229 ///
5230 /// **That advertisement is v1-only (Phase 114).** `experimental.tasks` is
5231 /// the 2025-11-25 spelling; a v2 (2026-07-28) client never sees it, because
5232 /// `project_capabilities_for_v2` strips both `experimental` and
5233 /// `capabilities.tasks` and v2 declares tasks through the `extensions` map
5234 /// key `io.modelcontextprotocol/tasks` instead (plan 114-05).
5235 #[cfg(not(target_arch = "wasm32"))]
5236 pub fn with_task_store(mut self, router: Arc<dyn crate::server::tasks::TaskRouter>) -> Self {
5237 // Auto-configure experimental.tasks capability from the router.
5238 let experimental = self
5239 .capabilities
5240 .experimental
5241 .get_or_insert_with(HashMap::new);
5242 experimental.insert("tasks".to_string(), router.task_capabilities());
5243
5244 self.task_router = Some(router);
5245 self
5246 }
5247
5248 /// Opt a tool OUT of the TOUT-02 double-wrap tripwire (D-08).
5249 ///
5250 /// The tripwire WARNs (every build) and `debug_assert!`-fails (debug/CI)
5251 /// when a tool returns a `ToolOutput::Payload` `Value` that STRUCTURALLY
5252 /// resembles an already-built `CallToolResult` (a non-empty `content` array
5253 /// of `Content`, or a `_meta` related-task envelope) — the silent
5254 /// double-wrap bug. Naming a tool here suppresses that check for it.
5255 ///
5256 /// SUPPRESSION SHOULD BE RARE AND REVIEWED: it disables a safety tripwire for
5257 /// one tool whose LEGITIMATE payload happens to trip the heuristic. Prefer
5258 /// returning [`ToolOutput::Result`] so the
5259 /// handler owns the full envelope verbatim, rather than suppressing. Reach
5260 /// for this only when a tool genuinely produces a plain `Value` that mimics a
5261 /// result shape and cannot be restructured.
5262 ///
5263 /// The same suppression set is carried into `ServerCore`, so both native
5264 /// dispatchers honor the opt-out identically (no drift).
5265 #[cfg(not(target_arch = "wasm32"))]
5266 #[must_use]
5267 pub fn suppress_double_wrap_check(mut self, name: impl Into<String>) -> Self {
5268 self.suppress_double_wrap.insert(name.into());
5269 self
5270 }
5271
5272 /// Returns an error if:
5273 /// - The server name is not set
5274 /// - The server version is not set
5275 /// - A tool declares `TaskSupport::Required` but no `TaskStore`/`TaskRouter`
5276 /// backend is configured (see [`Self::task_store`])
5277 #[allow(unused_mut)] // `self` is mutated only on non-wasm (capability rule).
5278 pub fn build(mut self) -> Result<Server> {
5279 let name = self
5280 .name
5281 .ok_or_else(|| crate::Error::validation("Server name is required"))?;
5282 let version = self
5283 .version
5284 .ok_or_else(|| crate::Error::validation("Server version is required"))?;
5285
5286 // Apply tool protections
5287 let tool_authorizer = if !self.tool_protections.is_empty() {
5288 if self.tool_authorizer.is_some() {
5289 // If there's an existing authorizer and tool protections are specified,
5290 // this is a configuration error
5291 return Err(crate::Error::validation(
5292 "Cannot use protect_tool() with a custom tool_authorizer. \
5293 Either use protect_tool() to configure scope-based authorization, \
5294 or provide a custom ToolAuthorizer implementation, but not both.",
5295 ));
5296 }
5297 // Create a ScopeBasedAuthorizer with all the tool protections
5298 let mut authorizer = auth::ScopeBasedAuthorizer::new();
5299 for (tool_name, scopes) in self.tool_protections {
5300 authorizer = authorizer.require_scopes(tool_name, scopes);
5301 }
5302 Some(Arc::new(authorizer) as Arc<dyn auth::ToolAuthorizer>)
5303 } else {
5304 self.tool_authorizer
5305 };
5306
5307 // Initialize tool middleware chain
5308 #[cfg(not(target_arch = "wasm32"))]
5309 let tool_middleware_chain = {
5310 let mut chain = tool_middleware::ToolMiddlewareChain::new();
5311 for middleware in self.tool_middlewares {
5312 chain.add(middleware);
5313 }
5314 Arc::new(RwLock::new(chain))
5315 };
5316
5317 // Build tool_infos cache at construction time (mirrors ServerCore pattern)
5318 let tool_infos: HashMap<String, ToolInfo> = self
5319 .tools
5320 .iter()
5321 .map(|(name, handler)| {
5322 let info = handler.metadata().unwrap_or_else(|| {
5323 ToolInfo::new(
5324 name.clone(),
5325 None,
5326 serde_json::json!({"type": "object", "properties": {}}),
5327 )
5328 });
5329 (name.clone(), info)
5330 })
5331 .collect();
5332
5333 // Apply host layer enrichment to tool _meta (e.g., ChatGPT openai/* keys)
5334 #[cfg(feature = "mcp-apps")]
5335 let tool_infos = {
5336 let mut infos = tool_infos;
5337 for host in &self.host_layers {
5338 for info in infos.values_mut() {
5339 if let Some(meta) = info._meta.as_mut() {
5340 core::enrich_meta_for_host(meta, *host);
5341 }
5342 }
5343 }
5344 infos
5345 };
5346
5347 // Build URI-to-tool-meta index for widget resource _meta propagation
5348 let uri_to_tool_meta = core::build_uri_to_tool_meta(&tool_infos);
5349
5350 // Apply the SHARED endpoint-backed `tasks`-capability rule (the SAME
5351 // free fn `ServerCoreBuilder::build` uses) now that `tool_infos` is
5352 // finalized: a store-backed `Server` auto-advertises `tasks`, and a
5353 // `TaskSupport::Required` tool with no backend is a build-time error.
5354 // Runs BEFORE `self.capabilities` is moved into the `Server` literal.
5355 #[cfg(not(target_arch = "wasm32"))]
5356 {
5357 let has_backend = self.task_store.is_some() || self.task_router.is_some();
5358 crate::server::task_dispatch::apply_tasks_capability_rule(
5359 &mut self.capabilities,
5360 &tool_infos,
5361 has_backend,
5362 )?;
5363 }
5364
5365 // Finalize accumulated skills exactly once and compose with the
5366 // user's `.resources(...)` slot if both are set. `.resources(...)`
5367 // itself stays "last write wins" — composition lives here so the
5368 // setter's semantics are unchanged for callers that don't use
5369 // skills.
5370 #[cfg(feature = "skills")]
5371 let final_resources: Option<Arc<dyn ResourceHandler>> =
5372 builder::finalize_skills_resources(self.pending_skills, self.resources);
5373 #[cfg(not(feature = "skills"))]
5374 let final_resources = self.resources;
5375
5376 // HTTP-04: advertising ANY subscription-delivered capability opts this
5377 // server into serving `subscriptions/listen`, whose registry is
5378 // INSTANCE-LOCAL. Warn at BUILD time — this is startup, and a silent
5379 // under-delivery behind a load balancer surfaces no error at runtime
5380 // (T-113-64).
5381 //
5382 // Gated on the v2 opt-in as well as the capability: `subscriptions/listen`
5383 // is a 2026-07-28-only route, so a v1-only server can never serve it and
5384 // the warning would be FALSE. It is not a rare corner either —
5385 // `ServerCapabilities::tools_only()` sets `tools.listChanged = true`, so
5386 // without this gate essentially every existing pmcp server would print a
5387 // warning about a stream it does not implement (D-04: zero era behaviour
5388 // on a non-opted-in server).
5389 if crate::types::protocol::context::is_v2_opted_in(&self.supported_protocol_versions)
5390 && crate::types::subscriptions::advertises_subscriptions(&self.capabilities)
5391 {
5392 tracing::warn!(
5393 target: "mcp.subscriptions",
5394 "a subscription-delivered capability is advertised, so subscriptions/listen \
5395 will be SERVED; its registry is INSTANCE-LOCAL, so notifications generated on \
5396 another instance are not delivered — supported for single-instance or \
5397 sticky-routed deployments only. Polling over Tasks remains the recommended \
5398 pmcp enterprise mechanism (D-11)."
5399 );
5400 }
5401
5402 // Resolve the server-owned `requestState` codec EXACTLY ONCE, here at
5403 // BUILD time (Phase 113, HTTP-02). A malformed CONFIGURED key fails the
5404 // build; an UNSET key falls back to a per-process key with a WARN emitted
5405 // from inside `from_env`, which is a genuine STARTUP warning because this
5406 // is startup. A v1-only server gets `None` and reads no env var at all.
5407 //
5408 // Both key arguments go BY REFERENCE, which closes copy 3 of 3
5409 // (D-113-P): the by-value form manufactured an unscrubbed stack copy on
5410 // every call. Because they are borrowed rather than moved, the two
5411 // fields are still owned by `self` here and drop through the zeroizing
5412 // destructor — on this path AND on every early `?` above, none of which
5413 // moves the key material anywhere.
5414 #[cfg(feature = "streamable-http")]
5415 let request_state_codec = request_state::resolve_codec_at_build(
5416 &self.supported_protocol_versions,
5417 self.request_state_key.as_ref(),
5418 &self.request_state_previous_keys,
5419 self.request_state_ttl,
5420 )?;
5421
5422 Ok(Server {
5423 info: {
5424 let mut info = Implementation::new(&name, &version);
5425 if let Some(url) = self.website_url {
5426 info = info.with_website_url(url);
5427 }
5428 if let Some(icons) = self.icons {
5429 info = info.with_icons(icons);
5430 }
5431 info
5432 },
5433 capabilities: self.capabilities,
5434 tools: self.tools,
5435 tool_infos,
5436 uri_to_tool_meta,
5437 prompts: self.prompts,
5438 resources: final_resources,
5439 completions: self.completions,
5440 sampling: self.sampling,
5441 client_capabilities: Arc::new(RwLock::new(None)),
5442 initialized: Arc::new(RwLock::new(false)),
5443 notification_tx: None,
5444 cancellation_manager: self.cancellation_manager,
5445 roots_manager: Arc::new(RwLock::new(self.roots_manager)),
5446 subscription_manager: Arc::new(RwLock::new(subscriptions::SubscriptionManager::new())),
5447 listen_registry: Arc::new(subscriptions::ListenRegistry::new()),
5448 elicitation_manager: None,
5449 server_request_dispatcher: None,
5450 peer_handle: None,
5451 auth_provider: self.auth_provider,
5452 tool_authorizer,
5453 #[cfg(not(target_arch = "wasm32"))]
5454 tool_middleware_chain,
5455 #[cfg(feature = "streamable-http")]
5456 http_middleware: self.http_middleware,
5457 #[cfg(not(target_arch = "wasm32"))]
5458 task_router: self.task_router,
5459 #[cfg(not(target_arch = "wasm32"))]
5460 task_store: self.task_store,
5461 #[cfg(not(target_arch = "wasm32"))]
5462 suppress_double_wrap: self.suppress_double_wrap,
5463 supported_protocol_versions: self.supported_protocol_versions,
5464 #[cfg(feature = "streamable-http")]
5465 request_state_codec,
5466 })
5467 }
5468}
5469
5470#[cfg(not(target_arch = "wasm32"))]
5471impl Default for ServerBuilder {
5472 fn default() -> Self {
5473 Self::new()
5474 }
5475}
5476
5477#[cfg(test)]
5478mod tests {
5479 use super::*;
5480 use crate::shared::Transport;
5481 use crate::types::{
5482 jsonrpc::ResponsePayload, ClientCapabilities, InitializeRequest, ServerCapabilities,
5483 TransportMessage,
5484 };
5485 use async_trait::async_trait;
5486 use serde_json::json;
5487 use std::sync::{Arc, Mutex};
5488 use tokio::time::timeout;
5489
5490 // -- requestState key material (D-113-P) --------------------------------
5491
5492 /// COMPILE-LEVEL guard on the FIELD TYPES, not on behaviour.
5493 ///
5494 /// The twin of `builder.rs`'s `request_state_key_field_is_the_zeroizing_type`.
5495 /// D-113-P named only `ServerCoreBuilder`; `ServerBuilder` carried the
5496 /// identical defect on the path most users actually take, so both need the
5497 /// guard. Reverting either field to bare `[u8; 32]` fails to compile here.
5498 #[cfg(feature = "streamable-http")]
5499 #[test]
5500 fn server_builder_request_state_key_field_is_the_zeroizing_type() {
5501 use crate::server::request_state::SecretKey;
5502 let builder = ServerBuilder::new()
5503 .with_request_state_key([0x11; 32])
5504 .with_request_state_previous_keys(vec![[0x22; 32]]);
5505
5506 let key: &Option<SecretKey> = &builder.request_state_key;
5507 let previous: &Vec<SecretKey> = &builder.request_state_previous_keys;
5508
5509 assert_eq!(key.as_deref(), Some(&[0x11u8; 32]));
5510 assert_eq!(previous.len(), 1);
5511 assert_eq!(**previous.first().expect("one previous key"), [0x22u8; 32]);
5512 }
5513
5514 /// The plumbing regression guard for `ServerBuilder`: a server configured
5515 /// with a key plus a rotated-out key must still mint under the current key
5516 /// and verify.
5517 #[cfg(feature = "streamable-http")]
5518 #[test]
5519 fn a_server_with_zeroizing_key_fields_still_mints_and_verifies() {
5520 use crate::server::request_state::{key_id_of, RequestBinding, Verdict};
5521
5522 const CURRENT: [u8; 32] = [0x11; 32];
5523 const ROTATED: [u8; 32] = [0x22; 32];
5524
5525 let server = Server::builder()
5526 .name("t")
5527 .version("1")
5528 .with_supported_protocol_versions([
5529 ProtocolVersion("2026-07-28".to_string()),
5530 ProtocolVersion("2025-11-25".to_string()),
5531 ])
5532 .with_request_state_key(CURRENT)
5533 .with_request_state_previous_keys(vec![ROTATED])
5534 .build()
5535 .expect("server builds");
5536
5537 let codec = server
5538 .request_state_codec()
5539 .expect("a v2 server has a codec");
5540 let params = json!({ "name": "t", "arguments": { "a": 1 } });
5541 let binding = RequestBinding::from_request("alice", "tools/call", ¶ms)
5542 .expect("a two-level fixture is far inside the canonical depth cap");
5543 let token = codec
5544 .mint(&json!({ "step": 1 }), &binding, 0, None)
5545 .expect("mint");
5546 assert!(
5547 matches!(codec.verify(&token, &binding), Verdict::Ok(_)),
5548 "the zeroizing field type must not disturb the key plumbing"
5549 );
5550 assert!(codec.accepting_key_ids().contains(&key_id_of(&ROTATED)));
5551 }
5552
5553 /// Mock transport for testing
5554 #[derive(Debug)]
5555 struct MockTransport {
5556 messages: Arc<Mutex<Vec<TransportMessage>>>,
5557 responses: Arc<Mutex<Vec<TransportMessage>>>,
5558 }
5559
5560 impl MockTransport {
5561 #[allow(dead_code)]
5562 fn new() -> Self {
5563 Self {
5564 messages: Arc::new(Mutex::new(Vec::new())),
5565 responses: Arc::new(Mutex::new(Vec::new())),
5566 }
5567 }
5568
5569 fn with_requests(requests: Vec<TransportMessage>) -> Self {
5570 Self {
5571 messages: Arc::new(Mutex::new(requests)),
5572 responses: Arc::new(Mutex::new(Vec::new())),
5573 }
5574 }
5575
5576 #[allow(dead_code)]
5577 fn add_request(&self, request: TransportMessage) {
5578 self.messages.lock().unwrap().push(request);
5579 }
5580
5581 #[allow(dead_code)]
5582 fn get_sent_responses(&self) -> Vec<TransportMessage> {
5583 self.responses.lock().unwrap().clone()
5584 }
5585 }
5586
5587 #[async_trait]
5588 impl Transport for MockTransport {
5589 async fn send(&mut self, message: TransportMessage) -> Result<()> {
5590 self.responses.lock().unwrap().push(message);
5591 Ok(())
5592 }
5593
5594 async fn receive(&mut self) -> Result<TransportMessage> {
5595 let mut messages = self.messages.lock().unwrap();
5596 messages
5597 .pop()
5598 .map_or_else(|| Err(Error::protocol_msg("No more messages")), Ok)
5599 }
5600
5601 async fn close(&mut self) -> Result<()> {
5602 Ok(())
5603 }
5604
5605 fn is_connected(&self) -> bool {
5606 !self.messages.lock().unwrap().is_empty()
5607 }
5608
5609 fn transport_type(&self) -> &'static str {
5610 "mock"
5611 }
5612 }
5613
5614 /// Mock tool handler for testing
5615 struct MockTool {
5616 result: Value,
5617 }
5618
5619 impl MockTool {
5620 fn new(result: Value) -> Self {
5621 Self { result }
5622 }
5623 }
5624
5625 #[async_trait]
5626 impl ToolHandler for MockTool {
5627 async fn handle(
5628 &self,
5629 _args: Value,
5630 _extra: crate::server::cancellation::RequestHandlerExtra,
5631 ) -> Result<Value> {
5632 Ok(self.result.clone())
5633 }
5634 }
5635
5636 /// Mock prompt handler for testing
5637 struct MockPrompt {
5638 result: crate::types::GetPromptResult,
5639 }
5640
5641 impl MockPrompt {
5642 fn new(result: crate::types::GetPromptResult) -> Self {
5643 Self { result }
5644 }
5645 }
5646
5647 #[async_trait]
5648 impl PromptHandler for MockPrompt {
5649 async fn handle(
5650 &self,
5651 _args: HashMap<String, String>,
5652 _extra: crate::server::cancellation::RequestHandlerExtra,
5653 ) -> Result<crate::types::GetPromptResult> {
5654 Ok(self.result.clone())
5655 }
5656 }
5657
5658 /// Mock resource handler for testing
5659 struct MockResource {
5660 resources: Vec<crate::types::ResourceInfo>,
5661 contents: HashMap<String, crate::types::ReadResourceResult>,
5662 }
5663
5664 impl MockResource {
5665 fn new() -> Self {
5666 Self {
5667 resources: Vec::new(),
5668 contents: HashMap::new(),
5669 }
5670 }
5671
5672 fn with_resource(mut self, uri: String, content: crate::types::ReadResourceResult) -> Self {
5673 self.contents.insert(uri, content);
5674 self
5675 }
5676 }
5677
5678 #[async_trait]
5679 impl ResourceHandler for MockResource {
5680 async fn read(
5681 &self,
5682 uri: &str,
5683 _extra: crate::server::cancellation::RequestHandlerExtra,
5684 ) -> Result<crate::types::ReadResourceResult> {
5685 self.contents
5686 .get(uri)
5687 .cloned()
5688 .ok_or_else(|| Error::not_found(format!("Resource '{}' not found", uri)))
5689 }
5690
5691 async fn list(
5692 &self,
5693 _cursor: Option<String>,
5694 _extra: crate::server::cancellation::RequestHandlerExtra,
5695 ) -> Result<crate::types::ListResourcesResult> {
5696 Ok(crate::types::ListResourcesResult {
5697 resources: self.resources.clone(),
5698 next_cursor: None,
5699 ttl_ms: None,
5700 cache_scope: None,
5701 })
5702 }
5703 }
5704
5705 #[test]
5706 fn test_server_builder() {
5707 let server = Server::builder()
5708 .name("test-server")
5709 .version("1.0.0")
5710 .capabilities(ServerCapabilities::tools_only())
5711 .tool("test-tool", MockTool::new(json!({"result": "success"})))
5712 .build()
5713 .unwrap();
5714
5715 assert_eq!(server.info.name, "test-server");
5716 assert_eq!(server.info.version, "1.0.0");
5717 assert!(server.tools.contains_key("test-tool"));
5718 }
5719
5720 #[test]
5721 fn test_server_builder_validation() {
5722 // Missing name
5723 let result = Server::builder().version("1.0.0").build();
5724 assert!(result.is_err());
5725
5726 // Missing version
5727 let result = Server::builder().name("test-server").build();
5728 assert!(result.is_err());
5729 }
5730
5731 #[tokio::test]
5732 async fn test_server_initialization() {
5733 let init_request = TransportMessage::Request {
5734 id: RequestId::from(1i64),
5735 request: Request::Client(Box::new(ClientRequest::Initialize(InitializeRequest {
5736 protocol_version: "2024-11-05".to_string(),
5737 capabilities: ClientCapabilities::minimal(),
5738 client_info: Implementation::new("test-client", "1.0.0"),
5739 }))),
5740 };
5741
5742 let transport = MockTransport::with_requests(vec![init_request]);
5743 let server = Server::builder()
5744 .name("test-server")
5745 .version("1.0.0")
5746 .capabilities(ServerCapabilities::tools_only())
5747 .build()
5748 .unwrap();
5749
5750 // Test server run for a short time
5751 let server_handle = tokio::spawn(async move {
5752 let _ = timeout(std::time::Duration::from_millis(100), server.run(transport)).await;
5753 });
5754
5755 // Wait for server to process
5756 let _ = timeout(std::time::Duration::from_millis(200), server_handle).await;
5757 }
5758
5759 #[tokio::test]
5760 async fn test_server_capabilities() {
5761 let server = Server::builder()
5762 .name("test-server")
5763 .version("1.0.0")
5764 .capabilities(ServerCapabilities::tools_only())
5765 .build()
5766 .unwrap();
5767
5768 assert!(!server.is_initialized().await);
5769 assert!(server.get_client_capabilities().await.is_none());
5770 }
5771
5772 #[tokio::test]
5773 async fn test_server_notifications() {
5774 let server = Server::builder()
5775 .name("test-server")
5776 .version("1.0.0")
5777 .build()
5778 .unwrap();
5779
5780 // Send notification (should not panic even without transport)
5781 server
5782 .send_notification(ServerNotification::ToolsChanged)
5783 .await;
5784 }
5785
5786 #[test]
5787 fn test_server_builder_with_all_handlers() {
5788 let prompt_result = crate::types::GetPromptResult {
5789 description: Some("Test prompt".to_string()),
5790 messages: vec![],
5791 _meta: None,
5792 };
5793
5794 let resource_content =
5795 crate::types::ReadResourceResult::new(vec![crate::types::Content::text(
5796 "Hello, world!",
5797 )]);
5798
5799 let server = Server::builder()
5800 .name("test-server")
5801 .version("1.0.0")
5802 .tool("test-tool", MockTool::new(json!({"result": "success"})))
5803 .prompt("test-prompt", MockPrompt::new(prompt_result))
5804 .resources(
5805 MockResource::new().with_resource("test://uri".to_string(), resource_content),
5806 )
5807 .build()
5808 .unwrap();
5809
5810 assert!(server.tools.contains_key("test-tool"));
5811 assert!(server.prompts.contains_key("test-prompt"));
5812 assert!(server.resources.is_some());
5813 }
5814
5815 #[tokio::test]
5816 async fn test_handle_request_initialize() {
5817 let server = Server::builder()
5818 .name("test-server")
5819 .version("1.0.0")
5820 .capabilities(ServerCapabilities::tools_only())
5821 .build()
5822 .unwrap();
5823
5824 let request = Request::Client(Box::new(ClientRequest::Initialize(InitializeRequest {
5825 protocol_version: "2024-11-05".to_string(),
5826 capabilities: ClientCapabilities::default(),
5827 client_info: Implementation::new("test-client", "1.0.0"),
5828 })));
5829
5830 let response = server
5831 .handle_request(RequestId::from(1i64), request, None)
5832 .await;
5833
5834 assert_eq!(response.id, RequestId::from(1i64));
5835 match response.payload {
5836 ResponsePayload::Result(_) => {
5837 assert!(server.is_initialized().await);
5838 },
5839 ResponsePayload::Error(_) => panic!("Expected success response"),
5840 }
5841 }
5842
5843 #[tokio::test]
5844 async fn test_handle_list_tools() {
5845 let server = Server::builder()
5846 .name("test-server")
5847 .version("1.0.0")
5848 .tool("test-tool", MockTool::new(json!({"result": "success"})))
5849 .build()
5850 .unwrap();
5851
5852 let request = Request::Client(Box::new(ClientRequest::ListTools(ListToolsRequest {
5853 cursor: None,
5854 })));
5855 let response = server
5856 .handle_request(RequestId::from(1i64), request, None)
5857 .await;
5858
5859 match response.payload {
5860 ResponsePayload::Result(result) => {
5861 let tools_result: ListToolsResult = serde_json::from_value(result).unwrap();
5862 assert_eq!(tools_result.tools.len(), 1);
5863 assert_eq!(tools_result.tools[0].name, "test-tool");
5864 },
5865 ResponsePayload::Error(_) => panic!("Expected success response"),
5866 }
5867 }
5868
5869 #[tokio::test]
5870 async fn test_handle_call_tool() {
5871 let server = Server::builder()
5872 .name("test-server")
5873 .version("1.0.0")
5874 .tool("test-tool", MockTool::new(json!({"result": "success"})))
5875 .build()
5876 .unwrap();
5877
5878 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
5879 name: "test-tool".to_string(),
5880 arguments: json!({"input": "test"}),
5881 _meta: None,
5882 task: None,
5883 })));
5884
5885 let response = server
5886 .handle_request(RequestId::from(1i64), request, None)
5887 .await;
5888
5889 match response.payload {
5890 ResponsePayload::Result(result) => {
5891 let call_result: CallToolResult = serde_json::from_value(result).unwrap();
5892 assert!(!call_result.is_error);
5893 assert_eq!(call_result.content.len(), 1);
5894 },
5895 ResponsePayload::Error(_) => panic!("Expected success response"),
5896 }
5897 }
5898
5899 /// Tool that reports the ingress-resolved era back through its result so a
5900 /// dispatch test can prove ingress→handler protocol-context threading on the
5901 /// high-level `Server` dispatch site.
5902 struct EraProbeServerTool;
5903
5904 #[async_trait]
5905 impl ToolHandler for EraProbeServerTool {
5906 async fn handle(
5907 &self,
5908 _args: Value,
5909 extra: crate::server::cancellation::RequestHandlerExtra,
5910 ) -> Result<Value> {
5911 Ok(json!({ "era": extra.era().map(|e| format!("{e:?}")) }))
5912 }
5913 }
5914
5915 fn probe_server_era(result: &Value) -> Value {
5916 let text = result["content"][0]["text"]
5917 .as_str()
5918 .expect("probe result carries text content");
5919 serde_json::from_str::<Value>(text).expect("probe text is JSON")["era"].clone()
5920 }
5921
5922 fn v2_probe_call() -> Request {
5923 let meta = crate::types::protocol::RequestMeta::new().with_meta(
5924 "io.modelcontextprotocol/protocolVersion",
5925 json!("2026-07-28"),
5926 );
5927 Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
5928 name: "probe".to_string(),
5929 arguments: json!({}),
5930 _meta: Some(meta),
5931 task: None,
5932 })))
5933 }
5934
5935 /// Cross-site parity: the high-level `Server` dispatch site resolves the SAME
5936 /// v2 era as `ServerCore` for identical `_meta` (both use the one shared
5937 /// resolver), visible in the handler (Pitfall 3, twin wiring).
5938 #[tokio::test]
5939 async fn test_server_dispatch_resolves_v2_era_parity() {
5940 use crate::types::protocol::PROTOCOL_VERSION_2026_07_28;
5941 use crate::types::ProtocolVersion;
5942
5943 let server = Server::builder()
5944 .name("probe-server")
5945 .version("1.0.0")
5946 .tool("probe", EraProbeServerTool)
5947 .with_supported_protocol_versions([
5948 ProtocolVersion("2025-11-25".to_string()),
5949 ProtocolVersion(PROTOCOL_VERSION_2026_07_28.to_string()),
5950 ])
5951 .build()
5952 .unwrap();
5953
5954 let response = server
5955 .handle_request(RequestId::from(1i64), v2_probe_call(), None)
5956 .await;
5957 match response.payload {
5958 ResponsePayload::Result(result) => {
5959 assert_eq!(probe_server_era(&result), json!("V2"));
5960 },
5961 ResponsePayload::Error(e) => panic!("probe call failed: {}", e.message),
5962 }
5963 }
5964
5965 /// Twin-site envelope parity (VERS-07): the high-level `Server` dispatch
5966 /// site injects the SAME v2 `resultType`/`serverInfo` envelope `ServerCore`
5967 /// does — via the ONE shared `core::inject_v2_result_envelope` helper — on a
5968 /// v2 object result.
5969 #[tokio::test]
5970 async fn test_server_dispatch_injects_v2_result_envelope_parity() {
5971 use crate::types::protocol::PROTOCOL_VERSION_2026_07_28;
5972 use crate::types::ProtocolVersion;
5973
5974 let server = Server::builder()
5975 .name("envelope-server")
5976 .version("3.2.1")
5977 .tool("probe", EraProbeServerTool)
5978 .with_supported_protocol_versions([
5979 ProtocolVersion("2025-11-25".to_string()),
5980 ProtocolVersion(PROTOCOL_VERSION_2026_07_28.to_string()),
5981 ])
5982 .build()
5983 .unwrap();
5984
5985 // v2 request → envelope injected.
5986 let response = server
5987 .handle_request(RequestId::from(1i64), v2_probe_call(), None)
5988 .await;
5989 let ResponsePayload::Result(v) = response.payload else {
5990 panic!("expected result");
5991 };
5992 assert_eq!(v["resultType"], "complete");
5993 // Plan 113-09 Task 3: the schema places server identity INSIDE
5994 // `result._meta`, not at the top level.
5995 let server_info = &v["_meta"][crate::server::core::RESERVED_SERVER_INFO_KEY];
5996 assert_eq!(server_info["name"], "envelope-server");
5997 assert_eq!(server_info["version"], "3.2.1");
5998 assert!(
5999 v.get("serverInfo").is_none(),
6000 "the envelope must not write a top-level serverInfo: {v}"
6001 );
6002 }
6003
6004 /// v1 byte-identity at the twin site: a non-opted-in `Server` gains NO
6005 /// `resultType`/`serverInfo` even with a v2 `_meta` signal (D-07).
6006 #[tokio::test]
6007 async fn test_server_dispatch_v1_no_envelope() {
6008 let server = Server::builder()
6009 .name("v1-envelope-server")
6010 .version("1.0.0")
6011 .tool("probe", EraProbeServerTool)
6012 .build()
6013 .unwrap();
6014
6015 let response = server
6016 .handle_request(RequestId::from(1i64), v2_probe_call(), None)
6017 .await;
6018 let ResponsePayload::Result(v) = response.payload else {
6019 panic!("expected result");
6020 };
6021 assert!(v.get("resultType").is_none(), "v1 must not gain resultType");
6022 assert!(v.get("serverInfo").is_none(), "v1 must not gain serverInfo");
6023 assert!(
6024 v.get("_meta").is_none(),
6025 "v1 must not gain the _meta the v2 envelope creates: {v}"
6026 );
6027 }
6028
6029 /// A non-opted-in high-level `Server` runs zero era-detection: the handler
6030 /// reads `era()==None` even with a v2 `_meta` signal (D-04 parity).
6031 #[tokio::test]
6032 async fn test_server_dispatch_non_opted_in_yields_none() {
6033 let server = Server::builder()
6034 .name("v1-server")
6035 .version("1.0.0")
6036 .tool("probe", EraProbeServerTool)
6037 .build()
6038 .unwrap();
6039
6040 let response = server
6041 .handle_request(RequestId::from(1i64), v2_probe_call(), None)
6042 .await;
6043 match response.payload {
6044 ResponsePayload::Result(result) => {
6045 assert_eq!(probe_server_era(&result), Value::Null);
6046 },
6047 ResponsePayload::Error(e) => panic!("probe call failed: {}", e.message),
6048 }
6049 }
6050
6051 #[tokio::test]
6052 async fn test_handle_call_tool_rejected_is_iserror_not_protocol_error() {
6053 // A handler returning `Error::tool_rejected` must surface through the
6054 // streamable-HTTP `Server` path as a SUCCESSFUL `CallToolResult`
6055 // with `isError: true` (message → content, details → structuredContent),
6056 // NOT a JSON-RPC protocol error. This is the Code Mode policy-rejection
6057 // envelope (e.g. "SELECT missing LIMIT") observed by `pmcp-sql-server`.
6058 struct RejectingTool;
6059 #[async_trait]
6060 impl ToolHandler for RejectingTool {
6061 async fn handle(
6062 &self,
6063 _args: Value,
6064 _extra: crate::server::cancellation::RequestHandlerExtra,
6065 ) -> Result<Value> {
6066 Err(Error::tool_rejected(
6067 "SELECT statements must declare a LIMIT",
6068 Some(json!({ "violations": [{ "rule": "missing_limit" }] })),
6069 ))
6070 }
6071 }
6072
6073 let server = Server::builder()
6074 .name("test-server")
6075 .version("1.0.0")
6076 .tool("reject", RejectingTool)
6077 .build()
6078 .unwrap();
6079
6080 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
6081 name: "reject".to_string(),
6082 arguments: json!({}),
6083 _meta: None,
6084 task: None,
6085 })));
6086
6087 let response = server
6088 .handle_request(RequestId::from(1i64), request, None)
6089 .await;
6090
6091 match response.payload {
6092 ResponsePayload::Result(result) => {
6093 let call_result: CallToolResult = serde_json::from_value(result).unwrap();
6094 assert!(call_result.is_error, "tool_rejected must set isError: true");
6095 let text = call_result
6096 .content
6097 .iter()
6098 .find_map(|c| match c {
6099 crate::types::Content::Text { text } => Some(text.clone()),
6100 _ => None,
6101 })
6102 .unwrap_or_default();
6103 assert!(
6104 text.contains("must declare a LIMIT"),
6105 "content must carry the rejection message, got: {text}"
6106 );
6107 let sc = call_result
6108 .structured_content
6109 .expect("structuredContent must carry the violation detail");
6110 assert_eq!(sc["violations"][0]["rule"], "missing_limit");
6111 },
6112 ResponsePayload::Error(e) => panic!(
6113 "tool_rejected must NOT be a protocol error, got {}: {}",
6114 e.code, e.message
6115 ),
6116 }
6117 }
6118
6119 #[tokio::test]
6120 async fn test_handle_call_tool_not_found() {
6121 let server = Server::builder()
6122 .name("test-server")
6123 .version("1.0.0")
6124 .build()
6125 .unwrap();
6126
6127 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
6128 name: "nonexistent-tool".to_string(),
6129 arguments: json!({}),
6130 _meta: None,
6131 task: None,
6132 })));
6133
6134 let response = server
6135 .handle_request(RequestId::from(1i64), request, None)
6136 .await;
6137
6138 match response.payload {
6139 ResponsePayload::Error(error) => {
6140 assert!(error.message.contains("not found"));
6141 },
6142 ResponsePayload::Result(_) => panic!("Expected error response"),
6143 }
6144 }
6145
6146 #[tokio::test]
6147 async fn test_handle_list_prompts() {
6148 let prompt_result = crate::types::GetPromptResult {
6149 description: Some("Test prompt".to_string()),
6150 messages: vec![],
6151 _meta: None,
6152 };
6153
6154 let server = Server::builder()
6155 .name("test-server")
6156 .version("1.0.0")
6157 .prompt("test-prompt", MockPrompt::new(prompt_result))
6158 .build()
6159 .unwrap();
6160
6161 let request = Request::Client(Box::new(ClientRequest::ListPrompts(ListPromptsRequest {
6162 cursor: None,
6163 })));
6164 let response = server
6165 .handle_request(RequestId::from(1i64), request, None)
6166 .await;
6167
6168 match response.payload {
6169 ResponsePayload::Result(result) => {
6170 let list_result: ListPromptsResult = serde_json::from_value(result).unwrap();
6171 assert_eq!(list_result.prompts.len(), 1);
6172 assert_eq!(list_result.prompts[0].name, "test-prompt");
6173 },
6174 ResponsePayload::Error(_) => panic!("Expected success response"),
6175 }
6176 }
6177
6178 #[tokio::test]
6179 async fn test_handle_get_prompt() {
6180 let prompt_result = crate::types::GetPromptResult {
6181 description: Some("Test prompt".to_string()),
6182 messages: vec![],
6183 _meta: None,
6184 };
6185
6186 let server = Server::builder()
6187 .name("test-server")
6188 .version("1.0.0")
6189 .prompt("test-prompt", MockPrompt::new(prompt_result.clone()))
6190 .build()
6191 .unwrap();
6192
6193 let request = Request::Client(Box::new(ClientRequest::GetPrompt(GetPromptRequest {
6194 name: "test-prompt".to_string(),
6195 arguments: HashMap::new(),
6196 _meta: None,
6197 })));
6198
6199 let response = server
6200 .handle_request(RequestId::from(1i64), request, None)
6201 .await;
6202
6203 match response.payload {
6204 ResponsePayload::Result(result) => {
6205 let get_result: crate::types::GetPromptResult =
6206 serde_json::from_value(result).unwrap();
6207 assert_eq!(get_result.description, prompt_result.description);
6208 },
6209 ResponsePayload::Error(_) => panic!("Expected success response"),
6210 }
6211 }
6212
6213 // -----------------------------------------------------------------------
6214 // Phase 112-09 (Gap B): the high-level `Server` twin threads protocol_context
6215 // + request_meta into prompt/resource handlers. Enters through the REAL
6216 // dispatch entrypoint (`process_client_request`), NOT the leaf handlers.
6217 // -----------------------------------------------------------------------
6218 #[derive(Clone, Debug, Default, PartialEq)]
6219 struct DispatchCaptured {
6220 era: Option<crate::types::protocol::Era>,
6221 has_client_info: bool,
6222 traceparent: Option<String>,
6223 }
6224
6225 struct DispatchCapturingPrompt(Arc<Mutex<Option<DispatchCaptured>>>);
6226
6227 #[async_trait]
6228 impl PromptHandler for DispatchCapturingPrompt {
6229 async fn handle(
6230 &self,
6231 _args: HashMap<String, String>,
6232 extra: crate::server::cancellation::RequestHandlerExtra,
6233 ) -> Result<crate::types::GetPromptResult> {
6234 *self.0.lock().unwrap() = Some(DispatchCaptured {
6235 era: extra.era(),
6236 has_client_info: extra.client_info().is_some(),
6237 traceparent: extra.trace_context().map(|t| t.traceparent),
6238 });
6239 Ok(crate::types::GetPromptResult::new(vec![], None))
6240 }
6241 }
6242
6243 struct DispatchCapturingResource(Arc<Mutex<Option<DispatchCaptured>>>);
6244
6245 #[async_trait]
6246 impl ResourceHandler for DispatchCapturingResource {
6247 async fn read(
6248 &self,
6249 _uri: &str,
6250 extra: crate::server::cancellation::RequestHandlerExtra,
6251 ) -> Result<crate::types::ReadResourceResult> {
6252 *self.0.lock().unwrap() = Some(DispatchCaptured {
6253 era: extra.era(),
6254 has_client_info: extra.client_info().is_some(),
6255 traceparent: extra.trace_context().map(|t| t.traceparent),
6256 });
6257 Ok(crate::types::ReadResourceResult::new(vec![
6258 crate::types::Content::text("ok"),
6259 ]))
6260 }
6261
6262 async fn list(
6263 &self,
6264 _cursor: Option<String>,
6265 _extra: crate::server::cancellation::RequestHandlerExtra,
6266 ) -> Result<crate::types::ListResourcesResult> {
6267 Ok(crate::types::ListResourcesResult {
6268 resources: vec![],
6269 next_cursor: None,
6270 ttl_ms: None,
6271 cache_scope: None,
6272 })
6273 }
6274 }
6275
6276 fn dispatch_v2_meta() -> crate::types::protocol::RequestMeta {
6277 crate::types::protocol::RequestMeta::new().with_meta(
6278 "traceparent",
6279 serde_json::json!("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"),
6280 )
6281 }
6282
6283 fn dispatch_v2_context() -> crate::types::protocol::ProtocolContext {
6284 crate::types::protocol::ProtocolContext::new(
6285 crate::types::protocol::Era::V2,
6286 ProtocolVersion(crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
6287 )
6288 .with_client_info(crate::types::Implementation::new("test-client", "9.9.9"))
6289 }
6290
6291 #[tokio::test]
6292 async fn prompt_resource_protocol_context_via_dispatch_server() {
6293 use crate::types::protocol::Era;
6294
6295 let pcap = Arc::new(Mutex::new(None));
6296 let rcap = Arc::new(Mutex::new(None));
6297 let server = Server::builder()
6298 .name("dispatch-server")
6299 .version("1.0.0")
6300 .prompt("greeting", DispatchCapturingPrompt(pcap.clone()))
6301 .resources(DispatchCapturingResource(rcap.clone()))
6302 .with_supported_protocol_versions([
6303 ProtocolVersion("2025-11-25".to_string()),
6304 ProtocolVersion(crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
6305 ])
6306 .build()
6307 .unwrap();
6308
6309 // --- v2 dispatch through process_client_request: era==V2, client_info,
6310 // and a populated trace_context (proves .with_request_meta threading).
6311 server
6312 .process_client_request(
6313 RequestId::from(1i64),
6314 ClientRequest::GetPrompt(GetPromptRequest {
6315 name: "greeting".to_string(),
6316 arguments: HashMap::new(),
6317 _meta: Some(dispatch_v2_meta()),
6318 }),
6319 None,
6320 Some(dispatch_v2_context()),
6321 &mut crate::server::core::DispatchEnvelopeClaim::default(),
6322 )
6323 .await
6324 .unwrap();
6325 server
6326 .process_client_request(
6327 RequestId::from(2i64),
6328 ClientRequest::ReadResource(ReadResourceRequest {
6329 uri: "mem://greeting".to_string(),
6330 _meta: Some(dispatch_v2_meta()),
6331 }),
6332 None,
6333 Some(dispatch_v2_context()),
6334 &mut crate::server::core::DispatchEnvelopeClaim::default(),
6335 )
6336 .await
6337 .unwrap();
6338
6339 for cap in [&pcap, &rcap] {
6340 let c = cap.lock().unwrap().clone().expect("handler ran");
6341 assert_eq!(c.era, Some(Era::V2));
6342 assert!(c.has_client_info);
6343 assert_eq!(
6344 c.traceparent.as_deref(),
6345 Some("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")
6346 );
6347 }
6348
6349 // --- opted-in v1 fallback: era==Some(V1) (distinct from None).
6350 let pcap = Arc::new(Mutex::new(None));
6351 let rcap = Arc::new(Mutex::new(None));
6352 let server = Server::builder()
6353 .name("dispatch-server")
6354 .version("1.0.0")
6355 .prompt("greeting", DispatchCapturingPrompt(pcap.clone()))
6356 .resources(DispatchCapturingResource(rcap.clone()))
6357 .with_supported_protocol_versions([
6358 ProtocolVersion("2025-11-25".to_string()),
6359 ProtocolVersion(crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
6360 ])
6361 .build()
6362 .unwrap();
6363 let v1 = crate::types::protocol::ProtocolContext::new(
6364 Era::V1,
6365 ProtocolVersion("2025-11-25".to_string()),
6366 );
6367 server
6368 .process_client_request(
6369 RequestId::from(3i64),
6370 ClientRequest::GetPrompt(GetPromptRequest {
6371 name: "greeting".to_string(),
6372 arguments: HashMap::new(),
6373 _meta: None,
6374 }),
6375 None,
6376 Some(v1.clone()),
6377 &mut crate::server::core::DispatchEnvelopeClaim::default(),
6378 )
6379 .await
6380 .unwrap();
6381 server
6382 .process_client_request(
6383 RequestId::from(4i64),
6384 ClientRequest::ReadResource(ReadResourceRequest {
6385 uri: "mem://greeting".to_string(),
6386 _meta: None,
6387 }),
6388 None,
6389 Some(v1),
6390 &mut crate::server::core::DispatchEnvelopeClaim::default(),
6391 )
6392 .await
6393 .unwrap();
6394 assert_eq!(pcap.lock().unwrap().clone().unwrap().era, Some(Era::V1));
6395 assert_eq!(rcap.lock().unwrap().clone().unwrap().era, Some(Era::V1));
6396
6397 // --- non-opted-in (protocol_context == None): era==None.
6398 let pcap = Arc::new(Mutex::new(None));
6399 let rcap = Arc::new(Mutex::new(None));
6400 let server = Server::builder()
6401 .name("dispatch-server")
6402 .version("1.0.0")
6403 .prompt("greeting", DispatchCapturingPrompt(pcap.clone()))
6404 .resources(DispatchCapturingResource(rcap.clone()))
6405 .build()
6406 .unwrap();
6407 server
6408 .process_client_request(
6409 RequestId::from(5i64),
6410 ClientRequest::GetPrompt(GetPromptRequest {
6411 name: "greeting".to_string(),
6412 arguments: HashMap::new(),
6413 _meta: None,
6414 }),
6415 None,
6416 None,
6417 &mut crate::server::core::DispatchEnvelopeClaim::default(),
6418 )
6419 .await
6420 .unwrap();
6421 server
6422 .process_client_request(
6423 RequestId::from(6i64),
6424 ClientRequest::ReadResource(ReadResourceRequest {
6425 uri: "mem://greeting".to_string(),
6426 _meta: None,
6427 }),
6428 None,
6429 None,
6430 &mut crate::server::core::DispatchEnvelopeClaim::default(),
6431 )
6432 .await
6433 .unwrap();
6434 assert_eq!(pcap.lock().unwrap().clone().unwrap().era, None);
6435 assert_eq!(rcap.lock().unwrap().clone().unwrap().era, None);
6436 }
6437
6438 #[tokio::test]
6439 async fn test_handle_list_resources() {
6440 let resource_content =
6441 crate::types::ReadResourceResult::new(vec![crate::types::Content::text(
6442 "Hello, world!",
6443 )]);
6444
6445 let server = Server::builder()
6446 .name("test-server")
6447 .version("1.0.0")
6448 .resources(
6449 MockResource::new().with_resource("test://uri".to_string(), resource_content),
6450 )
6451 .build()
6452 .unwrap();
6453
6454 let request = Request::Client(Box::new(ClientRequest::ListResources(
6455 ListResourcesRequest { cursor: None },
6456 )));
6457 let response = server
6458 .handle_request(RequestId::from(1i64), request, None)
6459 .await;
6460
6461 match response.payload {
6462 ResponsePayload::Result(result) => {
6463 let resources_result: ListResourcesResult = serde_json::from_value(result).unwrap();
6464 assert_eq!(resources_result.resources.len(), 0); // MockResource has empty list by default
6465 },
6466 ResponsePayload::Error(_) => panic!("Expected success response"),
6467 }
6468 }
6469
6470 #[tokio::test]
6471 async fn test_handle_read_resource() {
6472 let resource_content =
6473 crate::types::ReadResourceResult::new(vec![crate::types::Content::text(
6474 "Hello, world!",
6475 )]);
6476
6477 let server = Server::builder()
6478 .name("test-server")
6479 .version("1.0.0")
6480 .resources(
6481 MockResource::new()
6482 .with_resource("test://uri".to_string(), resource_content.clone()),
6483 )
6484 .build()
6485 .unwrap();
6486
6487 let request = Request::Client(Box::new(ClientRequest::ReadResource(ReadResourceRequest {
6488 uri: "test://uri".to_string(),
6489 _meta: None,
6490 })));
6491
6492 let response = server
6493 .handle_request(RequestId::from(1i64), request, None)
6494 .await;
6495
6496 match response.payload {
6497 ResponsePayload::Result(result) => {
6498 let read_result: crate::types::ReadResourceResult =
6499 serde_json::from_value(result).unwrap();
6500 assert_eq!(read_result.contents.len(), 1);
6501 },
6502 ResponsePayload::Error(_) => panic!("Expected success response"),
6503 }
6504 }
6505
6506 #[tokio::test]
6507 async fn test_handle_read_resource_not_found() {
6508 let server = Server::builder()
6509 .name("test-server")
6510 .version("1.0.0")
6511 .resources(MockResource::new())
6512 .build()
6513 .unwrap();
6514
6515 let request = Request::Client(Box::new(ClientRequest::ReadResource(ReadResourceRequest {
6516 uri: "nonexistent://uri".to_string(),
6517 _meta: None,
6518 })));
6519
6520 let response = server
6521 .handle_request(RequestId::from(1i64), request, None)
6522 .await;
6523
6524 match response.payload {
6525 ResponsePayload::Error(error) => {
6526 assert!(error.message.contains("not found"));
6527 },
6528 ResponsePayload::Result(_) => panic!("Expected error response"),
6529 }
6530 }
6531
6532 #[tokio::test]
6533 async fn test_handle_ping() {
6534 let server = Server::builder()
6535 .name("test-server")
6536 .version("1.0.0")
6537 .build()
6538 .unwrap();
6539
6540 let request = Request::Client(Box::new(ClientRequest::Ping));
6541 let response = server
6542 .handle_request(RequestId::from(1i64), request, None)
6543 .await;
6544
6545 match response.payload {
6546 ResponsePayload::Result(_) => {
6547 // Success
6548 },
6549 ResponsePayload::Error(_) => panic!("Expected success response"),
6550 }
6551 }
6552
6553 #[tokio::test]
6554 async fn test_handle_server_request() {
6555 let server = Server::builder()
6556 .name("test-server")
6557 .version("1.0.0")
6558 .build()
6559 .unwrap();
6560
6561 let request = Request::Server(Box::new(crate::types::ServerRequest::CreateMessage(
6562 Box::new(crate::types::CreateMessageParams {
6563 messages: vec![],
6564 model_preferences: None,
6565 system_prompt: None,
6566 include_context: crate::types::IncludeContext::None,
6567 temperature: None,
6568 max_tokens: None,
6569 stop_sequences: None,
6570 metadata: None,
6571 tools: None,
6572 tool_choice: None,
6573 }),
6574 )));
6575 let response = server
6576 .handle_request(RequestId::from(1i64), request, None)
6577 .await;
6578
6579 match response.payload {
6580 ResponsePayload::Error(error) => {
6581 assert_eq!(error.code, -32601);
6582 assert!(error.message.contains("not supported"));
6583 },
6584 ResponsePayload::Result(_) => panic!("Expected error response"),
6585 }
6586 }
6587
6588 // Tests for tool middleware support in ServerBuilder
6589 #[tokio::test]
6590 async fn test_server_builder_with_tool_middleware() {
6591 use crate::server::tool_middleware::{ToolContext, ToolMiddleware};
6592 use std::sync::atomic::{AtomicBool, Ordering};
6593
6594 // Create a simple middleware that sets a flag when called
6595 struct TestMiddleware {
6596 called: Arc<AtomicBool>,
6597 }
6598
6599 #[async_trait]
6600 impl ToolMiddleware for TestMiddleware {
6601 async fn on_request(
6602 &self,
6603 _tool_name: &str,
6604 _args: &mut Value,
6605 extra: &mut crate::server::cancellation::RequestHandlerExtra,
6606 _context: &ToolContext,
6607 ) -> Result<()> {
6608 self.called.store(true, Ordering::SeqCst);
6609 extra.set_metadata("middleware_executed".to_string(), "true".to_string());
6610 Ok(())
6611 }
6612 }
6613
6614 let middleware_called = Arc::new(AtomicBool::new(false));
6615 let middleware = Arc::new(TestMiddleware {
6616 called: Arc::clone(&middleware_called),
6617 });
6618
6619 // Build server with middleware
6620 let server = Server::builder()
6621 .name("test-server")
6622 .version("1.0.0")
6623 .tool("test_tool", MockTool::new(json!({"result": "success"})))
6624 .tool_middleware(middleware)
6625 .build()
6626 .unwrap();
6627
6628 // Call the tool
6629 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
6630 name: "test_tool".to_string(),
6631 arguments: json!({}),
6632 _meta: None,
6633 task: None,
6634 })));
6635
6636 let response = server
6637 .handle_request(RequestId::from(1i64), request, None)
6638 .await;
6639
6640 // Verify middleware was called
6641 assert!(middleware_called.load(Ordering::SeqCst));
6642
6643 // Verify tool executed successfully
6644 match response.payload {
6645 ResponsePayload::Result(_) => {}, // Success
6646 ResponsePayload::Error(e) => panic!("Expected success, got error: {:?}", e),
6647 }
6648 }
6649
6650 #[tokio::test]
6651 async fn test_server_builder_multiple_middlewares() {
6652 use crate::server::tool_middleware::{ToolContext, ToolMiddleware};
6653 use std::sync::atomic::{AtomicUsize, Ordering};
6654
6655 // Middleware that increments a counter
6656 struct CounterMiddleware {
6657 counter: Arc<AtomicUsize>,
6658 id: usize,
6659 }
6660
6661 #[async_trait]
6662 impl ToolMiddleware for CounterMiddleware {
6663 async fn on_request(
6664 &self,
6665 _tool_name: &str,
6666 _args: &mut Value,
6667 extra: &mut crate::server::cancellation::RequestHandlerExtra,
6668 _context: &ToolContext,
6669 ) -> Result<()> {
6670 let count = self.counter.fetch_add(1, Ordering::SeqCst);
6671 extra.set_metadata(format!("middleware_{}_order", self.id), count.to_string());
6672 Ok(())
6673 }
6674 }
6675
6676 let counter = Arc::new(AtomicUsize::new(0));
6677 let middleware1 = Arc::new(CounterMiddleware {
6678 counter: Arc::clone(&counter),
6679 id: 1,
6680 });
6681 let middleware2 = Arc::new(CounterMiddleware {
6682 counter: Arc::clone(&counter),
6683 id: 2,
6684 });
6685
6686 // Build server with multiple middlewares
6687 let server = Server::builder()
6688 .name("test-server")
6689 .version("1.0.0")
6690 .tool("test_tool", MockTool::new(json!({"result": "success"})))
6691 .tool_middleware(middleware1)
6692 .tool_middleware(middleware2)
6693 .build()
6694 .unwrap();
6695
6696 // Call the tool
6697 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
6698 name: "test_tool".to_string(),
6699 arguments: json!({}),
6700 _meta: None,
6701 task: None,
6702 })));
6703
6704 let _response = server
6705 .handle_request(RequestId::from(1i64), request, None)
6706 .await;
6707
6708 // Verify both middlewares were called in order
6709 assert_eq!(counter.load(Ordering::SeqCst), 2);
6710 }
6711
6712 #[tokio::test]
6713 async fn test_server_builder_middleware_with_typed_tools() {
6714 use crate::server::tool_middleware::{ToolContext, ToolMiddleware};
6715 use std::sync::atomic::{AtomicBool, Ordering};
6716
6717 // Middleware that injects OAuth token
6718 struct OAuthMiddleware {
6719 called: Arc<AtomicBool>,
6720 }
6721
6722 #[async_trait]
6723 impl ToolMiddleware for OAuthMiddleware {
6724 async fn on_request(
6725 &self,
6726 _tool_name: &str,
6727 _args: &mut Value,
6728 extra: &mut crate::server::cancellation::RequestHandlerExtra,
6729 _context: &ToolContext,
6730 ) -> Result<()> {
6731 self.called.store(true, Ordering::SeqCst);
6732 extra.set_metadata("oauth_token".to_string(), "test-token-123".to_string());
6733 Ok(())
6734 }
6735 }
6736
6737 // Tool that verifies OAuth token was injected
6738 struct OAuthVerifyTool;
6739
6740 #[async_trait]
6741 impl ToolHandler for OAuthVerifyTool {
6742 async fn handle(
6743 &self,
6744 _args: Value,
6745 extra: crate::server::cancellation::RequestHandlerExtra,
6746 ) -> Result<Value> {
6747 // Verify OAuth token was injected by middleware
6748 let token = extra.get_metadata("oauth_token");
6749 assert!(token.is_some());
6750 assert_eq!(token.unwrap(), "test-token-123");
6751 Ok(json!({"success": true}))
6752 }
6753 }
6754
6755 let middleware_called = Arc::new(AtomicBool::new(false));
6756 let middleware = Arc::new(OAuthMiddleware {
6757 called: Arc::clone(&middleware_called),
6758 });
6759
6760 let server = Server::builder()
6761 .name("test-server")
6762 .version("1.0.0")
6763 .tool("typed_tool", OAuthVerifyTool)
6764 .tool_middleware(middleware)
6765 .build()
6766 .unwrap();
6767
6768 // Call the typed tool
6769 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
6770 name: "typed_tool".to_string(),
6771 arguments: json!({}),
6772 _meta: None,
6773 task: None,
6774 })));
6775
6776 let response = server
6777 .handle_request(RequestId::from(1i64), request, None)
6778 .await;
6779
6780 // Verify middleware was called
6781 assert!(middleware_called.load(Ordering::SeqCst));
6782
6783 // Verify tool executed successfully
6784 match response.payload {
6785 ResponsePayload::Result(_) => {}, // Success
6786 ResponsePayload::Error(e) => panic!("Expected success, got error: {:?}", e),
6787 }
6788 }
6789
6790 #[tokio::test]
6791 async fn test_server_builder_middleware_error_handling() {
6792 use crate::server::tool_middleware::{ToolContext, ToolMiddleware};
6793
6794 // Middleware that rejects requests
6795 struct RejectMiddleware;
6796
6797 #[async_trait]
6798 impl ToolMiddleware for RejectMiddleware {
6799 async fn on_request(
6800 &self,
6801 _tool_name: &str,
6802 _args: &mut Value,
6803 _extra: &mut crate::server::cancellation::RequestHandlerExtra,
6804 _context: &ToolContext,
6805 ) -> Result<()> {
6806 Err(Error::validation("Middleware rejected request"))
6807 }
6808 }
6809
6810 // Build server with rejecting middleware
6811 let server = Server::builder()
6812 .name("test-server")
6813 .version("1.0.0")
6814 .tool("test_tool", MockTool::new(json!({"result": "success"})))
6815 .tool_middleware(Arc::new(RejectMiddleware))
6816 .build()
6817 .unwrap();
6818
6819 // Call the tool
6820 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
6821 name: "test_tool".to_string(),
6822 arguments: json!({}),
6823 _meta: None,
6824 task: None,
6825 })));
6826
6827 let response = server
6828 .handle_request(RequestId::from(1i64), request, None)
6829 .await;
6830
6831 // Verify request was rejected by middleware
6832 match response.payload {
6833 ResponsePayload::Error(e) => {
6834 assert!(e.message.contains("Middleware rejected request"));
6835 },
6836 ResponsePayload::Result(_) => panic!("Expected error from middleware"),
6837 }
6838 }
6839
6840 #[tokio::test]
6841 async fn test_server_builder_auto_capabilities_serialization() {
6842 // Test that ServerBuilder (used by Server::builder()) auto-sets capabilities
6843 // with proper serialization values
6844 let server = Server::builder()
6845 .name("test")
6846 .version("1.0.0")
6847 .tool("test-tool", MockTool::new(json!({"result": "ok"})))
6848 .prompt(
6849 "test-prompt",
6850 MockPrompt::new(crate::types::GetPromptResult {
6851 description: None,
6852 messages: vec![],
6853 _meta: None,
6854 }),
6855 )
6856 .resources(MockResource::new())
6857 .build()
6858 .unwrap();
6859
6860 let caps = &server.capabilities;
6861 let json = serde_json::to_value(caps).unwrap();
6862
6863 // Verify tools capability is present and properly structured
6864 let tools = json.get("tools").expect("tools should be present in JSON");
6865 assert!(tools.is_object(), "tools should be an object");
6866 let list_changed = tools.get("listChanged");
6867 assert!(
6868 list_changed.is_some(),
6869 "listChanged should be present in tools"
6870 );
6871 assert_eq!(
6872 list_changed.unwrap(),
6873 &serde_json::json!(false),
6874 "listChanged should be false"
6875 );
6876
6877 // Verify prompts capability
6878 let prompts = json
6879 .get("prompts")
6880 .expect("prompts should be present in JSON");
6881 assert!(prompts.is_object(), "prompts should be an object");
6882 assert!(
6883 prompts.get("listChanged").is_some(),
6884 "listChanged should be present in prompts"
6885 );
6886
6887 // Verify resources capability
6888 let resources = json
6889 .get("resources")
6890 .expect("resources should be present in JSON");
6891 assert!(resources.is_object(), "resources should be an object");
6892 assert!(
6893 resources.get("listChanged").is_some() || resources.get("subscribe").is_some(),
6894 "resources should have fields"
6895 );
6896
6897 println!(
6898 "Serialized capabilities: {}",
6899 serde_json::to_string_pretty(&json).unwrap()
6900 );
6901 }
6902
6903 /// Behavioral test for `ServerBuilder::tool_authorizer_arc` — the only
6904 /// non-mechanical lift among Phase 82's six `_arc` lifts.
6905 ///
6906 /// `tool_authorizer_arc` mirrors `tool_authorizer()`'s protection-clearing
6907 /// semantics: chaining `.protect_tool(...)` BEFORE `.tool_authorizer_arc(...)`
6908 /// must clear `tool_protections` so that `.build()` does NOT hit the
6909 /// mixed-config rejection branch at mod.rs `build()` (which fires if
6910 /// `tool_protections` is non-empty AND `tool_authorizer` is set).
6911 /// This test fills the verification gap source-greps cannot — proving
6912 /// the `.clear()` call actually fires.
6913 #[tokio::test]
6914 async fn tool_authorizer_arc_clears_tool_protections_and_allows_build() {
6915 // Define a no-op custom ToolAuthorizer for the test.
6916 struct NoopAuthorizer;
6917 #[async_trait]
6918 impl crate::server::auth::ToolAuthorizer for NoopAuthorizer {
6919 async fn can_access_tool(
6920 &self,
6921 _auth: &crate::server::auth::AuthContext,
6922 _tool_name: &str,
6923 ) -> crate::Result<bool> {
6924 Ok(true)
6925 }
6926 async fn required_scopes_for_tool(
6927 &self,
6928 _tool_name: &str,
6929 ) -> crate::Result<Vec<String>> {
6930 Ok(vec![])
6931 }
6932 }
6933
6934 // Build a server with BOTH protect_tool AND tool_authorizer_arc.
6935 // Without the clearing semantic, build() would return the
6936 // "Cannot use protect_tool() with a custom tool_authorizer" error.
6937 let builder = ServerBuilder::new()
6938 .name("test")
6939 .version("1")
6940 .protect_tool("delete", vec!["admin".to_string()])
6941 .tool_authorizer_arc(Arc::new(NoopAuthorizer));
6942
6943 // ASSERT 1: tool_protections was cleared by tool_authorizer_arc(),
6944 // visible because we are inside the same module and have access
6945 // to the private field.
6946 assert!(
6947 builder.tool_protections.is_empty(),
6948 "tool_authorizer_arc() must clear tool_protections to mirror tool_authorizer()"
6949 );
6950
6951 // ASSERT 2: build() succeeds — the mixed-config rejection branch
6952 // does NOT fire because protections was cleared.
6953 let build_result = builder.build();
6954 assert!(
6955 build_result.is_ok(),
6956 "build() should succeed after tool_authorizer_arc() clears protections; got Err({:?})",
6957 build_result.err()
6958 );
6959 }
6960}
6961
6962#[cfg(test)]
6963#[cfg(all(feature = "skills", not(target_arch = "wasm32")))]
6964mod skills_builder_tests {
6965 use super::*;
6966 use crate::server::cancellation::RequestHandlerExtra;
6967 use crate::server::skills::{Skill, SkillReference, Skills};
6968 use crate::types::Content;
6969 use async_trait::async_trait;
6970
6971 // ── Test 2.1a: single skill via ServerBuilder (public path) ──────
6972 #[test]
6973 fn test_2_1a_skill_method_single_skill_via_server_builder() {
6974 let server = Server::builder()
6975 .name("test")
6976 .version("1.0")
6977 .skill(Skill::new("foo", "body"))
6978 .build()
6979 .unwrap();
6980 assert!(server.capabilities.resources.is_some());
6981 }
6982
6983 // ── Test 2.2 (ServerBuilder): extensions capability ──────────────
6984 #[test]
6985 fn test_2_2_server_builder_skills_sets_extensions_capability() {
6986 let server = Server::builder()
6987 .name("test")
6988 .version("1.0")
6989 .skills(Skills::new().add(Skill::new("a", "")))
6990 .build()
6991 .unwrap();
6992 let ext = server
6993 .capabilities
6994 .extensions
6995 .as_ref()
6996 .expect("extensions should be set");
6997 assert_eq!(
6998 ext.get("io.modelcontextprotocol/skills"),
6999 Some(&serde_json::json!({}))
7000 );
7001 }
7002
7003 // ── Test 2.3 (ServerBuilder): resources capability ───────────────
7004 #[test]
7005 fn test_2_3_server_builder_skills_sets_resources_capability() {
7006 let server = Server::builder()
7007 .name("test")
7008 .version("1.0")
7009 .skills(Skills::new().add(Skill::new("a", "")))
7010 .build()
7011 .unwrap();
7012 let r = server
7013 .capabilities
7014 .resources
7015 .as_ref()
7016 .expect("resources should be set");
7017 assert_eq!(r.subscribe, Some(false));
7018 assert_eq!(r.list_changed, Some(false));
7019 }
7020
7021 // ── Test 2.4a: skills compose with existing resources (ServerBuilder) ─
7022 struct DocsHandler;
7023 #[async_trait]
7024 impl ResourceHandler for DocsHandler {
7025 async fn read(
7026 &self,
7027 uri: &str,
7028 _extra: RequestHandlerExtra,
7029 ) -> Result<crate::types::ReadResourceResult> {
7030 Ok(crate::types::ReadResourceResult::new(vec![Content::text(
7031 format!("DOCS:{uri}"),
7032 )]))
7033 }
7034 async fn list(
7035 &self,
7036 _cursor: Option<String>,
7037 _extra: RequestHandlerExtra,
7038 ) -> Result<crate::types::ListResourcesResult> {
7039 Ok(crate::types::ListResourcesResult::new(vec![
7040 crate::types::ResourceInfo::new("docs://handbook", "handbook"),
7041 ]))
7042 }
7043 }
7044
7045 #[test]
7046 fn test_2_4a_server_builder_skills_compose_with_existing_resources() {
7047 let server = Server::builder()
7048 .name("t")
7049 .version("1.0")
7050 .resources(DocsHandler)
7051 .skill(Skill::new("a", "skill-a"))
7052 .build()
7053 .unwrap();
7054 // Capability state must reflect both surfaces.
7055 assert!(server.capabilities.resources.is_some());
7056 let ext = server.capabilities.extensions.as_ref().unwrap();
7057 assert!(ext.contains_key("io.modelcontextprotocol/skills"));
7058 }
7059
7060 // ── Test 2.7 (ServerBuilder): bootstrap_skill_and_prompt ──────────
7061 #[test]
7062 fn test_2_7_server_builder_bootstrap_skill_and_prompt() {
7063 let server = Server::builder()
7064 .name("t")
7065 .version("1.0")
7066 .bootstrap_skill_and_prompt(Skill::new("c", "body-c"), "my_prompt")
7067 .build()
7068 .unwrap();
7069 assert!(server.has_prompt("my_prompt"));
7070 assert!(server.capabilities.prompts.is_some());
7071 let ext = server
7072 .capabilities
7073 .extensions
7074 .as_ref()
7075 .expect("extensions should be set");
7076 assert!(ext.contains_key("io.modelcontextprotocol/skills"));
7077 assert!(server.capabilities.resources.is_some());
7078 }
7079
7080 // ── Test 2.8: wire-level dual-surface invariant via ServerBuilder ─
7081 #[tokio::test]
7082 async fn test_2_8_bootstrap_skill_and_prompt_byte_equal_invariant() {
7083 let skill = Skill::new("x", "A").with_reference(SkillReference::new(
7084 "ref1.md",
7085 "text/markdown",
7086 "refbody",
7087 ));
7088 let expected_text = skill.as_prompt_text();
7089
7090 let server = Server::builder()
7091 .name("t")
7092 .version("1.0")
7093 .bootstrap_skill_and_prompt(skill, "x")
7094 .build()
7095 .unwrap();
7096
7097 let prompt = server
7098 .get_prompt("x")
7099 .expect("prompt 'x' must be registered");
7100 let result = prompt
7101 .handle(HashMap::new(), RequestHandlerExtra::default())
7102 .await
7103 .unwrap();
7104 assert_eq!(result.messages.len(), 1);
7105 match &result.messages[0].content {
7106 Content::Text { text } => assert_eq!(text, &expected_text),
7107 other => panic!("expected Content::Text, got {other:?}"),
7108 }
7109 }
7110
7111 // ── Test 2.9 (ServerBuilder): duplicate URI panics at .build() ───
7112 #[test]
7113 #[should_panic(expected = "duplicate")]
7114 fn test_2_9_server_builder_skills_panics_on_duplicate_uri_at_build() {
7115 let _ = Server::builder()
7116 .name("t")
7117 .version("1.0")
7118 .skill(Skill::new("x", "a"))
7119 .skill(Skill::new("x", "b"))
7120 .build()
7121 .unwrap();
7122 }
7123
7124 // ── Test 2.9a (ServerBuilder): try_skills returns Err on duplicate ─
7125 #[test]
7126 fn test_2_9a_server_builder_try_skills_returns_err_on_duplicate() {
7127 let res = Server::builder().name("t").version("1.0").try_skills(
7128 Skills::new()
7129 .add(Skill::new("x", "a"))
7130 .add(Skill::new("x", "b")),
7131 );
7132 assert!(res.is_err());
7133 match res {
7134 Err(crate::Error::Validation(_)) => {},
7135 Err(other) => panic!("expected Validation, got {other:?}"),
7136 Ok(_) => panic!("expected Err for duplicate"),
7137 }
7138 }
7139
7140 // ── Test 2.10 (ServerBuilder): capability merge preserves extensions ─
7141 #[test]
7142 fn test_2_10_server_builder_capability_merge_preserves_pre_existing_extensions() {
7143 let mut caps = crate::types::ServerCapabilities::default();
7144 let mut ext = HashMap::new();
7145 ext.insert("some.other/ext".to_string(), serde_json::json!({"foo": 1}));
7146 caps.extensions = Some(ext);
7147
7148 let server = Server::builder()
7149 .name("t")
7150 .version("1.0")
7151 .capabilities(caps)
7152 .skill(Skill::new("a", ""))
7153 .build()
7154 .unwrap();
7155 let ext = server.capabilities.extensions.as_ref().unwrap();
7156 assert!(ext.contains_key("some.other/ext"));
7157 assert!(ext.contains_key("io.modelcontextprotocol/skills"));
7158 }
7159
7160 // ── Test 2.11 (ServerBuilder): accumulator — all skills reachable ─
7161 #[test]
7162 fn test_2_11_server_builder_accumulator_repeated_skill_calls_all_reachable() {
7163 // Build a server with three .skill calls and confirm prompts/caps wire up.
7164 let server = Server::builder()
7165 .name("t")
7166 .version("1.0")
7167 .skill(Skill::new("a", "body-a"))
7168 .skill(Skill::new("b", "body-b"))
7169 .bootstrap_skill_and_prompt(Skill::new("c", "body-c"), "c_prompt")
7170 .build()
7171 .unwrap();
7172 assert!(server.has_prompt("c_prompt"));
7173 assert!(server.capabilities.resources.is_some());
7174 }
7175
7176 // ── Test 2.5a (ServerBuilder): .resources() semantics unchanged ──
7177 #[test]
7178 fn test_2_5a_server_builder_resources_replace_unchanged_no_skills() {
7179 struct A;
7180 #[async_trait]
7181 impl ResourceHandler for A {
7182 async fn read(
7183 &self,
7184 _uri: &str,
7185 _extra: RequestHandlerExtra,
7186 ) -> Result<crate::types::ReadResourceResult> {
7187 Ok(crate::types::ReadResourceResult::new(vec![Content::text(
7188 "A",
7189 )]))
7190 }
7191 async fn list(
7192 &self,
7193 _cursor: Option<String>,
7194 _extra: RequestHandlerExtra,
7195 ) -> Result<crate::types::ListResourcesResult> {
7196 Ok(crate::types::ListResourcesResult::new(vec![]))
7197 }
7198 }
7199 struct B;
7200 #[async_trait]
7201 impl ResourceHandler for B {
7202 async fn read(
7203 &self,
7204 _uri: &str,
7205 _extra: RequestHandlerExtra,
7206 ) -> Result<crate::types::ReadResourceResult> {
7207 Ok(crate::types::ReadResourceResult::new(vec![Content::text(
7208 "B",
7209 )]))
7210 }
7211 async fn list(
7212 &self,
7213 _cursor: Option<String>,
7214 _extra: RequestHandlerExtra,
7215 ) -> Result<crate::types::ListResourcesResult> {
7216 Ok(crate::types::ListResourcesResult::new(vec![]))
7217 }
7218 }
7219
7220 let server = Server::builder()
7221 .name("t")
7222 .version("1.0")
7223 .resources(A)
7224 .resources(B)
7225 .build()
7226 .unwrap();
7227 // No skills registered → no composition. Capabilities reflect resources only.
7228 assert!(server.capabilities.resources.is_some());
7229 // Skills extension should NOT have been auto-set.
7230 let ext = server.capabilities.extensions.as_ref();
7231 if let Some(ext_map) = ext {
7232 assert!(!ext_map.contains_key("io.modelcontextprotocol/skills"));
7233 }
7234 }
7235}
7236
7237#[cfg(test)]
7238#[cfg(not(target_arch = "wasm32"))]
7239mod tool_output_tests {
7240 use super::*;
7241 use crate::server::cancellation::RequestHandlerExtra;
7242 use async_trait::async_trait;
7243
7244 /// A handler that implements ONLY `handle` (no `handle_output` override) —
7245 /// the common case. It must route through the default `handle_output` and
7246 /// come back as `ToolOutput::Payload` equal to what `handle` returned.
7247 struct PlainHandler;
7248
7249 #[async_trait]
7250 impl ToolHandler for PlainHandler {
7251 async fn handle(&self, args: Value, _extra: RequestHandlerExtra) -> Result<Value> {
7252 Ok(serde_json::json!({ "echo": args }))
7253 }
7254 }
7255
7256 #[tokio::test]
7257 async fn default_handle_output_delegates_to_handle_as_payload() {
7258 let handler = PlainHandler;
7259 let extra = RequestHandlerExtra::new("req-1".to_string(), Default::default());
7260 let args = serde_json::json!({ "n": 42 });
7261
7262 let value_via_handle = handler.handle(args.clone(), extra.clone()).await.unwrap();
7263 let output = handler.handle_output(args, extra).await.unwrap();
7264
7265 match output {
7266 ToolOutput::Payload(v) => assert_eq!(
7267 v, value_via_handle,
7268 "default handle_output must wrap handle()'s value as Payload"
7269 ),
7270 other => panic!("expected ToolOutput::Payload, got {other:?}"),
7271 }
7272 }
7273}
7274
7275// ===========================================================================
7276// `attach_peer` precedence and authorization ordering.
7277//
7278// Phase 118.1 plan 11. Two claims, both of which need crate-internal access —
7279// `attach_peer` is private and `Server::peer_handle` is only ever set by
7280// `Server::run()` — so they live here rather than in an integration test:
7281//
7282// * T-118.1-11-04: when BOTH a global `Server::peer_handle` and a
7283// request-scoped `TransportBackchannel` peer are configured, the
7284// request-scoped one wins. A global handle cannot express WHICH session
7285// issued the request, so on a multiplexed transport it is the wrong answer.
7286// * `src/shared/peer.rs`'s authorization invariant: tool-level authz runs
7287// BEFORE the peer is wired, so a refused caller never reaches a handler body
7288// and therefore never sees `extra.peer()`.
7289// ===========================================================================
7290#[cfg(all(test, not(target_arch = "wasm32")))]
7291mod peer_precedence_tests {
7292 use super::*;
7293 use crate::server::auth::AuthContext;
7294 use crate::shared::peer::PeerHandle;
7295 use crate::types::protocol::context::TransportBackchannel;
7296 use crate::types::protocol::{Era, ProtocolContext, ProtocolVersion};
7297 use crate::types::roots::{ListRootsResult, Root};
7298 use crate::types::sampling::{CreateMessageParams, CreateMessageResult};
7299 use crate::types::ProgressToken;
7300 use crate::RequestHandlerExtra;
7301 use async_trait::async_trait;
7302 use std::sync::atomic::{AtomicBool, Ordering};
7303
7304 /// A peer that reports WHICH source supplied it, through the one method
7305 /// with an observable, source-specific answer.
7306 struct NamedPeer(&'static str);
7307
7308 #[async_trait]
7309 impl PeerHandle for NamedPeer {
7310 async fn sample(&self, _params: CreateMessageParams) -> Result<CreateMessageResult> {
7311 Err(Error::protocol(
7312 crate::ErrorCode::METHOD_NOT_FOUND,
7313 "not the method under test",
7314 ))
7315 }
7316
7317 async fn list_roots(&self) -> Result<ListRootsResult> {
7318 Ok(ListRootsResult {
7319 roots: vec![Root {
7320 uri: format!("file:///{}", self.0),
7321 name: Some(self.0.to_string()),
7322 }],
7323 })
7324 }
7325
7326 async fn progress_notify(
7327 &self,
7328 _token: ProgressToken,
7329 _progress: f64,
7330 _total: Option<f64>,
7331 _message: Option<String>,
7332 ) -> Result<()> {
7333 Ok(())
7334 }
7335 }
7336
7337 /// The name the attached peer answers with, or `None` if none was attached.
7338 async fn attached_peer_name(
7339 extra: &crate::server::cancellation::RequestHandlerExtra,
7340 ) -> Option<String> {
7341 let peer = extra.peer()?;
7342 let roots = peer.list_roots().await.expect("the fixture peer answers");
7343 roots.roots.first().and_then(|r| r.name.clone())
7344 }
7345
7346 /// A `ProtocolContext` carrying a request-scoped peer named `name`.
7347 fn context_with_peer(name: &'static str) -> ProtocolContext {
7348 let peer: Arc<dyn PeerHandle> = Arc::new(NamedPeer(name));
7349 ProtocolContext::new(
7350 Era::V1,
7351 ProtocolVersion(crate::types::protocol::LATEST_PROTOCOL_VERSION.to_string()),
7352 )
7353 .with_transport_backchannel(TransportBackchannel::new().with_peer(peer))
7354 }
7355
7356 fn bare_server() -> Server {
7357 Server::builder()
7358 .name("attach-peer-precedence")
7359 .version("1.0.0")
7360 .build()
7361 .expect("server builds")
7362 }
7363
7364 fn extra_with_context(
7365 context: Option<ProtocolContext>,
7366 ) -> crate::server::cancellation::RequestHandlerExtra {
7367 crate::server::cancellation::RequestHandlerExtra::new(
7368 "req-attach-peer".to_string(),
7369 crate::server::cancellation::RequestHandlerExtra::default().cancellation_token,
7370 )
7371 .with_protocol_context(context)
7372 }
7373
7374 /// THE precedence claim (T-118.1-11-04). Both sources configured at once.
7375 #[tokio::test]
7376 async fn the_request_scoped_peer_wins_over_the_global_peer_handle() {
7377 let mut server = bare_server();
7378 server.peer_handle = Some(Arc::new(NamedPeer("global")));
7379
7380 let extra = server.attach_peer(extra_with_context(Some(context_with_peer(
7381 "request-scoped",
7382 ))));
7383
7384 assert_eq!(
7385 attached_peer_name(&extra).await.as_deref(),
7386 Some("request-scoped"),
7387 "a request-scoped transport handle must win: the global `peer_handle` is a SINGLE \
7388 field and cannot express which session issued this request (T-118.1-11-04)"
7389 );
7390 }
7391
7392 /// The fallback is untouched: the in-process `Server::run` path attaches no
7393 /// backchannel, so it must still see the global handle.
7394 #[tokio::test]
7395 async fn the_global_handle_still_applies_when_no_backchannel_rides_the_context() {
7396 let mut server = bare_server();
7397 server.peer_handle = Some(Arc::new(NamedPeer("global")));
7398
7399 let with_no_context = server.attach_peer(extra_with_context(None));
7400 assert_eq!(
7401 attached_peer_name(&with_no_context).await.as_deref(),
7402 Some("global"),
7403 "with no protocol context at all the global handle must still apply"
7404 );
7405
7406 let bare_context = ProtocolContext::new(
7407 Era::V1,
7408 ProtocolVersion(crate::types::protocol::LATEST_PROTOCOL_VERSION.to_string()),
7409 );
7410 let with_peerless_context = server.attach_peer(extra_with_context(Some(bare_context)));
7411 assert_eq!(
7412 attached_peer_name(&with_peerless_context).await.as_deref(),
7413 Some("global"),
7414 "a context with no backchannel must fall through to the global handle"
7415 );
7416 }
7417
7418 /// Neither source configured: still a no-op, exactly as before.
7419 #[tokio::test]
7420 async fn attach_peer_is_a_no_op_when_neither_source_is_configured() {
7421 let server = bare_server();
7422 let extra = server.attach_peer(extra_with_context(None));
7423 assert!(
7424 extra.peer().is_none(),
7425 "with no global handle and no backchannel, `extra.peer()` stays None"
7426 );
7427 }
7428
7429 /// A request-scoped peer applies even with NO global handle — the
7430 /// `StreamableHTTP` case, where `Server::run()` never ran.
7431 #[tokio::test]
7432 async fn the_request_scoped_peer_applies_with_no_global_handle_at_all() {
7433 let server = bare_server();
7434 let extra = server.attach_peer(extra_with_context(Some(context_with_peer("transport"))));
7435 assert_eq!(
7436 attached_peer_name(&extra).await.as_deref(),
7437 Some("transport"),
7438 "the HTTP transport never calls `Server::run()`, so the request-scoped handle is \
7439 the ONLY source there"
7440 );
7441 }
7442
7443 // -----------------------------------------------------------------------
7444 // Authorization ordering.
7445 // -----------------------------------------------------------------------
7446
7447 /// Records whether its body ever ran, and reports the peer it saw.
7448 struct EntryRecordingTool(Arc<AtomicBool>);
7449
7450 #[async_trait]
7451 impl ToolHandler for EntryRecordingTool {
7452 async fn handle(&self, _args: Value, extra: RequestHandlerExtra) -> Result<Value> {
7453 self.0.store(true, Ordering::SeqCst);
7454 Ok(serde_json::json!({ "saw_peer": extra.peer().is_some() }))
7455 }
7456 }
7457
7458 /// Refuses every tool.
7459 struct DenyAll;
7460
7461 #[async_trait]
7462 impl crate::server::auth::ToolAuthorizer for DenyAll {
7463 async fn can_access_tool(&self, _auth: &AuthContext, _tool: &str) -> Result<bool> {
7464 Ok(false)
7465 }
7466
7467 async fn required_scopes_for_tool(&self, _tool_name: &str) -> Result<Vec<String>> {
7468 Ok(Vec::new())
7469 }
7470 }
7471
7472 /// An unauthorized caller never reaches the handler BODY, so it can never
7473 /// observe `extra.peer()` — regardless of which peer source is configured.
7474 ///
7475 /// The ordering this measures is structural: `handle_call_tool` runs the
7476 /// `tool_authorizer` check (`src/server/mod.rs`, immediately after the
7477 /// auth-context resolution) and only afterwards calls `attach_peer`.
7478 #[tokio::test]
7479 async fn an_unauthorized_caller_never_reaches_the_handler_body() {
7480 let entered = Arc::new(AtomicBool::new(false));
7481 let mut server = Server::builder()
7482 .name("authz-before-peer")
7483 .version("1.0.0")
7484 .tool("guarded", EntryRecordingTool(entered.clone()))
7485 .tool_authorizer(DenyAll)
7486 .build()
7487 .expect("server builds");
7488 // BOTH peer sources configured, so a leak through either would show up.
7489 server.peer_handle = Some(Arc::new(NamedPeer("global")));
7490
7491 let mut claim = crate::server::core::DispatchEnvelopeClaim::default();
7492 let result = server
7493 .handle_call_tool(
7494 RequestId::from(1i64),
7495 CallToolRequest {
7496 name: "guarded".to_string(),
7497 arguments: serde_json::json!({}),
7498 task: None,
7499 _meta: None,
7500 },
7501 Some(AuthContext::new("someone")),
7502 Some(context_with_peer("request-scoped")),
7503 &mut claim,
7504 )
7505 .await;
7506
7507 assert!(result.is_err(), "a denied tool call must return an error");
7508 assert!(
7509 !entered.load(Ordering::SeqCst),
7510 "the handler body must never run for an unauthorized caller — authz runs BEFORE \
7511 `attach_peer`, so a refused caller never sees `extra.peer()`"
7512 );
7513 }
7514}
7515
7516// ===========================================================================
7517// `attach_request_log_sink` at the `Server` root — the TWIN of
7518// `core_log_sink_tests` in `src/server/core.rs` (Phase 118.2 plan 06, CONF-10 /
7519// D-07).
7520//
7521// Same crate-internal-access reason as `peer_precedence_tests` above:
7522// `attach_peer`, `notification_tx_sink`, `progress_reporter_for` and
7523// `Server::notification_tx` are all private, and `TransportBackchannel` /
7524// `ProtocolContext::with_resolved_log_level` are `pub(crate)`. An integration
7525// test can construct none of them.
7526//
7527// The claims measured here that the `ServerCore` side CANNOT measure:
7528//
7529// * the `notification_tx`-derived fallback exists on this root and nowhere
7530// else, and the request-scoped sink still beats it;
7531// * D-07: the progress-token gate moved OFF the log sink and STAYED on the
7532// progress reporter. One request, both answers, in one test.
7533// ===========================================================================
7534#[cfg(all(test, not(target_arch = "wasm32")))]
7535mod log_sink_precedence_tests {
7536 use super::*;
7537 use crate::types::protocol::context::TransportBackchannel;
7538 use crate::types::protocol::{Era, ProtocolContext, ProtocolVersion};
7539 use crate::types::LoggingLevel;
7540 use std::sync::Mutex;
7541
7542 /// A sink that records every notification handed to it.
7543 #[derive(Clone, Default)]
7544 struct Capture(Arc<Mutex<Vec<Notification>>>);
7545
7546 impl Capture {
7547 fn sink(&self) -> Arc<dyn Fn(Notification) + Send + Sync> {
7548 let slot = Arc::clone(&self.0);
7549 Arc::new(move |notification| {
7550 slot.lock()
7551 .unwrap_or_else(std::sync::PoisonError::into_inner)
7552 .push(notification);
7553 })
7554 }
7555
7556 fn len(&self) -> usize {
7557 self.0
7558 .lock()
7559 .unwrap_or_else(std::sync::PoisonError::into_inner)
7560 .len()
7561 }
7562 }
7563
7564 fn bare_context() -> ProtocolContext {
7565 ProtocolContext::new(
7566 Era::V1,
7567 ProtocolVersion(crate::types::protocol::LATEST_PROTOCOL_VERSION.to_string()),
7568 )
7569 }
7570
7571 fn context_with_sink(capture: &Capture) -> ProtocolContext {
7572 bare_context().with_transport_backchannel(
7573 TransportBackchannel::new().with_notification_sink(capture.sink()),
7574 )
7575 }
7576
7577 fn extra_with_context(
7578 context: Option<ProtocolContext>,
7579 ) -> crate::server::cancellation::RequestHandlerExtra {
7580 crate::server::cancellation::RequestHandlerExtra::new(
7581 "req-attach-log-sink".to_string(),
7582 crate::server::cancellation::RequestHandlerExtra::default().cancellation_token,
7583 )
7584 .with_protocol_context(context)
7585 }
7586
7587 fn bare_server() -> Server {
7588 Server::builder()
7589 .name("attach-log-sink-precedence")
7590 .version("1.0.0")
7591 .build()
7592 .expect("server builds")
7593 }
7594
7595 /// The ROOT claim on this side: `Server::attach_peer` wires the log sink from
7596 /// the server-wide `notification_tx` when the request carries no
7597 /// back-channel of its own — the in-process `Server::run` path.
7598 #[tokio::test]
7599 async fn the_server_root_attaches_its_notification_tx_derived_fallback_log_sink() {
7600 let mut server = bare_server();
7601 let (tx, mut rx) = mpsc::channel(4);
7602 server.notification_tx = Some(tx);
7603
7604 let extra = server.attach_peer(extra_with_context(Some(bare_context())));
7605 extra
7606 .log(
7607 LoggingLevel::Warning,
7608 "through the notification_tx fallback",
7609 )
7610 .expect("the emitter always returns Ok");
7611
7612 let received = rx.try_recv().expect("the fallback sink must deliver");
7613 match received {
7614 Notification::Server(crate::types::ServerNotification::LogMessage(params)) => {
7615 assert_eq!(params.message, "through the notification_tx fallback");
7616 assert_eq!(params.level, LoggingLevel::Warning);
7617 },
7618 other => panic!("expected a LogMessage notification, got {other:?}"),
7619 }
7620 }
7621
7622 /// The precedence rule measured on THIS root too, with both sources live at
7623 /// once — the `ServerCore` twin cannot run this, because it has no
7624 /// `notification_tx` to lose to.
7625 #[tokio::test]
7626 async fn the_request_scoped_sink_wins_over_the_notification_tx_fallback() {
7627 let mut server = bare_server();
7628 let (tx, mut rx) = mpsc::channel(4);
7629 server.notification_tx = Some(tx);
7630 let request_scoped = Capture::default();
7631
7632 let extra =
7633 server.attach_peer(extra_with_context(Some(context_with_sink(&request_scoped))));
7634 extra
7635 .log(LoggingLevel::Warning, "which sink received me?")
7636 .expect("the emitter always returns Ok");
7637
7638 assert_eq!(
7639 request_scoped.len(),
7640 1,
7641 "the session-bound transport sink must win at the `Server` root exactly as it does at \
7642 the `ServerCore` root (T-118.2-06-02/03)"
7643 );
7644 assert!(
7645 rx.try_recv().is_err(),
7646 "the server-wide channel must NOT also receive one session's record"
7647 );
7648 }
7649
7650 /// D-07, stated as a test: ONE request with NO `progressToken` gets a LIVE
7651 /// log sink and a `None` progress reporter.
7652 ///
7653 /// The gate moved off the sink and stayed on the reporter. Unifying the two
7654 /// would either silence logs for every client that never asked for progress,
7655 /// or make progress notifications unconditional — a client that sent no token
7656 /// has nothing to correlate them with (T-118.2-06-04).
7657 #[tokio::test]
7658 async fn the_progress_token_gate_still_applies_to_progress_only() {
7659 let mut server = bare_server();
7660 let (tx, _rx) = mpsc::channel(4);
7661 server.notification_tx = Some(tx);
7662 let capture = Capture::default();
7663 let context = context_with_sink(&capture);
7664
7665 assert!(
7666 server.progress_reporter_for(None, Some(&context)).is_none(),
7667 "no `params._meta.progressToken` must still mean no progress reporter"
7668 );
7669
7670 let extra = server.attach_peer(extra_with_context(Some(context)));
7671 assert!(
7672 extra.log_sink.is_some(),
7673 "the log sink is UNGATED by the progress token — a client that never asked for \
7674 progress must still receive `notifications/message` (D-07)"
7675 );
7676 extra
7677 .log(LoggingLevel::Info, "no progress token on this request")
7678 .expect("the emitter always returns Ok");
7679 assert_eq!(
7680 capture.len(),
7681 1,
7682 "the record must reach the client even though this request has no progress reporter"
7683 );
7684 }
7685
7686 /// There is exactly ONE `notification_tx`-to-sink conversion, and both
7687 /// consumers read it. Measured behaviourally rather than by grep: the
7688 /// progress path and the log path must produce sinks that reach the SAME
7689 /// channel with the SAME non-blocking discipline.
7690 #[tokio::test]
7691 async fn the_progress_and_log_paths_share_one_notification_tx_sink() {
7692 let mut server = bare_server();
7693 let (tx, mut rx) = mpsc::channel(4);
7694 server.notification_tx = Some(tx);
7695
7696 let via_progress = server
7697 .progress_notification_sink(None)
7698 .expect("a server with a notification_tx has a sink");
7699 let via_log = server
7700 .notification_tx_sink()
7701 .expect("the same server has the same sink");
7702
7703 via_progress(Notification::Server(
7704 crate::types::ServerNotification::LogMessage(crate::types::LogMessageParams::new(
7705 LoggingLevel::Info,
7706 "via progress".to_string(),
7707 )),
7708 ));
7709 via_log(Notification::Server(
7710 crate::types::ServerNotification::LogMessage(crate::types::LogMessageParams::new(
7711 LoggingLevel::Info,
7712 "via log".to_string(),
7713 )),
7714 ));
7715
7716 assert!(rx.try_recv().is_ok(), "the progress-derived sink delivers");
7717 assert!(rx.try_recv().is_ok(), "the log-derived sink delivers too");
7718 }
7719}