pub struct Server { /* private fields */ }Expand description
A server, built.
§Examples
use std::sync::Arc;
use protocol_droid::application::server::Server;
use protocol_droid::domain::ir::JsonObject;
let server = Server::builder("demo")
.simple_tool(
"echo",
r#"{"type":"object","properties":{"text":{"type":"string"}}}"#,
Arc::new(|_ctx, args| {
Box::pin(async move {
// A handler answers with the tool's result object, not with
// a `Result`: MCP reports a tool failure as a successful
// call carrying `isError: true`, so "the tool failed" is
// something the handler *returns* rather than propagates.
// The result is built from a `serde_json::Value` so the
// peer's text is escaped by the serialiser. Hand-writing
// the document would make injection a matter of how many
// quotes a caller remembered.
let echoed = args.as_str().to_string();
let result = JsonObject::from_value(serde_json::json!({
"content": [{ "type": "text", "text": echoed }],
}));
result.unwrap_or_default()
})
}),
)
.expect("the schema above is valid")
.build();
assert_eq!(server.tool_count(), 1);
assert_eq!(server.name(), "demo");
assert!(server.handler("echo").is_some());
assert!(server.handler("missing").is_none());Implementations§
Source§impl Server
impl Server
Sourcepub async fn serve_stdio(self) -> Result<ServeReport, ServeError>
pub async fn serve_stdio(self) -> Result<ServeReport, ServeError>
Serves this server on stdin and stdout until the peer closes stdin.
The shortest complete serving path in the crate, and the one the
documentation elsewhere assumes exists. The revision is settled from the
peer’s own initialize, so this neither takes a version nor forces the
caller to pick one — which is the whole reason the crate exists.
The caller does not have to know that a Dispatcher and a
PeerSession are involved, or that the session’s profile is resolved
before the loop starts. Both stay reachable: this is a façade over the
types above, not a replacement for them.
§Errors
Propagates a ServeError for a read, write, or diagnostics failure.
A protocol error is not one of those: a peer whose frame this server
cannot serve is answered on the wire rather than raised here.
§Examples
let server = Server::builder("greeter").build();
let report = server.serve_stdio().await?;
eprintln!("{report:?}");Sourcepub async fn serve_http(self) -> Result<ServeReport, ServeError>
pub async fn serve_http(self) -> Result<ServeReport, ServeError>
Serves this server over Streamable HTTP, on loopback, until it stops.
The same one-call shape as serve_stdio, for the
same reason. The alternative — building an McpRouter, an
axum::Router, and a listener — moves the work onto the consumer
rather than removing it, and the pieces a consumer would have to reach
for are a SessionStore with no production IdSource in it and a
RequestHandler with no implementation for this Dispatcher anywhere
but the tests.
Binds 127.0.0.1:3000 and serves plain HTTP. That is a development
shape, not a deployment one: a non-loopback address and TLS in front of
it are the caller’s to arrange, and HttpBuilder is how.
§Errors
ServeError from building the route table, from binding, or from the
accept loop. A protocol error is not one of those: a peer whose request
this server cannot serve is answered on the wire rather than raised
here.
§Examples
let server = Server::builder("greeter").build();
let report = server.serve_http().await?;
eprintln!("listening on {}", report.addr);Sourcepub fn builder(name: impl Into<String>) -> ServerBuilder
pub fn builder(name: impl Into<String>) -> ServerBuilder
Starts a server with a name and nothing else.
Every method on the returned ServerBuilder is optional, so this
alone is a valid — if useless — server. That is the honest minimum: a
server with no tools is a real state, not a mistake to prevent at the
type level.
Sourcepub fn version(&self) -> &str
pub fn version(&self) -> &str
The version this server reports in serverInfo.
Free-form on the wire — the schema types it as a plain string — so
nothing constrains the format, only that a peer can read it. It lives
beside name because the two are the same fact: they are
the two fields of the serverInfo object, they are both about who this
server is rather than what it does, and splitting them would mean a
server whose name came from the builder and whose version came from
somewhere else.
Sourcepub fn tool_count(&self) -> usize
pub fn tool_count(&self) -> usize
How many tools are registered, hidden or not.
Sourcepub const fn request_state_key(&self) -> Option<&StateKey>
pub const fn request_state_key(&self) -> Option<&StateKey>
The key requestState is sealed under, if one was set.
Set by ServerBuilder::request_state_key, or filled from the OS
random source by a transport when the server is served without one. A
server with no key can still answer every request; it cannot hand out a
requestState, and a retry that carries one is refused as tampered,
because there is nothing to verify it against.
Sourcepub fn ensure_request_state_key(
&mut self,
key: impl FnOnce() -> Option<StateKey>,
)
pub fn ensure_request_state_key( &mut self, key: impl FnOnce() -> Option<StateKey>, )
Installs key if the server has none, for a transport that is about to
serve it.
Sourcepub fn handle(&self) -> ServerHandle
pub fn handle(&self) -> ServerHandle
A handle that adds and removes tools, resources and prompts while this
server is serving, announcing each change to the sessions that
negotiated the matching listChanged capability.
Sourcepub const fn idempotency(
&self,
) -> &Arc<dyn IdempotencyStore<Result = JsonObject>> ⓘ
pub const fn idempotency( &self, ) -> &Arc<dyn IdempotencyStore<Result = JsonObject>> ⓘ
The store tools/call is deduplicated through.
Exposed so a transport can answer “have you seen this id before?” without running the tool, which is what a resuming peer wants: it has dropped a connection and wants to know whether the call it never saw answered actually happened.
Sourcepub fn with_state<T: Send + Sync + 'static>(&mut self, value: T)
pub fn with_state<T: Send + Sync + 'static>(&mut self, value: T)
Registers a state value after the server is built.
The builder API is ServerBuilder::with_state; this method
exists for callers that built a server through a path that did
not surface the builder (the #[server] macro’s
into_server is the canonical one). Each call appends to
the existing map, so a server that already had state for one
type can grow state for a second.
Send + Sync + 'static is what every other port in this crate
asks for; a pool of non-Send handles cannot live in this
map.
Sourcepub fn add_tool(&mut self, tool: Tool, handler: ToolHandler)
pub fn add_tool(&mut self, tool: Tool, handler: ToolHandler)
Adds a tool to a server that was already built.
ServerBuilder::tool is the builder API and takes precedence. This
exists for the same reason with_state does: a
caller who went through #[server]’s into_server never saw a
builder, and until this there was nothing they could do about it.
It is not a convenience for schemas a Rust type can derive. #[tool]
builds inputSchema through schemars from the handler’s parameters,
and schemars cannot express what MCP asks a tool to be able to say:
$schema, $defs with an $anchor, allOf/anyOf, or if/then/
else. Those are SEP-1613 and SEP-2106 — a tool whose schema another
toolchain generated and shipped as JSON has exactly one way to reach a
peer through this crate, and before this method it had none through
the front door.
Registering and publishing together, as on the builder: a tool in
tools/list with no handler is one a client can see and not call, and
a handler with no entry is one that never runs. There is no state in
between for a caller to be wrong in.
It announces nothing: &mut self means nothing can be connected yet.
To change a server that is already serving, use Self::handle.
Infallible, unlike Tool::new: a Tool has already had its
inputSchema parsed and validated, so there is no second parse here to
fail.
Sourcepub fn state(&self) -> &Arc<StateMap> ⓘ
pub fn state(&self) -> &Arc<StateMap> ⓘ
The state map handlers read through
RequestContext::state.
A transport hands this to every RequestContext
it builds. A handler that never calls with_state on the
builder gets an empty map here, and state::<T>() returns
None for every T.
Sourcepub fn registry(&self) -> &RegistryStore
pub fn registry(&self) -> &RegistryStore
The registry, for the transport that serves it.
Takes &self and hands out an owned snapshot later, so a handler
publishing a new tool does not need the server borrowed mutably while
a request is in flight.
Sourcepub fn handler(&self, name: &str) -> Option<ToolHandler>
pub fn handler(&self, name: &str) -> Option<ToolHandler>
The handler for name, if there is one.
Owned, because the table it came from can be replaced the moment this returns: a handle may remove the tool while the caller still holds it.
Sourcepub fn reader(&self, uri: &str) -> Option<ResourceHandler>
pub fn reader(&self, uri: &str) -> Option<ResourceHandler>
The reader for uri, if one is registered.
None for a resource that is listed but has no reader, which is a
state ServerBuilder::resource_with cannot produce — registration
takes both together — and which therefore means the URI is simply not
this server’s.
Sourcepub fn getter(&self, name: &str) -> Option<PromptHandler>
pub fn getter(&self, name: &str) -> Option<PromptHandler>
The getter for name, if one is registered.
None under the same reasoning as reader.
Sourcepub async fn call(
&self,
ctx: &RequestContext,
name: &str,
args: JsonObject,
) -> Result<JsonObject, DispatchError>
pub async fn call( &self, ctx: &RequestContext, name: &str, args: JsonObject, ) -> Result<JsonObject, DispatchError>
Finds name and runs it.
The lookup and the call together rather than a handler() the caller
has to remember to use: a transport and an in-process test that each
wrote their own lookup would drift, and the drift would show up as a
tool that is in the registry and not callable.
Visibility is decided by the registry, and only by the registry. It
used to be a second, cheaper check over a cached required set held by
each tool entry — which covered features and not scopes. So a
scope-restricted tool was absent from tools/list and still ran on
tools/call, which is the existence oracle
UnknownTool exists to prevent.
The cached set is gone rather than kept as a belt-and-braces second check. Once the registry gate runs first, that check can only ever agree with it, so it decides nothing — and a check that decides nothing is a check that can drift into deciding the wrong thing without anyone noticing. One predicate for the whole surface is the property worth having.
§Errors
DispatchError::UnknownTool when no tool of that name is registered
or when this session may not see it — because it requires a
feature the profile lacks, or a scope the caller does not hold. The two
are deliberately the same answer: a different one for each would let a
caller read the existence of a tool it is not allowed to know about.
The server still logs which it was.
Trait Implementations§
Source§impl Debug for Server
impl Debug for Server
Source§fn fmt(&self, f: &mut Formatter<'_>) -> Result
fn fmt(&self, f: &mut Formatter<'_>) -> Result
Prints the name, the version, and the tool count.
Not the store, and the omission is deliberate: a built server always
has one, so printing that would say nothing. Whether a store was
supplied is a question about the builder, and the builder’s own
Debug is where it is answerable.