pub struct ServerBuilder { /* private fields */ }Expand description
Builder for creating servers.
Implementations§
Source§impl ServerBuilder
impl ServerBuilder
Sourcepub fn new() -> Self
pub fn new() -> Self
Create a new server builder.
Creates a new ServerBuilder with default capabilities and no handlers.
Use the builder methods to configure the server before calling build().
§Examples
use pmcp::ServerBuilder;
let builder = ServerBuilder::new();This is equivalent to using the default implementation:
use pmcp::ServerBuilder;
let builder = ServerBuilder::default();Sourcepub fn with_request_state_key(self, key: [u8; 32]) -> Self
Available on crate feature streamable-http only.
pub fn with_request_state_key(self, key: [u8; 32]) -> Self
streamable-http only.Configure the shared requestState minting key (Phase 113, HTTP-02, D-03).
With no call, the key is resolved from PMCP_REQUEST_STATE_KEY; when that
variable is unset the server generates a per-process key and WARNs at build
time (D-04). Calling this overrides the environment entirely, which is what
makes deterministic integration tests and multiple differently-configured
servers in one process possible.
The key must be shared byte-for-byte by every instance behind a load balancer that should be able to resume each other’s multi-round-trip requests.
Has no effect on a server that did not opt into the v2 (2026-07-28) era.
The parameter type is deliberately still [u8; 32]: the SDK owns the
copy it takes, not the caller’s (D-113-P, T-113-121).
Sourcepub fn with_request_state_previous_keys(self, keys: Vec<[u8; 32]>) -> Self
Available on crate feature streamable-http only.
pub fn with_request_state_previous_keys(self, keys: Vec<[u8; 32]>) -> Self
streamable-http only.Accept rotated-out requestState keys for VERIFICATION only.
Tokens minted under a listed key still verify, but new tokens are always minted under the current key — so a rotation does not strand in-flight continuations. With no call, only the current key is accepted.
Has no effect on a server that did not opt into the v2 (2026-07-28) era.
Sourcepub fn with_request_state_ttl(self, ttl: Duration) -> Self
Available on crate feature streamable-http only.
pub fn with_request_state_ttl(self, ttl: Duration) -> Self
streamable-http only.Configure the requestState continuation lifetime (D-05).
With no call, the lifetime is PMCP_REQUEST_STATE_TTL_SECS if parseable,
else 300 seconds. A builder value beats both.
Has no effect on a server that did not opt into the v2 (2026-07-28) era.
Sourcepub fn with_supported_protocol_versions(
self,
versions: impl IntoIterator<Item = ProtocolVersion>,
) -> Self
pub fn with_supported_protocol_versions( self, versions: impl IntoIterator<Item = ProtocolVersion>, ) -> Self
Opt into a protocol-version accept-list (Phase 112, VERS-01/02; D-02/D-04).
The high-level Server twin of
ServerCoreBuilder::with_supported_protocol_versions.
With no call, the server is v1-only and behaves exactly as today. An empty
accept-list falls back to the v1-only default (never all-reject).
Sourcepub fn version(self, version: impl Into<String>) -> Self
pub fn version(self, version: impl Into<String>) -> Self
Set the server version.
The server version identifies this specific version of the MCP server. This is required and will be sent to clients during initialization.
§Arguments
version- The version string (e.g., “1.0.0”, “2.1.3-beta”)
§Examples
use pmcp::Server;
let server = Server::builder()
.name("data-processor")
.version("2.1.0")
.build()?;Sourcepub fn website_url(self, url: impl Into<String>) -> Self
pub fn website_url(self, url: impl Into<String>) -> Self
Set the website URL for the server implementation (MCP 2025-11-25).
Sourcepub fn with_icons(self, icons: Vec<IconInfo>) -> Self
pub fn with_icons(self, icons: Vec<IconInfo>) -> Self
Set icons for the server implementation (MCP 2025-11-25).
Sourcepub fn capabilities(self, capabilities: ServerCapabilities) -> Self
pub fn capabilities(self, capabilities: ServerCapabilities) -> Self
Set server capabilities.
Configures the capabilities that this server supports. Capabilities inform clients about which MCP features are available.
§Arguments
capabilities- The server capabilities to advertise
§Examples
use pmcp::{Server, ServerCapabilities, ToolCapabilities};
let mut capabilities = ServerCapabilities::default();
capabilities.tools = Some(ToolCapabilities {
list_changed: Some(true),
});
let server = Server::builder()
.name("advanced-server")
.version("1.0.0")
.capabilities(capabilities)
.build()?;Sourcepub fn tool(
self,
name: impl Into<String>,
handler: impl ToolHandler + 'static,
) -> Self
pub fn tool( self, name: impl Into<String>, handler: impl ToolHandler + 'static, ) -> Self
Add a tool handler.
Registers a tool that clients can call via the tools/call method. Tools are the primary way servers provide functionality to clients.
§Arguments
name- The name of the tool (used by clients to call it)handler- The handler implementation for this tool
§Examples
use pmcp::{Server, ToolHandler};
use async_trait::async_trait;
use serde_json::Value;
struct FileListTool;
#[async_trait]
impl ToolHandler for FileListTool {
async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
let path = args["path"].as_str().unwrap_or(".");
// List files in path...
Ok(serde_json::json!({"files": ["file1.txt", "file2.txt"]}))
}
}
let server = Server::builder()
.name("file-server")
.version("1.0.0")
.tool("list_files", FileListTool{})
.build()?;Sourcepub fn tool_arc(
self,
name: impl Into<String>,
handler: Arc<dyn ToolHandler>,
) -> Self
pub fn tool_arc( self, name: impl Into<String>, handler: Arc<dyn ToolHandler>, ) -> Self
Add a tool handler with an Arc.
This variant lets the caller share the handler Arc between the
builder and an external in-process handler map (e.g., a downstream
toolkit’s handler registry) without writing a delegating wrapper
shim. Behavior is otherwise identical to Self::tool: the first
registration auto-enables capabilities.tools.
Sourcepub fn mcp_server<T: McpServer>(self, server: T) -> Self
pub fn mcp_server<T: McpServer>(self, server: T) -> Self
Register all tools and prompts from an #[mcp_server] annotated type.
This is the ergonomic counterpart to individually registering tools and
prompts. The server instance provides shared state via &self to all
tool and prompt methods.
§Examples
use pmcp::ServerBuilder;
#[mcp_server]
impl MyServer {
#[mcp_tool(description = "Query data")]
async fn query(&self, args: QueryArgs) -> Result<Value> { /* ... */ }
#[mcp_prompt(description = "Generate query")]
async fn query_prompt(&self, args: PromptArgs) -> Result<GetPromptResult> { /* ... */ }
}
let server = MyServer { db };
let builder = ServerBuilder::new()
.name("my-server")
.mcp_server(server);Sourcepub fn tool_typed<T, F, Fut>(self, name: impl Into<String>, handler: F) -> Selfwhere
T: DeserializeOwned + JsonSchema + Send + Sync + 'static,
F: Fn(T, RequestHandlerExtra) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Value>> + Send + 'static,
Available on crate feature schema-generation only.
pub fn tool_typed<T, F, Fut>(self, name: impl Into<String>, handler: F) -> Selfwhere
T: DeserializeOwned + JsonSchema + Send + Sync + 'static,
F: Fn(T, RequestHandlerExtra) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Value>> + Send + 'static,
schema-generation only.Add a type-safe tool handler with automatic schema generation.
This method provides first-class support for creating tools with:
- Automatic JSON schema generation from Rust types
- Compile-time type safety
- Runtime validation
- Field descriptions from doc comments
§Example
use pmcp::ServerBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct EchoArgs {
/// The message to echo
message: String,
/// Optional prefix
prefix: Option<String>,
}
let server = ServerBuilder::new()
.name("example")
.tool_typed("echo", |args: EchoArgs, _| {
Box::pin(async move {
let message = match args.prefix {
Some(p) => format!("{}: {}", p, args.message),
None => args.message,
};
Ok(serde_json::json!({ "message": message }))
})
})
.build();Sourcepub fn tool_typed_with_description<T, F, Fut>(
self,
name: impl Into<String>,
description: impl Into<String>,
handler: F,
) -> Selfwhere
T: DeserializeOwned + JsonSchema + Send + Sync + 'static,
F: Fn(T, RequestHandlerExtra) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Value>> + Send + 'static,
Available on crate feature schema-generation only.
pub fn tool_typed_with_description<T, F, Fut>(
self,
name: impl Into<String>,
description: impl Into<String>,
handler: F,
) -> Selfwhere
T: DeserializeOwned + JsonSchema + Send + Sync + 'static,
F: Fn(T, RequestHandlerExtra) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Value>> + Send + 'static,
schema-generation only.Add a type-safe tool handler with automatic schema generation and description.
This is a convenience overload that allows setting a description directly
without needing to chain .with_description().
§Example
use pmcp::ServerBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct EchoArgs {
/// The message to echo
message: String,
/// Optional prefix
prefix: Option<String>,
}
let server = ServerBuilder::new()
.name("example")
.tool_typed_with_description(
"echo",
"Echoes back a message with an optional prefix",
|args: EchoArgs, _| {
Box::pin(async move {
let message = match args.prefix {
Some(p) => format!("{}: {}", p, args.message),
None => args.message,
};
Ok(serde_json::json!({ "message": message }))
})
}
);Sourcepub fn tool_typed_sync<T, F>(self, name: impl Into<String>, handler: F) -> Selfwhere
T: DeserializeOwned + JsonSchema + Send + Sync + 'static,
F: Fn(T, RequestHandlerExtra) -> Result<Value> + Send + Sync + 'static,
Available on crate feature schema-generation only.
pub fn tool_typed_sync<T, F>(self, name: impl Into<String>, handler: F) -> Selfwhere
T: DeserializeOwned + JsonSchema + Send + Sync + 'static,
F: Fn(T, RequestHandlerExtra) -> Result<Value> + Send + Sync + 'static,
schema-generation only.Add a synchronous type-safe tool handler with automatic schema generation.
Similar to tool_typed but for synchronous handlers.
§Example
use pmcp::ServerBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct MathArgs {
/// First number
a: f64,
/// Second number
b: f64,
/// Operation to perform
op: String,
}
let server = ServerBuilder::new()
.name("example")
.tool_typed_sync("calculator", |args: MathArgs, _| {
let result = match args.op.as_str() {
"add" => args.a + args.b,
"subtract" => args.a - args.b,
"multiply" => args.a * args.b,
"divide" => args.a / args.b,
_ => return Err(pmcp::Error::Validation("Unknown operation".into())),
};
Ok(serde_json::json!({ "result": result }))
})
.build();Sourcepub fn tool_typed_sync_with_description<T, F>(
self,
name: impl Into<String>,
description: impl Into<String>,
handler: F,
) -> Selfwhere
T: DeserializeOwned + JsonSchema + Send + Sync + 'static,
F: Fn(T, RequestHandlerExtra) -> Result<Value> + Send + Sync + 'static,
Available on crate feature schema-generation only.
pub fn tool_typed_sync_with_description<T, F>(
self,
name: impl Into<String>,
description: impl Into<String>,
handler: F,
) -> Selfwhere
T: DeserializeOwned + JsonSchema + Send + Sync + 'static,
F: Fn(T, RequestHandlerExtra) -> Result<Value> + Send + Sync + 'static,
schema-generation only.Add a synchronous type-safe tool handler with automatic schema generation and description.
This is a convenience overload that allows setting a description directly
without needing to chain .with_description().
§Example
use pmcp::ServerBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct MathArgs {
/// First number
a: f64,
/// Second number
b: f64,
/// Operation to perform
op: String,
}
let server = ServerBuilder::new()
.name("example")
.tool_typed_sync_with_description(
"calculator",
"Performs synchronous mathematical operations",
|args: MathArgs, _| {
let result = match args.op.as_str() {
"add" => args.a + args.b,
"subtract" => args.a - args.b,
"multiply" => args.a * args.b,
"divide" => args.a / args.b,
_ => return Err(pmcp::Error::Validation("Unknown operation".into())),
};
Ok(serde_json::json!({ "result": result }))
}
);Sourcepub fn tool_typed_with_output<TIn, TOut>(
self,
name: impl Into<String>,
handler: impl Fn(TIn, RequestHandlerExtra) -> Pin<Box<dyn Future<Output = Result<TOut>> + Send>> + Send + Sync + 'static,
) -> Selfwhere
TIn: DeserializeOwned + JsonSchema + Send + Sync + 'static,
TOut: Serialize + JsonSchema + Send + Sync + 'static,
Available on crate feature schema-generation only.
pub fn tool_typed_with_output<TIn, TOut>(
self,
name: impl Into<String>,
handler: impl Fn(TIn, RequestHandlerExtra) -> Pin<Box<dyn Future<Output = Result<TOut>> + Send>> + Send + Sync + 'static,
) -> Selfwhere
TIn: DeserializeOwned + JsonSchema + Send + Sync + 'static,
TOut: Serialize + JsonSchema + Send + Sync + 'static,
schema-generation only.Add a type-safe tool handler with both input and output typing.
This method provides full type safety for both input and output types, which is useful for testing, documentation, and API contracts. Note that output schemas are not part of the MCP protocol but can be valuable for development and integration testing.
§Type Parameters
TIn- Input type that implementsJsonSchema,Deserialize,Send,SyncTOut- Output type that implementsJsonSchema,Serialize,Send,Sync
§Example
use pmcp::{ServerBuilder, TypedToolWithOutput};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(JsonSchema, Deserialize)]
struct MathInput { a: f64, b: f64, op: String }
#[derive(JsonSchema, Serialize)]
struct MathOutput { result: f64, operation: String }
let server = ServerBuilder::new()
.name("example")
.tool_typed_with_output::<MathInput, MathOutput>("math", |args, _| {
Box::pin(async move {
let result = match args.op.as_str() {
"add" => args.a + args.b,
"subtract" => args.a - args.b,
_ => return Err(pmcp::Error::Validation("Unknown operation".into())),
};
Ok(MathOutput {
result,
operation: args.op,
})
})
});Sourcepub fn tool_with_result<TIn>(
self,
name: impl Into<String>,
handler: impl Fn(TIn, RequestHandlerExtra) -> Pin<Box<dyn Future<Output = Result<CallToolResult>> + Send>> + Send + Sync + 'static,
) -> Self
Available on crate feature schema-generation only.
pub fn tool_with_result<TIn>( self, name: impl Into<String>, handler: impl Fn(TIn, RequestHandlerExtra) -> Pin<Box<dyn Future<Output = Result<CallToolResult>> + Send>> + Send + Sync + 'static, ) -> Self
schema-generation only.Register a tool whose async closure returns a full CallToolResult the
handler owns end-to-end, emitted to the wire VERBATIM.
This mirrors tool_typed_with_output but
fixes the return type to
CallToolResult, so a handler can attach
task augmentation (CallToolResult::with_related_task(...)), custom
_meta, structured content, or an error envelope in ONE call — no
hand-written ToolHandler impl required. The input
arg type TIn deserializes from the tool arguments exactly as with
tool_typed.
§⚠️ BYPASS WARNING — the returned result is sent to the wire VERBATIM
The closure’s CallToolResult is routed
through ToolOutput::Result and
therefore BYPASSES response middleware — redaction, sanitization, and
audit hooks (ToolMiddleware::on_response) DO NOT run — as well as
text-wrapping and widget enrichment. The handler owns its OWN redaction
and sanitization of both content and _meta, at the same trust level as
returning a raw Value today (D-04a). Request middleware still runs
before the handler, and handler errors still route through the normal
error path.
To advertise a human-readable tool description in tools/list, use
tool_with_result_and_description.
§Example
use pmcp::ServerBuilder;
use pmcp::types::CallToolResult;
use pmcp::types::tasks::TaskMetadata;
use pmcp::types::Content;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(JsonSchema, Deserialize)]
struct RunArgs { job: String }
let server = ServerBuilder::new()
.name("example")
.tool_with_result("start_job", |args: RunArgs, _extra| {
Box::pin(async move {
Ok(CallToolResult::new(vec![Content::text(
format!("started {}", args.job),
)])
.with_related_task(TaskMetadata::new("t1")))
})
})
.build();Sourcepub fn tool_with_result_and_description<TIn>(
self,
name: impl Into<String>,
description: impl Into<String>,
handler: impl Fn(TIn, RequestHandlerExtra) -> Pin<Box<dyn Future<Output = Result<CallToolResult>> + Send>> + Send + Sync + 'static,
) -> Self
Available on crate feature schema-generation only.
pub fn tool_with_result_and_description<TIn>( self, name: impl Into<String>, description: impl Into<String>, handler: impl Fn(TIn, RequestHandlerExtra) -> Pin<Box<dyn Future<Output = Result<CallToolResult>> + Send>> + Send + Sync + 'static, ) -> Self
schema-generation only.tool_with_result WITH a human-readable
description advertised in tools/list.
Identical to tool_with_result — including
the BYPASS WARNING documented there (the returned
CallToolResult goes to the wire
VERBATIM and skips response middleware) — but also sets the tool
description, mirroring
tool_typed_with_description.
A description materially improves LLM tool selection; prefer this
overload over the description-less
tool_with_result.
§Example
use pmcp::ServerBuilder;
use pmcp::types::CallToolResult;
use pmcp::types::tasks::TaskMetadata;
use pmcp::types::Content;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(JsonSchema, Deserialize)]
struct RunArgs { job: String }
let server = ServerBuilder::new()
.name("example")
.tool_with_result_and_description(
"start_job",
"Start a background export job and return its task handle",
|args: RunArgs, _extra| {
Box::pin(async move {
Ok(CallToolResult::new(vec![Content::text(
format!("started {}", args.job),
)])
.with_related_task(TaskMetadata::new("t1")))
})
},
)
.build();Sourcepub fn tool_typed_with_output_and_description<TIn, TOut>(
self,
name: impl Into<String>,
description: impl Into<String>,
handler: impl Fn(TIn, RequestHandlerExtra) -> Pin<Box<dyn Future<Output = Result<TOut>> + Send>> + Send + Sync + 'static,
) -> Selfwhere
TIn: DeserializeOwned + JsonSchema + Send + Sync + 'static,
TOut: Serialize + JsonSchema + Send + Sync + 'static,
Available on crate feature schema-generation only.
pub fn tool_typed_with_output_and_description<TIn, TOut>(
self,
name: impl Into<String>,
description: impl Into<String>,
handler: impl Fn(TIn, RequestHandlerExtra) -> Pin<Box<dyn Future<Output = Result<TOut>> + Send>> + Send + Sync + 'static,
) -> Selfwhere
TIn: DeserializeOwned + JsonSchema + Send + Sync + 'static,
TOut: Serialize + JsonSchema + Send + Sync + 'static,
schema-generation only.Add a type-safe tool handler with both input and output typing and description.
This is a convenience overload that allows setting a description directly
without needing to chain .with_description().
§Example
use pmcp::ServerBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(JsonSchema, Deserialize)]
struct MathInput { a: f64, b: f64, op: String }
#[derive(JsonSchema, Serialize)]
struct MathOutput { result: f64, operation: String }
let server = ServerBuilder::new()
.name("example")
.tool_typed_with_output_and_description::<MathInput, MathOutput>(
"math",
"Performs basic mathematical operations on two numbers",
|args, _| {
Box::pin(async move {
let result = match args.op.as_str() {
"add" => args.a + args.b,
"subtract" => args.a - args.b,
_ => return Err(pmcp::Error::Validation("Unknown operation".into())),
};
Ok(MathOutput { result, operation: args.op })
})
}
);Sourcepub fn prompt(
self,
name: impl Into<String>,
handler: impl PromptHandler + 'static,
) -> Self
pub fn prompt( self, name: impl Into<String>, handler: impl PromptHandler + 'static, ) -> Self
Add a prompt handler.
Registers a prompt that clients can retrieve via the prompts/get method. Prompts provide templates that clients can use for various tasks.
§Arguments
name- The name of the prompt (used by clients to retrieve it)handler- The handler implementation for this prompt
§Examples
use pmcp::{Server, PromptHandler, GetPromptResult, PromptMessage, Content};
use async_trait::async_trait;
use std::collections::HashMap;
struct CodeReviewPrompt;
#[async_trait]
impl PromptHandler for CodeReviewPrompt {
async fn handle(&self, args: HashMap<String, String>, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<GetPromptResult> {
let language = args.get("language").map(|s| s.as_str()).unwrap_or("unknown");
Ok(GetPromptResult::new(
vec![PromptMessage::user(pmcp::Content::text(format!(
"Please review this {} code:",
language
)))],
Some(format!("Code review prompt for {}", language)),
))
}
}
let server = Server::builder()
.name("code-server")
.version("1.0.0")
.prompt("code_review", CodeReviewPrompt{})
.build()?;Sourcepub fn prompt_arc(
self,
name: impl Into<String>,
handler: Arc<dyn PromptHandler>,
) -> Self
pub fn prompt_arc( self, name: impl Into<String>, handler: Arc<dyn PromptHandler>, ) -> Self
Add a prompt handler with an Arc.
This variant lets the caller share the handler Arc between the
builder and an external in-process handler map (e.g., a downstream
toolkit’s handler registry) without writing a delegating wrapper
shim. Behavior is otherwise identical to Self::prompt: the first
registration auto-enables capabilities.prompts.
Sourcepub fn prompt_workflow(self, workflow: SequentialWorkflow) -> Result<Self>
pub fn prompt_workflow(self, workflow: SequentialWorkflow) -> Result<Self>
Register a workflow-based prompt with automatic validation.
This method validates the workflow before registration and converts it to a prompt handler. The workflow’s instructions become the prompt messages, and the workflow’s arguments become the prompt arguments.
§Arguments
workflow- The workflow definition to register as a prompt
§Errors
Returns an error if the workflow validation fails (e.g., undefined bindings, undefined prompt arguments, etc.).
§Examples
use pmcp::{Server, ServerBuilder};
use pmcp::server::workflow::{SequentialWorkflow, InternalPromptMessage};
use pmcp::types::Role;
let workflow = SequentialWorkflow::new(
"code_review_workflow",
"Review code with multiple steps"
)
.argument("code", "Code to review", true)
.instruction(InternalPromptMessage::new(
Role::System,
"You are a code reviewer. Review the provided code carefully."
));
let server = Server::builder()
.name("code-server")
.version("1.0.0")
.prompt_workflow(workflow)?
.build()?;Sourcepub fn resources(self, handler: impl ResourceHandler + 'static) -> Self
pub fn resources(self, handler: impl ResourceHandler + 'static) -> Self
Set the resource handler.
Registers a resource handler that provides access to server resources. Resources allow clients to read files, configurations, or other data.
§Arguments
handler- The resource handler implementation
§Examples
use pmcp::{Server, ResourceHandler, ReadResourceResult, ListResourcesResult, ResourceInfo};
use async_trait::async_trait;
struct FileResourceHandler;
#[async_trait]
impl ResourceHandler for FileResourceHandler {
async fn read(&self, uri: &str, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<ReadResourceResult> {
// Read file content...
Ok(ReadResourceResult::new(vec![pmcp::Content::text("File content here")]))
}
async fn list(&self, _cursor: Option<String>, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<ListResourcesResult> {
Ok(ListResourcesResult::new(vec![
pmcp::ResourceInfo::new("file://example.txt", "example.txt")
.with_description("Example file")
.with_mime_type("text/plain"),
]))
}
}
let server = Server::builder()
.name("file-server")
.version("1.0.0")
.resources(FileResourceHandler{})
.build()?;Sourcepub fn resources_arc(self, handler: Arc<dyn ResourceHandler>) -> Self
pub fn resources_arc(self, handler: Arc<dyn ResourceHandler>) -> Self
Set the resource handler with an Arc.
This variant lets the caller share the handler Arc between the
builder and an external in-process handler map without writing a
delegating wrapper. Behavior is otherwise identical to
Self::resources: the first registration auto-enables
capabilities.resources.
Sourcepub fn completions(
self,
provider: impl CompletionProviderTrait + 'static,
) -> Self
pub fn completions( self, provider: impl CompletionProviderTrait + 'static, ) -> Self
Set the completion provider backing completion/complete.
The twin of
ServerCoreBuilder::completions —
same name, same signature, same single-provider shape — so a provider
registered through EITHER builder family reaches its own dispatcher. A
slot on one family with the dispatch arm on the other’s server would be
an unreachable seam that still answered the spec shape, which is exactly
the false green this pair exists to prevent.
A SINGLE, server-wide provider (the Self::resources shape, not the
name-keyed Self::prompt shape): the spec routes every
completion/complete to one seam and passes the ref as data. The
reference reaches the provider through
CompletionRequest::context
under the key ref/prompt or ref/resource.
Registering a provider auto-advertises capabilities.completions.
Not registering one is NOT an error: completion/complete still answers
{"completion": {"values": []}}.
§Examples
use pmcp::Server;
use pmcp::types::completable::StaticCompletionProvider;
let server = Server::builder()
.name("completion-server")
.version("1.0.0")
.completions(StaticCompletionProvider::from_strings(vec![
"alpha".to_string(),
"beta".to_string(),
]))
.build()?;Sourcepub fn completions_arc(self, provider: Arc<dyn CompletionProviderTrait>) -> Self
pub fn completions_arc(self, provider: Arc<dyn CompletionProviderTrait>) -> Self
Set the completion provider with an Arc.
This variant lets the caller share the provider Arc with something
outside the builder. Behavior is otherwise identical to
Self::completions.
Sourcepub fn sampling(self, handler: impl SamplingHandler + 'static) -> Self
pub fn sampling(self, handler: impl SamplingHandler + 'static) -> Self
Set the sampling handler.
Registers a sampling handler that provides LLM functionality. This allows the server to act as a language model provider.
§Arguments
handler- The sampling handler implementation
§Examples
use pmcp::{Server, SamplingHandler, CreateMessageParams, CreateMessageResult};
use async_trait::async_trait;
struct MockLLM;
#[async_trait]
impl SamplingHandler for MockLLM {
async fn create_message(&self, params: CreateMessageParams, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<CreateMessageResult> {
// Process the messages and generate a response
Ok(CreateMessageResult::new(pmcp::Content::text("Generated response"), "mock-llm-v1")
.with_usage(pmcp::TokenUsage::new(10, 5, 15))
.with_stop_reason("end_of_text"))
}
}
let server = Server::builder()
.name("llm-server")
.version("1.0.0")
.sampling(MockLLM{})
.build()?;Sourcepub fn sampling_arc(self, handler: Arc<dyn SamplingHandler>) -> Self
pub fn sampling_arc(self, handler: Arc<dyn SamplingHandler>) -> Self
Set the sampling handler with an Arc.
This variant lets the caller share the handler Arc between the
builder and an external in-process handler map without writing a
delegating wrapper. Uses the donor’s if is_none capability
auto-enable so an explicit prior .capabilities(custom) is not
clobbered by a later _arc registration.
Sourcepub fn auth_provider(self, provider: impl AuthProvider + 'static) -> Self
pub fn auth_provider(self, provider: impl AuthProvider + 'static) -> Self
Build the server.
Constructs the final Server instance from the configured builder. This validates that required fields (name and version) are set.
§Examples
use pmcp::{Server, ToolHandler};
use async_trait::async_trait;
use serde_json::Value;
struct PingTool;
#[async_trait]
impl ToolHandler for PingTool {
async fn handle(&self, _args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
Ok(serde_json::json!({"response": "pong"}))
}
}
let server = Server::builder()
.name("ping-server")
.version("1.0.0")
.tool("ping", PingTool{})
.build()?;
// Server is now ready to run
// server.run_stdio().await?;Set the authentication provider.
Configures an authentication provider that will validate incoming requests. When set, the server will use this provider to authenticate requests before processing them.
§Arguments
provider- The authentication provider implementation
§Examples
use pmcp::{Server, auth::ProxyProvider};
let auth_provider = ProxyProvider::with_upstream("https://oauth.example.com");
let server = Server::builder()
.name("secure-server")
.version("1.0.0")
.auth_provider(auth_provider)
.build()?;Sourcepub fn auth_provider_arc(self, provider: Arc<dyn AuthProvider>) -> Self
pub fn auth_provider_arc(self, provider: Arc<dyn AuthProvider>) -> Self
Set the authentication provider with an Arc.
This variant lets the caller share the provider Arc between the
builder and an external in-process registry without writing a
delegating wrapper. Behavior is otherwise identical to
Self::auth_provider.
Set the tool authorizer.
Configures a tool authorizer for fine-grained access control. The authorizer determines which tools authenticated users can access based on their authentication context.
§Arguments
authorizer- The tool authorization implementation
§Examples
use pmcp::{Server, auth::ScopeBasedAuthorizer};
let authorizer = ScopeBasedAuthorizer::new()
.require_scopes("sensitive_tool", vec!["admin".to_string()])
.default_scopes(vec!["read".to_string()]);
let server = Server::builder()
.name("secure-server")
.version("1.0.0")
.tool_authorizer(authorizer)
.build()?;Set the tool authorizer with an Arc.
This variant lets the caller share the authorizer Arc between
the builder and an external in-process registry without writing a
delegating wrapper. Mirrors Self::tool_authorizer’s
protection-clearing semantics: if any prior protect_tool()
configurations exist, they are cleared and a tracing::warn! is
emitted under target "mcp.auth", since a custom authorizer
supersedes scope-based tool protections.
Sourcepub fn protect_tool(
self,
tool_name: impl Into<String>,
scopes: Vec<String>,
) -> Self
pub fn protect_tool( self, tool_name: impl Into<String>, scopes: Vec<String>, ) -> Self
Protect a specific tool with required scopes.
This is a convenience method that creates or updates a scope-based authorizer to require specific scopes for accessing the named tool.
§Arguments
tool_name- The name of the tool to protectscopes- The required scopes for accessing this tool
§Examples
use pmcp::Server;
let server = Server::builder()
.name("secure-server")
.version("1.0.0")
.protect_tool("delete_data", vec!["admin".to_string(), "write".to_string()])
.protect_tool("read_data", vec!["read".to_string()])
.build()?;Sourcepub fn tool_middleware(self, middleware: Arc<dyn ToolMiddleware>) -> Self
pub fn tool_middleware(self, middleware: Arc<dyn ToolMiddleware>) -> Self
Add tool middleware for cross-cutting concerns.
Tool middleware allows you to inject cross-cutting concerns into tool execution, such as OAuth token injection, logging, metrics, or request transformation. Middleware is executed in the order it’s added, both for request processing (before tool execution) and response processing (after tool execution).
This method brings middleware support to the high-level ServerBuilder API,
enabling developers to use both typed tool registration AND middleware without
dropping down to the lower-level ServerCoreBuilder API.
§Arguments
middleware- The middleware implementation to add to the chain
§Examples
§OAuth Token Injection Middleware
use pmcp::server::tool_middleware::{ToolMiddleware, ToolContext};
use pmcp::server::cancellation::RequestHandlerExtra;
use pmcp::Server;
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::Value;
struct OAuthInjectionMiddleware;
#[async_trait]
impl ToolMiddleware for OAuthInjectionMiddleware {
async fn on_request(
&self,
_tool_name: &str,
_args: &mut Value,
extra: &mut RequestHandlerExtra,
_context: &ToolContext,
) -> pmcp::Result<()> {
// Extract OAuth token from auth_context and inject into metadata
if let Some(auth_ctx) = extra.auth_context() {
if let Some(token) = &auth_ctx.token {
extra.set_metadata("oauth_token".to_string(), token.clone());
}
}
Ok(())
}
}
let server = Server::builder()
.name("oauth-server")
.version("1.0.0")
.tool_middleware(Arc::new(OAuthInjectionMiddleware))
.build()?;§Combining with Typed Tools
use pmcp::Server;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct ListGamesArgs {
filter: Option<String>,
}
let server = Server::builder()
.name("game-server")
.version("1.0.0")
.tool_typed_with_description(
"list_games",
"List all available games",
|args: ListGamesArgs, extra| {
Box::pin(async move {
// Access OAuth token injected by middleware
let _token = extra.get_metadata("oauth_token");
Ok(serde_json::json!({"games": []}))
})
}
)
// .tool_middleware(Arc::new(oauth_middleware)) // Works with typed tools!
.build()?;§Middleware Execution Order
Multiple middleware are executed in FIFO order for requests and FIFO for responses:
Request: Middleware1 → Middleware2 → Tool Handler
Response: Tool Handler → Middleware1 → Middleware2Sourcepub fn with_observability(self, config: ObservabilityConfig) -> Self
pub fn with_observability(self, config: ObservabilityConfig) -> Self
Enable observability for this server.
This adds observability middleware that provides:
- Distributed tracing with trace/span IDs
- Request/response event logging
- Metrics emission (duration, count, errors)
The backend is automatically selected based on the configuration:
- “console” - Pretty or JSON output to stdout (development)
- “cloudwatch” - AWS
CloudWatchEMF format (production) - “null” - Discards all events (testing)
§Examples
use pmcp::Server;
use pmcp::server::observability::ObservabilityConfig;
// Development: console output with pretty printing
let server = Server::builder()
.name("my-server")
.version("1.0.0")
.with_observability(ObservabilityConfig::development())
.build()?;
// Production: CloudWatch with EMF metrics
let server = Server::builder()
.name("my-server")
.version("1.0.0")
.with_observability(ObservabilityConfig::production())
.build()?;
// Auto-detect environment (Lambda vs local)
let config = if std::env::var("AWS_LAMBDA_FUNCTION_NAME").is_ok() {
ObservabilityConfig::production()
} else {
ObservabilityConfig::development()
};
let server = Server::builder()
.name("my-server")
.version("1.0.0")
.with_observability(config)
.build()?;Sourcepub fn with_observability_backend(
self,
config: ObservabilityConfig,
backend: Arc<dyn ObservabilityBackend>,
) -> Self
pub fn with_observability_backend( self, config: ObservabilityConfig, backend: Arc<dyn ObservabilityBackend>, ) -> Self
Enable observability with a custom backend.
Use this when you need a custom backend implementation (e.g., Datadog, custom metrics).
§Examples
use pmcp::Server;
use pmcp::server::observability::{ObservabilityConfig, ObservabilityBackend};
use std::sync::Arc;
struct MyCustomBackend;
#[async_trait]
impl ObservabilityBackend for MyCustomBackend {
// ... custom implementation
}
let server = Server::builder()
.name("my-server")
.version("1.0.0")
.with_observability_backend(
ObservabilityConfig::development(),
Arc::new(MyCustomBackend),
)
.build()?;Sourcepub fn with_tool_description(
self,
tool_name: impl Into<String>,
description: impl Into<String>,
) -> Self
👎Deprecated since 1.6.0: Use tool_typed_with_description() and similar variants instead
pub fn with_tool_description( self, tool_name: impl Into<String>, description: impl Into<String>, ) -> Self
Use tool_typed_with_description() and similar variants instead
Add a description to a tool (Note: Limited support).
Important: Due to the immutable design of tool handlers, this method cannot retroactively add descriptions to already-registered tools.
Recommended: Use the *_with_description variants instead:
.tool_typed_with_description().tool_typed_sync_with_description().tool_typed_with_output_and_description()
This method is provided for API completeness but will log warnings when used, encouraging migration to the preferred approaches.
§Preferred Examples
use pmcp::ServerBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct MathArgs { a: f64, b: f64 }
// Preferred: Use the direct description variants
let server = ServerBuilder::new()
.name("example")
.tool_typed_with_description(
"add",
"Adds two numbers together",
|args: MathArgs, _| {
Box::pin(async move {
Ok(serde_json::json!({ "result": args.a + args.b }))
})
}
)
.build();Sourcepub fn with_http_middleware(
self,
middleware: Arc<ServerHttpMiddlewareChain>,
) -> Self
Available on crate feature streamable-http only.
pub fn with_http_middleware( self, middleware: Arc<ServerHttpMiddlewareChain>, ) -> Self
streamable-http only.Configure HTTP middleware chain for StreamableHttpServer.
This is a convenience method that stores the HTTP middleware chain
so it can be retrieved later when creating a StreamableHttpServer.
§Arguments
middleware- The HTTP middleware chain
§Examples
use pmcp::Server;
use pmcp::server::http_middleware::{ServerHttpLoggingMiddleware, ServerHttpMiddlewareChain};
use std::sync::Arc;
let mut http_chain = ServerHttpMiddlewareChain::new();
http_chain.add(Arc::new(ServerHttpLoggingMiddleware::new()));
let server = Server::builder()
.name("my-server")
.version("1.0.0")
.with_http_middleware(Arc::new(http_chain))
.build()?;
// Later when creating StreamableHttpServer:
// let config = StreamableHttpServerConfig {
// http_middleware: server.http_middleware(),
// ..Default::default()
// };Sourcepub fn with_host_layer(self, host: HostType) -> Self
Available on crate feature mcp-apps only.
pub fn with_host_layer(self, host: HostType) -> Self
mcp-apps only.Add a host layer for MCP Apps metadata enrichment.
Host layers enrich tool _meta at build time with host-specific keys.
For example, HostType::ChatGpt adds openai/outputTemplate and
openai/widgetAccessible derived from the standard ui.resourceUri.
This is opt-in — standard MCP Apps hosts (Claude Desktop, etc.) work without any host layer. Duplicates are ignored.
Sourcepub fn task_store(self, store: Arc<dyn TaskStore>) -> Self
pub fn task_store(self, store: Arc<dyn TaskStore>) -> Self
Build the server.
Constructs the final Server instance from the configured builder. This validates that required fields (name and version) are set.
§Errors
Register a TaskStore for MCP
Tasks on the high-level HTTP-facing Server (RECOMMENDED tools-as-Tasks
path).
This is the recommended, all-typed path for exposing a tool as an async
MCP Task over the Server / StreamableHttpServer path: pair a
task-capable TypedTool (marked
with_task_support(TaskSupport::Required))
with a store here, and the SDK serves tasks/* typed from the store —
you never hand-write tasks/* wire JSON, and the store mints the task id.
For the legacy experimental router path, use Self::with_task_store
(which takes a TaskRouter, NOT a
TaskStore).
When a task store is registered, the server:
- Auto-advertises
ServerCapabilities.tasks(with list and cancel support) ininitialize— the mere presence of a store flips the capability on, unless an explicittaskscapability was already configured (additive-only; an explicit value is preserved verbatim). - Handles the
tasks/*surface via the store. The method set is ERA-DEPENDENT (Phase 114): v1 (2025-11-25) servestasks/get,tasks/result,tasks/listandtasks/cancel; v2 (2026-07-28) servestasks/get,tasks/updateandtasks/cancel, and answers-32601for the two retired methods - Resolves task owner from auth context. v1 falls back through OAuth subject → client ID → session ID; v2 has no session to fall back to and binds fail-closed on an auth-configured server (TASK-05, D-07)
A tool declaring
TaskSupport::Required
with NO store (or router) makes Self::build return an Err, rather
than advertising a hollow tasks capability whose endpoints cannot work.
§Examples
use std::sync::Arc;
use pmcp::Server;
use pmcp::server::task_store::{InMemoryTaskStore, TaskStore};
use pmcp::server::typed_tool::TypedTool;
use pmcp::types::{TaskSupport, ToolExecution};
let task_tool = TypedTool::new_with_schema(
"summarize",
serde_json::json!({ "type": "object" }),
|_args: serde_json::Value, _extra| {
Box::pin(async { Ok(serde_json::json!({ "status": "completed" })) })
},
)
.with_description("Summarize asynchronously as an MCP Task")
.with_execution(ToolExecution::new().with_task_support(TaskSupport::Required));
let store = Arc::new(InMemoryTaskStore::new()) as Arc<dyn TaskStore>;
let server = Server::builder()
.name("my-server")
.version("1.0.0")
.tool("summarize", task_tool)
.task_store(store) // presence of a store auto-advertises the `tasks` capability
.build()?;Sourcepub fn with_task_store(self, router: Arc<dyn TaskRouter>) -> Self
pub fn with_task_store(self, router: Arc<dyn TaskRouter>) -> Self
Register a legacy experimental
TaskRouter for MCP Tasks on the
high-level Server.
NAMING NOTE: despite the with_task_store name, this setter accepts a
TaskRouter (the legacy,
experimental router-backed path), NOT a
TaskStore. The setter for an
actual TaskStore (the RECOMMENDED polling path) is
Self::task_store. This carried-over naming mirrors
ServerCoreBuilder::with_task_store; the API is additive-only, so the
confusing pair is documented here rather than renamed.
Registering a router auto-configures the experimental.tasks capability
from the router’s task_capabilities().
That advertisement is v1-only (Phase 114). experimental.tasks is
the 2025-11-25 spelling; a v2 (2026-07-28) client never sees it, because
project_capabilities_for_v2 strips both experimental and
capabilities.tasks and v2 declares tasks through the extensions map
key io.modelcontextprotocol/tasks instead (plan 114-05).
Sourcepub fn suppress_double_wrap_check(self, name: impl Into<String>) -> Self
pub fn suppress_double_wrap_check(self, name: impl Into<String>) -> Self
Opt a tool OUT of the TOUT-02 double-wrap tripwire (D-08).
The tripwire WARNs (every build) and debug_assert!-fails (debug/CI)
when a tool returns a ToolOutput::Payload Value that STRUCTURALLY
resembles an already-built CallToolResult (a non-empty content array
of Content, or a _meta related-task envelope) — the silent
double-wrap bug. Naming a tool here suppresses that check for it.
SUPPRESSION SHOULD BE RARE AND REVIEWED: it disables a safety tripwire for
one tool whose LEGITIMATE payload happens to trip the heuristic. Prefer
returning ToolOutput::Result so the
handler owns the full envelope verbatim, rather than suppressing. Reach
for this only when a tool genuinely produces a plain Value that mimics a
result shape and cannot be restructured.
The same suppression set is carried into ServerCore, so both native
dispatchers honor the opt-out identically (no drift).
Sourcepub fn build(self) -> Result<Server>
pub fn build(self) -> Result<Server>
Returns an error if:
- The server name is not set
- The server version is not set
- A tool declares
TaskSupport::Requiredbut noTaskStore/TaskRouterbackend is configured (seeSelf::task_store)