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::{Protocol, ProtocolOptions, 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::sync::Arc;
23
24#[cfg(not(target_arch = "wasm32"))]
25use crate::runtime::RwLock;
26#[cfg(not(target_arch = "wasm32"))]
27use tokio::sync::mpsc;
28
29// Core modules (currently native-only due to dependencies)
30#[cfg(not(target_arch = "wasm32"))]
31pub mod adapters;
32#[cfg(not(target_arch = "wasm32"))]
33pub mod builder;
34#[cfg(not(target_arch = "wasm32"))]
35pub mod core;
36pub mod limits;
37
38// Native-only modules (require tokio, threading, etc.)
39#[cfg(not(target_arch = "wasm32"))]
40pub mod auth;
41#[cfg(not(target_arch = "wasm32"))]
42pub mod batch;
43/// Builder-scoped middleware executor for workflow registration.
44#[cfg(not(target_arch = "wasm32"))]
45pub mod builder_middleware_executor;
46#[cfg(not(target_arch = "wasm32"))]
47pub mod cancellation;
48/// Dynamic resource provider system for pattern-based resource routing.
49#[cfg(not(target_arch = "wasm32"))]
50pub mod dynamic_resources;
51#[cfg(not(target_arch = "wasm32"))]
52pub mod http_middleware;
53/// Middleware executor abstraction for consistent tool execution.
54#[cfg(not(target_arch = "wasm32"))]
55pub mod middleware_executor;
56/// Concrete `PeerHandle` implementation delegating to the
57/// `ServerRequestDispatcher`.
58#[cfg(not(target_arch = "wasm32"))]
59pub(crate) mod peer_impl;
60#[cfg(not(target_arch = "wasm32"))]
61pub mod preset;
62/// Progress reporting support for long-running operations.
63#[cfg(not(target_arch = "wasm32"))]
64pub mod progress;
65/// Outbound server-to-client request dispatcher with response correlation.
66#[cfg(not(target_arch = "wasm32"))]
67pub(crate) mod server_request_dispatcher;
68/// Simple prompt implementations with metadata support.
69#[cfg(not(target_arch = "wasm32"))]
70pub mod simple_prompt;
71/// Simple resource implementations with builder pattern support.
72#[cfg(not(target_arch = "wasm32"))]
73pub mod simple_resources;
74/// Simple tool implementations with schema support.
75#[cfg(not(target_arch = "wasm32"))]
76pub mod simple_tool;
77/// SDK-level task store trait and in-memory implementation.
78#[cfg(not(target_arch = "wasm32"))]
79pub mod task_store;
80/// Task routing trait for MCP Tasks integration.
81#[cfg(not(target_arch = "wasm32"))]
82pub mod tasks;
83/// Tool middleware for cross-cutting concerns in tool execution.
84#[cfg(not(target_arch = "wasm32"))]
85pub mod tool_middleware;
86
87/// Observability infrastructure for tracing, metrics, and logging.
88#[cfg(not(target_arch = "wasm32"))]
89pub mod observability;
90/// Workflow-based prompt system with type-safe handles and ergonomic builders.
91#[cfg(not(target_arch = "wasm32"))]
92pub mod workflow;
93
94/// State extractor for `#[mcp_tool]` shared state injection.
95#[cfg(not(target_arch = "wasm32"))]
96pub mod state;
97
98/// Typed tool implementations with automatic schema generation.
99#[cfg(not(target_arch = "wasm32"))]
100pub mod typed_tool;
101
102/// Typed prompt implementations with automatic argument schema generation.
103#[cfg(not(target_arch = "wasm32"))]
104pub mod typed_prompt;
105
106/// UI resource implementations for MCP Apps Extension (SEP-1865).
107#[cfg(not(target_arch = "wasm32"))]
108pub mod ui;
109
110/// MCP Apps Extension - Interactive UI support for multiple MCP hosts.
111///
112/// Provides adapters for `ChatGPT` Apps, MCP Apps (SEP-1865), and MCP-UI.
113#[cfg(all(not(target_arch = "wasm32"), feature = "mcp-apps"))]
114pub mod mcp_apps;
115
116/// Agent Skills (SEP-2640) — [`skills::Skill`] / [`skills::SkillReference`] /
117/// [`skills::Skills`] plus a dual-surface `PromptHandler` fallback.
118///
119/// Gated on `feature = "skills"` AND `not(target_arch = "wasm32")`: the
120/// module's contents consume [`ResourceHandler`] and [`PromptHandler`],
121/// which are themselves non-wasm-only.
122#[cfg(all(feature = "skills", not(target_arch = "wasm32")))]
123pub mod skills;
124
125/// Re-export the public Skills DX types so callers can `use
126/// pmcp::server::{Skill, SkillReference, Skills}` without descending
127/// into the `skills::` submodule path. The canonical path remains
128/// `pmcp::server::skills::*`.
129#[cfg(all(feature = "skills", not(target_arch = "wasm32")))]
130pub use skills::{Skill, SkillReference, Skills};
131
132/// Validation helpers for typed tools.
133#[cfg(not(target_arch = "wasm32"))]
134pub mod validation;
135
136/// Schema utilities for normalizing and inlining JSON schemas.
137#[cfg(feature = "schema-generation")]
138pub mod schema_utils;
139
140/// Standard error codes for validation with client elicitation support.
141#[cfg(not(target_arch = "wasm32"))]
142pub mod error_codes;
143
144/// Cross-platform path validation with security constraints.
145#[cfg(not(target_arch = "wasm32"))]
146pub mod path_validation;
147
148/// WASM-compatible typed tools with automatic schema generation.
149#[cfg(target_arch = "wasm32")]
150pub mod wasm_typed_tool;
151
152// For WASM, provide a simple stub for RequestHandlerExtra
153#[cfg(target_arch = "wasm32")]
154pub mod cancellation {
155 /// Stub for WASM - no cancellation support
156 #[derive(Debug, Clone, Default)]
157 pub struct RequestHandlerExtra;
158}
159/// Axum Router convenience function for secure MCP server hosting.
160#[cfg(feature = "streamable-http")]
161pub mod axum_router;
162#[cfg(not(target_arch = "wasm32"))]
163pub mod dynamic;
164#[cfg(not(target_arch = "wasm32"))]
165pub mod elicitation;
166#[cfg(not(target_arch = "wasm32"))]
167pub mod notification_debouncer;
168#[cfg(all(not(target_arch = "wasm32"), feature = "resource-watcher"))]
169pub mod resource_watcher;
170#[cfg(not(target_arch = "wasm32"))]
171pub mod roots;
172#[cfg(all(not(target_arch = "wasm32"), feature = "streamable-http"))]
173pub mod streamable_http_server;
174#[cfg(not(target_arch = "wasm32"))]
175pub mod subscriptions;
176/// Tower middleware layers for MCP HTTP security (DNS rebinding, security headers).
177#[cfg(feature = "streamable-http")]
178pub mod tower_layers;
179#[cfg(not(target_arch = "wasm32"))]
180pub mod transport;
181
182// WASM-specific modules and types
183#[cfg(target_arch = "wasm32")]
184pub mod wasi_adapter;
185#[cfg(target_arch = "wasm32")]
186pub mod wasm_core;
187#[cfg(target_arch = "wasm32")]
188pub mod wasm_server;
189#[cfg(all(test, target_arch = "wasm32"))]
190mod wasm_server_tests;
191
192// WASM-compatible protocol handler trait
193#[cfg(target_arch = "wasm32")]
194pub use wasi_protocol::ProtocolHandler;
195
196#[cfg(target_arch = "wasm32")]
197mod wasi_protocol {
198 use crate::error::Result;
199 use crate::types::{JSONRPCResponse, Notification, Request, RequestId};
200 use async_trait::async_trait;
201
202 /// Protocol-agnostic request handler trait for WASM.
203 ///
204 /// This is a simplified version of the ProtocolHandler trait that
205 /// doesn't depend on native-only types like handlers and managers.
206 #[async_trait(?Send)]
207 pub trait ProtocolHandler {
208 /// Handle a single request and return a response.
209 async fn handle_request(&self, id: RequestId, request: Request) -> JSONRPCResponse;
210
211 /// Handle a notification (no response expected).
212 async fn handle_notification(&self, notification: Notification) -> Result<()>;
213 }
214}
215
216#[cfg(test)]
217mod adapter_tests;
218#[cfg(test)]
219mod core_tests;
220
221/// Handler for tool execution.
222#[cfg(not(target_arch = "wasm32"))]
223#[async_trait]
224pub trait ToolHandler: Send + Sync {
225 /// Handle a tool call with the given arguments.
226 async fn handle(&self, args: Value, extra: cancellation::RequestHandlerExtra) -> Result<Value>;
227
228 /// Get tool metadata including description and schema.
229 /// Returns None to use default empty metadata.
230 fn metadata(&self) -> Option<crate::types::ToolInfo> {
231 None
232 }
233}
234
235/// Handler for prompt generation.
236#[cfg(not(target_arch = "wasm32"))]
237#[async_trait]
238pub trait PromptHandler: Send + Sync {
239 /// Generate a prompt with the given arguments.
240 async fn handle(
241 &self,
242 args: HashMap<String, String>,
243 extra: cancellation::RequestHandlerExtra,
244 ) -> Result<crate::types::GetPromptResult>;
245
246 /// Get prompt metadata including description and arguments schema.
247 /// Returns None to use default empty metadata.
248 fn metadata(&self) -> Option<crate::types::PromptInfo> {
249 None
250 }
251}
252
253/// Handler for resource access.
254#[cfg(not(target_arch = "wasm32"))]
255#[async_trait]
256pub trait ResourceHandler: Send + Sync {
257 /// Read a resource at the given URI.
258 async fn read(
259 &self,
260 uri: &str,
261 extra: cancellation::RequestHandlerExtra,
262 ) -> Result<crate::types::ReadResourceResult>;
263
264 /// List available resources.
265 async fn list(
266 &self,
267 _cursor: Option<String>,
268 extra: cancellation::RequestHandlerExtra,
269 ) -> Result<crate::types::ListResourcesResult>;
270}
271
272/// Handler for message sampling (LLM operations).
273#[cfg(not(target_arch = "wasm32"))]
274#[async_trait]
275pub trait SamplingHandler: Send + Sync {
276 /// Create a message using the language model.
277 async fn create_message(
278 &self,
279 params: crate::types::CreateMessageParams,
280 extra: cancellation::RequestHandlerExtra,
281 ) -> Result<crate::types::CreateMessageResult>;
282}
283
284/// MCP server implementation.
285///
286/// # Examples
287///
288/// ```rust,no_run
289/// use pmcp::{Server, ServerCapabilities, ToolHandler};
290/// use async_trait::async_trait;
291/// use serde_json::Value;
292///
293/// struct MyTool;
294///
295/// #[async_trait]
296/// impl ToolHandler for MyTool {
297/// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
298/// Ok(serde_json::json!({"result": "success"}))
299/// }
300/// }
301///
302/// # async fn example() -> pmcp::Result<()> {
303/// let server = Server::builder()
304/// .name("my-server")
305/// .version("1.0.0")
306/// .tool("my-tool", MyTool)
307/// .build()?;
308///
309/// server.run_stdio().await?;
310/// # Ok(())
311/// # }
312/// ```
313#[cfg(not(target_arch = "wasm32"))]
314#[allow(dead_code)]
315pub struct Server {
316 info: Implementation,
317 capabilities: ServerCapabilities,
318 tools: HashMap<String, Arc<dyn ToolHandler>>,
319 tool_infos: HashMap<String, ToolInfo>,
320 /// Cached URI-to-tool-meta index for widget resource `_meta` propagation.
321 uri_to_tool_meta: HashMap<String, serde_json::Map<String, serde_json::Value>>,
322 prompts: HashMap<String, Arc<dyn PromptHandler>>,
323 resources: Option<Arc<dyn ResourceHandler>>,
324 sampling: Option<Arc<dyn SamplingHandler>>,
325 client_capabilities: Arc<RwLock<Option<ClientCapabilities>>>,
326 initialized: Arc<RwLock<bool>>,
327 /// Channel for sending notifications
328 notification_tx: Option<mpsc::Sender<Notification>>,
329 /// Cancellation manager for request cancellation
330 cancellation_manager: cancellation::CancellationManager,
331 /// Roots manager for directory/URI registration
332 roots_manager: Arc<RwLock<roots::RootsManager>>,
333 /// Subscription manager for resource subscriptions
334 subscription_manager: Arc<RwLock<subscriptions::SubscriptionManager>>,
335 /// Elicitation manager for user input requests
336 elicitation_manager: Option<Arc<elicitation::ElicitationManager>>,
337 /// Outbound server-to-client request dispatcher with response correlation.
338 /// Wired in `Server::run`; `None` outside the run lifecycle.
339 #[allow(clippy::struct_field_names)]
340 server_request_dispatcher: Option<Arc<server_request_dispatcher::ServerRequestDispatcher>>,
341 /// Cached peer handle built alongside the dispatcher so dispatch sites
342 /// clone the Arc rather than allocating a new `DispatchPeerHandle` per
343 /// request. `None` outside the run lifecycle.
344 peer_handle: Option<Arc<dyn crate::shared::peer::PeerHandle>>,
345 /// Authentication provider for validating requests
346 auth_provider: Option<Arc<dyn auth::AuthProvider>>,
347 /// Tool authorizer for fine-grained access control
348 tool_authorizer: Option<Arc<dyn auth::ToolAuthorizer>>,
349 /// Tool middleware chain for cross-cutting concerns in tool execution
350 #[cfg(not(target_arch = "wasm32"))]
351 tool_middleware_chain: Arc<RwLock<tool_middleware::ToolMiddlewareChain>>,
352 /// HTTP middleware chain for `StreamableHttpServer` (configured via `ServerBuilder`)
353 #[cfg(feature = "streamable-http")]
354 http_middleware: Option<Arc<http_middleware::ServerHttpMiddlewareChain>>,
355}
356
357#[cfg(not(target_arch = "wasm32"))]
358impl std::fmt::Debug for Server {
359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360 f.debug_struct("Server")
361 .field("info", &self.info)
362 .field("capabilities", &self.capabilities)
363 .field("tools", &self.tools.keys().collect::<Vec<_>>())
364 .field("prompts", &self.prompts.keys().collect::<Vec<_>>())
365 .field("resources", &self.resources.is_some())
366 .field("sampling", &self.sampling.is_some())
367 .field("initialized", &self.initialized)
368 .finish()
369 }
370}
371
372#[cfg(not(target_arch = "wasm32"))]
373impl Server {
374 /// Check if a tool exists
375 pub fn has_tool(&self, name: &str) -> bool {
376 self.tools.contains_key(name)
377 }
378
379 /// Check if a prompt exists
380 pub fn has_prompt(&self, name: &str) -> bool {
381 self.prompts.contains_key(name)
382 }
383
384 /// Get a prompt handler by name.
385 ///
386 /// Returns a borrowed reference to the registered prompt handler `Arc`,
387 /// or `None` if no prompt with that name has been registered. Callers
388 /// who need ownership can `Arc::clone(...)` the returned reference.
389 ///
390 /// # Handler-level testing pattern
391 ///
392 /// This accessor is the public API surface for the handler-level
393 /// integration testing pattern documented in the testing chapter of
394 /// the PMCP book: build a `Server`, retrieve the handler by name,
395 /// invoke `.handle(...).await` directly with a synthetic
396 /// `RequestHandlerExtra`. It exercises handler logic in isolation
397 /// without spinning up a transport.
398 ///
399 /// # What this pattern skips
400 ///
401 /// This pattern exercises handler logic only. The JSONRPC dispatch
402 /// path (`Server::handle_request`) is bypassed, so `auth_provider`,
403 /// `tool_authorizer`, and `tool_middleware` are **not** invoked. For
404 /// full-pipeline tests that exercise the security pipeline, drive a
405 /// real transport (stdio or streamable-http) with a `pmcp::Client`.
406 ///
407 /// # Examples
408 ///
409 /// ```rust
410 /// use std::collections::HashMap;
411 /// use std::sync::Arc;
412 /// use async_trait::async_trait;
413 /// use pmcp::{PromptHandler, Server};
414 /// use pmcp::types::{GetPromptResult, PromptMessage, Content};
415 /// use pmcp::types::content::Role;
416 ///
417 /// struct GreetingPrompt;
418 ///
419 /// #[async_trait]
420 /// impl PromptHandler for GreetingPrompt {
421 /// async fn handle(
422 /// &self,
423 /// args: HashMap<String, String>,
424 /// _extra: pmcp::RequestHandlerExtra,
425 /// ) -> pmcp::Result<GetPromptResult> {
426 /// let who = args.get("name").cloned().unwrap_or_else(|| "world".to_string());
427 /// Ok(GetPromptResult::new(
428 /// vec![PromptMessage::new(Role::User, Content::text(format!("Hello, {}!", who)))],
429 /// Some("Greeting prompt".to_string()),
430 /// ))
431 /// }
432 /// }
433 ///
434 /// # async fn example() -> pmcp::Result<()> {
435 /// let server = Server::builder()
436 /// .name("demo")
437 /// .version("0.1")
438 /// .prompt_arc("greet", Arc::new(GreetingPrompt))
439 /// .build()?;
440 ///
441 /// let handler = server.get_prompt("greet").expect("registered above");
442 /// let mut args = HashMap::new();
443 /// args.insert("name".to_string(), "claude".to_string());
444 /// let result = handler
445 /// .handle(args, pmcp::RequestHandlerExtra::default())
446 /// .await?;
447 /// assert_eq!(result.messages.len(), 1);
448 /// # Ok(())
449 /// # }
450 /// ```
451 pub fn get_prompt(&self, name: &str) -> Option<&Arc<dyn PromptHandler>> {
452 self.prompts.get(name)
453 }
454
455 /// Get a tool handler by name.
456 ///
457 /// Returns a borrowed reference to the registered tool handler `Arc`,
458 /// or `None` if no tool with that name has been registered. Callers
459 /// who need ownership can `Arc::clone(...)` the returned reference.
460 ///
461 /// # Handler-level testing pattern
462 ///
463 /// This accessor is the public API surface for the handler-level
464 /// integration testing pattern: build a `Server`, retrieve the
465 /// handler by name, invoke `.handle(...).await` directly with a
466 /// synthetic `RequestHandlerExtra`. It exercises handler logic in
467 /// isolation without spinning up a transport, which is the primary
468 /// shape downstream toolkit authors use to assert on a built
469 /// `pmcp::Server`'s registered handlers.
470 ///
471 /// # What this pattern skips
472 ///
473 /// This pattern exercises handler logic only. The JSONRPC dispatch
474 /// path (`Server::handle_request`) is bypassed, so `auth_provider`,
475 /// `tool_authorizer`, and `tool_middleware` are **not** invoked. For
476 /// full-pipeline tests that exercise the security pipeline, drive a
477 /// real transport (stdio or streamable-http) with a `pmcp::Client`.
478 ///
479 /// # Examples
480 ///
481 /// ```rust
482 /// use std::sync::Arc;
483 /// use async_trait::async_trait;
484 /// use pmcp::{Server, ToolHandler};
485 /// use serde_json::Value;
486 ///
487 /// struct EchoTool;
488 ///
489 /// #[async_trait]
490 /// impl ToolHandler for EchoTool {
491 /// async fn handle(
492 /// &self,
493 /// args: Value,
494 /// _extra: pmcp::RequestHandlerExtra,
495 /// ) -> pmcp::Result<Value> {
496 /// Ok(serde_json::json!({ "echoed": args }))
497 /// }
498 /// }
499 ///
500 /// # async fn example() -> pmcp::Result<()> {
501 /// let server = Server::builder()
502 /// .name("demo")
503 /// .version("0.1")
504 /// .tool_arc("echo", Arc::new(EchoTool))
505 /// .build()?;
506 ///
507 /// let handler = server.get_tool("echo").expect("registered above");
508 /// let result = handler
509 /// .handle(serde_json::json!({"msg": "hi"}), pmcp::RequestHandlerExtra::default())
510 /// .await?;
511 /// assert_eq!(result, serde_json::json!({"echoed": {"msg": "hi"}}));
512 /// # Ok(())
513 /// # }
514 /// ```
515 pub fn get_tool(&self, name: &str) -> Option<&Arc<dyn ToolHandler>> {
516 self.tools.get(name)
517 }
518
519 /// Get the HTTP middleware chain configured via `ServerBuilder`.
520 ///
521 /// Returns the HTTP middleware chain that was set using
522 /// `ServerBuilder::with_http_middleware()`. This can be used when
523 /// creating a `StreamableHttpServer`.
524 ///
525 /// # Examples
526 ///
527 /// ```rust,no_run
528 /// # #[cfg(feature = "streamable-http")]
529 /// # {
530 /// use pmcp::Server;
531 /// use pmcp::server::streamable_http_server::StreamableHttpServerConfig;
532 ///
533 /// # async fn example() -> pmcp::Result<()> {
534 /// let server = Server::builder()
535 /// .name("my-server")
536 /// .version("1.0.0")
537 /// // ... with_http_middleware() called here
538 /// .build()?;
539 ///
540 /// let config = StreamableHttpServerConfig {
541 /// http_middleware: server.http_middleware(),
542 /// ..Default::default()
543 /// };
544 /// # Ok(())
545 /// # }
546 /// # }
547 /// ```
548 #[cfg(feature = "streamable-http")]
549 pub fn http_middleware(&self) -> Option<Arc<http_middleware::ServerHttpMiddlewareChain>> {
550 self.http_middleware.clone()
551 }
552
553 /// Get the authentication provider configured via `ServerBuilder`.
554 ///
555 /// Returns the authentication provider that was set using
556 /// `ServerBuilder::auth_provider()`. This can be used by transport
557 /// layers to validate incoming requests and extract auth context.
558 pub fn get_auth_provider(&self) -> Option<Arc<dyn auth::AuthProvider>> {
559 self.auth_provider.clone()
560 }
561
562 /// Build tool and resource registries for workflow expansion.
563 ///
564 /// Creates `HashMap` registries that can be used to build an `ExpansionContext`
565 /// for converting workflow prompts to protocol types. The registries are
566 /// automatically populated from all registered tools and resources.
567 ///
568 /// Returns a tuple of (`tools_map`, `resources_map`) that can be used with
569 /// `ExpansionContext`.
570 ///
571 /// # Examples
572 ///
573 /// ```rust,ignore
574 /// use pmcp::Server;
575 /// use pmcp::server::workflow::{InternalPromptMessage, ToolHandle, PromptContent, conversion::ExpansionContext};
576 /// use pmcp::types::Role;
577 ///
578 /// # async fn example() -> pmcp::Result<()> {
579 /// let server = Server::builder()
580 /// .name("example-server")
581 /// .version("1.0.0")
582 /// .build()?;
583 ///
584 /// // Build registries from registered tools/resources
585 /// let (tools, resources) = server.build_expansion_registries();
586 ///
587 /// // Create expansion context
588 /// let ctx = ExpansionContext {
589 /// tools: &tools,
590 /// resources: &resources,
591 /// };
592 ///
593 /// // Use it to convert workflow prompts to protocol types
594 /// let msg = InternalPromptMessage::new(
595 /// Role::System,
596 /// PromptContent::ToolHandle(ToolHandle::new("my_tool"))
597 /// );
598 /// let protocol_msg = msg.to_protocol(&ctx)?;
599 /// # Ok(())
600 /// # }
601 /// ```
602 pub fn build_expansion_registries(
603 &self,
604 ) -> (
605 HashMap<Arc<str>, workflow::conversion::ToolInfo>,
606 HashMap<Arc<str>, workflow::conversion::ResourceInfo>,
607 ) {
608 use std::collections::HashMap;
609
610 // Build tools map from registered tool handlers
611 let mut tools_map = HashMap::new();
612 for (name, handler) in &self.tools {
613 if let Some(metadata) = handler.metadata() {
614 tools_map.insert(
615 Arc::from(name.as_str()),
616 workflow::conversion::ToolInfo {
617 name: metadata.name,
618 description: metadata.description.unwrap_or_default(),
619 input_schema: metadata.input_schema,
620 },
621 );
622 }
623 }
624
625 // Build resources map (currently empty - resources don't have metadata())
626 // This could be enhanced in the future when resources have better metadata
627 let resources_map = HashMap::new();
628
629 (tools_map, resources_map)
630 }
631
632 /// Send a notification.
633 ///
634 /// Sends a notification to the connected client. Notifications are one-way
635 /// messages that don't expect a response.
636 ///
637 /// # Arguments
638 ///
639 /// * `notification` - The server notification to send
640 ///
641 /// # Examples
642 ///
643 /// ```rust,no_run
644 /// use pmcp::{Server, ServerNotification, ProgressNotification, ProgressToken};
645 ///
646 /// # async fn example() -> pmcp::Result<()> {
647 /// let server = Server::builder()
648 /// .name("example-server")
649 /// .version("1.0.0")
650 /// .build()?;
651 ///
652 /// // Send a progress notification
653 /// let progress = ProgressNotification::new(
654 /// ProgressToken::String("task-123".to_string()),
655 /// 50.0,
656 /// Some("Processing...".to_string()),
657 /// );
658 ///
659 /// server.send_notification(ServerNotification::Progress(progress)).await;
660 /// # Ok(())
661 /// # }
662 /// ```
663 pub async fn send_notification(&self, notification: ServerNotification) {
664 if let Some(tx) = &self.notification_tx {
665 let _ = tx.send(Notification::Server(notification)).await;
666 }
667 }
668
669 /// Get client capabilities.
670 ///
671 /// Returns the capabilities that the client declared during initialization.
672 /// This can be used to check if the client supports specific features.
673 ///
674 /// # Examples
675 ///
676 /// ```rust,no_run
677 /// use pmcp::Server;
678 ///
679 /// # async fn example() -> pmcp::Result<()> {
680 /// let server = Server::builder()
681 /// .name("example-server")
682 /// .version("1.0.0")
683 /// .build()?;
684 ///
685 /// // Check client capabilities after initialization
686 /// if let Some(capabilities) = server.get_client_capabilities().await {
687 /// if capabilities.sampling.is_some() {
688 /// println!("Client supports LLM sampling requests");
689 /// }
690 /// if capabilities.elicitation.is_some() {
691 /// println!("Client supports user input requests");
692 /// }
693 /// }
694 /// # Ok(())
695 /// # }
696 /// ```
697 ///
698 /// # Returns
699 ///
700 /// - `Some(ClientCapabilities)` if the client has been initialized
701 /// - `None` if the client hasn't initialized yet
702 pub async fn get_client_capabilities(&self) -> Option<ClientCapabilities> {
703 self.client_capabilities.read().await.clone()
704 }
705
706 /// Check if the server is initialized.
707 ///
708 /// Returns true if the initialization handshake with a client has completed.
709 /// The server must be initialized before it can process most requests.
710 ///
711 /// # Examples
712 ///
713 /// ```rust,no_run
714 /// use pmcp::Server;
715 ///
716 /// # async fn example() -> pmcp::Result<()> {
717 /// let server = Server::builder()
718 /// .name("example-server")
719 /// .version("1.0.0")
720 /// .build()?;
721 ///
722 /// if server.is_initialized().await {
723 /// println!("Server is ready to handle requests");
724 /// } else {
725 /// println!("Waiting for client initialization");
726 /// }
727 /// # Ok(())
728 /// # }
729 /// ```
730 pub async fn is_initialized(&self) -> bool {
731 *self.initialized.read().await
732 }
733 /// Create a new server builder.
734 ///
735 /// Returns a `ServerBuilder` for configuring and constructing a new MCP server.
736 /// The builder pattern allows you to set server information, capabilities,
737 /// and register handlers before building the final server instance.
738 ///
739 /// # Examples
740 ///
741 /// ```rust,no_run
742 /// use pmcp::{Server, ToolHandler};
743 /// use async_trait::async_trait;
744 /// use serde_json::Value;
745 ///
746 /// struct HelloTool;
747 ///
748 /// #[async_trait]
749 /// impl ToolHandler for HelloTool {
750 /// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
751 /// Ok(serde_json::json!({"message": "Hello, World!"}))
752 /// }
753 /// }
754 ///
755 /// # async fn example() -> pmcp::Result<()> {
756 /// let server = Server::builder()
757 /// .name("greeting-server")
758 /// .version("1.0.0")
759 /// .tool("hello", HelloTool{})
760 /// .build()?;
761 /// # Ok(())
762 /// # }
763 /// ```
764 pub fn builder() -> ServerBuilder {
765 ServerBuilder::new()
766 }
767
768 /// Run the server with stdio transport.
769 ///
770 /// Starts the server using stdin/stdout for communication.
771 /// This is the standard way to run MCP servers as they communicate
772 /// via JSON-RPC over stdio.
773 ///
774 /// # Examples
775 ///
776 /// ```rust,no_run
777 /// use pmcp::{Server, ToolHandler};
778 /// use async_trait::async_trait;
779 /// use serde_json::Value;
780 ///
781 /// struct EchoTool;
782 ///
783 /// #[async_trait]
784 /// impl ToolHandler for EchoTool {
785 /// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
786 /// Ok(args) // Echo the input
787 /// }
788 /// }
789 ///
790 /// # async fn example() -> pmcp::Result<()> {
791 /// let server = Server::builder()
792 /// .name("echo-server")
793 /// .version("1.0.0")
794 /// .tool("echo", EchoTool{})
795 /// .build()?;
796 ///
797 /// // This will run indefinitely, handling client requests
798 /// server.run_stdio().await?;
799 /// # Ok(())
800 /// # }
801 /// ```
802 ///
803 /// # Errors
804 ///
805 /// Returns an error if:
806 /// - The stdio transport fails to initialize
807 /// - Communication with the client fails
808 /// - The server encounters an unrecoverable error
809 pub async fn run_stdio(self) -> Result<()> {
810 let transport = crate::shared::StdioTransport::new();
811 self.run(transport).await
812 }
813
814 /// Run the server with a custom transport.
815 ///
816 /// Starts the server using a custom transport implementation.
817 /// This allows for different communication mechanisms beyond stdio,
818 /// such as TCP sockets, `WebSockets`, or other protocols.
819 ///
820 /// # Arguments
821 ///
822 /// * `transport` - The transport implementation to use for communication
823 ///
824 /// # Examples
825 ///
826 /// ```rust,no_run
827 /// use pmcp::{Server, StdioTransport, ToolHandler};
828 /// use async_trait::async_trait;
829 /// use serde_json::Value;
830 ///
831 /// struct CalculatorTool;
832 ///
833 /// #[async_trait]
834 /// impl ToolHandler for CalculatorTool {
835 /// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
836 /// let a = args["a"].as_f64().unwrap_or(0.0);
837 /// let b = args["b"].as_f64().unwrap_or(0.0);
838 /// Ok(serde_json::json!({"result": a + b}))
839 /// }
840 /// }
841 ///
842 /// # async fn example() -> pmcp::Result<()> {
843 /// let server = Server::builder()
844 /// .name("calculator-server")
845 /// .version("1.0.0")
846 /// .tool("add", CalculatorTool{})
847 /// .build()?;
848 ///
849 /// let transport = StdioTransport::new();
850 /// server.run(transport).await?;
851 /// # Ok(())
852 /// # }
853 /// ```
854 ///
855 /// # Errors
856 ///
857 /// Returns an error if:
858 /// - The transport fails to initialize or operate
859 /// - Communication with the client fails
860 /// - The server encounters an unrecoverable error
861 pub async fn run<T: crate::shared::Transport + 'static>(mut self, transport: T) -> Result<()> {
862 let (notification_tx, notification_rx) = mpsc::channel(100);
863 self.notification_tx = Some(notification_tx);
864
865 // Hook cancellation manager to send notifications via the same channel
866 if let Some(tx) = &self.notification_tx {
867 let tx = tx.clone();
868 self.cancellation_manager
869 .set_notification_sender(Arc::new(move |notification| {
870 let _ = tx.try_send(notification);
871 }));
872 }
873
874 // Outbound server-to-client request channel + dispatcher. Drain task
875 // wraps each `(correlation_id, ServerRequest)` as
876 // `TransportMessage::Request` and forwards to the transport;
877 // `handle_transport_message` routes responses back through
878 // `dispatcher.handle_response`.
879 let (outbound_tx, outbound_rx) =
880 mpsc::channel::<(String, crate::types::ServerRequest)>(100);
881 let dispatcher = Arc::new(
882 server_request_dispatcher::ServerRequestDispatcher::new_with_channel(outbound_tx),
883 );
884 let peer: Arc<dyn crate::shared::peer::PeerHandle> = Arc::new(
885 crate::server::peer_impl::DispatchPeerHandle::new(dispatcher.clone()),
886 );
887 self.peer_handle = Some(peer);
888 self.server_request_dispatcher = Some(dispatcher);
889
890 let server = Arc::new(self);
891 let transport = Arc::new(RwLock::new(transport));
892 let protocol = Arc::new(RwLock::new(Protocol::new(ProtocolOptions::default())));
893
894 Self::spawn_notification_handler(transport.clone(), notification_rx);
895 server_request_dispatcher::spawn_server_request_drain(transport.clone(), outbound_rx);
896 Self::spawn_message_handler(server.clone(), transport.clone(), protocol);
897
898 // Keep the main task alive
899 Self::run_main_loop().await
900 }
901
902 /// Attach the cached peer handle to `extra` when a dispatcher is configured.
903 /// No-op on wasm32 and when running outside the `run()` lifecycle.
904 #[inline]
905 fn attach_peer(
906 &self,
907 extra: crate::server::cancellation::RequestHandlerExtra,
908 ) -> crate::server::cancellation::RequestHandlerExtra {
909 #[cfg(not(target_arch = "wasm32"))]
910 if let Some(peer) = self.peer_handle.as_ref() {
911 return extra.with_peer(peer.clone());
912 }
913 extra
914 }
915
916 /// Spawn task to handle outgoing notifications.
917 fn spawn_notification_handler(
918 transport: Arc<RwLock<impl crate::shared::Transport + 'static>>,
919 mut notification_rx: mpsc::Receiver<Notification>,
920 ) {
921 tokio::spawn(async move {
922 while let Some(notification) = notification_rx.recv().await {
923 if let Err(e) =
924 Self::send_notification_through_transport(&transport, notification).await
925 {
926 Self::log_error(&format!("Failed to send notification: {}", e)).await;
927 }
928 }
929 });
930 }
931
932 /// Spawn task to handle incoming messages.
933 fn spawn_message_handler(
934 server: Arc<Self>,
935 transport: Arc<RwLock<impl crate::shared::Transport + 'static>>,
936 _protocol: Arc<RwLock<Protocol>>,
937 ) {
938 tokio::spawn(async move {
939 loop {
940 let message = match Self::receive_message_from_transport(&transport).await {
941 Ok(msg) => msg,
942 Err(e) => {
943 Self::log_error(&format!("Transport receive error: {}", e)).await;
944 break;
945 },
946 };
947
948 if let Err(e) = Self::handle_transport_message(&server, &transport, message).await {
949 Self::log_error(&format!("Message handling error: {}", e)).await;
950 break;
951 }
952 }
953 });
954 }
955
956 /// Send a notification through the transport.
957 async fn send_notification_through_transport(
958 transport: &Arc<RwLock<impl crate::shared::Transport>>,
959 notification: Notification,
960 ) -> Result<()> {
961 let mut t = transport.write().await;
962 t.send(TransportMessage::Notification(notification)).await
963 }
964
965 /// Receive a message from the transport.
966 async fn receive_message_from_transport(
967 transport: &Arc<RwLock<impl crate::shared::Transport>>,
968 ) -> Result<TransportMessage> {
969 let mut t = transport.write().await;
970 t.receive().await
971 }
972
973 /// Handle a transport message.
974 async fn handle_transport_message(
975 server: &Arc<Self>,
976 transport: &Arc<RwLock<impl crate::shared::Transport>>,
977 message: TransportMessage,
978 ) -> Result<()> {
979 match message {
980 TransportMessage::Request { id, request } => {
981 Self::handle_request_message(server, transport, id, request).await
982 },
983 TransportMessage::Response(response) => {
984 // Route correlated client responses through the dispatcher so
985 // pending dispatches resolve.
986 if let Some(dispatcher) = &server.server_request_dispatcher {
987 let correlation_id = response.id.to_string();
988 let payload = match &response.payload {
989 crate::types::jsonrpc::ResponsePayload::Result(value) => value.clone(),
990 crate::types::jsonrpc::ResponsePayload::Error(err) => {
991 // Represent errors as a JSON object so callers can
992 // distinguish — dispatch() returns the Value as-is.
993 serde_json::to_value(err).unwrap_or(Value::Null)
994 },
995 };
996 if let Err(e) = dispatcher.handle_response(&correlation_id, payload).await {
997 Self::log_warning(&format!(
998 "Failed to route response {}: {}",
999 correlation_id, e
1000 ))
1001 .await;
1002 }
1003 } else {
1004 Self::log_warning("Server received response but no dispatcher configured")
1005 .await;
1006 }
1007 Ok(())
1008 },
1009 TransportMessage::Notification(notification) => {
1010 // Handle client cancellation notifications
1011 if let Notification::Client(crate::types::ClientNotification::Cancelled(params)) =
1012 ¬ification
1013 {
1014 let request_id = params.request_id.to_string();
1015 server
1016 .cancellation_manager
1017 .cancel_request_silent(request_id)
1018 .await?;
1019 }
1020
1021 Self::log_debug("Server received notification").await;
1022 Ok(())
1023 },
1024 }
1025 }
1026
1027 /// Handle a request message.
1028 async fn handle_request_message(
1029 server: &Arc<Self>,
1030 transport: &Arc<RwLock<impl crate::shared::Transport>>,
1031 id: RequestId,
1032 request: Request,
1033 ) -> Result<()> {
1034 let response = server.handle_request(id, request, None).await;
1035 let mut t = transport.write().await;
1036 t.send(TransportMessage::Response(response)).await
1037 }
1038
1039 /// Log an error message.
1040 async fn log_error(message: &str) {
1041 crate::log(crate::types::LogLevel::Error, message, None).await;
1042 }
1043
1044 /// Log a warning message.
1045 async fn log_warning(message: &str) {
1046 crate::log(crate::types::LogLevel::Warning, message, None).await;
1047 }
1048
1049 /// Log a debug message.
1050 async fn log_debug(message: &str) {
1051 crate::log(crate::types::LogLevel::Debug, message, None).await;
1052 }
1053
1054 /// Run the main event loop.
1055 async fn run_main_loop() -> Result<()> {
1056 loop {
1057 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
1058 }
1059 }
1060
1061 async fn handle_request(
1062 &self,
1063 id: RequestId,
1064 request: Request,
1065 auth_context: Option<auth::AuthContext>,
1066 ) -> JSONRPCResponse {
1067 match request {
1068 Request::Client(ref boxed_req)
1069 if matches!(**boxed_req, ClientRequest::Initialize(_)) =>
1070 {
1071 let ClientRequest::Initialize(init_req) = boxed_req.as_ref() else {
1072 unreachable!("Pattern matched for Initialize");
1073 };
1074 // Store client capabilities
1075 *self.client_capabilities.write().await = Some(init_req.capabilities.clone());
1076 *self.initialized.write().await = true;
1077
1078 let negotiated_version =
1079 crate::negotiate_protocol_version(&init_req.protocol_version);
1080
1081 let result = InitializeResult {
1082 protocol_version: ProtocolVersion(negotiated_version.to_string()),
1083 capabilities: self.capabilities.clone(),
1084 server_info: self.info.clone(),
1085 instructions: None,
1086 };
1087 JSONRPCResponse {
1088 jsonrpc: "2.0".to_string(),
1089 id: id.clone(),
1090 payload: crate::types::jsonrpc::ResponsePayload::Result(
1091 serde_json::to_value(result).unwrap(),
1092 ),
1093 }
1094 },
1095 Request::Client(boxed_req) => {
1096 self.handle_client_request(id, *boxed_req, auth_context)
1097 .await
1098 },
1099 Request::Server(_) => JSONRPCResponse {
1100 jsonrpc: "2.0".to_string(),
1101 id,
1102 payload: crate::types::jsonrpc::ResponsePayload::Error(
1103 crate::types::jsonrpc::JSONRPCError {
1104 code: -32601,
1105 message: "Server requests not supported by server".to_string(),
1106 data: None,
1107 },
1108 ),
1109 },
1110 }
1111 }
1112
1113 async fn handle_client_request(
1114 &self,
1115 id: RequestId,
1116 request: ClientRequest,
1117 auth_context: Option<auth::AuthContext>,
1118 ) -> JSONRPCResponse {
1119 let result = self
1120 .process_client_request(id.clone(), request, auth_context)
1121 .await;
1122 Self::create_response(id, result)
1123 }
1124
1125 /// Process a client request and return the result.
1126 async fn process_client_request(
1127 &self,
1128 request_id: RequestId,
1129 request: ClientRequest,
1130 auth_context: Option<auth::AuthContext>,
1131 ) -> Result<serde_json::Value> {
1132 match request {
1133 ClientRequest::Initialize(_) => {
1134 // Already handled above
1135 unreachable!("Initialize should be handled separately")
1136 },
1137 ClientRequest::ListTools(req) => self.handle_list_tools(req),
1138 ClientRequest::CallTool(req) => {
1139 self.handle_call_tool(request_id, req, auth_context).await
1140 },
1141 ClientRequest::ListPrompts(req) => self.handle_list_prompts(req),
1142 ClientRequest::GetPrompt(req) => {
1143 self.handle_get_prompt(request_id, req, auth_context).await
1144 },
1145 ClientRequest::ListResources(req) => {
1146 self.handle_list_resources(request_id, req, auth_context)
1147 .await
1148 },
1149 ClientRequest::ReadResource(req) => {
1150 self.handle_read_resource(request_id, req, auth_context)
1151 .await
1152 },
1153 ClientRequest::ListResourceTemplates(req) => {
1154 Self::handle_list_resource_templates(self, req)
1155 },
1156 ClientRequest::Subscribe(_)
1157 | ClientRequest::Unsubscribe(_)
1158 | ClientRequest::Complete(_)
1159 | ClientRequest::SetLoggingLevel { level: _ }
1160 | ClientRequest::Ping => Ok(serde_json::json!({})),
1161 ClientRequest::CreateMessage(req) => self.handle_create_message(request_id, *req).await,
1162 // Note: Elicitation responses are now handled as the response to
1163 // ServerRequest::ElicitationCreate in the JSON-RPC response flow,
1164 // not as a separate client request variant.
1165 // Task requests (experimental MCP Tasks) — routed directly in each arm below
1166 ClientRequest::TasksGet(_)
1167 | ClientRequest::TasksResult(_)
1168 | ClientRequest::TasksList(_)
1169 | ClientRequest::TasksCancel(_) => Err(crate::Error::protocol(
1170 crate::ErrorCode::METHOD_NOT_FOUND,
1171 "Tasks not supported: no task router configured",
1172 )),
1173 }
1174 }
1175
1176 /// Create a JSON-RPC response from a result.
1177 fn create_response(id: RequestId, result: Result<serde_json::Value>) -> JSONRPCResponse {
1178 match result {
1179 Ok(value) => JSONRPCResponse {
1180 jsonrpc: "2.0".to_string(),
1181 id,
1182 payload: crate::types::jsonrpc::ResponsePayload::Result(value),
1183 },
1184 Err(e) => JSONRPCResponse {
1185 jsonrpc: "2.0".to_string(),
1186 id,
1187 payload: crate::types::jsonrpc::ResponsePayload::Error(
1188 crate::types::jsonrpc::JSONRPCError {
1189 code: -32603,
1190 message: e.to_string(),
1191 data: None,
1192 },
1193 ),
1194 },
1195 }
1196 }
1197
1198 fn handle_list_tools(&self, _req: ListToolsRequest) -> Result<Value> {
1199 let tools: Vec<ToolInfo> = self.tool_infos.values().cloned().collect();
1200
1201 Ok(serde_json::to_value(ListToolsResult {
1202 tools,
1203 next_cursor: None,
1204 })?)
1205 }
1206
1207 async fn handle_call_tool(
1208 &self,
1209 request_id: RequestId,
1210 req: CallToolRequest,
1211 auth_context: Option<auth::AuthContext>,
1212 ) -> Result<Value> {
1213 let handler = self
1214 .tools
1215 .get(&req.name)
1216 .ok_or_else(|| Error::not_found(format!("Tool '{}' not found", req.name)))?;
1217
1218 let request_id_str = request_id.to_string();
1219 let cancellation_token = self
1220 .cancellation_manager
1221 .create_token(request_id_str.clone())
1222 .await;
1223
1224 // Auth context now comes from the transport layer
1225 // Validate authentication if auth provider is configured
1226 let validated_auth_context = if let Some(auth_provider) = &self.auth_provider {
1227 // If auth_context was provided by transport, use it; otherwise validate
1228 if auth_context.is_some() {
1229 auth_context
1230 } else {
1231 // Fallback: try to validate without headers (for backward compatibility)
1232 auth_provider.validate_request(None).await?
1233 }
1234 } else {
1235 auth_context // No auth provider, just use what was provided
1236 };
1237
1238 // Check tool authorization if tool authorizer is configured
1239 if let (Some(auth_ctx), Some(authorizer)) = (&validated_auth_context, &self.tool_authorizer)
1240 {
1241 if !authorizer.can_access_tool(auth_ctx, &req.name).await? {
1242 return Err(Error::protocol(
1243 crate::error::ErrorCode::AUTHENTICATION_REQUIRED,
1244 format!("Access denied for tool '{}'", req.name),
1245 ));
1246 }
1247 }
1248
1249 // Create progress reporter if progress token is provided
1250 #[allow(clippy::used_underscore_binding)] // _meta is part of MCP protocol spec
1251 let progress_reporter = req
1252 ._meta
1253 .as_ref()
1254 .and_then(|meta| meta.progress_token.as_ref())
1255 .and_then(|token| {
1256 self.notification_tx.as_ref().map(|tx| {
1257 let tx = tx.clone();
1258 let reporter = crate::server::progress::ServerProgressReporter::new(
1259 token.clone(),
1260 Arc::new(move |notification| {
1261 let _ = tx.try_send(notification);
1262 }),
1263 );
1264 Arc::new(reporter) as Arc<dyn crate::server::progress::ProgressReporter>
1265 })
1266 });
1267
1268 let mut extra = self.attach_peer(
1269 crate::server::cancellation::RequestHandlerExtra::new(
1270 request_id.to_string(),
1271 cancellation_token,
1272 )
1273 .with_auth_context(validated_auth_context)
1274 .with_progress_reporter(progress_reporter),
1275 );
1276
1277 // Execute tool with middleware (native-only)
1278 #[cfg(not(target_arch = "wasm32"))]
1279 let result = {
1280 // Create tool context for middleware
1281 let context = tool_middleware::ToolContext::new(&req.name, &request_id_str);
1282
1283 // Clone arguments for middleware processing
1284 let mut args = req.arguments;
1285
1286 // Process request through tool middleware chain
1287 // Middleware rejection short-circuits tool execution
1288 self.tool_middleware_chain
1289 .read()
1290 .await
1291 .process_request(&req.name, &mut args, &mut extra, &context)
1292 .await?;
1293
1294 // Execute the tool with potentially modified args and extra
1295 let mut result = handler.handle(args, extra).await;
1296
1297 // Process response through tool middleware chain
1298 if let Err(e) = self
1299 .tool_middleware_chain
1300 .read()
1301 .await
1302 .process_response(&req.name, &mut result, &context)
1303 .await
1304 {
1305 // Log error but continue with original result
1306 tracing::warn!("Tool response middleware processing failed: {}", e);
1307 }
1308
1309 // If tool execution failed, call handle_tool_error
1310 if let Err(ref e) = result {
1311 self.tool_middleware_chain
1312 .read()
1313 .await
1314 .handle_tool_error(&req.name, e, &context)
1315 .await;
1316 }
1317
1318 result
1319 };
1320
1321 // On WASM, execute tool directly without middleware
1322 #[cfg(target_arch = "wasm32")]
1323 let result = handler.handle(req.arguments, extra).await;
1324
1325 // Token cleanup is unconditional (success or failure) and does not
1326 // touch `result`, so it runs once before the value/error split.
1327 self.cancellation_manager
1328 .remove_token(&request_id_str)
1329 .await;
1330 let result = match result {
1331 Ok(v) => v,
1332 // `Error::ToolRejected` is an APPLICATION-level rejection (e.g.
1333 // Code Mode policy: a SELECT missing its LIMIT), not a protocol
1334 // fault. Map it to a successful `CallToolResult { isError: true }`
1335 // (message → content, details → structuredContent) so the model
1336 // reads the reason and retries with corrected input, instead of
1337 // `?`-propagating a JSON-RPC error that reads as a server crash.
1338 // All other errors keep propagating as protocol errors.
1339 Err(Error::ToolRejected { message, details }) => {
1340 return Ok(serde_json::to_value(CallToolResult::rejected(
1341 message, details,
1342 ))?);
1343 },
1344 Err(e) => return Err(e),
1345 };
1346 // Build CallToolResult, adding structured_content for widget tools
1347 let text = result.to_string();
1348 let mut call_result = CallToolResult::new(vec![crate::types::Content::text(text)]);
1349
1350 if let Some(info) = self.tool_infos.get(&req.name) {
1351 call_result = call_result.with_widget_enrichment(info, result);
1352 }
1353
1354 Ok(serde_json::to_value(call_result)?)
1355 }
1356
1357 fn handle_list_prompts(&self, _req: ListPromptsRequest) -> Result<Value> {
1358 let prompts = self
1359 .prompts
1360 .iter()
1361 .map(|(name, handler)| {
1362 // Use prompt metadata if provided, otherwise use defaults
1363 if let Some(mut info) = handler.metadata() {
1364 // Ensure the name matches the registered name
1365 info.name.clone_from(name);
1366 info
1367 } else {
1368 crate::types::PromptInfo::new(name)
1369 }
1370 })
1371 .collect::<Vec<_>>();
1372
1373 Ok(serde_json::to_value(ListPromptsResult {
1374 prompts,
1375 next_cursor: None,
1376 })?)
1377 }
1378
1379 async fn handle_get_prompt(
1380 &self,
1381 request_id: RequestId,
1382 req: GetPromptRequest,
1383 auth_context: Option<auth::AuthContext>,
1384 ) -> Result<Value> {
1385 let handler = self
1386 .prompts
1387 .get(&req.name)
1388 .ok_or_else(|| Error::not_found(format!("Prompt '{}' not found", req.name)))?;
1389
1390 let request_id_str = request_id.to_string();
1391 let cancellation_token = self
1392 .cancellation_manager
1393 .create_token(request_id_str.clone())
1394 .await;
1395
1396 // Create progress reporter if progress token is provided
1397 #[allow(clippy::used_underscore_binding)] // _meta is part of MCP protocol spec
1398 let progress_reporter = req
1399 ._meta
1400 .as_ref()
1401 .and_then(|meta| meta.progress_token.as_ref())
1402 .and_then(|token| {
1403 self.notification_tx.as_ref().map(|tx| {
1404 let tx = tx.clone();
1405 let reporter = crate::server::progress::ServerProgressReporter::new(
1406 token.clone(),
1407 Arc::new(move |notification| {
1408 let _ = tx.try_send(notification);
1409 }),
1410 );
1411 Arc::new(reporter) as Arc<dyn crate::server::progress::ProgressReporter>
1412 })
1413 });
1414
1415 let extra = self.attach_peer(
1416 crate::server::cancellation::RequestHandlerExtra::new(
1417 request_id_str.clone(),
1418 cancellation_token,
1419 )
1420 .with_auth_context(auth_context)
1421 .with_progress_reporter(progress_reporter),
1422 );
1423 let result = match handler.handle(req.arguments, extra).await {
1424 Ok(v) => {
1425 self.cancellation_manager
1426 .remove_token(&request_id_str)
1427 .await;
1428 Ok(v)
1429 },
1430 Err(e) => {
1431 self.cancellation_manager
1432 .remove_token(&request_id_str)
1433 .await;
1434 Err(e)
1435 },
1436 }?;
1437 Ok(serde_json::to_value(result)?)
1438 }
1439
1440 async fn handle_list_resources(
1441 &self,
1442 request_id: RequestId,
1443 req: ListResourcesRequest,
1444 auth_context: Option<auth::AuthContext>,
1445 ) -> Result<Value> {
1446 if let Some(handler) = &self.resources {
1447 let request_id_str = request_id.to_string();
1448 let cancellation_token = self
1449 .cancellation_manager
1450 .create_token(request_id_str.clone())
1451 .await;
1452 let extra = self.attach_peer(
1453 crate::server::cancellation::RequestHandlerExtra::new(
1454 request_id_str.clone(),
1455 cancellation_token,
1456 )
1457 .with_auth_context(auth_context),
1458 );
1459 let mut result = match handler.list(req.cursor, extra).await {
1460 Ok(v) => {
1461 self.cancellation_manager
1462 .remove_token(&request_id_str)
1463 .await;
1464 Ok(v)
1465 },
1466 Err(e) => {
1467 self.cancellation_manager
1468 .remove_token(&request_id_str)
1469 .await;
1470 Err(e)
1471 },
1472 }?;
1473 // Enrich ResourceInfo with tool _meta for widget resources
1474 if !self.uri_to_tool_meta.is_empty() {
1475 for resource in &mut result.resources {
1476 if let Some(tool_meta) = self.uri_to_tool_meta.get(&resource.uri) {
1477 let meta = resource.meta.get_or_insert_with(serde_json::Map::new);
1478 crate::types::ui::deep_merge(meta, tool_meta.clone());
1479 }
1480 }
1481 }
1482 Ok(serde_json::to_value(result)?)
1483 } else {
1484 Ok(serde_json::to_value(ListResourcesResult {
1485 resources: vec![],
1486 next_cursor: None,
1487 })?)
1488 }
1489 }
1490
1491 async fn handle_read_resource(
1492 &self,
1493 request_id: RequestId,
1494 req: ReadResourceRequest,
1495 auth_context: Option<auth::AuthContext>,
1496 ) -> Result<Value> {
1497 let handler = self
1498 .resources
1499 .as_ref()
1500 .ok_or_else(|| Error::not_found("No resource handler configured".to_string()))?;
1501
1502 let request_id_str = request_id.to_string();
1503 let cancellation_token = self
1504 .cancellation_manager
1505 .create_token(request_id_str.clone())
1506 .await;
1507
1508 // Create progress reporter if progress token is provided
1509 #[allow(clippy::used_underscore_binding)] // _meta is part of MCP protocol spec
1510 let progress_reporter = req
1511 ._meta
1512 .as_ref()
1513 .and_then(|meta| meta.progress_token.as_ref())
1514 .and_then(|token| {
1515 self.notification_tx.as_ref().map(|tx| {
1516 let tx = tx.clone();
1517 let reporter = crate::server::progress::ServerProgressReporter::new(
1518 token.clone(),
1519 Arc::new(move |notification| {
1520 let _ = tx.try_send(notification);
1521 }),
1522 );
1523 Arc::new(reporter) as Arc<dyn crate::server::progress::ProgressReporter>
1524 })
1525 });
1526
1527 let extra = self.attach_peer(
1528 crate::server::cancellation::RequestHandlerExtra::new(
1529 request_id_str.clone(),
1530 cancellation_token,
1531 )
1532 .with_auth_context(auth_context)
1533 .with_progress_reporter(progress_reporter),
1534 );
1535 let mut result = match handler.read(&req.uri, extra).await {
1536 Ok(v) => {
1537 self.cancellation_manager
1538 .remove_token(&request_id_str)
1539 .await;
1540 Ok(v)
1541 },
1542 Err(e) => {
1543 self.cancellation_manager
1544 .remove_token(&request_id_str)
1545 .await;
1546 Err(e)
1547 },
1548 }?;
1549 // Merge tool descriptor keys into content _meta for widget resources
1550 if !self.uri_to_tool_meta.is_empty() {
1551 for content in &mut result.contents {
1552 if let crate::types::Content::Resource { uri, meta, .. } = content {
1553 if let Some(tool_meta) = self.uri_to_tool_meta.get(uri.as_str()) {
1554 let content_meta = meta.get_or_insert_with(serde_json::Map::new);
1555 crate::types::ui::deep_merge(content_meta, tool_meta.clone());
1556 }
1557 }
1558 }
1559 }
1560 Ok(serde_json::to_value(result)?)
1561 }
1562
1563 #[allow(clippy::unused_self)]
1564 fn handle_list_resource_templates(&self, _req: ListResourceTemplatesRequest) -> Result<Value> {
1565 Ok(serde_json::to_value(ListResourceTemplatesResult {
1566 resource_templates: vec![],
1567 next_cursor: None,
1568 })?)
1569 }
1570
1571 async fn handle_create_message(
1572 &self,
1573 request_id: RequestId,
1574 req: crate::types::CreateMessageParams,
1575 ) -> Result<Value> {
1576 let handler = self
1577 .sampling
1578 .as_ref()
1579 .ok_or_else(|| Error::not_found("No sampling handler configured".to_string()))?;
1580
1581 let request_id_str = request_id.to_string();
1582 let cancellation_token = self
1583 .cancellation_manager
1584 .create_token(request_id_str.clone())
1585 .await;
1586 let extra = self.attach_peer(crate::server::cancellation::RequestHandlerExtra::new(
1587 request_id_str.clone(),
1588 cancellation_token,
1589 ));
1590 let result = match handler.create_message(req, extra).await {
1591 Ok(v) => {
1592 self.cancellation_manager
1593 .remove_token(&request_id_str)
1594 .await;
1595 Ok(v)
1596 },
1597 Err(e) => {
1598 self.cancellation_manager
1599 .remove_token(&request_id_str)
1600 .await;
1601 Err(e)
1602 },
1603 }?;
1604 Ok(serde_json::to_value(result)?)
1605 }
1606
1607 /// Register a root directory or URI that the server has access to.
1608 ///
1609 /// This method allows the server to announce to clients that it has
1610 /// access to specific file system roots or URIs. This is useful for
1611 /// resource handlers that need to expose filesystem access or other
1612 /// URI-based resources.
1613 ///
1614 /// # Arguments
1615 ///
1616 /// * `uri` - The root URI to register (e.g., `file:///home/user/project`)
1617 /// * `name` - Optional human-readable name for the root
1618 ///
1619 /// # Returns
1620 ///
1621 /// An unregister function that can be called to remove the root registration.
1622 ///
1623 /// # Examples
1624 ///
1625 /// ```rust,no_run
1626 /// use pmcp::Server;
1627 ///
1628 /// # async fn example() -> pmcp::Result<()> {
1629 /// let server = Server::builder()
1630 /// .name("file-server")
1631 /// .version("1.0.0")
1632 /// .build()?;
1633 ///
1634 /// // Register a project root
1635 /// let unregister = server.register_root(
1636 /// "file:///home/user/project",
1637 /// Some("My Project".to_string())
1638 /// ).await?;
1639 ///
1640 /// // Later, unregister the root
1641 /// unregister();
1642 /// # Ok(())
1643 /// # }
1644 /// ```
1645 pub async fn register_root(
1646 &self,
1647 uri: impl Into<String>,
1648 name: Option<String>,
1649 ) -> Result<impl FnOnce() + Send + 'static> {
1650 let mut roots_manager = self.roots_manager.write().await;
1651 if let Some(tx) = &self.notification_tx {
1652 roots_manager.set_notification_sender({
1653 let tx = tx.clone();
1654 move |server_notification| {
1655 let _ = tx.try_send(Notification::Server(server_notification));
1656 }
1657 });
1658 }
1659 roots_manager.register_root(uri.into(), name).await
1660 }
1661
1662 /// Get the list of registered roots.
1663 ///
1664 /// Returns a list of all currently registered root URIs and their
1665 /// associated names. Roots are directories or URIs that the server
1666 /// has announced access to.
1667 ///
1668 /// # Returns
1669 ///
1670 /// A vector of `Root` objects containing URI and optional name.
1671 ///
1672 /// # Examples
1673 ///
1674 /// ```rust,no_run
1675 /// use pmcp::Server;
1676 ///
1677 /// # async fn example() -> pmcp::Result<()> {
1678 /// let server = Server::builder()
1679 /// .name("file-server")
1680 /// .version("1.0.0")
1681 /// .build()?;
1682 ///
1683 /// // Register some roots
1684 /// server.register_root("file:///home/user/project1", Some("Project 1".to_string())).await?;
1685 /// server.register_root("file:///home/user/project2", None).await?;
1686 ///
1687 /// // Get the list of roots
1688 /// let roots = server.get_roots().await;
1689 /// println!("Registered {} roots", roots.len());
1690 /// # Ok(())
1691 /// # }
1692 /// ```
1693 pub async fn get_roots(&self) -> Vec<roots::Root> {
1694 let roots_manager = self.roots_manager.read().await;
1695 roots_manager.get_roots().await
1696 }
1697
1698 /// Subscribe a client to resource updates.
1699 ///
1700 /// This method allows the server to track which clients are interested
1701 /// in updates to specific resources. When a resource changes, the server
1702 /// can notify all subscribed clients.
1703 ///
1704 /// # Arguments
1705 ///
1706 /// * `uri` - The resource URI to subscribe to
1707 /// * `client_id` - Identifier for the subscribing client
1708 ///
1709 /// # Examples
1710 ///
1711 /// ```rust,no_run
1712 /// use pmcp::Server;
1713 ///
1714 /// # async fn example() -> pmcp::Result<()> {
1715 /// let server = Server::builder()
1716 /// .name("file-server")
1717 /// .version("1.0.0")
1718 /// .build()?;
1719 ///
1720 /// // Subscribe client to resource updates
1721 /// server.subscribe_resource(
1722 /// "file:///project/file.txt".to_string(),
1723 /// "client-123".to_string()
1724 /// ).await?;
1725 /// # Ok(())
1726 /// # }
1727 /// ```
1728 pub async fn subscribe_resource(&self, uri: String, client_id: String) -> Result<()> {
1729 if uri.is_empty() || client_id.is_empty() {
1730 return Err(Error::invalid_params("URI and client_id must not be empty"));
1731 }
1732
1733 let mut subscription_manager = self.subscription_manager.write().await;
1734 if let Some(tx) = &self.notification_tx {
1735 subscription_manager.set_notification_sender({
1736 let tx = tx.clone();
1737 move |notification| {
1738 let _ = tx.try_send(Notification::Server(notification));
1739 }
1740 });
1741 }
1742
1743 subscription_manager.subscribe(uri, client_id).await
1744 }
1745
1746 /// Cancel a request that is currently being processed.
1747 ///
1748 /// This method allows the server to cancel ongoing requests, which is
1749 /// useful for implementing request timeouts or client-requested cancellations.
1750 ///
1751 /// # Arguments
1752 ///
1753 /// * `request_id` - The ID of the request to cancel
1754 /// * `reason` - Optional reason for cancellation
1755 ///
1756 /// # Examples
1757 ///
1758 /// ```rust,no_run
1759 /// use pmcp::Server;
1760 ///
1761 /// # async fn example() -> pmcp::Result<()> {
1762 /// let server = Server::builder()
1763 /// .name("cancel-server")
1764 /// .version("1.0.0")
1765 /// .build()?;
1766 ///
1767 /// // Cancel a request
1768 /// server.cancel_request(
1769 /// "request-123".to_string(),
1770 /// Some("User requested cancellation".to_string())
1771 /// ).await?;
1772 /// # Ok(())
1773 /// # }
1774 /// ```
1775 pub async fn cancel_request(&self, request_id: String, reason: Option<String>) -> Result<()> {
1776 if request_id.is_empty() {
1777 return Err(Error::invalid_params("Request ID must not be empty"));
1778 }
1779
1780 self.cancellation_manager
1781 .cancel_request(request_id, reason)
1782 .await
1783 }
1784
1785 /// Unsubscribe a client from resource updates.
1786 ///
1787 /// This method removes a client's subscription to a specific resource,
1788 /// so they will no longer receive notifications when that resource changes.
1789 ///
1790 /// # Arguments
1791 ///
1792 /// * `uri` - The resource URI to unsubscribe from
1793 /// * `client_id` - Identifier for the client to unsubscribe
1794 ///
1795 /// # Examples
1796 ///
1797 /// ```rust,no_run
1798 /// use pmcp::Server;
1799 ///
1800 /// # async fn example() -> pmcp::Result<()> {
1801 /// let server = Server::builder()
1802 /// .name("file-server")
1803 /// .version("1.0.0")
1804 /// .build()?;
1805 ///
1806 /// // Unsubscribe client from resource updates
1807 /// server.unsubscribe_resource(
1808 /// "file:///project/file.txt".to_string(),
1809 /// "client-123".to_string()
1810 /// ).await?;
1811 /// # Ok(())
1812 /// # }
1813 /// ```
1814 pub async fn unsubscribe_resource(&self, uri: String, client_id: String) -> Result<()> {
1815 if uri.is_empty() || client_id.is_empty() {
1816 return Err(Error::invalid_params("URI and client_id must not be empty"));
1817 }
1818
1819 let subscription_manager = self.subscription_manager.read().await;
1820 subscription_manager.unsubscribe(uri, client_id).await
1821 }
1822
1823 /// Notify subscribers that a resource has been updated.
1824 ///
1825 /// # Arguments
1826 ///
1827 /// * `uri` - The URI of the resource that was updated
1828 ///
1829 /// # Returns
1830 ///
1831 /// The number of subscribers that were notified.
1832 pub async fn notify_resource_updated(&self, uri: String) -> Result<usize> {
1833 let mut subscription_manager = self.subscription_manager.write().await;
1834 if let Some(tx) = &self.notification_tx {
1835 subscription_manager.set_notification_sender({
1836 let tx = tx.clone();
1837 move |notification| {
1838 let _ = tx.try_send(Notification::Server(notification));
1839 }
1840 });
1841 }
1842 subscription_manager.notify_resource_updated(uri).await
1843 }
1844}
1845
1846/// Trait for types annotated with `#[mcp_server]`.
1847///
1848/// Generated by the `#[mcp_server]` proc macro. Provides bulk registration of
1849/// tools and prompts via `register()`. Users should call `.mcp_server(instance)`
1850/// on the builder instead of implementing this trait manually.
1851///
1852/// # Examples
1853///
1854/// ```rust,ignore
1855/// use pmcp::ServerBuilder;
1856///
1857/// #[mcp_server]
1858/// impl MyServer {
1859/// #[mcp_tool(description = "Query data")]
1860/// async fn query(&self, args: QueryArgs) -> Result<Value> { /* ... */ }
1861///
1862/// #[mcp_prompt(description = "Generate query")]
1863/// async fn query_prompt(&self, args: PromptArgs) -> Result<GetPromptResult> { /* ... */ }
1864/// }
1865///
1866/// let server = MyServer { db };
1867/// let builder = ServerBuilder::new()
1868/// .mcp_server(server);
1869/// ```
1870#[cfg(not(target_arch = "wasm32"))]
1871pub trait McpServer {
1872 /// Register all tools and prompts from this server on the builder.
1873 fn register(self, builder: ServerBuilder) -> ServerBuilder;
1874}
1875
1876/// Builder for creating servers.
1877#[cfg(not(target_arch = "wasm32"))]
1878pub struct ServerBuilder {
1879 name: Option<String>,
1880 version: Option<String>,
1881 capabilities: ServerCapabilities,
1882 tools: HashMap<String, Arc<dyn ToolHandler>>,
1883 prompts: HashMap<String, Arc<dyn PromptHandler>>,
1884 resources: Option<Arc<dyn ResourceHandler>>,
1885 sampling: Option<Arc<dyn SamplingHandler>>,
1886 /// Cancellation manager for request cancellation
1887 cancellation_manager: cancellation::CancellationManager,
1888 /// Roots manager for directory/URI registration
1889 roots_manager: roots::RootsManager,
1890 /// Authentication provider for validating requests
1891 auth_provider: Option<Arc<dyn auth::AuthProvider>>,
1892 /// Tool authorizer for fine-grained access control
1893 tool_authorizer: Option<Arc<dyn auth::ToolAuthorizer>>,
1894 /// Tool protection requirements to be applied at build time
1895 tool_protections: HashMap<String, Vec<String>>,
1896 /// Tool middleware chain for cross-cutting concerns
1897 #[cfg(not(target_arch = "wasm32"))]
1898 tool_middlewares: Vec<Arc<dyn tool_middleware::ToolMiddleware>>,
1899 /// HTTP middleware chain for `StreamableHttpServer`
1900 #[cfg(feature = "streamable-http")]
1901 http_middleware: Option<Arc<http_middleware::ServerHttpMiddlewareChain>>,
1902 /// Host layers for MCP Apps metadata enrichment (e.g., `ChatGPT`)
1903 #[cfg(feature = "mcp-apps")]
1904 host_layers: Vec<crate::types::mcp_apps::HostType>,
1905 /// Optional website URL for the server implementation (MCP 2025-11-25)
1906 website_url: Option<String>,
1907 /// Optional icons for the server implementation (MCP 2025-11-25)
1908 icons: Option<Vec<crate::types::protocol::IconInfo>>,
1909 /// Accumulated SEP-2640 Agent Skills. The registry is finalized into a
1910 /// single `SkillsHandler` exactly once at `.build()` time so chained
1911 /// `.skill(...)` / `.skills(...)` calls never produce nested wrappers.
1912 #[cfg(feature = "skills")]
1913 pending_skills: Option<skills::Skills>,
1914}
1915
1916#[cfg(not(target_arch = "wasm32"))]
1917impl std::fmt::Debug for ServerBuilder {
1918 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1919 f.debug_struct("ServerBuilder")
1920 .field("name", &self.name)
1921 .field("version", &self.version)
1922 .field("capabilities", &self.capabilities)
1923 .field("tools", &self.tools.keys().collect::<Vec<_>>())
1924 .field("prompts", &self.prompts.keys().collect::<Vec<_>>())
1925 .field("resources", &self.resources.is_some())
1926 .field("sampling", &self.sampling.is_some())
1927 .finish()
1928 }
1929}
1930
1931#[cfg(not(target_arch = "wasm32"))]
1932impl ServerBuilder {
1933 /// Create a new server builder.
1934 ///
1935 /// Creates a new `ServerBuilder` with default capabilities and no handlers.
1936 /// Use the builder methods to configure the server before calling `build()`.
1937 ///
1938 /// # Examples
1939 ///
1940 /// ```rust,no_run
1941 /// use pmcp::ServerBuilder;
1942 ///
1943 /// let builder = ServerBuilder::new();
1944 /// ```
1945 ///
1946 /// This is equivalent to using the default implementation:
1947 ///
1948 /// ```rust,no_run
1949 /// use pmcp::ServerBuilder;
1950 ///
1951 /// let builder = ServerBuilder::default();
1952 /// ```
1953 pub fn new() -> Self {
1954 Self {
1955 name: None,
1956 version: None,
1957 capabilities: ServerCapabilities::default(),
1958 tools: HashMap::new(),
1959 prompts: HashMap::new(),
1960 resources: None,
1961 sampling: None,
1962 cancellation_manager: cancellation::CancellationManager::new(),
1963 roots_manager: roots::RootsManager::new(),
1964 auth_provider: None,
1965 tool_authorizer: None,
1966 tool_protections: HashMap::new(),
1967 #[cfg(not(target_arch = "wasm32"))]
1968 tool_middlewares: Vec::new(),
1969 #[cfg(feature = "streamable-http")]
1970 http_middleware: None,
1971 #[cfg(feature = "mcp-apps")]
1972 host_layers: Vec::new(),
1973 website_url: None,
1974 icons: None,
1975 #[cfg(feature = "skills")]
1976 pending_skills: None,
1977 }
1978 }
1979
1980 /// Set the server name.
1981 ///
1982 /// The server name identifies this MCP server implementation.
1983 /// This is required and will be sent to clients during initialization.
1984 ///
1985 /// # Arguments
1986 ///
1987 /// * `name` - The name of the server
1988 ///
1989 /// # Examples
1990 ///
1991 /// ```rust,no_run
1992 /// use pmcp::Server;
1993 ///
1994 /// let server = Server::builder()
1995 /// .name("file-manager")
1996 /// .version("1.0.0")
1997 /// .build()?;
1998 /// # Ok::<(), pmcp::Error>(())
1999 /// ```
2000 pub fn name(mut self, name: impl Into<String>) -> Self {
2001 self.name = Some(name.into());
2002 self
2003 }
2004
2005 /// Set the server version.
2006 ///
2007 /// The server version identifies this specific version of the MCP server.
2008 /// This is required and will be sent to clients during initialization.
2009 ///
2010 /// # Arguments
2011 ///
2012 /// * `version` - The version string (e.g., "1.0.0", "2.1.3-beta")
2013 ///
2014 /// # Examples
2015 ///
2016 /// ```rust,no_run
2017 /// use pmcp::Server;
2018 ///
2019 /// let server = Server::builder()
2020 /// .name("data-processor")
2021 /// .version("2.1.0")
2022 /// .build()?;
2023 /// # Ok::<(), pmcp::Error>(())
2024 /// ```
2025 pub fn version(mut self, version: impl Into<String>) -> Self {
2026 self.version = Some(version.into());
2027 self
2028 }
2029
2030 /// Set the website URL for the server implementation (MCP 2025-11-25).
2031 pub fn website_url(mut self, url: impl Into<String>) -> Self {
2032 self.website_url = Some(url.into());
2033 self
2034 }
2035
2036 /// Set icons for the server implementation (MCP 2025-11-25).
2037 pub fn with_icons(mut self, icons: Vec<crate::types::protocol::IconInfo>) -> Self {
2038 self.icons = Some(icons);
2039 self
2040 }
2041
2042 /// Set server capabilities.
2043 ///
2044 /// Configures the capabilities that this server supports.
2045 /// Capabilities inform clients about which MCP features are available.
2046 ///
2047 /// # Arguments
2048 ///
2049 /// * `capabilities` - The server capabilities to advertise
2050 ///
2051 /// # Examples
2052 ///
2053 /// ```rust,no_run
2054 /// use pmcp::{Server, ServerCapabilities, ToolCapabilities};
2055 ///
2056 /// let mut capabilities = ServerCapabilities::default();
2057 /// capabilities.tools = Some(ToolCapabilities {
2058 /// list_changed: Some(true),
2059 /// });
2060 ///
2061 /// let server = Server::builder()
2062 /// .name("advanced-server")
2063 /// .version("1.0.0")
2064 /// .capabilities(capabilities)
2065 /// .build()?;
2066 /// # Ok::<(), pmcp::Error>(())
2067 /// ```
2068 pub fn capabilities(mut self, capabilities: ServerCapabilities) -> Self {
2069 self.capabilities = capabilities;
2070 self
2071 }
2072
2073 /// Add a tool handler.
2074 ///
2075 /// Registers a tool that clients can call via the tools/call method.
2076 /// Tools are the primary way servers provide functionality to clients.
2077 ///
2078 /// # Arguments
2079 ///
2080 /// * `name` - The name of the tool (used by clients to call it)
2081 /// * `handler` - The handler implementation for this tool
2082 ///
2083 /// # Examples
2084 ///
2085 /// ```rust,no_run
2086 /// use pmcp::{Server, ToolHandler};
2087 /// use async_trait::async_trait;
2088 /// use serde_json::Value;
2089 ///
2090 /// struct FileListTool;
2091 ///
2092 /// #[async_trait]
2093 /// impl ToolHandler for FileListTool {
2094 /// async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
2095 /// let path = args["path"].as_str().unwrap_or(".");
2096 /// // List files in path...
2097 /// Ok(serde_json::json!({"files": ["file1.txt", "file2.txt"]}))
2098 /// }
2099 /// }
2100 ///
2101 /// let server = Server::builder()
2102 /// .name("file-server")
2103 /// .version("1.0.0")
2104 /// .tool("list_files", FileListTool{})
2105 /// .build()?;
2106 /// # Ok::<(), pmcp::Error>(())
2107 /// ```
2108 pub fn tool(mut self, name: impl Into<String>, handler: impl ToolHandler + 'static) -> Self {
2109 self.tools.insert(name.into(), Arc::new(handler));
2110
2111 // Update capabilities to include tools
2112 // Use Some(false) instead of None to ensure the field serializes properly
2113 if self.capabilities.tools.is_none() {
2114 self.capabilities.tools = Some(crate::types::ToolCapabilities {
2115 list_changed: Some(false),
2116 });
2117 }
2118
2119 self
2120 }
2121
2122 /// Add a tool handler with an Arc.
2123 ///
2124 /// This variant lets the caller share the handler `Arc` between the
2125 /// builder and an external in-process handler map (e.g., a downstream
2126 /// toolkit's handler registry) without writing a delegating wrapper
2127 /// shim. Behavior is otherwise identical to [`Self::tool`]: the first
2128 /// registration auto-enables `capabilities.tools`.
2129 pub fn tool_arc(mut self, name: impl Into<String>, handler: Arc<dyn ToolHandler>) -> Self {
2130 let name = name.into();
2131 self.tools.insert(name, handler);
2132
2133 // Update capabilities to include tools
2134 // Use Some(false) instead of None to ensure the field serializes properly
2135 if self.capabilities.tools.is_none() {
2136 self.capabilities.tools = Some(crate::types::ToolCapabilities {
2137 list_changed: Some(false),
2138 });
2139 }
2140
2141 self
2142 }
2143
2144 /// Register all tools and prompts from an `#[mcp_server]` annotated type.
2145 ///
2146 /// This is the ergonomic counterpart to individually registering tools and
2147 /// prompts. The server instance provides shared state via `&self` to all
2148 /// tool and prompt methods.
2149 ///
2150 /// # Examples
2151 ///
2152 /// ```rust,ignore
2153 /// use pmcp::ServerBuilder;
2154 ///
2155 /// #[mcp_server]
2156 /// impl MyServer {
2157 /// #[mcp_tool(description = "Query data")]
2158 /// async fn query(&self, args: QueryArgs) -> Result<Value> { /* ... */ }
2159 ///
2160 /// #[mcp_prompt(description = "Generate query")]
2161 /// async fn query_prompt(&self, args: PromptArgs) -> Result<GetPromptResult> { /* ... */ }
2162 /// }
2163 ///
2164 /// let server = MyServer { db };
2165 /// let builder = ServerBuilder::new()
2166 /// .name("my-server")
2167 /// .mcp_server(server);
2168 /// ```
2169 pub fn mcp_server<T: McpServer>(self, server: T) -> Self {
2170 server.register(self)
2171 }
2172
2173 /// Add a type-safe tool handler with automatic schema generation.
2174 ///
2175 /// This method provides first-class support for creating tools with:
2176 /// - Automatic JSON schema generation from Rust types
2177 /// - Compile-time type safety
2178 /// - Runtime validation
2179 /// - Field descriptions from doc comments
2180 ///
2181 /// # Example
2182 /// ```no_run
2183 /// # #[cfg(feature = "schema-generation")]
2184 /// # {
2185 /// use pmcp::ServerBuilder;
2186 /// use schemars::JsonSchema;
2187 /// use serde::{Deserialize, Serialize};
2188 ///
2189 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
2190 /// struct EchoArgs {
2191 /// /// The message to echo
2192 /// message: String,
2193 /// /// Optional prefix
2194 /// prefix: Option<String>,
2195 /// }
2196 ///
2197 /// # #[tokio::main]
2198 /// # async fn main() -> Result<(), pmcp::Error> {
2199 /// let server = ServerBuilder::new()
2200 /// .name("example")
2201 /// .tool_typed("echo", |args: EchoArgs, _| {
2202 /// Box::pin(async move {
2203 /// let message = match args.prefix {
2204 /// Some(p) => format!("{}: {}", p, args.message),
2205 /// None => args.message,
2206 /// };
2207 /// Ok(serde_json::json!({ "message": message }))
2208 /// })
2209 /// })
2210 /// .build();
2211 /// # Ok::<(), pmcp::Error>(())
2212 /// # }
2213 /// # }
2214 /// ```
2215 #[cfg(feature = "schema-generation")]
2216 pub fn tool_typed<T, F, Fut>(mut self, name: impl Into<String>, handler: F) -> Self
2217 where
2218 T: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
2219 F: Fn(T, crate::RequestHandlerExtra) -> Fut + Send + Sync + 'static,
2220 Fut: std::future::Future<Output = crate::Result<serde_json::Value>> + Send + 'static,
2221 {
2222 use crate::server::typed_tool::TypedTool;
2223 use std::pin::Pin;
2224
2225 let name_str = name.into();
2226
2227 // Wrap the handler to return Pin<Box<dyn Future>>
2228 let wrapped_handler = move |args: T,
2229 extra: crate::RequestHandlerExtra|
2230 -> Pin<
2231 Box<dyn std::future::Future<Output = crate::Result<serde_json::Value>> + Send>,
2232 > { Box::pin(handler(args, extra)) };
2233
2234 let tool = TypedTool::new(name_str.clone(), wrapped_handler);
2235 self.tools.insert(name_str, Arc::new(tool));
2236
2237 // Update capabilities to include tools
2238 if self.capabilities.tools.is_none() {
2239 self.capabilities.tools = Some(crate::types::ToolCapabilities {
2240 list_changed: Some(false),
2241 });
2242 }
2243
2244 self
2245 }
2246
2247 /// Add a type-safe tool handler with automatic schema generation and description.
2248 ///
2249 /// This is a convenience overload that allows setting a description directly
2250 /// without needing to chain `.with_description()`.
2251 ///
2252 /// # Example
2253 /// ```no_run
2254 /// # #[cfg(feature = "schema-generation")]
2255 /// # {
2256 /// use pmcp::ServerBuilder;
2257 /// use schemars::JsonSchema;
2258 /// use serde::{Deserialize, Serialize};
2259 ///
2260 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
2261 /// struct EchoArgs {
2262 /// /// The message to echo
2263 /// message: String,
2264 /// /// Optional prefix
2265 /// prefix: Option<String>,
2266 /// }
2267 ///
2268 /// let server = ServerBuilder::new()
2269 /// .name("example")
2270 /// .tool_typed_with_description(
2271 /// "echo",
2272 /// "Echoes back a message with an optional prefix",
2273 /// |args: EchoArgs, _| {
2274 /// Box::pin(async move {
2275 /// let message = match args.prefix {
2276 /// Some(p) => format!("{}: {}", p, args.message),
2277 /// None => args.message,
2278 /// };
2279 /// Ok(serde_json::json!({ "message": message }))
2280 /// })
2281 /// }
2282 /// );
2283 /// # }
2284 /// ```
2285 #[cfg(feature = "schema-generation")]
2286 pub fn tool_typed_with_description<T, F, Fut>(
2287 mut self,
2288 name: impl Into<String>,
2289 description: impl Into<String>,
2290 handler: F,
2291 ) -> Self
2292 where
2293 T: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
2294 F: Fn(T, crate::RequestHandlerExtra) -> Fut + Send + Sync + 'static,
2295 Fut: std::future::Future<Output = crate::Result<serde_json::Value>> + Send + 'static,
2296 {
2297 use crate::server::typed_tool::TypedTool;
2298 use std::pin::Pin;
2299
2300 let name_str = name.into();
2301
2302 // Wrap the handler to return Pin<Box<dyn Future>>
2303 let wrapped_handler = move |args: T,
2304 extra: crate::RequestHandlerExtra|
2305 -> Pin<
2306 Box<dyn std::future::Future<Output = crate::Result<serde_json::Value>> + Send>,
2307 > { Box::pin(handler(args, extra)) };
2308
2309 let tool = TypedTool::new(name_str.clone(), wrapped_handler).with_description(description);
2310 self.tools.insert(name_str, Arc::new(tool));
2311
2312 // Update capabilities to include tools
2313 if self.capabilities.tools.is_none() {
2314 self.capabilities.tools = Some(crate::types::ToolCapabilities {
2315 list_changed: Some(false),
2316 });
2317 }
2318
2319 self
2320 }
2321
2322 /// Add a synchronous type-safe tool handler with automatic schema generation.
2323 ///
2324 /// Similar to `tool_typed` but for synchronous handlers.
2325 ///
2326 /// # Example
2327 /// ```no_run
2328 /// # #[cfg(feature = "schema-generation")]
2329 /// # {
2330 /// use pmcp::ServerBuilder;
2331 /// use schemars::JsonSchema;
2332 /// use serde::{Deserialize, Serialize};
2333 ///
2334 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
2335 /// struct MathArgs {
2336 /// /// First number
2337 /// a: f64,
2338 /// /// Second number
2339 /// b: f64,
2340 /// /// Operation to perform
2341 /// op: String,
2342 /// }
2343 ///
2344 /// # fn main() -> Result<(), pmcp::Error> {
2345 /// let server = ServerBuilder::new()
2346 /// .name("example")
2347 /// .tool_typed_sync("calculator", |args: MathArgs, _| {
2348 /// let result = match args.op.as_str() {
2349 /// "add" => args.a + args.b,
2350 /// "subtract" => args.a - args.b,
2351 /// "multiply" => args.a * args.b,
2352 /// "divide" => args.a / args.b,
2353 /// _ => return Err(pmcp::Error::Validation("Unknown operation".into())),
2354 /// };
2355 /// Ok(serde_json::json!({ "result": result }))
2356 /// })
2357 /// .build();
2358 /// # Ok::<(), pmcp::Error>(())
2359 /// # }
2360 /// # }
2361 /// ```
2362 #[cfg(feature = "schema-generation")]
2363 pub fn tool_typed_sync<T, F>(mut self, name: impl Into<String>, handler: F) -> Self
2364 where
2365 T: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
2366 F: Fn(T, crate::RequestHandlerExtra) -> crate::Result<serde_json::Value>
2367 + Send
2368 + Sync
2369 + 'static,
2370 {
2371 use crate::server::typed_tool::TypedSyncTool;
2372 let name_str = name.into();
2373 let tool = TypedSyncTool::new(name_str.clone(), handler);
2374 self.tools.insert(name_str, Arc::new(tool));
2375
2376 // Update capabilities to include tools
2377 if self.capabilities.tools.is_none() {
2378 self.capabilities.tools = Some(crate::types::ToolCapabilities {
2379 list_changed: Some(false),
2380 });
2381 }
2382
2383 self
2384 }
2385
2386 /// Add a synchronous type-safe tool handler with automatic schema generation and description.
2387 ///
2388 /// This is a convenience overload that allows setting a description directly
2389 /// without needing to chain `.with_description()`.
2390 ///
2391 /// # Example
2392 /// ```no_run
2393 /// # #[cfg(feature = "schema-generation")]
2394 /// # {
2395 /// use pmcp::ServerBuilder;
2396 /// use schemars::JsonSchema;
2397 /// use serde::{Deserialize, Serialize};
2398 ///
2399 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
2400 /// struct MathArgs {
2401 /// /// First number
2402 /// a: f64,
2403 /// /// Second number
2404 /// b: f64,
2405 /// /// Operation to perform
2406 /// op: String,
2407 /// }
2408 ///
2409 /// let server = ServerBuilder::new()
2410 /// .name("example")
2411 /// .tool_typed_sync_with_description(
2412 /// "calculator",
2413 /// "Performs synchronous mathematical operations",
2414 /// |args: MathArgs, _| {
2415 /// let result = match args.op.as_str() {
2416 /// "add" => args.a + args.b,
2417 /// "subtract" => args.a - args.b,
2418 /// "multiply" => args.a * args.b,
2419 /// "divide" => args.a / args.b,
2420 /// _ => return Err(pmcp::Error::Validation("Unknown operation".into())),
2421 /// };
2422 /// Ok(serde_json::json!({ "result": result }))
2423 /// }
2424 /// );
2425 /// # }
2426 /// ```
2427 #[cfg(feature = "schema-generation")]
2428 pub fn tool_typed_sync_with_description<T, F>(
2429 mut self,
2430 name: impl Into<String>,
2431 description: impl Into<String>,
2432 handler: F,
2433 ) -> Self
2434 where
2435 T: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
2436 F: Fn(T, crate::RequestHandlerExtra) -> crate::Result<serde_json::Value>
2437 + Send
2438 + Sync
2439 + 'static,
2440 {
2441 use crate::server::typed_tool::TypedSyncTool;
2442 let name_str = name.into();
2443 let tool = TypedSyncTool::new(name_str.clone(), handler).with_description(description);
2444 self.tools.insert(name_str, Arc::new(tool));
2445
2446 // Update capabilities to include tools
2447 if self.capabilities.tools.is_none() {
2448 self.capabilities.tools = Some(crate::types::ToolCapabilities {
2449 list_changed: Some(false),
2450 });
2451 }
2452
2453 self
2454 }
2455
2456 /// Add a type-safe tool handler with both input and output typing.
2457 ///
2458 /// This method provides full type safety for both input and output types,
2459 /// which is useful for testing, documentation, and API contracts.
2460 /// Note that output schemas are not part of the MCP protocol but can be
2461 /// valuable for development and integration testing.
2462 ///
2463 /// # Type Parameters
2464 ///
2465 /// * `TIn` - Input type that implements `JsonSchema`, `Deserialize`, `Send`, `Sync`
2466 /// * `TOut` - Output type that implements `JsonSchema`, `Serialize`, `Send`, `Sync`
2467 ///
2468 /// # Example
2469 /// ```no_run
2470 /// # #[cfg(feature = "schema-generation")]
2471 /// # {
2472 /// use pmcp::{ServerBuilder, TypedToolWithOutput};
2473 /// use schemars::JsonSchema;
2474 /// use serde::{Deserialize, Serialize};
2475 ///
2476 /// #[derive(JsonSchema, Deserialize)]
2477 /// struct MathInput { a: f64, b: f64, op: String }
2478 ///
2479 /// #[derive(JsonSchema, Serialize)]
2480 /// struct MathOutput { result: f64, operation: String }
2481 ///
2482 /// let server = ServerBuilder::new()
2483 /// .name("example")
2484 /// .tool_typed_with_output::<MathInput, MathOutput>("math", |args, _| {
2485 /// Box::pin(async move {
2486 /// let result = match args.op.as_str() {
2487 /// "add" => args.a + args.b,
2488 /// "subtract" => args.a - args.b,
2489 /// _ => return Err(pmcp::Error::Validation("Unknown operation".into())),
2490 /// };
2491 /// Ok(MathOutput {
2492 /// result,
2493 /// operation: args.op,
2494 /// })
2495 /// })
2496 /// });
2497 /// # }
2498 /// ```
2499 #[cfg(feature = "schema-generation")]
2500 pub fn tool_typed_with_output<TIn, TOut>(
2501 mut self,
2502 name: impl Into<String>,
2503 handler: impl Fn(
2504 TIn,
2505 crate::RequestHandlerExtra,
2506 )
2507 -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<TOut>> + Send>>
2508 + Send
2509 + Sync
2510 + 'static,
2511 ) -> Self
2512 where
2513 TIn: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
2514 TOut: serde::Serialize + schemars::JsonSchema + Send + Sync + 'static,
2515 {
2516 use crate::server::typed_tool::TypedToolWithOutput;
2517
2518 let name_str = name.into();
2519 let tool = TypedToolWithOutput::new(name_str.clone(), handler);
2520 self.tools.insert(name_str, Arc::new(tool));
2521
2522 // Update capabilities to include tools
2523 if self.capabilities.tools.is_none() {
2524 self.capabilities.tools = Some(crate::types::ToolCapabilities {
2525 list_changed: Some(false),
2526 });
2527 }
2528
2529 self
2530 }
2531
2532 /// Add a type-safe tool handler with both input and output typing and description.
2533 ///
2534 /// This is a convenience overload that allows setting a description directly
2535 /// without needing to chain `.with_description()`.
2536 ///
2537 /// # Example
2538 /// ```no_run
2539 /// # #[cfg(feature = "schema-generation")]
2540 /// # {
2541 /// use pmcp::ServerBuilder;
2542 /// use schemars::JsonSchema;
2543 /// use serde::{Deserialize, Serialize};
2544 ///
2545 /// #[derive(JsonSchema, Deserialize)]
2546 /// struct MathInput { a: f64, b: f64, op: String }
2547 ///
2548 /// #[derive(JsonSchema, Serialize)]
2549 /// struct MathOutput { result: f64, operation: String }
2550 ///
2551 /// let server = ServerBuilder::new()
2552 /// .name("example")
2553 /// .tool_typed_with_output_and_description::<MathInput, MathOutput>(
2554 /// "math",
2555 /// "Performs basic mathematical operations on two numbers",
2556 /// |args, _| {
2557 /// Box::pin(async move {
2558 /// let result = match args.op.as_str() {
2559 /// "add" => args.a + args.b,
2560 /// "subtract" => args.a - args.b,
2561 /// _ => return Err(pmcp::Error::Validation("Unknown operation".into())),
2562 /// };
2563 /// Ok(MathOutput { result, operation: args.op })
2564 /// })
2565 /// }
2566 /// );
2567 /// # }
2568 /// ```
2569 #[cfg(feature = "schema-generation")]
2570 pub fn tool_typed_with_output_and_description<TIn, TOut>(
2571 mut self,
2572 name: impl Into<String>,
2573 description: impl Into<String>,
2574 handler: impl Fn(
2575 TIn,
2576 crate::RequestHandlerExtra,
2577 )
2578 -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<TOut>> + Send>>
2579 + Send
2580 + Sync
2581 + 'static,
2582 ) -> Self
2583 where
2584 TIn: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static,
2585 TOut: serde::Serialize + schemars::JsonSchema + Send + Sync + 'static,
2586 {
2587 use crate::server::typed_tool::TypedToolWithOutput;
2588
2589 let name_str = name.into();
2590 let tool =
2591 TypedToolWithOutput::new(name_str.clone(), handler).with_description(description);
2592 self.tools.insert(name_str, Arc::new(tool));
2593
2594 // Update capabilities to include tools
2595 if self.capabilities.tools.is_none() {
2596 self.capabilities.tools = Some(crate::types::ToolCapabilities {
2597 list_changed: Some(false),
2598 });
2599 }
2600
2601 self
2602 }
2603
2604 /// Add a prompt handler.
2605 ///
2606 /// Registers a prompt that clients can retrieve via the prompts/get method.
2607 /// Prompts provide templates that clients can use for various tasks.
2608 ///
2609 /// # Arguments
2610 ///
2611 /// * `name` - The name of the prompt (used by clients to retrieve it)
2612 /// * `handler` - The handler implementation for this prompt
2613 ///
2614 /// # Examples
2615 ///
2616 /// ```rust,no_run
2617 /// use pmcp::{Server, PromptHandler, GetPromptResult, PromptMessage, Content};
2618 /// use async_trait::async_trait;
2619 /// use std::collections::HashMap;
2620 ///
2621 /// struct CodeReviewPrompt;
2622 ///
2623 /// #[async_trait]
2624 /// impl PromptHandler for CodeReviewPrompt {
2625 /// async fn handle(&self, args: HashMap<String, String>, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<GetPromptResult> {
2626 /// let language = args.get("language").map(|s| s.as_str()).unwrap_or("unknown");
2627 /// Ok(GetPromptResult::new(
2628 /// vec![PromptMessage::user(pmcp::Content::text(format!(
2629 /// "Please review this {} code:",
2630 /// language
2631 /// )))],
2632 /// Some(format!("Code review prompt for {}", language)),
2633 /// ))
2634 /// }
2635 /// }
2636 ///
2637 /// let server = Server::builder()
2638 /// .name("code-server")
2639 /// .version("1.0.0")
2640 /// .prompt("code_review", CodeReviewPrompt{})
2641 /// .build()?;
2642 /// # Ok::<(), pmcp::Error>(())
2643 /// ```
2644 pub fn prompt(
2645 mut self,
2646 name: impl Into<String>,
2647 handler: impl PromptHandler + 'static,
2648 ) -> Self {
2649 self.prompts.insert(name.into(), Arc::new(handler));
2650
2651 // Update capabilities to include prompts
2652 // Use Some(false) instead of None to ensure the field serializes properly
2653 if self.capabilities.prompts.is_none() {
2654 self.capabilities.prompts = Some(crate::types::PromptCapabilities {
2655 list_changed: Some(false),
2656 });
2657 }
2658
2659 self
2660 }
2661
2662 /// Add a prompt handler with an Arc.
2663 ///
2664 /// This variant lets the caller share the handler `Arc` between the
2665 /// builder and an external in-process handler map (e.g., a downstream
2666 /// toolkit's handler registry) without writing a delegating wrapper
2667 /// shim. Behavior is otherwise identical to [`Self::prompt`]: the first
2668 /// registration auto-enables `capabilities.prompts`.
2669 pub fn prompt_arc(mut self, name: impl Into<String>, handler: Arc<dyn PromptHandler>) -> Self {
2670 let name = name.into();
2671 self.prompts.insert(name, handler);
2672
2673 // Update capabilities to include prompts
2674 // Use Some(false) instead of None to ensure the field serializes properly
2675 if self.capabilities.prompts.is_none() {
2676 self.capabilities.prompts = Some(crate::types::PromptCapabilities {
2677 list_changed: Some(false),
2678 });
2679 }
2680
2681 self
2682 }
2683
2684 /// Register a workflow-based prompt with automatic validation.
2685 ///
2686 /// This method validates the workflow before registration and converts it
2687 /// to a prompt handler. The workflow's instructions become the prompt messages,
2688 /// and the workflow's arguments become the prompt arguments.
2689 ///
2690 /// # Arguments
2691 ///
2692 /// * `workflow` - The workflow definition to register as a prompt
2693 ///
2694 /// # Errors
2695 ///
2696 /// Returns an error if the workflow validation fails (e.g., undefined bindings,
2697 /// undefined prompt arguments, etc.).
2698 ///
2699 /// # Examples
2700 ///
2701 /// ```rust,no_run
2702 /// use pmcp::{Server, ServerBuilder};
2703 /// use pmcp::server::workflow::{SequentialWorkflow, InternalPromptMessage};
2704 /// use pmcp::types::Role;
2705 ///
2706 /// # fn main() -> pmcp::Result<()> {
2707 /// let workflow = SequentialWorkflow::new(
2708 /// "code_review_workflow",
2709 /// "Review code with multiple steps"
2710 /// )
2711 /// .argument("code", "Code to review", true)
2712 /// .instruction(InternalPromptMessage::new(
2713 /// Role::System,
2714 /// "You are a code reviewer. Review the provided code carefully."
2715 /// ));
2716 ///
2717 /// let server = Server::builder()
2718 /// .name("code-server")
2719 /// .version("1.0.0")
2720 /// .prompt_workflow(workflow)?
2721 /// .build()?;
2722 /// # Ok(())
2723 /// # }
2724 /// ```
2725 pub fn prompt_workflow(mut self, workflow: workflow::SequentialWorkflow) -> Result<Self> {
2726 // Validate the workflow before registration
2727 workflow
2728 .validate()
2729 .map_err(|e| Error::Validation(format!("Workflow validation failed: {}", e)))?;
2730
2731 // Build tool and resource registries from currently registered handlers
2732 // Note: This captures the current state of registered tools/resources
2733 let mut tools = std::collections::HashMap::new();
2734 for (name, handler) in &self.tools {
2735 if let Some(metadata) = handler.metadata() {
2736 tools.insert(
2737 Arc::from(name.as_str()),
2738 workflow::conversion::ToolInfo {
2739 name: metadata.name,
2740 description: metadata.description.unwrap_or_default(),
2741 input_schema: metadata.input_schema,
2742 },
2743 );
2744 }
2745 }
2746
2747 // Build tool handlers map for workflow execution
2748 // Clone Arc references for shared ownership
2749 let mut tool_handlers: std::collections::HashMap<Arc<str>, Arc<dyn ToolHandler>> =
2750 std::collections::HashMap::new();
2751 for (name, handler) in &self.tools {
2752 tool_handlers.insert(Arc::from(name.as_str()), Arc::clone(handler));
2753 }
2754
2755 // Get the workflow name before moving it
2756 let name = workflow.name().to_string();
2757
2758 // Create workflow prompt handler with tool execution and resource fetching capability
2759 // Note: Workflow prompts in ServerBuilder do not currently execute tool middleware.
2760 // For middleware support in workflow tool execution, use ServerCoreBuilder.
2761 let handler = workflow::WorkflowPromptHandler::new(
2762 workflow,
2763 tools,
2764 tool_handlers,
2765 self.resources.clone(),
2766 );
2767
2768 // Register as a prompt
2769 self.prompts.insert(name, Arc::new(handler));
2770
2771 // Update capabilities to include prompts
2772 // This ensures prompts/list returns the workflow prompts
2773 if self.capabilities.prompts.is_none() {
2774 self.capabilities.prompts = Some(crate::types::PromptCapabilities {
2775 list_changed: Some(false),
2776 });
2777 }
2778
2779 Ok(self)
2780 }
2781
2782 /// Set the resource handler.
2783 ///
2784 /// Registers a resource handler that provides access to server resources.
2785 /// Resources allow clients to read files, configurations, or other data.
2786 ///
2787 /// # Arguments
2788 ///
2789 /// * `handler` - The resource handler implementation
2790 ///
2791 /// # Examples
2792 ///
2793 /// ```rust,no_run
2794 /// use pmcp::{Server, ResourceHandler, ReadResourceResult, ListResourcesResult, ResourceInfo};
2795 /// use async_trait::async_trait;
2796 ///
2797 /// struct FileResourceHandler;
2798 ///
2799 /// #[async_trait]
2800 /// impl ResourceHandler for FileResourceHandler {
2801 /// async fn read(&self, uri: &str, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<ReadResourceResult> {
2802 /// // Read file content...
2803 /// Ok(ReadResourceResult::new(vec![pmcp::Content::text("File content here")]))
2804 /// }
2805 ///
2806 /// async fn list(&self, _cursor: Option<String>, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<ListResourcesResult> {
2807 /// Ok(ListResourcesResult::new(vec![
2808 /// pmcp::ResourceInfo::new("file://example.txt", "example.txt")
2809 /// .with_description("Example file")
2810 /// .with_mime_type("text/plain"),
2811 /// ]))
2812 /// }
2813 /// }
2814 ///
2815 /// let server = Server::builder()
2816 /// .name("file-server")
2817 /// .version("1.0.0")
2818 /// .resources(FileResourceHandler{})
2819 /// .build()?;
2820 /// # Ok::<(), pmcp::Error>(())
2821 /// ```
2822 pub fn resources(mut self, handler: impl ResourceHandler + 'static) -> Self {
2823 self.resources = Some(Arc::new(handler));
2824
2825 // Update capabilities to include resources
2826 // Use Some(false) instead of None to ensure fields serialize properly
2827 if self.capabilities.resources.is_none() {
2828 self.capabilities.resources = Some(crate::types::ResourceCapabilities {
2829 subscribe: Some(false),
2830 list_changed: Some(false),
2831 });
2832 }
2833
2834 self
2835 }
2836
2837 /// Set the resource handler with an Arc.
2838 ///
2839 /// This variant lets the caller share the handler `Arc` between the
2840 /// builder and an external in-process handler map without writing a
2841 /// delegating wrapper. Behavior is otherwise identical to
2842 /// [`Self::resources`]: the first registration auto-enables
2843 /// `capabilities.resources`.
2844 pub fn resources_arc(mut self, handler: Arc<dyn ResourceHandler>) -> Self {
2845 self.resources = Some(handler);
2846
2847 // Update capabilities to include resources
2848 // Use Some(false) instead of None to ensure fields serialize properly
2849 if self.capabilities.resources.is_none() {
2850 self.capabilities.resources = Some(crate::types::ResourceCapabilities {
2851 subscribe: Some(false),
2852 list_changed: Some(false),
2853 });
2854 }
2855
2856 self
2857 }
2858
2859 /// Register a single SEP-2640 Agent Skill.
2860 ///
2861 /// Convenience over [`Self::skills`] for the single-skill case. The skill
2862 /// is accumulated and finalized into a `SkillsHandler` exactly once at
2863 /// [`Self::build`] time, then composed with any `.resources(...)`
2864 /// handler set on this builder.
2865 ///
2866 /// # Panics
2867 ///
2868 /// Panics at `.build()` time if multiple registered skills resolve to
2869 /// the same `skill://` URI. Use [`Self::try_skills`] with a pre-built
2870 /// [`skills::Skills`] registry to surface duplicates as a `Result`.
2871 ///
2872 /// # Examples
2873 ///
2874 /// ```rust,no_run
2875 /// # #[cfg(feature = "skills")] {
2876 /// use pmcp::{Server, server::skills::Skill};
2877 ///
2878 /// # fn example() -> pmcp::Result<()> {
2879 /// let server = Server::builder()
2880 /// .name("my-server")
2881 /// .version("1.0.0")
2882 /// .skill(Skill::new("hello", "# Hello skill"))
2883 /// .build()?;
2884 /// # Ok(())
2885 /// # }
2886 /// # }
2887 /// ```
2888 #[cfg(feature = "skills")]
2889 #[must_use]
2890 pub fn skill(self, skill: skills::Skill) -> Self {
2891 self.skills(skills::Skills::new().add(skill))
2892 }
2893
2894 /// Register a registry of SEP-2640 Agent Skills.
2895 ///
2896 /// Merges into any prior accumulated skills (a previous `.skill(...)` or
2897 /// `.skills(...)` call). The accumulated registry is finalized into a
2898 /// single `SkillsHandler` exactly once at [`Self::build`] time, then
2899 /// composed at most once with any `.resources(...)` handler.
2900 ///
2901 /// # Panics
2902 ///
2903 /// Panics at `.build()` if two registered skills resolve to the same
2904 /// `skill://` URI. Use [`Self::try_skills`] for fallible registration.
2905 #[cfg(feature = "skills")]
2906 #[must_use]
2907 pub fn skills(mut self, skills_registry: skills::Skills) -> Self {
2908 let merged = match self.pending_skills.take() {
2909 Some(prior) => prior.merge(skills_registry),
2910 None => skills_registry,
2911 };
2912 self.pending_skills = Some(merged);
2913 skills::set_skills_capabilities(&mut self.capabilities);
2914 self
2915 }
2916
2917 /// Fallible variant of [`Self::skills`] — returns `Err` immediately if
2918 /// the merged registry would contain duplicate URIs. Useful for
2919 /// runtime-dynamic registration where panicking is unacceptable.
2920 ///
2921 /// # Errors
2922 ///
2923 /// Returns `Err(pmcp::Error::Validation)` if the merged registry would
2924 /// produce duplicate `skill://` URIs.
2925 #[cfg(feature = "skills")]
2926 pub fn try_skills(mut self, skills_registry: skills::Skills) -> Result<Self> {
2927 let merged = match self.pending_skills.take() {
2928 Some(prior) => prior.merge(skills_registry),
2929 None => skills_registry,
2930 };
2931 // Probe by cloning + into_handler; discard the handler. The real
2932 // construction happens in `.build()` once everything is settled.
2933 merged.clone().into_handler()?;
2934 self.pending_skills = Some(merged);
2935 skills::set_skills_capabilities(&mut self.capabilities);
2936 Ok(self)
2937 }
2938
2939 /// Register a skill AND a parallel prompt that returns the same content.
2940 ///
2941 /// The dual-surface bootstrap: both surfaces are derived from one
2942 /// [`skills::Skill`] value so they cannot drift. The byte-equality
2943 /// between surfaces is asserted by the skills integration test.
2944 #[cfg(feature = "skills")]
2945 #[must_use]
2946 pub fn bootstrap_skill_and_prompt(
2947 self,
2948 skill: skills::Skill,
2949 prompt_name: impl Into<String>,
2950 ) -> Self {
2951 let prompt_handler = skills::SkillPromptHandler::new(skill.clone());
2952 self.skill(skill).prompt(prompt_name, prompt_handler)
2953 }
2954
2955 /// Set the sampling handler.
2956 ///
2957 /// Registers a sampling handler that provides LLM functionality.
2958 /// This allows the server to act as a language model provider.
2959 ///
2960 /// # Arguments
2961 ///
2962 /// * `handler` - The sampling handler implementation
2963 ///
2964 /// # Examples
2965 ///
2966 /// ```rust,no_run
2967 /// use pmcp::{Server, SamplingHandler, CreateMessageParams, CreateMessageResult};
2968 /// use async_trait::async_trait;
2969 ///
2970 /// struct MockLLM;
2971 ///
2972 /// #[async_trait]
2973 /// impl SamplingHandler for MockLLM {
2974 /// async fn create_message(&self, params: CreateMessageParams, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<CreateMessageResult> {
2975 /// // Process the messages and generate a response
2976 /// Ok(CreateMessageResult::new(pmcp::Content::text("Generated response"), "mock-llm-v1")
2977 /// .with_usage(pmcp::TokenUsage::new(10, 5, 15))
2978 /// .with_stop_reason("end_of_text"))
2979 /// }
2980 /// }
2981 ///
2982 /// let server = Server::builder()
2983 /// .name("llm-server")
2984 /// .version("1.0.0")
2985 /// .sampling(MockLLM{})
2986 /// .build()?;
2987 /// # Ok::<(), pmcp::Error>(())
2988 /// ```
2989 pub fn sampling(mut self, handler: impl SamplingHandler + 'static) -> Self {
2990 self.sampling = Some(Arc::new(handler));
2991 // Enable sampling capability
2992 self.capabilities.sampling = Some(crate::types::SamplingCapabilities::default());
2993 self
2994 }
2995
2996 /// Set the sampling handler with an Arc.
2997 ///
2998 /// This variant lets the caller share the handler `Arc` between the
2999 /// builder and an external in-process handler map without writing a
3000 /// delegating wrapper. Uses the donor's `if is_none` capability
3001 /// auto-enable so an explicit prior `.capabilities(custom)` is not
3002 /// clobbered by a later `_arc` registration.
3003 pub fn sampling_arc(mut self, handler: Arc<dyn SamplingHandler>) -> Self {
3004 self.sampling = Some(handler);
3005
3006 // Update capabilities to include sampling
3007 if self.capabilities.sampling.is_none() {
3008 self.capabilities.sampling = Some(crate::types::SamplingCapabilities::default());
3009 }
3010
3011 self
3012 }
3013
3014 /// Build the server.
3015 ///
3016 /// Constructs the final Server instance from the configured builder.
3017 /// This validates that required fields (name and version) are set.
3018 ///
3019 /// # Examples
3020 ///
3021 /// ```rust,no_run
3022 /// use pmcp::{Server, ToolHandler};
3023 /// use async_trait::async_trait;
3024 /// use serde_json::Value;
3025 ///
3026 /// struct PingTool;
3027 ///
3028 /// #[async_trait]
3029 /// impl ToolHandler for PingTool {
3030 /// async fn handle(&self, _args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
3031 /// Ok(serde_json::json!({"response": "pong"}))
3032 /// }
3033 /// }
3034 ///
3035 /// let server = Server::builder()
3036 /// .name("ping-server")
3037 /// .version("1.0.0")
3038 /// .tool("ping", PingTool{})
3039 /// .build()?;
3040 ///
3041 /// // Server is now ready to run
3042 /// // server.run_stdio().await?;
3043 /// # Ok::<(), pmcp::Error>(())
3044 /// ```
3045 /// Set the authentication provider.
3046 ///
3047 /// Configures an authentication provider that will validate incoming requests.
3048 /// When set, the server will use this provider to authenticate requests before
3049 /// processing them.
3050 ///
3051 /// # Arguments
3052 ///
3053 /// * `provider` - The authentication provider implementation
3054 ///
3055 /// # Examples
3056 ///
3057 /// ```rust,no_run
3058 /// use pmcp::{Server, auth::ProxyProvider};
3059 ///
3060 /// let auth_provider = ProxyProvider::with_upstream("https://oauth.example.com");
3061 ///
3062 /// let server = Server::builder()
3063 /// .name("secure-server")
3064 /// .version("1.0.0")
3065 /// .auth_provider(auth_provider)
3066 /// .build()?;
3067 /// # Ok::<(), pmcp::Error>(())
3068 /// ```
3069 pub fn auth_provider(mut self, provider: impl auth::AuthProvider + 'static) -> Self {
3070 self.auth_provider = Some(Arc::new(provider));
3071 self
3072 }
3073
3074 /// Set the authentication provider with an Arc.
3075 ///
3076 /// This variant lets the caller share the provider `Arc` between the
3077 /// builder and an external in-process registry without writing a
3078 /// delegating wrapper. Behavior is otherwise identical to
3079 /// [`Self::auth_provider`].
3080 pub fn auth_provider_arc(mut self, provider: Arc<dyn auth::AuthProvider>) -> Self {
3081 self.auth_provider = Some(provider);
3082 self
3083 }
3084
3085 /// Set the tool authorizer.
3086 ///
3087 /// Configures a tool authorizer for fine-grained access control.
3088 /// The authorizer determines which tools authenticated users can access
3089 /// based on their authentication context.
3090 ///
3091 /// # Arguments
3092 ///
3093 /// * `authorizer` - The tool authorization implementation
3094 ///
3095 /// # Examples
3096 ///
3097 /// ```rust,no_run
3098 /// use pmcp::{Server, auth::ScopeBasedAuthorizer};
3099 ///
3100 /// let authorizer = ScopeBasedAuthorizer::new()
3101 /// .require_scopes("sensitive_tool", vec!["admin".to_string()])
3102 /// .default_scopes(vec!["read".to_string()]);
3103 ///
3104 /// let server = Server::builder()
3105 /// .name("secure-server")
3106 /// .version("1.0.0")
3107 /// .tool_authorizer(authorizer)
3108 /// .build()?;
3109 /// # Ok::<(), pmcp::Error>(())
3110 /// ```
3111 pub fn tool_authorizer(mut self, authorizer: impl auth::ToolAuthorizer + 'static) -> Self {
3112 if !self.tool_protections.is_empty() {
3113 // Log a warning - custom authorizer supersedes protect_tool() configurations
3114 tracing::warn!(
3115 target: "mcp.auth",
3116 "Setting a custom tool_authorizer clears any previous protect_tool() configurations"
3117 );
3118 self.tool_protections.clear();
3119 }
3120 self.tool_authorizer = Some(Arc::new(authorizer));
3121 self
3122 }
3123
3124 /// Set the tool authorizer with an Arc.
3125 ///
3126 /// This variant lets the caller share the authorizer `Arc` between
3127 /// the builder and an external in-process registry without writing a
3128 /// delegating wrapper. Mirrors [`Self::tool_authorizer`]'s
3129 /// protection-clearing semantics: if any prior `protect_tool()`
3130 /// configurations exist, they are cleared and a `tracing::warn!` is
3131 /// emitted under target `"mcp.auth"`, since a custom authorizer
3132 /// supersedes scope-based tool protections.
3133 pub fn tool_authorizer_arc(mut self, authorizer: Arc<dyn auth::ToolAuthorizer>) -> Self {
3134 if !self.tool_protections.is_empty() {
3135 // Log a warning - custom authorizer supersedes protect_tool() configurations
3136 tracing::warn!(
3137 target: "mcp.auth",
3138 "Setting a custom tool_authorizer clears any previous protect_tool() configurations"
3139 );
3140 self.tool_protections.clear();
3141 }
3142 self.tool_authorizer = Some(authorizer);
3143 self
3144 }
3145
3146 /// Protect a specific tool with required scopes.
3147 ///
3148 /// This is a convenience method that creates or updates a scope-based authorizer
3149 /// to require specific scopes for accessing the named tool.
3150 ///
3151 /// # Arguments
3152 ///
3153 /// * `tool_name` - The name of the tool to protect
3154 /// * `scopes` - The required scopes for accessing this tool
3155 ///
3156 /// # Examples
3157 ///
3158 /// ```rust,no_run
3159 /// use pmcp::Server;
3160 ///
3161 /// let server = Server::builder()
3162 /// .name("secure-server")
3163 /// .version("1.0.0")
3164 /// .protect_tool("delete_data", vec!["admin".to_string(), "write".to_string()])
3165 /// .protect_tool("read_data", vec!["read".to_string()])
3166 /// .build()?;
3167 /// # Ok::<(), pmcp::Error>(())
3168 /// ```
3169 pub fn protect_tool(mut self, tool_name: impl Into<String>, scopes: Vec<String>) -> Self {
3170 // Store the tool protection requirements to be applied at build time
3171 self.tool_protections.insert(tool_name.into(), scopes);
3172 self
3173 }
3174
3175 /// Add tool middleware for cross-cutting concerns.
3176 ///
3177 /// Tool middleware allows you to inject cross-cutting concerns into tool execution,
3178 /// such as OAuth token injection, logging, metrics, or request transformation.
3179 /// Middleware is executed in the order it's added, both for request processing
3180 /// (before tool execution) and response processing (after tool execution).
3181 ///
3182 /// This method brings middleware support to the high-level `ServerBuilder` API,
3183 /// enabling developers to use both typed tool registration AND middleware without
3184 /// dropping down to the lower-level `ServerCoreBuilder` API.
3185 ///
3186 /// # Arguments
3187 ///
3188 /// * `middleware` - The middleware implementation to add to the chain
3189 ///
3190 /// # Examples
3191 ///
3192 /// ## OAuth Token Injection Middleware
3193 ///
3194 /// ```rust,no_run
3195 /// use pmcp::server::tool_middleware::{ToolMiddleware, ToolContext};
3196 /// use pmcp::server::cancellation::RequestHandlerExtra;
3197 /// use pmcp::Server;
3198 /// use std::sync::Arc;
3199 /// use async_trait::async_trait;
3200 /// use serde_json::Value;
3201 ///
3202 /// struct OAuthInjectionMiddleware;
3203 ///
3204 /// #[async_trait]
3205 /// impl ToolMiddleware for OAuthInjectionMiddleware {
3206 /// async fn on_request(
3207 /// &self,
3208 /// _tool_name: &str,
3209 /// _args: &mut Value,
3210 /// extra: &mut RequestHandlerExtra,
3211 /// _context: &ToolContext,
3212 /// ) -> pmcp::Result<()> {
3213 /// // Extract OAuth token from auth_context and inject into metadata
3214 /// if let Some(auth_ctx) = extra.auth_context() {
3215 /// if let Some(token) = &auth_ctx.token {
3216 /// extra.set_metadata("oauth_token".to_string(), token.clone());
3217 /// }
3218 /// }
3219 /// Ok(())
3220 /// }
3221 /// }
3222 ///
3223 /// let server = Server::builder()
3224 /// .name("oauth-server")
3225 /// .version("1.0.0")
3226 /// .tool_middleware(Arc::new(OAuthInjectionMiddleware))
3227 /// .build()?;
3228 /// # Ok::<(), pmcp::Error>(())
3229 /// ```
3230 ///
3231 /// ## Combining with Typed Tools
3232 ///
3233 /// ```rust,no_run
3234 /// # #[cfg(feature = "schema-generation")]
3235 /// # {
3236 /// use pmcp::Server;
3237 /// use schemars::JsonSchema;
3238 /// use serde::{Deserialize, Serialize};
3239 ///
3240 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
3241 /// struct ListGamesArgs {
3242 /// filter: Option<String>,
3243 /// }
3244 ///
3245 /// let server = Server::builder()
3246 /// .name("game-server")
3247 /// .version("1.0.0")
3248 /// .tool_typed_with_description(
3249 /// "list_games",
3250 /// "List all available games",
3251 /// |args: ListGamesArgs, extra| {
3252 /// Box::pin(async move {
3253 /// // Access OAuth token injected by middleware
3254 /// let _token = extra.get_metadata("oauth_token");
3255 /// Ok(serde_json::json!({"games": []}))
3256 /// })
3257 /// }
3258 /// )
3259 /// // .tool_middleware(Arc::new(oauth_middleware)) // Works with typed tools!
3260 /// .build()?;
3261 /// # }
3262 /// # Ok::<(), pmcp::Error>(())
3263 /// ```
3264 ///
3265 /// # Middleware Execution Order
3266 ///
3267 /// Multiple middleware are executed in FIFO order for requests and FIFO for responses:
3268 ///
3269 /// ```text
3270 /// Request: Middleware1 → Middleware2 → Tool Handler
3271 /// Response: Tool Handler → Middleware1 → Middleware2
3272 /// ```
3273 #[cfg(not(target_arch = "wasm32"))]
3274 pub fn tool_middleware(mut self, middleware: Arc<dyn tool_middleware::ToolMiddleware>) -> Self {
3275 self.tool_middlewares.push(middleware);
3276 self
3277 }
3278
3279 /// Enable observability for this server.
3280 ///
3281 /// This adds observability middleware that provides:
3282 /// - Distributed tracing with trace/span IDs
3283 /// - Request/response event logging
3284 /// - Metrics emission (duration, count, errors)
3285 ///
3286 /// The backend is automatically selected based on the configuration:
3287 /// - "console" - Pretty or JSON output to stdout (development)
3288 /// - "cloudwatch" - AWS `CloudWatch` EMF format (production)
3289 /// - "null" - Discards all events (testing)
3290 ///
3291 /// # Examples
3292 ///
3293 /// ```rust,no_run
3294 /// use pmcp::Server;
3295 /// use pmcp::server::observability::ObservabilityConfig;
3296 ///
3297 /// // Development: console output with pretty printing
3298 /// let server = Server::builder()
3299 /// .name("my-server")
3300 /// .version("1.0.0")
3301 /// .with_observability(ObservabilityConfig::development())
3302 /// .build()?;
3303 ///
3304 /// // Production: CloudWatch with EMF metrics
3305 /// let server = Server::builder()
3306 /// .name("my-server")
3307 /// .version("1.0.0")
3308 /// .with_observability(ObservabilityConfig::production())
3309 /// .build()?;
3310 ///
3311 /// // Auto-detect environment (Lambda vs local)
3312 /// let config = if std::env::var("AWS_LAMBDA_FUNCTION_NAME").is_ok() {
3313 /// ObservabilityConfig::production()
3314 /// } else {
3315 /// ObservabilityConfig::development()
3316 /// };
3317 /// let server = Server::builder()
3318 /// .name("my-server")
3319 /// .version("1.0.0")
3320 /// .with_observability(config)
3321 /// .build()?;
3322 /// # Ok::<(), pmcp::Error>(())
3323 /// ```
3324 #[cfg(not(target_arch = "wasm32"))]
3325 pub fn with_observability(mut self, config: observability::ObservabilityConfig) -> Self {
3326 if !config.enabled {
3327 return self;
3328 }
3329
3330 // Create backend based on configuration
3331 let backend: Arc<dyn observability::ObservabilityBackend> = match config.backend.as_str() {
3332 "cloudwatch" => Arc::new(observability::CloudWatchBackend::new(
3333 config.cloudwatch.clone(),
3334 )),
3335 "null" => Arc::new(observability::NullBackend),
3336 _ => Arc::new(observability::ConsoleBackend::new(config.console.pretty)),
3337 };
3338
3339 // Get server name for middleware (use placeholder if not yet set)
3340 let server_name = self.name.clone().unwrap_or_else(|| "unknown".to_string());
3341
3342 // Create and add the observability middleware
3343 let middleware =
3344 observability::McpObservabilityMiddleware::new(server_name, config, backend);
3345 self.tool_middlewares.push(Arc::new(middleware));
3346
3347 self
3348 }
3349
3350 /// Enable observability with a custom backend.
3351 ///
3352 /// Use this when you need a custom backend implementation (e.g., Datadog, custom metrics).
3353 ///
3354 /// # Examples
3355 ///
3356 /// ```rust,ignore
3357 /// use pmcp::Server;
3358 /// use pmcp::server::observability::{ObservabilityConfig, ObservabilityBackend};
3359 /// use std::sync::Arc;
3360 ///
3361 /// struct MyCustomBackend;
3362 ///
3363 /// #[async_trait]
3364 /// impl ObservabilityBackend for MyCustomBackend {
3365 /// // ... custom implementation
3366 /// }
3367 ///
3368 /// let server = Server::builder()
3369 /// .name("my-server")
3370 /// .version("1.0.0")
3371 /// .with_observability_backend(
3372 /// ObservabilityConfig::development(),
3373 /// Arc::new(MyCustomBackend),
3374 /// )
3375 /// .build()?;
3376 /// ```
3377 #[cfg(not(target_arch = "wasm32"))]
3378 pub fn with_observability_backend(
3379 mut self,
3380 config: observability::ObservabilityConfig,
3381 backend: Arc<dyn observability::ObservabilityBackend>,
3382 ) -> Self {
3383 if !config.enabled {
3384 return self;
3385 }
3386
3387 // Get server name for middleware (use placeholder if not yet set)
3388 let server_name = self.name.clone().unwrap_or_else(|| "unknown".to_string());
3389
3390 // Create and add the observability middleware
3391 let middleware =
3392 observability::McpObservabilityMiddleware::new(server_name, config, backend);
3393 self.tool_middlewares.push(Arc::new(middleware));
3394
3395 self
3396 }
3397
3398 /// Add a description to a tool (Note: Limited support).
3399 ///
3400 /// **Important**: Due to the immutable design of tool handlers, this method
3401 /// cannot retroactively add descriptions to already-registered tools.
3402 ///
3403 /// **Recommended**: Use the `*_with_description` variants instead:
3404 /// - `.tool_typed_with_description()`
3405 /// - `.tool_typed_sync_with_description()`
3406 /// - `.tool_typed_with_output_and_description()`
3407 ///
3408 /// This method is provided for API completeness but will log warnings
3409 /// when used, encouraging migration to the preferred approaches.
3410 ///
3411 /// # Preferred Examples
3412 ///
3413 /// ```rust,no_run
3414 /// # #[cfg(feature = "schema-generation")]
3415 /// # {
3416 /// use pmcp::ServerBuilder;
3417 /// use schemars::JsonSchema;
3418 /// use serde::{Deserialize, Serialize};
3419 ///
3420 /// #[derive(Debug, Deserialize, Serialize, JsonSchema)]
3421 /// struct MathArgs { a: f64, b: f64 }
3422 ///
3423 /// // Preferred: Use the direct description variants
3424 /// let server = ServerBuilder::new()
3425 /// .name("example")
3426 /// .tool_typed_with_description(
3427 /// "add",
3428 /// "Adds two numbers together",
3429 /// |args: MathArgs, _| {
3430 /// Box::pin(async move {
3431 /// Ok(serde_json::json!({ "result": args.a + args.b }))
3432 /// })
3433 /// }
3434 /// )
3435 /// .build();
3436 /// # }
3437 /// ```
3438 #[deprecated(
3439 since = "1.6.0",
3440 note = "Use tool_typed_with_description() and similar variants instead"
3441 )]
3442 pub fn with_tool_description(
3443 self,
3444 tool_name: impl Into<String>,
3445 description: impl Into<String>,
3446 ) -> Self {
3447 let tool_name = tool_name.into();
3448 let _description = description.into();
3449
3450 tracing::warn!(
3451 "with_tool_description('{}') called but cannot modify immutable tools. \
3452 Use tool_typed_with_description() variants instead.",
3453 tool_name
3454 );
3455
3456 self
3457 }
3458
3459 /// Configure HTTP middleware chain for `StreamableHttpServer`.
3460 ///
3461 /// This is a convenience method that stores the HTTP middleware chain
3462 /// so it can be retrieved later when creating a `StreamableHttpServer`.
3463 ///
3464 /// # Arguments
3465 ///
3466 /// * `middleware` - The HTTP middleware chain
3467 ///
3468 /// # Examples
3469 ///
3470 /// ```rust,no_run
3471 /// # #[cfg(feature = "streamable-http")]
3472 /// # fn example() -> Result<(), pmcp::Error> {
3473 /// use pmcp::Server;
3474 /// use pmcp::server::http_middleware::{ServerHttpLoggingMiddleware, ServerHttpMiddlewareChain};
3475 /// use std::sync::Arc;
3476 ///
3477 /// let mut http_chain = ServerHttpMiddlewareChain::new();
3478 /// http_chain.add(Arc::new(ServerHttpLoggingMiddleware::new()));
3479 ///
3480 /// let server = Server::builder()
3481 /// .name("my-server")
3482 /// .version("1.0.0")
3483 /// .with_http_middleware(Arc::new(http_chain))
3484 /// .build()?;
3485 ///
3486 /// // Later when creating StreamableHttpServer:
3487 /// // let config = StreamableHttpServerConfig {
3488 /// // http_middleware: server.http_middleware(),
3489 /// // ..Default::default()
3490 /// // };
3491 /// # Ok(())
3492 /// # }
3493 /// ```
3494 #[cfg(feature = "streamable-http")]
3495 pub fn with_http_middleware(
3496 mut self,
3497 middleware: Arc<http_middleware::ServerHttpMiddlewareChain>,
3498 ) -> Self {
3499 self.http_middleware = Some(middleware);
3500 self
3501 }
3502
3503 /// Add a host layer for MCP Apps metadata enrichment.
3504 ///
3505 /// Host layers enrich tool `_meta` at build time with host-specific keys.
3506 /// For example, `HostType::ChatGpt` adds `openai/outputTemplate` and
3507 /// `openai/widgetAccessible` derived from the standard `ui.resourceUri`.
3508 ///
3509 /// This is opt-in — standard MCP Apps hosts (Claude Desktop, etc.) work
3510 /// without any host layer. Duplicates are ignored.
3511 #[cfg(feature = "mcp-apps")]
3512 pub fn with_host_layer(mut self, host: crate::types::mcp_apps::HostType) -> Self {
3513 if !self.host_layers.contains(&host) {
3514 self.host_layers.push(host);
3515 }
3516 self
3517 }
3518
3519 /// Build the server.
3520 ///
3521 /// Constructs the final Server instance from the configured builder.
3522 /// This validates that required fields (name and version) are set.
3523 ///
3524 /// # Errors
3525 ///
3526 /// Returns an error if:
3527 /// - The server name is not set
3528 /// - The server version is not set
3529 pub fn build(self) -> Result<Server> {
3530 let name = self
3531 .name
3532 .ok_or_else(|| crate::Error::validation("Server name is required"))?;
3533 let version = self
3534 .version
3535 .ok_or_else(|| crate::Error::validation("Server version is required"))?;
3536
3537 // Apply tool protections
3538 let tool_authorizer = if !self.tool_protections.is_empty() {
3539 if self.tool_authorizer.is_some() {
3540 // If there's an existing authorizer and tool protections are specified,
3541 // this is a configuration error
3542 return Err(crate::Error::validation(
3543 "Cannot use protect_tool() with a custom tool_authorizer. \
3544 Either use protect_tool() to configure scope-based authorization, \
3545 or provide a custom ToolAuthorizer implementation, but not both.",
3546 ));
3547 }
3548 // Create a ScopeBasedAuthorizer with all the tool protections
3549 let mut authorizer = auth::ScopeBasedAuthorizer::new();
3550 for (tool_name, scopes) in self.tool_protections {
3551 authorizer = authorizer.require_scopes(tool_name, scopes);
3552 }
3553 Some(Arc::new(authorizer) as Arc<dyn auth::ToolAuthorizer>)
3554 } else {
3555 self.tool_authorizer
3556 };
3557
3558 // Initialize tool middleware chain
3559 #[cfg(not(target_arch = "wasm32"))]
3560 let tool_middleware_chain = {
3561 let mut chain = tool_middleware::ToolMiddlewareChain::new();
3562 for middleware in self.tool_middlewares {
3563 chain.add(middleware);
3564 }
3565 Arc::new(RwLock::new(chain))
3566 };
3567
3568 // Build tool_infos cache at construction time (mirrors ServerCore pattern)
3569 let tool_infos: HashMap<String, ToolInfo> = self
3570 .tools
3571 .iter()
3572 .map(|(name, handler)| {
3573 let info = handler.metadata().unwrap_or_else(|| {
3574 ToolInfo::new(
3575 name.clone(),
3576 None,
3577 serde_json::json!({"type": "object", "properties": {}}),
3578 )
3579 });
3580 (name.clone(), info)
3581 })
3582 .collect();
3583
3584 // Apply host layer enrichment to tool _meta (e.g., ChatGPT openai/* keys)
3585 #[cfg(feature = "mcp-apps")]
3586 let tool_infos = {
3587 let mut infos = tool_infos;
3588 for host in &self.host_layers {
3589 for info in infos.values_mut() {
3590 if let Some(meta) = info._meta.as_mut() {
3591 core::enrich_meta_for_host(meta, *host);
3592 }
3593 }
3594 }
3595 infos
3596 };
3597
3598 // Build URI-to-tool-meta index for widget resource _meta propagation
3599 let uri_to_tool_meta = core::build_uri_to_tool_meta(&tool_infos);
3600
3601 // Finalize accumulated skills exactly once and compose with the
3602 // user's `.resources(...)` slot if both are set. `.resources(...)`
3603 // itself stays "last write wins" — composition lives here so the
3604 // setter's semantics are unchanged for callers that don't use
3605 // skills.
3606 #[cfg(feature = "skills")]
3607 let final_resources: Option<Arc<dyn ResourceHandler>> =
3608 builder::finalize_skills_resources(self.pending_skills, self.resources);
3609 #[cfg(not(feature = "skills"))]
3610 let final_resources = self.resources;
3611
3612 Ok(Server {
3613 info: {
3614 let mut info = Implementation::new(&name, &version);
3615 if let Some(url) = self.website_url {
3616 info = info.with_website_url(url);
3617 }
3618 if let Some(icons) = self.icons {
3619 info = info.with_icons(icons);
3620 }
3621 info
3622 },
3623 capabilities: self.capabilities,
3624 tools: self.tools,
3625 tool_infos,
3626 uri_to_tool_meta,
3627 prompts: self.prompts,
3628 resources: final_resources,
3629 sampling: self.sampling,
3630 client_capabilities: Arc::new(RwLock::new(None)),
3631 initialized: Arc::new(RwLock::new(false)),
3632 notification_tx: None,
3633 cancellation_manager: self.cancellation_manager,
3634 roots_manager: Arc::new(RwLock::new(self.roots_manager)),
3635 subscription_manager: Arc::new(RwLock::new(subscriptions::SubscriptionManager::new())),
3636 elicitation_manager: None,
3637 server_request_dispatcher: None,
3638 peer_handle: None,
3639 auth_provider: self.auth_provider,
3640 tool_authorizer,
3641 #[cfg(not(target_arch = "wasm32"))]
3642 tool_middleware_chain,
3643 #[cfg(feature = "streamable-http")]
3644 http_middleware: self.http_middleware,
3645 })
3646 }
3647}
3648
3649#[cfg(not(target_arch = "wasm32"))]
3650impl Default for ServerBuilder {
3651 fn default() -> Self {
3652 Self::new()
3653 }
3654}
3655
3656#[cfg(test)]
3657mod tests {
3658 use super::*;
3659 use crate::shared::Transport;
3660 use crate::types::{
3661 jsonrpc::ResponsePayload, ClientCapabilities, InitializeRequest, ServerCapabilities,
3662 TransportMessage,
3663 };
3664 use async_trait::async_trait;
3665 use serde_json::json;
3666 use std::sync::{Arc, Mutex};
3667 use tokio::time::timeout;
3668
3669 /// Mock transport for testing
3670 #[derive(Debug)]
3671 struct MockTransport {
3672 messages: Arc<Mutex<Vec<TransportMessage>>>,
3673 responses: Arc<Mutex<Vec<TransportMessage>>>,
3674 }
3675
3676 impl MockTransport {
3677 #[allow(dead_code)]
3678 fn new() -> Self {
3679 Self {
3680 messages: Arc::new(Mutex::new(Vec::new())),
3681 responses: Arc::new(Mutex::new(Vec::new())),
3682 }
3683 }
3684
3685 fn with_requests(requests: Vec<TransportMessage>) -> Self {
3686 Self {
3687 messages: Arc::new(Mutex::new(requests)),
3688 responses: Arc::new(Mutex::new(Vec::new())),
3689 }
3690 }
3691
3692 #[allow(dead_code)]
3693 fn add_request(&self, request: TransportMessage) {
3694 self.messages.lock().unwrap().push(request);
3695 }
3696
3697 #[allow(dead_code)]
3698 fn get_sent_responses(&self) -> Vec<TransportMessage> {
3699 self.responses.lock().unwrap().clone()
3700 }
3701 }
3702
3703 #[async_trait]
3704 impl Transport for MockTransport {
3705 async fn send(&mut self, message: TransportMessage) -> Result<()> {
3706 self.responses.lock().unwrap().push(message);
3707 Ok(())
3708 }
3709
3710 async fn receive(&mut self) -> Result<TransportMessage> {
3711 let mut messages = self.messages.lock().unwrap();
3712 messages
3713 .pop()
3714 .map_or_else(|| Err(Error::protocol_msg("No more messages")), Ok)
3715 }
3716
3717 async fn close(&mut self) -> Result<()> {
3718 Ok(())
3719 }
3720
3721 fn is_connected(&self) -> bool {
3722 !self.messages.lock().unwrap().is_empty()
3723 }
3724
3725 fn transport_type(&self) -> &'static str {
3726 "mock"
3727 }
3728 }
3729
3730 /// Mock tool handler for testing
3731 struct MockTool {
3732 result: Value,
3733 }
3734
3735 impl MockTool {
3736 fn new(result: Value) -> Self {
3737 Self { result }
3738 }
3739 }
3740
3741 #[async_trait]
3742 impl ToolHandler for MockTool {
3743 async fn handle(
3744 &self,
3745 _args: Value,
3746 _extra: crate::server::cancellation::RequestHandlerExtra,
3747 ) -> Result<Value> {
3748 Ok(self.result.clone())
3749 }
3750 }
3751
3752 /// Mock prompt handler for testing
3753 struct MockPrompt {
3754 result: crate::types::GetPromptResult,
3755 }
3756
3757 impl MockPrompt {
3758 fn new(result: crate::types::GetPromptResult) -> Self {
3759 Self { result }
3760 }
3761 }
3762
3763 #[async_trait]
3764 impl PromptHandler for MockPrompt {
3765 async fn handle(
3766 &self,
3767 _args: HashMap<String, String>,
3768 _extra: crate::server::cancellation::RequestHandlerExtra,
3769 ) -> Result<crate::types::GetPromptResult> {
3770 Ok(self.result.clone())
3771 }
3772 }
3773
3774 /// Mock resource handler for testing
3775 struct MockResource {
3776 resources: Vec<crate::types::ResourceInfo>,
3777 contents: HashMap<String, crate::types::ReadResourceResult>,
3778 }
3779
3780 impl MockResource {
3781 fn new() -> Self {
3782 Self {
3783 resources: Vec::new(),
3784 contents: HashMap::new(),
3785 }
3786 }
3787
3788 fn with_resource(mut self, uri: String, content: crate::types::ReadResourceResult) -> Self {
3789 self.contents.insert(uri, content);
3790 self
3791 }
3792 }
3793
3794 #[async_trait]
3795 impl ResourceHandler for MockResource {
3796 async fn read(
3797 &self,
3798 uri: &str,
3799 _extra: crate::server::cancellation::RequestHandlerExtra,
3800 ) -> Result<crate::types::ReadResourceResult> {
3801 self.contents
3802 .get(uri)
3803 .cloned()
3804 .ok_or_else(|| Error::not_found(format!("Resource '{}' not found", uri)))
3805 }
3806
3807 async fn list(
3808 &self,
3809 _cursor: Option<String>,
3810 _extra: crate::server::cancellation::RequestHandlerExtra,
3811 ) -> Result<crate::types::ListResourcesResult> {
3812 Ok(crate::types::ListResourcesResult {
3813 resources: self.resources.clone(),
3814 next_cursor: None,
3815 })
3816 }
3817 }
3818
3819 #[test]
3820 fn test_server_builder() {
3821 let server = Server::builder()
3822 .name("test-server")
3823 .version("1.0.0")
3824 .capabilities(ServerCapabilities::tools_only())
3825 .tool("test-tool", MockTool::new(json!({"result": "success"})))
3826 .build()
3827 .unwrap();
3828
3829 assert_eq!(server.info.name, "test-server");
3830 assert_eq!(server.info.version, "1.0.0");
3831 assert!(server.tools.contains_key("test-tool"));
3832 }
3833
3834 #[test]
3835 fn test_server_builder_validation() {
3836 // Missing name
3837 let result = Server::builder().version("1.0.0").build();
3838 assert!(result.is_err());
3839
3840 // Missing version
3841 let result = Server::builder().name("test-server").build();
3842 assert!(result.is_err());
3843 }
3844
3845 #[tokio::test]
3846 async fn test_server_initialization() {
3847 let init_request = TransportMessage::Request {
3848 id: RequestId::from(1i64),
3849 request: Request::Client(Box::new(ClientRequest::Initialize(InitializeRequest {
3850 protocol_version: "2024-11-05".to_string(),
3851 capabilities: ClientCapabilities::minimal(),
3852 client_info: Implementation::new("test-client", "1.0.0"),
3853 }))),
3854 };
3855
3856 let transport = MockTransport::with_requests(vec![init_request]);
3857 let server = Server::builder()
3858 .name("test-server")
3859 .version("1.0.0")
3860 .capabilities(ServerCapabilities::tools_only())
3861 .build()
3862 .unwrap();
3863
3864 // Test server run for a short time
3865 let server_handle = tokio::spawn(async move {
3866 let _ = timeout(std::time::Duration::from_millis(100), server.run(transport)).await;
3867 });
3868
3869 // Wait for server to process
3870 let _ = timeout(std::time::Duration::from_millis(200), server_handle).await;
3871 }
3872
3873 #[tokio::test]
3874 async fn test_server_capabilities() {
3875 let server = Server::builder()
3876 .name("test-server")
3877 .version("1.0.0")
3878 .capabilities(ServerCapabilities::tools_only())
3879 .build()
3880 .unwrap();
3881
3882 assert!(!server.is_initialized().await);
3883 assert!(server.get_client_capabilities().await.is_none());
3884 }
3885
3886 #[tokio::test]
3887 async fn test_server_notifications() {
3888 let server = Server::builder()
3889 .name("test-server")
3890 .version("1.0.0")
3891 .build()
3892 .unwrap();
3893
3894 // Send notification (should not panic even without transport)
3895 server
3896 .send_notification(ServerNotification::ToolsChanged)
3897 .await;
3898 }
3899
3900 #[test]
3901 fn test_server_builder_with_all_handlers() {
3902 let prompt_result = crate::types::GetPromptResult {
3903 description: Some("Test prompt".to_string()),
3904 messages: vec![],
3905 _meta: None,
3906 };
3907
3908 let resource_content =
3909 crate::types::ReadResourceResult::new(vec![crate::types::Content::text(
3910 "Hello, world!",
3911 )]);
3912
3913 let server = Server::builder()
3914 .name("test-server")
3915 .version("1.0.0")
3916 .tool("test-tool", MockTool::new(json!({"result": "success"})))
3917 .prompt("test-prompt", MockPrompt::new(prompt_result))
3918 .resources(
3919 MockResource::new().with_resource("test://uri".to_string(), resource_content),
3920 )
3921 .build()
3922 .unwrap();
3923
3924 assert!(server.tools.contains_key("test-tool"));
3925 assert!(server.prompts.contains_key("test-prompt"));
3926 assert!(server.resources.is_some());
3927 }
3928
3929 #[tokio::test]
3930 async fn test_handle_request_initialize() {
3931 let server = Server::builder()
3932 .name("test-server")
3933 .version("1.0.0")
3934 .capabilities(ServerCapabilities::tools_only())
3935 .build()
3936 .unwrap();
3937
3938 let request = Request::Client(Box::new(ClientRequest::Initialize(InitializeRequest {
3939 protocol_version: "2024-11-05".to_string(),
3940 capabilities: ClientCapabilities::default(),
3941 client_info: Implementation::new("test-client", "1.0.0"),
3942 })));
3943
3944 let response = server
3945 .handle_request(RequestId::from(1i64), request, None)
3946 .await;
3947
3948 assert_eq!(response.id, RequestId::from(1i64));
3949 match response.payload {
3950 ResponsePayload::Result(_) => {
3951 assert!(server.is_initialized().await);
3952 },
3953 ResponsePayload::Error(_) => panic!("Expected success response"),
3954 }
3955 }
3956
3957 #[tokio::test]
3958 async fn test_handle_list_tools() {
3959 let server = Server::builder()
3960 .name("test-server")
3961 .version("1.0.0")
3962 .tool("test-tool", MockTool::new(json!({"result": "success"})))
3963 .build()
3964 .unwrap();
3965
3966 let request = Request::Client(Box::new(ClientRequest::ListTools(ListToolsRequest {
3967 cursor: None,
3968 })));
3969 let response = server
3970 .handle_request(RequestId::from(1i64), request, None)
3971 .await;
3972
3973 match response.payload {
3974 ResponsePayload::Result(result) => {
3975 let tools_result: ListToolsResult = serde_json::from_value(result).unwrap();
3976 assert_eq!(tools_result.tools.len(), 1);
3977 assert_eq!(tools_result.tools[0].name, "test-tool");
3978 },
3979 ResponsePayload::Error(_) => panic!("Expected success response"),
3980 }
3981 }
3982
3983 #[tokio::test]
3984 async fn test_handle_call_tool() {
3985 let server = Server::builder()
3986 .name("test-server")
3987 .version("1.0.0")
3988 .tool("test-tool", MockTool::new(json!({"result": "success"})))
3989 .build()
3990 .unwrap();
3991
3992 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
3993 name: "test-tool".to_string(),
3994 arguments: json!({"input": "test"}),
3995 _meta: None,
3996 task: None,
3997 })));
3998
3999 let response = server
4000 .handle_request(RequestId::from(1i64), request, None)
4001 .await;
4002
4003 match response.payload {
4004 ResponsePayload::Result(result) => {
4005 let call_result: CallToolResult = serde_json::from_value(result).unwrap();
4006 assert!(!call_result.is_error);
4007 assert_eq!(call_result.content.len(), 1);
4008 },
4009 ResponsePayload::Error(_) => panic!("Expected success response"),
4010 }
4011 }
4012
4013 #[tokio::test]
4014 async fn test_handle_call_tool_rejected_is_iserror_not_protocol_error() {
4015 // A handler returning `Error::tool_rejected` must surface through the
4016 // streamable-HTTP `Server` path as a SUCCESSFUL `CallToolResult`
4017 // with `isError: true` (message → content, details → structuredContent),
4018 // NOT a JSON-RPC protocol error. This is the Code Mode policy-rejection
4019 // envelope (e.g. "SELECT missing LIMIT") observed by `pmcp-sql-server`.
4020 struct RejectingTool;
4021 #[async_trait]
4022 impl ToolHandler for RejectingTool {
4023 async fn handle(
4024 &self,
4025 _args: Value,
4026 _extra: crate::server::cancellation::RequestHandlerExtra,
4027 ) -> Result<Value> {
4028 Err(Error::tool_rejected(
4029 "SELECT statements must declare a LIMIT",
4030 Some(json!({ "violations": [{ "rule": "missing_limit" }] })),
4031 ))
4032 }
4033 }
4034
4035 let server = Server::builder()
4036 .name("test-server")
4037 .version("1.0.0")
4038 .tool("reject", RejectingTool)
4039 .build()
4040 .unwrap();
4041
4042 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
4043 name: "reject".to_string(),
4044 arguments: json!({}),
4045 _meta: None,
4046 task: None,
4047 })));
4048
4049 let response = server
4050 .handle_request(RequestId::from(1i64), request, None)
4051 .await;
4052
4053 match response.payload {
4054 ResponsePayload::Result(result) => {
4055 let call_result: CallToolResult = serde_json::from_value(result).unwrap();
4056 assert!(call_result.is_error, "tool_rejected must set isError: true");
4057 let text = call_result
4058 .content
4059 .iter()
4060 .find_map(|c| match c {
4061 crate::types::Content::Text { text } => Some(text.clone()),
4062 _ => None,
4063 })
4064 .unwrap_or_default();
4065 assert!(
4066 text.contains("must declare a LIMIT"),
4067 "content must carry the rejection message, got: {text}"
4068 );
4069 let sc = call_result
4070 .structured_content
4071 .expect("structuredContent must carry the violation detail");
4072 assert_eq!(sc["violations"][0]["rule"], "missing_limit");
4073 },
4074 ResponsePayload::Error(e) => panic!(
4075 "tool_rejected must NOT be a protocol error, got {}: {}",
4076 e.code, e.message
4077 ),
4078 }
4079 }
4080
4081 #[tokio::test]
4082 async fn test_handle_call_tool_not_found() {
4083 let server = Server::builder()
4084 .name("test-server")
4085 .version("1.0.0")
4086 .build()
4087 .unwrap();
4088
4089 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
4090 name: "nonexistent-tool".to_string(),
4091 arguments: json!({}),
4092 _meta: None,
4093 task: None,
4094 })));
4095
4096 let response = server
4097 .handle_request(RequestId::from(1i64), request, None)
4098 .await;
4099
4100 match response.payload {
4101 ResponsePayload::Error(error) => {
4102 assert!(error.message.contains("not found"));
4103 },
4104 ResponsePayload::Result(_) => panic!("Expected error response"),
4105 }
4106 }
4107
4108 #[tokio::test]
4109 async fn test_handle_list_prompts() {
4110 let prompt_result = crate::types::GetPromptResult {
4111 description: Some("Test prompt".to_string()),
4112 messages: vec![],
4113 _meta: None,
4114 };
4115
4116 let server = Server::builder()
4117 .name("test-server")
4118 .version("1.0.0")
4119 .prompt("test-prompt", MockPrompt::new(prompt_result))
4120 .build()
4121 .unwrap();
4122
4123 let request = Request::Client(Box::new(ClientRequest::ListPrompts(ListPromptsRequest {
4124 cursor: None,
4125 })));
4126 let response = server
4127 .handle_request(RequestId::from(1i64), request, None)
4128 .await;
4129
4130 match response.payload {
4131 ResponsePayload::Result(result) => {
4132 let list_result: ListPromptsResult = serde_json::from_value(result).unwrap();
4133 assert_eq!(list_result.prompts.len(), 1);
4134 assert_eq!(list_result.prompts[0].name, "test-prompt");
4135 },
4136 ResponsePayload::Error(_) => panic!("Expected success response"),
4137 }
4138 }
4139
4140 #[tokio::test]
4141 async fn test_handle_get_prompt() {
4142 let prompt_result = crate::types::GetPromptResult {
4143 description: Some("Test prompt".to_string()),
4144 messages: vec![],
4145 _meta: None,
4146 };
4147
4148 let server = Server::builder()
4149 .name("test-server")
4150 .version("1.0.0")
4151 .prompt("test-prompt", MockPrompt::new(prompt_result.clone()))
4152 .build()
4153 .unwrap();
4154
4155 let request = Request::Client(Box::new(ClientRequest::GetPrompt(GetPromptRequest {
4156 name: "test-prompt".to_string(),
4157 arguments: HashMap::new(),
4158 _meta: None,
4159 })));
4160
4161 let response = server
4162 .handle_request(RequestId::from(1i64), request, None)
4163 .await;
4164
4165 match response.payload {
4166 ResponsePayload::Result(result) => {
4167 let get_result: crate::types::GetPromptResult =
4168 serde_json::from_value(result).unwrap();
4169 assert_eq!(get_result.description, prompt_result.description);
4170 },
4171 ResponsePayload::Error(_) => panic!("Expected success response"),
4172 }
4173 }
4174
4175 #[tokio::test]
4176 async fn test_handle_list_resources() {
4177 let resource_content =
4178 crate::types::ReadResourceResult::new(vec![crate::types::Content::text(
4179 "Hello, world!",
4180 )]);
4181
4182 let server = Server::builder()
4183 .name("test-server")
4184 .version("1.0.0")
4185 .resources(
4186 MockResource::new().with_resource("test://uri".to_string(), resource_content),
4187 )
4188 .build()
4189 .unwrap();
4190
4191 let request = Request::Client(Box::new(ClientRequest::ListResources(
4192 ListResourcesRequest { cursor: None },
4193 )));
4194 let response = server
4195 .handle_request(RequestId::from(1i64), request, None)
4196 .await;
4197
4198 match response.payload {
4199 ResponsePayload::Result(result) => {
4200 let resources_result: ListResourcesResult = serde_json::from_value(result).unwrap();
4201 assert_eq!(resources_result.resources.len(), 0); // MockResource has empty list by default
4202 },
4203 ResponsePayload::Error(_) => panic!("Expected success response"),
4204 }
4205 }
4206
4207 #[tokio::test]
4208 async fn test_handle_read_resource() {
4209 let resource_content =
4210 crate::types::ReadResourceResult::new(vec![crate::types::Content::text(
4211 "Hello, world!",
4212 )]);
4213
4214 let server = Server::builder()
4215 .name("test-server")
4216 .version("1.0.0")
4217 .resources(
4218 MockResource::new()
4219 .with_resource("test://uri".to_string(), resource_content.clone()),
4220 )
4221 .build()
4222 .unwrap();
4223
4224 let request = Request::Client(Box::new(ClientRequest::ReadResource(ReadResourceRequest {
4225 uri: "test://uri".to_string(),
4226 _meta: None,
4227 })));
4228
4229 let response = server
4230 .handle_request(RequestId::from(1i64), request, None)
4231 .await;
4232
4233 match response.payload {
4234 ResponsePayload::Result(result) => {
4235 let read_result: crate::types::ReadResourceResult =
4236 serde_json::from_value(result).unwrap();
4237 assert_eq!(read_result.contents.len(), 1);
4238 },
4239 ResponsePayload::Error(_) => panic!("Expected success response"),
4240 }
4241 }
4242
4243 #[tokio::test]
4244 async fn test_handle_read_resource_not_found() {
4245 let server = Server::builder()
4246 .name("test-server")
4247 .version("1.0.0")
4248 .resources(MockResource::new())
4249 .build()
4250 .unwrap();
4251
4252 let request = Request::Client(Box::new(ClientRequest::ReadResource(ReadResourceRequest {
4253 uri: "nonexistent://uri".to_string(),
4254 _meta: None,
4255 })));
4256
4257 let response = server
4258 .handle_request(RequestId::from(1i64), request, None)
4259 .await;
4260
4261 match response.payload {
4262 ResponsePayload::Error(error) => {
4263 assert!(error.message.contains("not found"));
4264 },
4265 ResponsePayload::Result(_) => panic!("Expected error response"),
4266 }
4267 }
4268
4269 #[tokio::test]
4270 async fn test_handle_ping() {
4271 let server = Server::builder()
4272 .name("test-server")
4273 .version("1.0.0")
4274 .build()
4275 .unwrap();
4276
4277 let request = Request::Client(Box::new(ClientRequest::Ping));
4278 let response = server
4279 .handle_request(RequestId::from(1i64), request, None)
4280 .await;
4281
4282 match response.payload {
4283 ResponsePayload::Result(_) => {
4284 // Success
4285 },
4286 ResponsePayload::Error(_) => panic!("Expected success response"),
4287 }
4288 }
4289
4290 #[tokio::test]
4291 async fn test_handle_server_request() {
4292 let server = Server::builder()
4293 .name("test-server")
4294 .version("1.0.0")
4295 .build()
4296 .unwrap();
4297
4298 let request = Request::Server(Box::new(crate::types::ServerRequest::CreateMessage(
4299 Box::new(crate::types::CreateMessageParams {
4300 messages: vec![],
4301 model_preferences: None,
4302 system_prompt: None,
4303 include_context: crate::types::IncludeContext::None,
4304 temperature: None,
4305 max_tokens: None,
4306 stop_sequences: None,
4307 metadata: None,
4308 tools: None,
4309 tool_choice: None,
4310 }),
4311 )));
4312 let response = server
4313 .handle_request(RequestId::from(1i64), request, None)
4314 .await;
4315
4316 match response.payload {
4317 ResponsePayload::Error(error) => {
4318 assert_eq!(error.code, -32601);
4319 assert!(error.message.contains("not supported"));
4320 },
4321 ResponsePayload::Result(_) => panic!("Expected error response"),
4322 }
4323 }
4324
4325 // Tests for tool middleware support in ServerBuilder
4326 #[tokio::test]
4327 async fn test_server_builder_with_tool_middleware() {
4328 use crate::server::tool_middleware::{ToolContext, ToolMiddleware};
4329 use std::sync::atomic::{AtomicBool, Ordering};
4330
4331 // Create a simple middleware that sets a flag when called
4332 struct TestMiddleware {
4333 called: Arc<AtomicBool>,
4334 }
4335
4336 #[async_trait]
4337 impl ToolMiddleware for TestMiddleware {
4338 async fn on_request(
4339 &self,
4340 _tool_name: &str,
4341 _args: &mut Value,
4342 extra: &mut crate::server::cancellation::RequestHandlerExtra,
4343 _context: &ToolContext,
4344 ) -> Result<()> {
4345 self.called.store(true, Ordering::SeqCst);
4346 extra.set_metadata("middleware_executed".to_string(), "true".to_string());
4347 Ok(())
4348 }
4349 }
4350
4351 let middleware_called = Arc::new(AtomicBool::new(false));
4352 let middleware = Arc::new(TestMiddleware {
4353 called: Arc::clone(&middleware_called),
4354 });
4355
4356 // Build server with middleware
4357 let server = Server::builder()
4358 .name("test-server")
4359 .version("1.0.0")
4360 .tool("test_tool", MockTool::new(json!({"result": "success"})))
4361 .tool_middleware(middleware)
4362 .build()
4363 .unwrap();
4364
4365 // Call the tool
4366 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
4367 name: "test_tool".to_string(),
4368 arguments: json!({}),
4369 _meta: None,
4370 task: None,
4371 })));
4372
4373 let response = server
4374 .handle_request(RequestId::from(1i64), request, None)
4375 .await;
4376
4377 // Verify middleware was called
4378 assert!(middleware_called.load(Ordering::SeqCst));
4379
4380 // Verify tool executed successfully
4381 match response.payload {
4382 ResponsePayload::Result(_) => {}, // Success
4383 ResponsePayload::Error(e) => panic!("Expected success, got error: {:?}", e),
4384 }
4385 }
4386
4387 #[tokio::test]
4388 async fn test_server_builder_multiple_middlewares() {
4389 use crate::server::tool_middleware::{ToolContext, ToolMiddleware};
4390 use std::sync::atomic::{AtomicUsize, Ordering};
4391
4392 // Middleware that increments a counter
4393 struct CounterMiddleware {
4394 counter: Arc<AtomicUsize>,
4395 id: usize,
4396 }
4397
4398 #[async_trait]
4399 impl ToolMiddleware for CounterMiddleware {
4400 async fn on_request(
4401 &self,
4402 _tool_name: &str,
4403 _args: &mut Value,
4404 extra: &mut crate::server::cancellation::RequestHandlerExtra,
4405 _context: &ToolContext,
4406 ) -> Result<()> {
4407 let count = self.counter.fetch_add(1, Ordering::SeqCst);
4408 extra.set_metadata(format!("middleware_{}_order", self.id), count.to_string());
4409 Ok(())
4410 }
4411 }
4412
4413 let counter = Arc::new(AtomicUsize::new(0));
4414 let middleware1 = Arc::new(CounterMiddleware {
4415 counter: Arc::clone(&counter),
4416 id: 1,
4417 });
4418 let middleware2 = Arc::new(CounterMiddleware {
4419 counter: Arc::clone(&counter),
4420 id: 2,
4421 });
4422
4423 // Build server with multiple middlewares
4424 let server = Server::builder()
4425 .name("test-server")
4426 .version("1.0.0")
4427 .tool("test_tool", MockTool::new(json!({"result": "success"})))
4428 .tool_middleware(middleware1)
4429 .tool_middleware(middleware2)
4430 .build()
4431 .unwrap();
4432
4433 // Call the tool
4434 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
4435 name: "test_tool".to_string(),
4436 arguments: json!({}),
4437 _meta: None,
4438 task: None,
4439 })));
4440
4441 let _response = server
4442 .handle_request(RequestId::from(1i64), request, None)
4443 .await;
4444
4445 // Verify both middlewares were called in order
4446 assert_eq!(counter.load(Ordering::SeqCst), 2);
4447 }
4448
4449 #[tokio::test]
4450 async fn test_server_builder_middleware_with_typed_tools() {
4451 use crate::server::tool_middleware::{ToolContext, ToolMiddleware};
4452 use std::sync::atomic::{AtomicBool, Ordering};
4453
4454 // Middleware that injects OAuth token
4455 struct OAuthMiddleware {
4456 called: Arc<AtomicBool>,
4457 }
4458
4459 #[async_trait]
4460 impl ToolMiddleware for OAuthMiddleware {
4461 async fn on_request(
4462 &self,
4463 _tool_name: &str,
4464 _args: &mut Value,
4465 extra: &mut crate::server::cancellation::RequestHandlerExtra,
4466 _context: &ToolContext,
4467 ) -> Result<()> {
4468 self.called.store(true, Ordering::SeqCst);
4469 extra.set_metadata("oauth_token".to_string(), "test-token-123".to_string());
4470 Ok(())
4471 }
4472 }
4473
4474 // Tool that verifies OAuth token was injected
4475 struct OAuthVerifyTool;
4476
4477 #[async_trait]
4478 impl ToolHandler for OAuthVerifyTool {
4479 async fn handle(
4480 &self,
4481 _args: Value,
4482 extra: crate::server::cancellation::RequestHandlerExtra,
4483 ) -> Result<Value> {
4484 // Verify OAuth token was injected by middleware
4485 let token = extra.get_metadata("oauth_token");
4486 assert!(token.is_some());
4487 assert_eq!(token.unwrap(), "test-token-123");
4488 Ok(json!({"success": true}))
4489 }
4490 }
4491
4492 let middleware_called = Arc::new(AtomicBool::new(false));
4493 let middleware = Arc::new(OAuthMiddleware {
4494 called: Arc::clone(&middleware_called),
4495 });
4496
4497 let server = Server::builder()
4498 .name("test-server")
4499 .version("1.0.0")
4500 .tool("typed_tool", OAuthVerifyTool)
4501 .tool_middleware(middleware)
4502 .build()
4503 .unwrap();
4504
4505 // Call the typed tool
4506 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
4507 name: "typed_tool".to_string(),
4508 arguments: json!({}),
4509 _meta: None,
4510 task: None,
4511 })));
4512
4513 let response = server
4514 .handle_request(RequestId::from(1i64), request, None)
4515 .await;
4516
4517 // Verify middleware was called
4518 assert!(middleware_called.load(Ordering::SeqCst));
4519
4520 // Verify tool executed successfully
4521 match response.payload {
4522 ResponsePayload::Result(_) => {}, // Success
4523 ResponsePayload::Error(e) => panic!("Expected success, got error: {:?}", e),
4524 }
4525 }
4526
4527 #[tokio::test]
4528 async fn test_server_builder_middleware_error_handling() {
4529 use crate::server::tool_middleware::{ToolContext, ToolMiddleware};
4530
4531 // Middleware that rejects requests
4532 struct RejectMiddleware;
4533
4534 #[async_trait]
4535 impl ToolMiddleware for RejectMiddleware {
4536 async fn on_request(
4537 &self,
4538 _tool_name: &str,
4539 _args: &mut Value,
4540 _extra: &mut crate::server::cancellation::RequestHandlerExtra,
4541 _context: &ToolContext,
4542 ) -> Result<()> {
4543 Err(Error::validation("Middleware rejected request"))
4544 }
4545 }
4546
4547 // Build server with rejecting middleware
4548 let server = Server::builder()
4549 .name("test-server")
4550 .version("1.0.0")
4551 .tool("test_tool", MockTool::new(json!({"result": "success"})))
4552 .tool_middleware(Arc::new(RejectMiddleware))
4553 .build()
4554 .unwrap();
4555
4556 // Call the tool
4557 let request = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
4558 name: "test_tool".to_string(),
4559 arguments: json!({}),
4560 _meta: None,
4561 task: None,
4562 })));
4563
4564 let response = server
4565 .handle_request(RequestId::from(1i64), request, None)
4566 .await;
4567
4568 // Verify request was rejected by middleware
4569 match response.payload {
4570 ResponsePayload::Error(e) => {
4571 assert!(e.message.contains("Middleware rejected request"));
4572 },
4573 ResponsePayload::Result(_) => panic!("Expected error from middleware"),
4574 }
4575 }
4576
4577 #[tokio::test]
4578 async fn test_server_builder_auto_capabilities_serialization() {
4579 // Test that ServerBuilder (used by Server::builder()) auto-sets capabilities
4580 // with proper serialization values
4581 let server = Server::builder()
4582 .name("test")
4583 .version("1.0.0")
4584 .tool("test-tool", MockTool::new(json!({"result": "ok"})))
4585 .prompt(
4586 "test-prompt",
4587 MockPrompt::new(crate::types::GetPromptResult {
4588 description: None,
4589 messages: vec![],
4590 _meta: None,
4591 }),
4592 )
4593 .resources(MockResource::new())
4594 .build()
4595 .unwrap();
4596
4597 let caps = &server.capabilities;
4598 let json = serde_json::to_value(caps).unwrap();
4599
4600 // Verify tools capability is present and properly structured
4601 let tools = json.get("tools").expect("tools should be present in JSON");
4602 assert!(tools.is_object(), "tools should be an object");
4603 let list_changed = tools.get("listChanged");
4604 assert!(
4605 list_changed.is_some(),
4606 "listChanged should be present in tools"
4607 );
4608 assert_eq!(
4609 list_changed.unwrap(),
4610 &serde_json::json!(false),
4611 "listChanged should be false"
4612 );
4613
4614 // Verify prompts capability
4615 let prompts = json
4616 .get("prompts")
4617 .expect("prompts should be present in JSON");
4618 assert!(prompts.is_object(), "prompts should be an object");
4619 assert!(
4620 prompts.get("listChanged").is_some(),
4621 "listChanged should be present in prompts"
4622 );
4623
4624 // Verify resources capability
4625 let resources = json
4626 .get("resources")
4627 .expect("resources should be present in JSON");
4628 assert!(resources.is_object(), "resources should be an object");
4629 assert!(
4630 resources.get("listChanged").is_some() || resources.get("subscribe").is_some(),
4631 "resources should have fields"
4632 );
4633
4634 println!(
4635 "Serialized capabilities: {}",
4636 serde_json::to_string_pretty(&json).unwrap()
4637 );
4638 }
4639
4640 /// Behavioral test for `ServerBuilder::tool_authorizer_arc` — the only
4641 /// non-mechanical lift among Phase 82's six `_arc` lifts.
4642 ///
4643 /// `tool_authorizer_arc` mirrors `tool_authorizer()`'s protection-clearing
4644 /// semantics: chaining `.protect_tool(...)` BEFORE `.tool_authorizer_arc(...)`
4645 /// must clear `tool_protections` so that `.build()` does NOT hit the
4646 /// mixed-config rejection branch at mod.rs `build()` (which fires if
4647 /// `tool_protections` is non-empty AND `tool_authorizer` is set).
4648 /// This test fills the verification gap source-greps cannot — proving
4649 /// the `.clear()` call actually fires.
4650 #[tokio::test]
4651 async fn tool_authorizer_arc_clears_tool_protections_and_allows_build() {
4652 // Define a no-op custom ToolAuthorizer for the test.
4653 struct NoopAuthorizer;
4654 #[async_trait]
4655 impl crate::server::auth::ToolAuthorizer for NoopAuthorizer {
4656 async fn can_access_tool(
4657 &self,
4658 _auth: &crate::server::auth::AuthContext,
4659 _tool_name: &str,
4660 ) -> crate::Result<bool> {
4661 Ok(true)
4662 }
4663 async fn required_scopes_for_tool(
4664 &self,
4665 _tool_name: &str,
4666 ) -> crate::Result<Vec<String>> {
4667 Ok(vec![])
4668 }
4669 }
4670
4671 // Build a server with BOTH protect_tool AND tool_authorizer_arc.
4672 // Without the clearing semantic, build() would return the
4673 // "Cannot use protect_tool() with a custom tool_authorizer" error.
4674 let builder = ServerBuilder::new()
4675 .name("test")
4676 .version("1")
4677 .protect_tool("delete", vec!["admin".to_string()])
4678 .tool_authorizer_arc(Arc::new(NoopAuthorizer));
4679
4680 // ASSERT 1: tool_protections was cleared by tool_authorizer_arc(),
4681 // visible because we are inside the same module and have access
4682 // to the private field.
4683 assert!(
4684 builder.tool_protections.is_empty(),
4685 "tool_authorizer_arc() must clear tool_protections to mirror tool_authorizer()"
4686 );
4687
4688 // ASSERT 2: build() succeeds — the mixed-config rejection branch
4689 // does NOT fire because protections was cleared.
4690 let build_result = builder.build();
4691 assert!(
4692 build_result.is_ok(),
4693 "build() should succeed after tool_authorizer_arc() clears protections; got Err({:?})",
4694 build_result.err()
4695 );
4696 }
4697}
4698
4699#[cfg(test)]
4700#[cfg(all(feature = "skills", not(target_arch = "wasm32")))]
4701mod skills_builder_tests {
4702 use super::*;
4703 use crate::server::cancellation::RequestHandlerExtra;
4704 use crate::server::skills::{Skill, SkillReference, Skills};
4705 use crate::types::Content;
4706 use async_trait::async_trait;
4707
4708 // ── Test 2.1a: single skill via ServerBuilder (public path) ──────
4709 #[test]
4710 fn test_2_1a_skill_method_single_skill_via_server_builder() {
4711 let server = Server::builder()
4712 .name("test")
4713 .version("1.0")
4714 .skill(Skill::new("foo", "body"))
4715 .build()
4716 .unwrap();
4717 assert!(server.capabilities.resources.is_some());
4718 }
4719
4720 // ── Test 2.2 (ServerBuilder): extensions capability ──────────────
4721 #[test]
4722 fn test_2_2_server_builder_skills_sets_extensions_capability() {
4723 let server = Server::builder()
4724 .name("test")
4725 .version("1.0")
4726 .skills(Skills::new().add(Skill::new("a", "")))
4727 .build()
4728 .unwrap();
4729 let ext = server
4730 .capabilities
4731 .extensions
4732 .as_ref()
4733 .expect("extensions should be set");
4734 assert_eq!(
4735 ext.get("io.modelcontextprotocol/skills"),
4736 Some(&serde_json::json!({}))
4737 );
4738 }
4739
4740 // ── Test 2.3 (ServerBuilder): resources capability ───────────────
4741 #[test]
4742 fn test_2_3_server_builder_skills_sets_resources_capability() {
4743 let server = Server::builder()
4744 .name("test")
4745 .version("1.0")
4746 .skills(Skills::new().add(Skill::new("a", "")))
4747 .build()
4748 .unwrap();
4749 let r = server
4750 .capabilities
4751 .resources
4752 .as_ref()
4753 .expect("resources should be set");
4754 assert_eq!(r.subscribe, Some(false));
4755 assert_eq!(r.list_changed, Some(false));
4756 }
4757
4758 // ── Test 2.4a: skills compose with existing resources (ServerBuilder) ─
4759 struct DocsHandler;
4760 #[async_trait]
4761 impl ResourceHandler for DocsHandler {
4762 async fn read(
4763 &self,
4764 uri: &str,
4765 _extra: RequestHandlerExtra,
4766 ) -> Result<crate::types::ReadResourceResult> {
4767 Ok(crate::types::ReadResourceResult::new(vec![Content::text(
4768 format!("DOCS:{uri}"),
4769 )]))
4770 }
4771 async fn list(
4772 &self,
4773 _cursor: Option<String>,
4774 _extra: RequestHandlerExtra,
4775 ) -> Result<crate::types::ListResourcesResult> {
4776 Ok(crate::types::ListResourcesResult::new(vec![
4777 crate::types::ResourceInfo::new("docs://handbook", "handbook"),
4778 ]))
4779 }
4780 }
4781
4782 #[test]
4783 fn test_2_4a_server_builder_skills_compose_with_existing_resources() {
4784 let server = Server::builder()
4785 .name("t")
4786 .version("1.0")
4787 .resources(DocsHandler)
4788 .skill(Skill::new("a", "skill-a"))
4789 .build()
4790 .unwrap();
4791 // Capability state must reflect both surfaces.
4792 assert!(server.capabilities.resources.is_some());
4793 let ext = server.capabilities.extensions.as_ref().unwrap();
4794 assert!(ext.contains_key("io.modelcontextprotocol/skills"));
4795 }
4796
4797 // ── Test 2.7 (ServerBuilder): bootstrap_skill_and_prompt ──────────
4798 #[test]
4799 fn test_2_7_server_builder_bootstrap_skill_and_prompt() {
4800 let server = Server::builder()
4801 .name("t")
4802 .version("1.0")
4803 .bootstrap_skill_and_prompt(Skill::new("c", "body-c"), "my_prompt")
4804 .build()
4805 .unwrap();
4806 assert!(server.has_prompt("my_prompt"));
4807 assert!(server.capabilities.prompts.is_some());
4808 let ext = server
4809 .capabilities
4810 .extensions
4811 .as_ref()
4812 .expect("extensions should be set");
4813 assert!(ext.contains_key("io.modelcontextprotocol/skills"));
4814 assert!(server.capabilities.resources.is_some());
4815 }
4816
4817 // ── Test 2.8: wire-level dual-surface invariant via ServerBuilder ─
4818 #[tokio::test]
4819 async fn test_2_8_bootstrap_skill_and_prompt_byte_equal_invariant() {
4820 let skill = Skill::new("x", "A").with_reference(SkillReference::new(
4821 "ref1.md",
4822 "text/markdown",
4823 "refbody",
4824 ));
4825 let expected_text = skill.as_prompt_text();
4826
4827 let server = Server::builder()
4828 .name("t")
4829 .version("1.0")
4830 .bootstrap_skill_and_prompt(skill, "x")
4831 .build()
4832 .unwrap();
4833
4834 let prompt = server
4835 .get_prompt("x")
4836 .expect("prompt 'x' must be registered");
4837 let result = prompt
4838 .handle(HashMap::new(), RequestHandlerExtra::default())
4839 .await
4840 .unwrap();
4841 assert_eq!(result.messages.len(), 1);
4842 match &result.messages[0].content {
4843 Content::Text { text } => assert_eq!(text, &expected_text),
4844 other => panic!("expected Content::Text, got {other:?}"),
4845 }
4846 }
4847
4848 // ── Test 2.9 (ServerBuilder): duplicate URI panics at .build() ───
4849 #[test]
4850 #[should_panic(expected = "duplicate")]
4851 fn test_2_9_server_builder_skills_panics_on_duplicate_uri_at_build() {
4852 let _ = Server::builder()
4853 .name("t")
4854 .version("1.0")
4855 .skill(Skill::new("x", "a"))
4856 .skill(Skill::new("x", "b"))
4857 .build()
4858 .unwrap();
4859 }
4860
4861 // ── Test 2.9a (ServerBuilder): try_skills returns Err on duplicate ─
4862 #[test]
4863 fn test_2_9a_server_builder_try_skills_returns_err_on_duplicate() {
4864 let res = Server::builder().name("t").version("1.0").try_skills(
4865 Skills::new()
4866 .add(Skill::new("x", "a"))
4867 .add(Skill::new("x", "b")),
4868 );
4869 assert!(res.is_err());
4870 match res {
4871 Err(crate::Error::Validation(_)) => {},
4872 Err(other) => panic!("expected Validation, got {other:?}"),
4873 Ok(_) => panic!("expected Err for duplicate"),
4874 }
4875 }
4876
4877 // ── Test 2.10 (ServerBuilder): capability merge preserves extensions ─
4878 #[test]
4879 fn test_2_10_server_builder_capability_merge_preserves_pre_existing_extensions() {
4880 let mut caps = crate::types::ServerCapabilities::default();
4881 let mut ext = HashMap::new();
4882 ext.insert("some.other/ext".to_string(), serde_json::json!({"foo": 1}));
4883 caps.extensions = Some(ext);
4884
4885 let server = Server::builder()
4886 .name("t")
4887 .version("1.0")
4888 .capabilities(caps)
4889 .skill(Skill::new("a", ""))
4890 .build()
4891 .unwrap();
4892 let ext = server.capabilities.extensions.as_ref().unwrap();
4893 assert!(ext.contains_key("some.other/ext"));
4894 assert!(ext.contains_key("io.modelcontextprotocol/skills"));
4895 }
4896
4897 // ── Test 2.11 (ServerBuilder): accumulator — all skills reachable ─
4898 #[test]
4899 fn test_2_11_server_builder_accumulator_repeated_skill_calls_all_reachable() {
4900 // Build a server with three .skill calls and confirm prompts/caps wire up.
4901 let server = Server::builder()
4902 .name("t")
4903 .version("1.0")
4904 .skill(Skill::new("a", "body-a"))
4905 .skill(Skill::new("b", "body-b"))
4906 .bootstrap_skill_and_prompt(Skill::new("c", "body-c"), "c_prompt")
4907 .build()
4908 .unwrap();
4909 assert!(server.has_prompt("c_prompt"));
4910 assert!(server.capabilities.resources.is_some());
4911 }
4912
4913 // ── Test 2.5a (ServerBuilder): .resources() semantics unchanged ──
4914 #[test]
4915 fn test_2_5a_server_builder_resources_replace_unchanged_no_skills() {
4916 struct A;
4917 #[async_trait]
4918 impl ResourceHandler for A {
4919 async fn read(
4920 &self,
4921 _uri: &str,
4922 _extra: RequestHandlerExtra,
4923 ) -> Result<crate::types::ReadResourceResult> {
4924 Ok(crate::types::ReadResourceResult::new(vec![Content::text(
4925 "A",
4926 )]))
4927 }
4928 async fn list(
4929 &self,
4930 _cursor: Option<String>,
4931 _extra: RequestHandlerExtra,
4932 ) -> Result<crate::types::ListResourcesResult> {
4933 Ok(crate::types::ListResourcesResult::new(vec![]))
4934 }
4935 }
4936 struct B;
4937 #[async_trait]
4938 impl ResourceHandler for B {
4939 async fn read(
4940 &self,
4941 _uri: &str,
4942 _extra: RequestHandlerExtra,
4943 ) -> Result<crate::types::ReadResourceResult> {
4944 Ok(crate::types::ReadResourceResult::new(vec![Content::text(
4945 "B",
4946 )]))
4947 }
4948 async fn list(
4949 &self,
4950 _cursor: Option<String>,
4951 _extra: RequestHandlerExtra,
4952 ) -> Result<crate::types::ListResourcesResult> {
4953 Ok(crate::types::ListResourcesResult::new(vec![]))
4954 }
4955 }
4956
4957 let server = Server::builder()
4958 .name("t")
4959 .version("1.0")
4960 .resources(A)
4961 .resources(B)
4962 .build()
4963 .unwrap();
4964 // No skills registered → no composition. Capabilities reflect resources only.
4965 assert!(server.capabilities.resources.is_some());
4966 // Skills extension should NOT have been auto-set.
4967 let ext = server.capabilities.extensions.as_ref();
4968 if let Some(ext_map) = ext {
4969 assert!(!ext_map.contains_key("io.modelcontextprotocol/skills"));
4970 }
4971 }
4972}