pub struct McpServer { /* private fields */ }Expand description
An HTTP server that implements the MCP 2024-11-05 protocol.
Register tools, resources, and prompts with the builder methods, then pass
the server to [Server::run] (or [Server::run_tls]) as an Application.
Requests that do not match the MCP endpoint fall through to the built-in
App controller chain.
Implementations§
Source§impl McpServer
impl McpServer
Sourcepub fn new(name: impl Into<String>, version: impl Into<String>) -> Self
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self
Create a new McpServer. The default MCP endpoint is POST /mcp.
Sourcepub fn page_size(self, n: usize) -> Self
pub fn page_size(self, n: usize) -> Self
Cap tools/list, resources/list, and prompts/list to at most n
items per response, enabling cursor-based pagination: a response with
more items remaining includes "nextCursor", an opaque string the
client echoes back as params.cursor on its next call to get the next
page. n is clamped to a minimum of 1.
Without calling this, every registered tool/resource/prompt is returned in a single response — the default, and the only behavior before pagination existed.
use rust_web_server::mcp::McpServer;
let server = McpServer::new("my-server", "1.0").page_size(50);Sourcepub fn notify(&self, method: &str, params_json: Option<&str>)
pub fn notify(&self, method: &str, params_json: Option<&str>)
Push a JSON-RPC notification (no id — fire-and-forget, per the
spec) to every client currently connected to the GET /mcp SSE
stream, framed as an SSE data: event.
params_json, if given, must already be a valid JSON value (usually
an object) — it’s spliced in verbatim, not escaped or re-serialized.
Never blocks: a client whose event buffer is full (not reading fast
enough) is treated the same as a disconnected one and dropped from
the broadcast list, same as notify would drop it anyway.
use rust_web_server::mcp::McpServer;
let server = McpServer::new("my-server", "1.0");
server.notify("notifications/message", Some(r#"{"level":"info","data":"hello"}"#));Sourcepub fn notify_resource_updated(&self, uri: &str)
pub fn notify_resource_updated(&self, uri: &str)
Push notifications/resources/updated for uri to every session
that called resources/subscribe for it — the mechanism behind
live-updating resource panels (e.g. Claude Desktop watching a
config file or a dashboard resource for changes). Call this from
wherever your application actually changes the underlying data a
resource represents (a file watcher, a webhook handler, a database
trigger poll, …).
A no-op if nobody has subscribed to uri — including if every
subscriber’s GET /mcp SSE connection has since disconnected
(pruned the same way Self::notify prunes dead broadcast clients,
but the subscriptions bookkeeping for uri itself is untouched
either way; only Self::do_resource_unsubscribe removes that).
use rust_web_server::mcp::McpServer;
let server = McpServer::new("my-server", "1.0");
// Elsewhere, e.g. after reloading a watched config file:
server.notify_resource_updated("config://main");Sourcepub fn logging_enabled(self) -> Self
pub fn logging_enabled(self) -> Self
Advertise the logging capability ("logging":{}) in initialize’s
response, so clients know they can call logging/setLevel and expect
notifications/message log entries over the GET /mcp SSE stream.
This only changes what’s advertised — Self::log pushes log
entries regardless of whether this was called, same as Self::notify
works unconditionally. A spec-honest client simply wouldn’t send
logging/setLevel in the first place without seeing the capability.
use rust_web_server::mcp::McpServer;
let server = McpServer::new("my-server", "1.0").logging_enabled();Sourcepub fn log(&self, level: LogLevel, logger: Option<&str>, data_json: &str)
pub fn log(&self, level: LogLevel, logger: Option<&str>, data_json: &str)
Push a notifications/message log entry to every client connected to
the GET /mcp SSE stream, if level is at or above the level most
recently set via a client’s logging/setLevel request (or
LogLevel::Debug — i.e. every level — if none has been set yet).
data_json must already be a valid JSON value (the spec allows any
type here, not just an object — a plain string is fine) — it’s
spliced in verbatim, not escaped or re-serialized. logger, if
given, identifies the log’s source (e.g. a module or subsystem name)
and is escaped automatically.
use rust_web_server::mcp::{LogLevel, McpServer};
let server = McpServer::new("my-server", "1.0").logging_enabled();
server.log(LogLevel::Warning, Some("database"), r#""connection pool exhausted""#);Sourcepub fn register_tool<F>(
&self,
name: &str,
description: &str,
input_schema: &str,
handler: F,
)
pub fn register_tool<F>( &self, name: &str, description: &str, input_schema: &str, handler: F, )
Register a callable tool at runtime, exactly like Self::tool but
without needing to own the server (and usable after it’s already
serving requests). Pushes notifications/tools/list_changed.
use rust_web_server::mcp::{McpContent, McpServer};
let server = McpServer::new("my-server", "1.0");
// Later, from any thread holding a clone of `server`:
server.register_tool("refresh_cache", "Reload the in-memory cache", "{}", |_args| {
Ok(McpContent::text("cache refreshed"))
});
let existed = server.remove_tool("refresh_cache");
assert!(existed);Sourcepub fn register_async_tool<F, Fut>(
&self,
name: &str,
description: &str,
input_schema: &str,
handler: F,
)
pub fn register_async_tool<F, Fut>( &self, name: &str, description: &str, input_schema: &str, handler: F, )
Register an async fn tool handler at runtime, exactly like
Self::async_tool but usable after the server is already serving
requests. Requires the http2 feature — see async_tool’s docs for
the async-to-sync bridging details.
Sourcepub fn remove_tool(&self, name: &str) -> bool
pub fn remove_tool(&self, name: &str) -> bool
Remove a previously-registered tool by name — a sync tool registered
via Self::tool/Self::register_tool, or (with the http2
feature) an async one via Self::async_tool/Self::register_async_tool;
both collections are checked. Returns true if a tool with that name
existed in either and was removed. Pushes notifications/tools/list_changed
only when something was actually removed.
Sourcepub fn register_resource<F>(
&self,
uri_template: &str,
name: &str,
description: &str,
handler: F,
)
pub fn register_resource<F>( &self, uri_template: &str, name: &str, description: &str, handler: F, )
Register a readable resource at runtime, exactly like Self::resource.
Pushes notifications/resources/list_changed.
Sourcepub fn remove_resource(&self, uri_template: &str) -> bool
pub fn remove_resource(&self, uri_template: &str) -> bool
Remove a previously-registered resource by its exact uri_template
(the same string passed to Self::register_resource/Self::resource,
not a concrete URI). Returns true if it existed. Pushes
notifications/resources/list_changed only when something was removed.
Sourcepub fn register_prompt<F>(&self, name: &str, description: &str, handler: F)
pub fn register_prompt<F>(&self, name: &str, description: &str, handler: F)
Register a prompt template at runtime, exactly like Self::prompt
(no argument definitions — use Self::remove_prompt +
Self::register_prompt if you need to change a prompt’s arguments
later; there is no dynamic equivalent of Self::prompt_with_args).
Pushes notifications/prompts/list_changed.
Sourcepub fn remove_prompt(&self, name: &str) -> bool
pub fn remove_prompt(&self, name: &str) -> bool
Remove a previously-registered prompt by name. Returns true if it
existed. Pushes notifications/prompts/list_changed only when
something was removed.
Sourcepub fn require_bearer(self, token: impl Into<String>) -> Self
pub fn require_bearer(self, token: impl Into<String>) -> Self
Require a bearer token on every request to the MCP endpoint.
The client must send Authorization: Bearer <token>. Requests with a
missing or wrong token receive 401 Unauthorized before any JSON-RPC
processing occurs.
Store the token in an environment variable — never hard-code it:
use rust_web_server::app::App;
use rust_web_server::core::New;
let app = App::new()
.mcp("my-server", "1.0")
.require_bearer(std::env::var("MCP_TOKEN").expect("MCP_TOKEN not set"));Claude Desktop config:
{ "mcpServers": { "my-server": {
"url": "http://localhost:7878/mcp",
"headers": { "Authorization": "Bearer <token>" }
}}}Sourcepub fn wrap(self, app: impl Application + Send + Sync + 'static) -> Self
pub fn wrap(self, app: impl Application + Send + Sync + 'static) -> Self
Wrap an existing Application so that non-MCP requests are forwarded
to it instead of the built-in App.
Use this when your existing server has custom routes, state, or middleware that you want to keep alongside the MCP endpoint:
use rust_web_server::app::App;
use rust_web_server::mcp::{McpServer, McpContent};
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use rust_web_server::test_client::TestClient;
let existing_app = App::with_state(42u32)
.get("/api/hello", |_req, _params, _conn, _state| {
Response::get_response(&STATUS_CODE_REASON_PHRASE.n200_ok, None, None)
});
let server = McpServer::new("my-app", "1.0")
.tool("ping", "Ping", "{}", |_| Ok(McpContent::text("pong")))
.wrap(existing_app);
// Both /mcp and /api/hello are now handled by the same server.
let client = TestClient::new(server);Sourcepub fn at(self, path: impl Into<String>) -> Self
pub fn at(self, path: impl Into<String>) -> Self
Override the HTTP path for the MCP endpoint (default "/mcp").
Sourcepub fn tool<F>(
self,
name: &str,
description: &str,
input_schema: &str,
handler: F,
) -> Self
pub fn tool<F>( self, name: &str, description: &str, input_schema: &str, handler: F, ) -> Self
Register a callable tool.
name— tool identifier (snake_case recommended)description— human-readable description shown to the AIinput_schema— JSON Schema object for the tool’s argumentshandler— closure receiving the rawargumentsJSON string
The handler returns McpContent on success or an error string. An
error is returned to the client as isError: true (not a protocol error).
Use Self::tool_with_context instead if the handler needs the
caller’s identity, session, or headers.
Sourcepub fn async_tool<F, Fut>(
self,
name: &str,
description: &str,
input_schema: &str,
handler: F,
) -> Self
pub fn async_tool<F, Fut>( self, name: &str, description: &str, input_schema: &str, handler: F, ) -> Self
Register an async fn tool handler — for a tool whose work is
naturally async (an AsyncClient HTTP call, an async database
query, awaiting another future) rather than forcing it through a
blocking call inside a plain Self::tool handler.
Requires the http2 feature (tokio). tools/call bridges into the
handler’s future via [crate::async_bridge::block_on_isolated] —
the same mechanism crate::proxy::H2ReverseProxy and
crate::async_state::AsyncAppWithState already use to call async
code from a synchronous Application::execute — rather than
tokio::task::block_in_place, which panics outside a multi_thread
runtime; block_on_isolated works under any runtime flavor,
including current_thread.
Like Self::tool (not Self::tool_with_context), the handler
only receives arguments — there is no async equivalent of
tool_with_context or tool_annotated yet.
use rust_web_server::mcp::{McpContent, McpServer};
let server = McpServer::new("my-server", "1.0")
.async_tool("call_api", "Call an external API", "{}", |_args: &str| async move {
Ok(McpContent::text("response"))
});Sourcepub fn tool_annotated<F>(
self,
name: &str,
description: &str,
input_schema: &str,
annotations: ToolAnnotations,
handler: F,
) -> Self
pub fn tool_annotated<F>( self, name: &str, description: &str, input_schema: &str, annotations: ToolAnnotations, handler: F, ) -> Self
Register a callable tool with ToolAnnotations — behavioral hints
(read-only, destructive, idempotent, open-world) that MCP clients use
to decide whether to warn or confirm before calling it. Otherwise
identical to Self::tool — the handler still only receives
arguments, not McpContext (there is currently no single builder
combining annotations with per-request context; call Self::tool_with_context
instead if you need context and don’t need annotations).
use rust_web_server::mcp::{McpContent, McpServer, ToolAnnotations};
let server = McpServer::new("my-server", "1.0")
.tool_annotated(
"delete_file",
"Delete a file from disk",
r#"{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}"#,
ToolAnnotations {
destructive_hint: Some(true),
read_only_hint: Some(false),
idempotent_hint: Some(true), // deleting twice = deleting once
..Default::default()
},
|_args| Ok(McpContent::text("deleted")),
);Sourcepub fn tool_with_context<F>(
self,
name: &str,
description: &str,
input_schema: &str,
handler: F,
) -> Self
pub fn tool_with_context<F>( self, name: &str, description: &str, input_schema: &str, handler: F, ) -> Self
Register a callable tool whose handler also receives McpContext —
caller identity/session info derived from this request’s headers and
whatever clientInfo this session sent at initialize time.
Same name/description/input_schema semantics as Self::tool;
the only difference is the handler’s first parameter.
use rust_web_server::mcp::{McpContent, McpServer};
let server = McpServer::new("my-server", "1.0")
.tool_with_context(
"whoami",
"Report the caller's client info",
"{}",
|ctx, _args| {
let name = ctx.client_name.as_deref().unwrap_or("unknown client");
Ok(McpContent::text(format!("Called by {name}")))
},
);Sourcepub fn resource<F>(
self,
uri_template: &str,
name: &str,
description: &str,
handler: F,
) -> Self
pub fn resource<F>( self, uri_template: &str, name: &str, description: &str, handler: F, ) -> Self
Register a readable resource.
uri_template uses {param} placeholders, e.g. "user://{id}".
The handler receives the full concrete URI string.
Sourcepub fn prompt<F>(self, name: &str, description: &str, handler: F) -> Self
pub fn prompt<F>(self, name: &str, description: &str, handler: F) -> Self
Register a prompt template.
The handler receives the raw arguments JSON string and returns a
list of PromptMessage values.
Sourcepub fn prompt_with_args<F>(
self,
name: &str,
description: &str,
args: Vec<PromptArgDef>,
handler: F,
) -> Self
pub fn prompt_with_args<F>( self, name: &str, description: &str, args: Vec<PromptArgDef>, handler: F, ) -> Self
Register a prompt template with explicit argument definitions.
Sourcepub fn completion<F>(self, ref_type: &str, ref_name: &str, handler: F) -> Self
pub fn completion<F>(self, ref_type: &str, ref_name: &str, handler: F) -> Self
Register an argument-completion provider for one named argument of a tool or prompt, so clients like Cursor and VS Code can offer autocomplete while the user fills in that argument.
ref_type is "tool" or "prompt" — matched against the incoming
completion/complete request’s ref.type ("ref/tool"/"ref/prompt"
on the wire) with the "ref/" prefix stripped. ref_name is the
tool or prompt name this applies to. The handler receives the
argument’s name and whatever partial value the user has typed so
far, and returns candidate completion strings (or an error, mapped
to a JSON-RPC INVALID_PARAMS response).
initialize advertises the completions capability automatically
once at least one .completion() has been registered — there’s no
separate opt-in flag to remember.
use rust_web_server::mcp::McpServer;
let server = McpServer::new("my-server", "1.0")
.completion("tool", "deploy", |arg_name, _partial| {
match arg_name {
"region" => Ok(vec!["us-east-1".to_string(), "eu-west-1".to_string()]),
_ => Ok(vec![]),
}
});Sourcepub fn handle_request(&self, body: &str) -> Response
pub fn handle_request(&self, body: &str) -> Response
Process a raw JSON-RPC body and return an HTTP response.
Equivalent to Self::handle_request_with_context with an empty
McpContext — tool handlers registered via
Self::tool_with_context will see every field as None. Prefer
calling through Application::execute (i.e. actually serving HTTP
requests) when you need real per-request context; this method exists
for calling the JSON-RPC layer directly, e.g. in tests.
Sourcepub fn handle_request_with_context(
&self,
body: &str,
ctx: McpContext,
) -> Response
pub fn handle_request_with_context( &self, body: &str, ctx: McpContext, ) -> Response
Process a raw JSON-RPC body with an explicit McpContext and return
an HTTP response. Self::execute calls this with a context built
from the request’s headers and this session’s stored clientInfo;
Self::handle_request calls this with an empty context.
On a successful initialize, this mints a new session id (reusing
crate::request_id::generate_request_id’s ID generator), records
params.clientInfo under it, and returns the id in an
Mcp-Session-Id response header — the client is expected to echo that
header back on subsequent requests so later tools/calls in the same
session can look their clientInfo back up.