Skip to main content

Server

Struct Server 

Source
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

Source

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:?}");
Source

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);
Source

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.

Source

pub fn name(&self) -> &str

The name this server was built with.

Source

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.

Source

pub fn tool_count(&self) -> usize

How many tools are registered, hidden or not.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn getter(&self, name: &str) -> Option<PromptHandler>

The getter for name, if one is registered.

None under the same reasoning as reader.

Source

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

Source§

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.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more