Skip to main content

protocol_droid/application/server/
builder.rs

1//! [`Server`] — the builder a user of this crate actually touches.
2//!
3//! # What it is
4//!
5//! A server is a [`RegistryStore`], a `Vec` of tool handlers, and the
6//! negotiation inputs a connection will need. It is a value, not a running
7//! process: nothing here binds a port or spawns a task. T13 and T14 supply the
8//! transports that take one of these and give it somewhere to run, and until
9//! then a test can drive it in-process.
10//!
11//! # Why a builder and not a `new`
12//!
13//! A server needs a name before it needs anything else, and most of what
14//! follows is optional. A `Server::new(name, tools, resources, prompts)` is
15//! four positional arguments in an order nobody can hold in their head, and it
16//! is wrong in five of the six shapes a caller wants. The builder makes the
17//! order free and names each thing at the call site.
18
19use std::num::NonZeroUsize;
20use std::sync::Arc;
21
22use crate::adapters::secondary::idempotency::InMemoryIdempotencyStore;
23use crate::application::idempotency::IdempotencyStore;
24use crate::application::mcp::input_required::StateKey;
25use crate::application::registry::store::RegistryStore;
26use crate::application::server::context::RequestContext;
27use crate::application::server::live::{Handlers, Live, ServerHandle, ToolEntry};
28use crate::domain::ir::JsonObject;
29use crate::domain::registry::{
30    Primitive, PrimitiveId, PrimitiveKind, Prompt, Registered, Resource, ResourceTemplate, Tool,
31};
32
33// `DispatchError` lives in `error` now — it is one of the two error types the
34// server API returns, and keeping the pair together is the point of that
35// module. Re-exported here because it was defined here: `Server::call` returns
36// it and every caller reaching for the return type names it through this
37// module, so `server::builder::DispatchError` has to keep resolving.
38pub use super::error::DispatchError;
39
40/// A tool handler, and what it answers with.
41///
42/// The boxed form rather than a generic parameter, deliberately: a server holds
43/// a heterogeneous set of these behind one name, so there is nothing to
44/// monomorphise over. The alternative — a generic `Server<H>` — buys nothing
45/// here and makes the struct unnameable in a field.
46///
47/// The [`Send`] bound is load-bearing. A handler runs on the runtime, and a
48/// handler that is not `Send` cannot be sent to it.
49pub type ToolHandler = Arc<dyn Fn(RequestContext, JsonObject) -> BoxFuture + Send + Sync>;
50
51/// The future a [`ToolHandler`] returns.
52///
53/// Its own alias because spelling `Pin<Box<dyn Future<...> + Send>>` at every
54/// call site is how one of them ends up without the `Send`.
55pub type BoxFuture = std::pin::Pin<Box<dyn Future<Output = JsonObject> + Send>>;
56
57/// The future a [`ResourceHandler`] or [`PromptHandler`] returns.
58///
59/// Fallsible where [`BoxFuture`] is not, because a read is allowed to fail for
60/// a reason the peer should see as a refusal: a resource whose backing store
61/// is unreachable is not an empty resource.
62pub type BoxReadFuture =
63    std::pin::Pin<Box<dyn Future<Output = Result<JsonObject, DispatchError>> + Send>>;
64
65/// Serves one `resources/read`, given the URI the peer asked for.
66///
67/// Takes the URI rather than closing over it, because the URI is the request.
68/// A handler that closed over its own registration would have to be one per
69/// resource, and the point of the lookup table is that the dispatcher finds the
70/// right one from the request the peer actually sent.
71///
72/// Takes the URI by value rather than by reference. A reference would put a
73/// lifetime on the handler type, and a closure whose signature carries one
74/// cannot be inferred as higher-ranked — so the coercion to this type fails
75/// with a lifetime error aimed at the `#[server]` attribute. The dispatcher
76/// already owns the string it parsed out of the request, so this costs one
77/// clone and buys a handler type with no lifetimes in it at all.
78pub type ResourceHandler = Arc<dyn Fn(RequestContext, String) -> BoxReadFuture + Send + Sync>;
79
80/// Serves one `prompts/get`, given the `arguments` the peer sent.
81///
82/// Takes the whole argument object rather than a decoded one: prompt arguments
83/// are free-form by revision — a template like `{diff}` or `{focus}` is the
84/// specification's own idiom — so there is no fixed struct to decode into
85/// without turning a user's template into this crate's invention.
86/// Owned arguments, for the same reason as [`ResourceHandler`].
87pub type PromptHandler = Arc<dyn Fn(RequestContext, JsonObject) -> BoxReadFuture + Send + Sync>;
88
89/// A server, built.
90///
91/// # Examples
92///
93/// ```
94/// use std::sync::Arc;
95///
96/// use protocol_droid::application::server::Server;
97/// use protocol_droid::domain::ir::JsonObject;
98///
99/// let server = Server::builder("demo")
100///     .simple_tool(
101///         "echo",
102///         r#"{"type":"object","properties":{"text":{"type":"string"}}}"#,
103///         Arc::new(|_ctx, args| {
104///             Box::pin(async move {
105///                 // A handler answers with the tool's result object, not with
106///                 // a `Result`: MCP reports a tool failure as a successful
107///                 // call carrying `isError: true`, so "the tool failed" is
108///                 // something the handler *returns* rather than propagates.
109///
110///                 // The result is built from a `serde_json::Value` so the
111///                 // peer's text is escaped by the serialiser. Hand-writing
112///                 // the document would make injection a matter of how many
113///                 // quotes a caller remembered.
114///                 let echoed = args.as_str().to_string();
115///                 let result = JsonObject::from_value(serde_json::json!({
116///                     "content": [{ "type": "text", "text": echoed }],
117///                 }));
118///                 result.unwrap_or_default()
119///             })
120///         }),
121///     )
122///     .expect("the schema above is valid")
123///     .build();
124///
125/// assert_eq!(server.tool_count(), 1);
126/// assert_eq!(server.name(), "demo");
127/// assert!(server.handler("echo").is_some());
128/// assert!(server.handler("missing").is_none());
129/// ```
130// Manual `Debug` rather than derived, for the reason `Dispatcher` has one: the
131// store is a `dyn` port with no readable state, so the derived form would
132// print `idempotency: <unknown>` on every line of a post-mortem and say
133// nothing. What a reader wants is the name, the tool count, and whether a
134// store was ever supplied — the third because a server still on the default
135// has an in-memory bound, and that is the question to ask first when a
136// recorded result has gone missing across a restart.
137pub struct Server {
138    name: String,
139    version: String,
140    /// The registry, the handler table, and who is told when they change —
141    /// shared with every [`ServerHandle`] so a change made while serving lands
142    /// in the same place requests read from.
143    live: Arc<Live>,
144    /// Deduplicates `tools/call` by request.
145    ///
146    /// A trait object rather than a concrete store because a caller serving
147    /// over HTTP wants a bound that outlives one process — a retried call may
148    /// arrive at a different replica — and a concrete `InMemoryIdempotencyStore`
149    /// in this field would be the kind of type that compiles everywhere and is
150    /// only correct on the one host. The default is the in-memory store, which
151    /// is correct for a single process and honest about being so.
152    idempotency: Arc<dyn IdempotencyStore<Result = JsonObject>>,
153    /// The type-keyed state handlers read through
154    /// [`RequestContext::state`](crate::application::server::context::RequestContext::state).
155    ///
156    /// `Arc` so a `Server` is cheap to clone into a `Dispatcher`, and
157    /// the dispatcher is what the transport's per-request handler
158    /// actually reaches into. An empty map is the default; a server
159    /// that never called [`ServerBuilder::with_state`] gets an empty
160    /// map and `state::<T>()` returns `None` for every `T`.
161    state: Arc<crate::application::server::context::StateMap>,
162    /// What SEP-2322 `requestState` is sealed under. See
163    /// [`Server::request_state_key`].
164    state_key: Option<StateKey>,
165}
166
167impl core::fmt::Debug for Server {
168    /// Prints the name, the version, and the tool count.
169    ///
170    /// Not the store, and the omission is deliberate: a built server always
171    /// has one, so printing that would say nothing. Whether a store was
172    /// *supplied* is a question about the builder, and the builder's own
173    /// `Debug` is where it is answerable.
174    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
175        f.debug_struct("Server")
176            .field("name", &self.name)
177            .field("version", &self.version)
178            .field("tool_count", &self.tool_count())
179            .finish_non_exhaustive()
180    }
181}
182
183impl Server {
184    /// Serves this server on stdin and stdout until the peer closes stdin.
185    ///
186    /// The shortest complete serving path in the crate, and the one the
187    /// documentation elsewhere assumes exists. The revision is settled from the
188    /// peer's own `initialize`, so this neither takes a version nor forces the
189    /// caller to pick one — which is the whole reason the crate exists.
190    ///
191    /// The caller does not have to know that a [`Dispatcher`] and a
192    /// [`PeerSession`] are involved, or that the session's profile is resolved
193    /// before the loop starts. Both stay reachable: this is a façade over the
194    /// types above, not a replacement for them.
195    ///
196    /// [`Dispatcher`]: crate::application::server::dispatch::Dispatcher
197    /// [`PeerSession`]: crate::application::server::context::PeerSession
198    ///
199    /// # Errors
200    ///
201    /// Propagates a [`ServeError`] for a read, write, or diagnostics failure.
202    /// A protocol error is not one of those: a peer whose frame this server
203    /// cannot serve is answered on the wire rather than raised here.
204    ///
205    /// [`ServeError`]: crate::adapters::primary::stdio::ServeError
206    ///
207    /// # Examples
208    ///
209    /// ```no_run
210    /// # use protocol_droid::application::server::Server;
211    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
212    /// let server = Server::builder("greeter").build();
213    /// let report = server.serve_stdio().await?;
214    /// eprintln!("{report:?}");
215    /// # Ok(())
216    /// # }
217    /// ```
218    #[cfg(feature = "stdio")]
219    pub async fn serve_stdio(
220        self,
221    ) -> Result<
222        crate::adapters::primary::stdio::ServeReport,
223        crate::adapters::primary::stdio::ServeError,
224    > {
225        let mut loop_ = crate::application::server::serve::StdioBuilder::new(self).build();
226        loop_.serve().await
227    }
228
229    /// Serves this server over Streamable HTTP, on loopback, until it stops.
230    ///
231    /// The same one-call shape as [`serve_stdio`](Self::serve_stdio), for the
232    /// same reason. The alternative — building an `McpRouter`, an
233    /// `axum::Router`, and a listener — moves the work onto the consumer
234    /// rather than removing it, and the pieces a consumer would have to reach
235    /// for are a `SessionStore` with no production `IdSource` in it and a
236    /// `RequestHandler` with no implementation for this `Dispatcher` anywhere
237    /// but the tests.
238    ///
239    /// Binds `127.0.0.1:3000` and serves plain HTTP. That is a development
240    /// shape, not a deployment one: a non-loopback address and TLS in front of
241    /// it are the caller's to arrange, and [`HttpBuilder`] is how.
242    ///
243    /// # Errors
244    ///
245    /// [`ServeError`] from building the route table, from binding, or from the
246    /// accept loop. A protocol error is not one of those: a peer whose request
247    /// this server cannot serve is answered on the wire rather than raised
248    /// here.
249    ///
250    /// # Examples
251    ///
252    /// ```no_run
253    /// # use protocol_droid::application::server::Server;
254    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
255    /// let server = Server::builder("greeter").build();
256    /// let report = server.serve_http().await?;
257    /// eprintln!("listening on {}", report.addr);
258    /// # Ok(())
259    /// # }
260    /// ```
261    ///
262    /// [`HttpBuilder`]: crate::adapters::primary::transport::http::HttpBuilder
263    /// [`ServeError`]: crate::adapters::primary::transport::http::ServeError
264    #[cfg(feature = "http")]
265    pub async fn serve_http(
266        self,
267    ) -> Result<
268        crate::adapters::primary::transport::http::ServeReport,
269        crate::adapters::primary::transport::http::ServeError,
270    > {
271        crate::adapters::primary::transport::http::HttpBuilder::new(self)
272            .serve()
273            .await
274    }
275
276    /// Starts a server with a name and nothing else.
277    ///
278    /// Every method on the returned [`ServerBuilder`] is optional, so this
279    /// alone is a valid — if useless — server. That is the honest minimum: a
280    /// server with no tools is a real state, not a mistake to prevent at the
281    /// type level.
282    #[must_use]
283    pub fn builder(name: impl Into<String>) -> ServerBuilder {
284        ServerBuilder {
285            name: name.into(),
286            version: env!("CARGO_PKG_VERSION").to_string(),
287            registry: RegistryStore::new(),
288            tools: Vec::new(),
289            resource_reads: Vec::new(),
290            prompt_gets: Vec::new(),
291            idempotency: None,
292            state: Vec::new(),
293            state_key: None,
294        }
295    }
296
297    /// The name this server was built with.
298    #[must_use]
299    pub fn name(&self) -> &str {
300        &self.name
301    }
302
303    /// The version this server reports in `serverInfo`.
304    ///
305    /// Free-form on the wire — the schema types it as a plain `string` — so
306    /// nothing constrains the format, only that a peer can read it. It lives
307    /// beside [`name`](Self::name) because the two are the same fact: they are
308    /// the two fields of the `serverInfo` object, they are both about who this
309    /// server is rather than what it does, and splitting them would mean a
310    /// server whose `name` came from the builder and whose `version` came from
311    /// somewhere else.
312    #[must_use]
313    pub fn version(&self) -> &str {
314        &self.version
315    }
316
317    /// How many tools are registered, hidden or not.
318    #[must_use]
319    pub fn tool_count(&self) -> usize {
320        self.live.tool_count()
321    }
322
323    /// The key `requestState` is sealed under, if one was set.
324    ///
325    /// Set by [`ServerBuilder::request_state_key`], or filled from the OS
326    /// random source by a transport when the server is served without one. A
327    /// server with no key can still answer every request; it cannot hand out a
328    /// `requestState`, and a retry that carries one is refused as tampered,
329    /// because there is nothing to verify it against.
330    #[must_use]
331    pub const fn request_state_key(&self) -> Option<&StateKey> {
332        self.state_key.as_ref()
333    }
334
335    /// Installs `key` if the server has none, for a transport that is about to
336    /// serve it.
337    pub fn ensure_request_state_key(&mut self, key: impl FnOnce() -> Option<StateKey>) {
338        if self.state_key.is_none() {
339            self.state_key = key();
340        }
341    }
342
343    /// A handle that adds and removes tools, resources and prompts while this
344    /// server is serving, announcing each change to the sessions that
345    /// negotiated the matching `listChanged` capability.
346    #[must_use]
347    pub fn handle(&self) -> ServerHandle {
348        ServerHandle::new(Arc::clone(&self.live))
349    }
350
351    /// The store `tools/call` is deduplicated through.
352    ///
353    /// Exposed so a transport can answer "have you seen this id before?"
354    /// without running the tool, which is what a resuming peer wants: it has
355    /// dropped a connection and wants to know whether the call it never saw
356    /// answered actually happened.
357    #[must_use]
358    pub const fn idempotency(&self) -> &Arc<dyn IdempotencyStore<Result = JsonObject>> {
359        &self.idempotency
360    }
361
362    /// Registers a state value after the server is built.
363    ///
364    /// The builder API is `ServerBuilder::with_state`; this method
365    /// exists for callers that built a server through a path that did
366    /// not surface the builder (the `#[server]` macro's
367    /// `into_server` is the canonical one). Each call appends to
368    /// the existing map, so a server that already had state for one
369    /// type can grow state for a second.
370    ///
371    /// `Send + Sync + 'static` is what every other port in this crate
372    /// asks for; a pool of non-`Send` handles cannot live in this
373    /// map.
374    pub fn with_state<T: Send + Sync + 'static>(&mut self, value: T) {
375        Arc::make_mut(&mut self.state).insert(
376            std::any::TypeId::of::<T>(),
377            Arc::new(value) as Arc<dyn std::any::Any + Send + Sync>,
378        );
379    }
380
381    /// Adds a tool to a server that was already built.
382    ///
383    /// [`ServerBuilder::tool`] is the builder API and takes precedence. This
384    /// exists for the same reason [`with_state`](Self::with_state) does: a
385    /// caller who went through `#[server]`'s `into_server` never saw a
386    /// builder, and until this there was nothing they could do about it.
387    ///
388    /// It is not a convenience for schemas a Rust type can derive. `#[tool]`
389    /// builds `inputSchema` through `schemars` from the handler's parameters,
390    /// and `schemars` cannot express what MCP asks a tool to be able to say:
391    /// `$schema`, `$defs` with an `$anchor`, `allOf`/`anyOf`, or `if`/`then`/
392    /// `else`. Those are SEP-1613 and SEP-2106 — a tool whose schema another
393    /// toolchain generated and shipped as JSON has exactly one way to reach a
394    /// peer through this crate, and before this method it had none through
395    /// the front door.
396    ///
397    /// Registering and publishing together, as on the builder: a tool in
398    /// `tools/list` with no handler is one a client can see and not call, and
399    /// a handler with no entry is one that never runs. There is no state in
400    /// between for a caller to be wrong in.
401    ///
402    /// It announces nothing: `&mut self` means nothing can be connected yet.
403    /// To change a server that is already serving, use [`Self::handle`].
404    ///
405    /// Infallible, unlike [`Tool::new`]: a [`Tool`] has already had its
406    /// `inputSchema` parsed and validated, so there is no second parse here to
407    /// fail.
408    pub fn add_tool(&mut self, tool: Tool, handler: ToolHandler) {
409        let name = tool.name.clone();
410        let _published = self
411            .live
412            .add(Registered::from(Primitive::from(tool)), |table| {
413                table.tools.retain(|entry| entry.name != name);
414                table.tools.push(ToolEntry { name, handler });
415            });
416    }
417
418    /// The state map handlers read through
419    /// [`RequestContext::state`](crate::application::server::context::RequestContext::state).
420    ///
421    /// A transport hands this to every [`RequestContext`]
422    /// it builds. A handler that never calls `with_state` on the
423    /// builder gets an empty map here, and `state::<T>()` returns
424    /// `None` for every `T`.
425    #[must_use]
426    pub fn state(&self) -> &Arc<crate::application::server::context::StateMap> {
427        &self.state
428    }
429
430    /// The registry, for the transport that serves it.
431    ///
432    /// Takes `&self` and hands out an owned snapshot later, so a handler
433    /// publishing a new tool does not need the server borrowed mutably while
434    /// a request is in flight.
435    #[must_use]
436    pub fn registry(&self) -> &RegistryStore {
437        self.live.registry()
438    }
439
440    /// The handler for `name`, if there is one.
441    ///
442    /// Owned, because the table it came from can be replaced the moment this
443    /// returns: a handle may remove the tool while the caller still holds it.
444    #[must_use]
445    pub fn handler(&self, name: &str) -> Option<ToolHandler> {
446        self.live.tool(name)
447    }
448
449    /// The reader for `uri`, if one is registered.
450    ///
451    /// `None` for a resource that is listed but has no reader, which is a
452    /// state [`ServerBuilder::resource_with`] cannot produce — registration
453    /// takes both together — and which therefore means the URI is simply not
454    /// this server's.
455    #[must_use]
456    pub fn reader(&self, uri: &str) -> Option<ResourceHandler> {
457        self.live.reader(uri)
458    }
459
460    /// The getter for `name`, if one is registered.
461    ///
462    /// `None` under the same reasoning as [`reader`](Self::reader).
463    #[must_use]
464    pub fn getter(&self, name: &str) -> Option<PromptHandler> {
465        self.live.getter(name)
466    }
467
468    /// Finds `name` and runs it.
469    ///
470    /// The lookup and the call together rather than a `handler()` the caller
471    /// has to remember to use: a transport and an in-process test that each
472    /// wrote their own lookup would drift, and the drift would show up as a
473    /// tool that is in the registry and not callable.
474    ///
475    /// Visibility is decided by the **registry**, and only by the registry. It
476    /// used to be a second, cheaper check over a cached `required` set held by
477    /// each tool entry — which covered features and not scopes. So a
478    /// scope-restricted tool was absent from `tools/list` and still ran on
479    /// `tools/call`, which is the existence oracle
480    /// [`UnknownTool`](DispatchError::UnknownTool) exists to prevent.
481    ///
482    /// The cached set is gone rather than kept as a belt-and-braces second
483    /// check. Once the registry gate runs first, that check can only ever
484    /// agree with it, so it decides nothing — and a check that decides nothing
485    /// is a check that can drift into deciding the wrong thing without anyone
486    /// noticing. One predicate for the whole surface is the property worth
487    /// having.
488    ///
489    /// # Errors
490    ///
491    /// [`DispatchError::UnknownTool`] when no tool of that name is registered
492    /// **or** when this session may not see it — because it requires a
493    /// feature the profile lacks, or a scope the caller does not hold. The two
494    /// are deliberately the same answer: a different one for each would let a
495    /// caller read the existence of a tool it is not allowed to know about.
496    /// The server still logs which it was.
497    pub async fn call(
498        &self,
499        ctx: &RequestContext,
500        name: &str,
501        args: JsonObject,
502    ) -> Result<JsonObject, DispatchError> {
503        let handler = self.handler(name).ok_or(DispatchError::UnknownTool)?;
504
505        // The gate, once, for both halves of the surface. After this the tool
506        // is known to exist *and* to be visible to this session, so the
507        // handler is reached with no further checking to disagree with.
508        if self
509            .registry()
510            .get_visible(
511                ctx.peer().auth(),
512                &ctx.peer().profile(),
513                PrimitiveKind::Tool,
514                name,
515            )
516            .is_none()
517        {
518            return Err(DispatchError::UnknownTool);
519        }
520
521        Ok(handler(ctx.clone(), args).await)
522    }
523}
524
525/// The builder behind [`Server::builder`].
526pub struct ServerBuilder {
527    name: String,
528    version: String,
529    registry: RegistryStore,
530    tools: Vec<ToolEntry>,
531    /// Resources with their readers, in registration order.
532    ///
533    /// A `Vec` rather than a map because a server registers a handful of
534    /// resources and a linear scan over a few entries beats a hash of a
535    /// `PrimitiveId` that has to be computed first.
536    resource_reads: Vec<(PrimitiveId, ResourceHandler)>,
537    /// Prompts with their getters, in registration order.
538    prompt_gets: Vec<(PrimitiveId, PromptHandler)>,
539    idempotency: Option<Arc<dyn IdempotencyStore<Result = JsonObject>>>,
540    /// The state entries built up through `with_state` calls. Held as a
541    /// `Vec<(TypeId, Arc<dyn Any>)>` and re-keyed at build time because
542    /// the `TypeId` of `T` is only known to `with_state`, and pushing
543    /// into a `HashMap` keyed by a value the caller cannot see is the
544    /// wrong shape for a builder.
545    state: Vec<(std::any::TypeId, Arc<dyn std::any::Any + Send + Sync>)>,
546    state_key: Option<StateKey>,
547}
548
549impl core::fmt::Debug for ServerBuilder {
550    /// Prints the same three facts as [`Server`], plus whether a store was
551    /// supplied.
552    ///
553    /// The last is the one worth having here, and it is the question to ask
554    /// first when a recorded result has gone missing across a restart: a
555    /// server still on the default has an in-process bound, and no way to see
556    /// that from the built server it became.
557    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
558        f.debug_struct("ServerBuilder")
559            .field("name", &self.name)
560            .field("version", &self.version)
561            .field("tool_count", &self.tools.len())
562            .field("idempotency_supplied", &self.idempotency.is_some())
563            .finish_non_exhaustive()
564    }
565}
566
567impl ServerBuilder {
568    /// Sets the key SEP-2322 `requestState` is sealed under.
569    ///
570    /// Optional: a transport fills one from the OS random source when it is
571    /// absent. Set it explicitly when several replicas serve one endpoint —
572    /// they must share it, or a retry that lands on a different replica finds
573    /// its state refused.
574    #[must_use]
575    pub const fn request_state_key(mut self, key: StateKey) -> Self {
576        self.state_key = Some(key);
577        self
578    }
579
580    /// Sets the version this server reports in `serverInfo`.
581    ///
582    /// Defaults to the version of the crate that built it, which is right for
583    /// a server that *is* this crate — the examples, the conformance runner —
584    /// and wrong for one that merely links it. A library user shipping their
585    /// own server should say so here rather than let the dependency's version
586    /// stand in for theirs, which would have every downstream server claiming
587    /// to be whatever `protocol-droid` happened to be pinned at.
588    #[must_use]
589    pub fn version(mut self, version: impl Into<String>) -> Self {
590        self.version = version.into();
591        self
592    }
593
594    /// Replaces the store `tools/call` is deduplicated through.
595    ///
596    /// For a store that outlives the process — a shared cache, a table — or
597    /// for a test that wants to assert on what was recorded. A server that
598    /// keeps its own `IdSource` across restarts has a gap the in-memory
599    /// default cannot close: a call recorded before a restart is unknown
600    /// after it, and the retry runs the tool again.
601    #[must_use]
602    pub fn idempotency(mut self, store: Arc<dyn IdempotencyStore<Result = JsonObject>>) -> Self {
603        self.idempotency = Some(store);
604        self
605    }
606
607    /// Registers a value handlers can read through
608    /// [`RequestContext::state`](crate::application::server::context::RequestContext::state).
609    ///
610    /// `T` is keyed by `TypeId`, so two `with_state` calls with the same
611    /// `T` keep the later value (the registry is a `HashMap`, and the
612    /// later write is the one the lookup will see). The `Send + Sync`
613    /// bounds are the same every other port in this crate asks for; a
614    /// pool of non-`Send` handles cannot live in this map.
615    ///
616    /// The point of this method is the `#[tool]` problem: a macro that
617    /// generates module-scope items cannot see `self`, so the handler
618    /// it generates has no captured environment. Storing the state
619    /// here and reading it through the request context is the
620    /// standard answer, and the only one that keeps the macro
621    /// out of the receiver business.
622    #[must_use]
623    pub fn with_state<T: Send + Sync + 'static>(mut self, value: T) -> Self {
624        self.state.push((
625            std::any::TypeId::of::<T>(),
626            Arc::new(value) as Arc<dyn std::any::Any + Send + Sync>,
627        ));
628        self
629    }
630
631    /// Adds a tool, and publishes it to the registry in the same step.
632    ///
633    /// Takes a whole [`Tool`] rather than a name and a schema string, because
634    /// a schema string can only ever express the plainest tool there is. An
635    /// output schema, annotations and icons all change what the tool
636    /// _requires_ of a session, and a builder that cannot name them would
637    /// register a tool whose requirement nothing had computed.
638    ///
639    /// Registering and publishing together is the point: a tool in the list
640    /// but not in the registry is a tool a client can see and not call, and
641    /// one in neither is a handler that never runs. There is no state in
642    /// between for a caller to be wrong in.
643    #[must_use]
644    pub fn tool(self, tool: Tool, handler: ToolHandler) -> Self {
645        self.restricted_tool(Registered::from(Primitive::from(tool)), handler)
646    }
647
648    /// Adds a tool that carries its own requirements.
649    ///
650    /// [`tool`](Self::tool) takes a [`Tool`], and a `Tool` cannot say
651    /// "callers need the `files:read` scope" — that lives on [`Registered`],
652    /// which wraps a primitive with the policy it must satisfy. So without this
653    /// method the scope half of `is_visible` is reachable from no builder
654    /// call, `auth.rs` is dead code, and a server can be built that has no way
655    /// to express an authorisation rule at all. That is the same dead-gate
656    /// shape T11 found, one layer up.
657    ///
658    /// A `Registered` carrying something other than a tool is registered
659    /// under its own id and left out of the call table: it stays visible to
660    /// `tools/list` and the listing is the truth, so nothing is silently
661    /// dropped, but there is no handler to name for a `tools/call` of it.
662    #[must_use]
663    pub fn restricted_tool(mut self, registered: Registered, handler: ToolHandler) -> Self {
664        if let Primitive::Tool(tool) = registered.primitive() {
665            self.tools.push(ToolEntry {
666                name: tool.name.clone(),
667                handler,
668            });
669        }
670        self.registry.register(registered);
671        self
672    }
673
674    /// Adds a plain tool: a name, an input schema, and a handler.
675    ///
676    /// The common case, and the one a tool without an output schema or
677    /// annotations fits. It exists so the common case is one line rather than
678    /// a `Tool::new(...)?` at every call site, and it returns a `Result`
679    /// because the schema is the one fallible step.
680    /// # Errors
681    ///
682    /// The [`JsonObject`](crate::domain::ir::JsonObjectError) from parsing
683    /// `input_schema`. The schema is validated here, once, so a malformed one
684    /// fails at build time rather than on the first call.
685    pub fn simple_tool(
686        self,
687        name: impl Into<PrimitiveId>,
688        input_schema: &str,
689        handler: ToolHandler,
690    ) -> Result<Self, crate::domain::ir::JsonObjectError> {
691        Ok(self.tool(Tool::new(name, input_schema)?, handler))
692    }
693
694    /// Adds a resource.
695    ///
696    /// Adds a resource together with the reader that answers `resources/read`
697    /// for it.
698    ///
699    /// Both at once, and there is deliberately no `resource(resource)` beside
700    /// it. A resource that is listed by `resources/list` and then refused by
701    /// `resources/read` is a server telling a peer something false, and the
702    /// dispatcher has to answer `InvalidParams` for the read because it has no
703    /// other way to distinguish that from a URI the server never had. Making
704    /// the reader a separate opt-in would put that state one method call away,
705    /// and the failure would surface to a client rather than at the call site
706    /// that built it.
707    #[must_use]
708    pub fn resource_with(self, resource: Resource, reader: ResourceHandler) -> Self {
709        let mut builder = self;
710        builder.resource_reads.push((resource.uri.clone(), reader));
711        builder
712            .registry
713            .register(Registered::from(Primitive::from(resource)));
714        builder
715    }
716
717    /// Adds a resource template.
718    ///
719    /// Templates are listed by their own method, and they are a distinct
720    /// [`PrimitiveKind`] in the registry, so a template needs its own entry
721    /// point rather than sharing `resource`'s.
722    #[must_use]
723    pub fn resource_template(self, template: ResourceTemplate) -> Self {
724        self.registry
725            .register(Registered::from(Primitive::from(template)));
726        self
727    }
728
729    /// Adds a prompt together with the getter that answers `prompts/get` for it.
730    ///
731    /// Required together for the reason [`resource_with`](Self::resource_with)
732    /// takes both.
733    #[must_use]
734    pub fn prompt_with(self, prompt: Prompt, getter: PromptHandler) -> Self {
735        let mut builder = self;
736        builder.prompt_gets.push((prompt.name.clone(), getter));
737        builder
738            .registry
739            .register(Registered::from(Primitive::from(prompt)));
740        builder
741    }
742
743    /// Finishes the server.
744    #[must_use]
745    pub fn build(self) -> Server {
746        // Deduplicating by default rather than on request. A caller who has
747        // never heard of idempotency is the caller most likely to be serving
748        // a retriable HTTP transport, where a dropped connection turns one
749        // call into two, and a store that has to be opted into is a store
750        // nobody will think to opt into. The default is the bound that fails
751        // safe: a server that forgets runs a tool once rather than twice.
752        let idempotency = self.idempotency.unwrap_or_else(|| {
753            // 4096 keys is generous for a single process and small enough to
754            // bound memory. The capacity is a policy number, not a constant
755            // with a meaning, so it is named rather than inlined.
756            const DEFAULT_KEYS: NonZeroUsize = match NonZeroUsize::new(4096) {
757                Some(value) => value,
758                // Unreachable: 4096 is not zero. A `match` rather than an
759                // `unwrap` because the crate denies that.
760                None => NonZeroUsize::MIN,
761            };
762            Arc::new(InMemoryIdempotencyStore::with_capacity(DEFAULT_KEYS))
763        });
764        Server {
765            name: self.name,
766            version: self.version,
767            live: Arc::new(Live::new(
768                self.registry,
769                Handlers {
770                    tools: self.tools,
771                    resources: self.resource_reads,
772                    prompts: self.prompt_gets,
773                },
774            )),
775            idempotency,
776            state: Arc::new(
777                self.state
778                    .into_iter()
779                    .collect::<std::collections::HashMap<_, _>>(),
780            ),
781            state_key: self.state_key,
782        }
783    }
784}
785
786#[cfg(test)]
787mod tests {
788    use std::sync::Arc;
789
790    use super::{DispatchError, Server};
791    use crate::application::server::context::{PeerSession, RequestContext};
792    use crate::application::session::SinkError;
793    use crate::domain::feature::FeatureProfile;
794    use crate::domain::ir::{JsonObject, JsonObjectError};
795    use crate::domain::registry::PrimitiveKind;
796    use crate::domain::session::{CancellationToken, SessionId};
797    use crate::domain::{ProtocolVersion, RequestId, TransportKind};
798
799    /// What a test here can fail with.
800    #[derive(Debug)]
801    enum Failure {
802        /// The test's own complaint.
803        Message(String),
804        /// A fixture was not a JSON object.
805        Fixture(JsonObjectError),
806    }
807
808    impl core::fmt::Display for Failure {
809        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
810            match self {
811                Self::Message(text) => f.write_str(text),
812                Self::Fixture(error) => write!(f, "bad fixture: {error}"),
813            }
814        }
815    }
816
817    impl std::error::Error for Failure {}
818
819    impl From<String> for Failure {
820        fn from(text: String) -> Self {
821            Self::Message(text)
822        }
823    }
824
825    impl From<&str> for Failure {
826        fn from(text: &str) -> Self {
827            Self::Message(text.to_string())
828        }
829    }
830
831    impl From<JsonObjectError> for Failure {
832        fn from(error: JsonObjectError) -> Self {
833            Self::Fixture(error)
834        }
835    }
836
837    type TestError = Failure;
838
839    /// A handler that answers with a fixed object, so a test can tell one tool
840    /// from another without every case writing its own closure.
841    fn answering(answer: &str) -> Result<super::ToolHandler, TestError> {
842        let answer = JsonObject::new(answer)?;
843        Ok(Arc::new(move |_ctx: RequestContext, _args: JsonObject| {
844            let answer = answer.clone();
845            Box::pin(async move { answer })
846        }))
847    }
848
849    /// A port pair that answers nothing and records nothing.
850    ///
851    /// The builder tests never elicit, sample, or emit progress — they are
852    /// about registration and dispatch — so the ports exist only to satisfy
853    /// `PeerSession`. A `RefCell` is safe here precisely because nothing
854    /// touches them, and a real channel would be a second thing to get wrong
855    /// in a test that is not about channels.
856    struct Inert;
857
858    impl crate::application::session::FrameSink for Inert {
859        fn send(&self, _frame: &str) -> impl Future<Output = Result<(), SinkError>> + Send {
860            std::future::ready(Ok(()))
861        }
862    }
863
864    impl crate::application::server::context::CallbackExchange for Inert {
865        fn exchange(
866            &self,
867            _method: &str,
868            _params: &JsonObject,
869        ) -> impl Future<Output = Result<JsonObject, crate::application::server::ExchangeError>> + Send
870        {
871            std::future::ready(Err(crate::application::server::ExchangeError::new(
872                "inert in these tests",
873            )))
874        }
875    }
876
877    fn peer_at(profile: FeatureProfile) -> PeerSession {
878        PeerSession::new(
879            ProtocolVersion::V2025_06_18,
880            profile,
881            SessionId::new(1),
882            TransportKind::Stdio,
883            Arc::new(Inert),
884            Arc::new(Inert),
885        )
886    }
887
888    fn ctx_for(profile: FeatureProfile) -> RequestContext {
889        RequestContext::for_request(
890            &peer_at(profile),
891            RequestId::number(1),
892            CancellationToken::new(),
893            None,
894        )
895    }
896
897    /// A tool added to the builder is in the registry in the same step.
898    ///
899    /// The builder's claim is that there is no state in between — no tool that
900    /// is listable but not callable. A registry the builder has to be told
901    /// about separately cannot make that claim, so this is the test of the
902    /// claim and not a restatement of the code.
903    #[test]
904    fn a_registered_tool_is_in_the_registry_immediately() -> Result<(), TestError> {
905        let server = Server::builder("demo")
906            .simple_tool("echo", r#"{"type":"object"}"#, answering("{}")?)?
907            .build();
908
909        assert_eq!(server.tool_count(), 1);
910        assert_eq!(server.registry().snapshot().len(PrimitiveKind::Tool), 1);
911        Ok(())
912    }
913
914    /// A tool added after `into_server` lists *and* runs.
915    ///
916    /// Both halves, because the registry and the handler table are two places
917    /// a method like this could write to only one of. A tool in `tools/list`
918    /// with no handler is one a client can see and not call — the failure mode
919    /// the builder's own doc calls out — and it would pass a listing-only test.
920    #[test]
921    fn a_tool_added_after_the_build_lists_and_runs() -> Result<(), TestError> {
922        use crate::domain::registry::{Primitive, Tool};
923
924        let mut server = Server::builder("demo")
925            .simple_tool("echo", r#"{"type":"object"}"#, answering("{}")?)?
926            .build();
927
928        // The schema is the one thing `add_tool` exists for: `$defs`, an
929        // `$anchor` and `if`/`then`/`else` are JSON Schema 2020-12 keywords no
930        // Rust type derives, so this is the whole reason the method is here.
931        let raw = Tool::new(
932            "json_schema_2020_12",
933            r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","$defs":{"address":{"$anchor":"addressDef","type":"string"}},"if":{"properties":{"a":{"const":1}}},"then":{"required":["b"]}}"#,
934        )?;
935        server.add_tool(raw, answering(r#"{"ok":true}"#)?);
936
937        assert_eq!(server.tool_count(), 2);
938        assert_eq!(server.registry().snapshot().len(PrimitiveKind::Tool), 2);
939        assert!(server.handler("json_schema_2020_12").is_some());
940
941        // The listing, not just the count: the schema has to reach `tools/list`
942        // with the keywords intact, since that is the wire the suite reads.
943        let snapshot = server.registry().snapshot();
944        let advertised = snapshot
945            .list(PrimitiveKind::Tool)
946            .iter()
947            .find(|tool| tool.id().as_str() == "json_schema_2020_12")
948            .ok_or("the added tool is not in the registry")?;
949        let Primitive::Tool(advertised) = advertised.primitive() else {
950            return Err("a tool listing held something that is not a tool".into());
951        };
952        let text = advertised.input_schema.as_str();
953        for keyword in ["$schema", "$defs", "$anchor", "if", "then"] {
954            if !text.contains(keyword) {
955                return Err(format!("{keyword} did not reach the listing").into());
956            }
957        }
958        Ok(())
959    }
960
961    /// A malformed input schema fails at build time, not on the first call.
962    #[test]
963    fn a_malformed_input_schema_is_rejected_at_build_time() -> Result<(), TestError> {
964        // The assertion is on the `Result` itself rather than on an error
965        // message: the error's text is not the contract, the timing of the
966        // failure is. A test that unwrapped and matched on a string would pass
967        // against a builder that validated for the wrong reason.
968        let handler = answering("{}")?;
969        let built = Server::builder("demo").simple_tool("bad", "{not json", handler);
970
971        match built {
972            Ok(_) => Err(Failure::from(
973                "a malformed input schema was accepted at build time",
974            )),
975            Err(_) => Ok(()),
976        }
977    }
978
979    /// Two tools with one name is one tool, not two.
980    ///
981    /// T11's store treats a re-registration as a replacement, and the builder
982    /// has to agree or the registry would list a name the dispatcher cannot
983    /// resolve to a single handler.
984    #[test]
985    fn a_second_tool_of_the_same_name_replaces_the_first() -> Result<(), TestError> {
986        let server = Server::builder("demo")
987            .simple_tool(
988                "echo",
989                r#"{"type":"object"}"#,
990                answering(r#"{"first":true}"#)?,
991            )?
992            .simple_tool(
993                "echo",
994                r#"{"type":"object"}"#,
995                answering(r#"{"second":true}"#)?,
996            )?
997            .build();
998
999        assert_eq!(server.tool_count(), 2, "both handlers are kept");
1000        assert_eq!(
1001            server.registry().snapshot().len(PrimitiveKind::Tool),
1002            1,
1003            "but the registry holds one name"
1004        );
1005        Ok(())
1006    }
1007
1008    /// An unknown tool is refused without reaching any handler.
1009    #[tokio::test]
1010    async fn an_unknown_tool_is_refused() -> Result<(), TestError> {
1011        let server = Server::builder("demo")
1012            .simple_tool("echo", r#"{"type":"object"}"#, answering("{}")?)?
1013            .build();
1014        let ctx = ctx_for(FeatureProfile::all());
1015
1016        match server.call(&ctx, "nope", JsonObject::new("{}")?).await {
1017            Ok(answer) => Err(Failure::from(format!("expected a refusal, got {answer}"))),
1018            Err(DispatchError::UnknownTool) => Ok(()),
1019            Err(other) => Err(Failure::from(format!(
1020                "refused for the wrong reason: {other}"
1021            ))),
1022        }
1023    }
1024
1025    /// A registered tool whose required features the session lacks is refused
1026    /// by `call`, not run and refused afterwards.
1027    ///
1028    /// A tool with an output schema requires `ToolStructuredOutput`. A session
1029    /// that never negotiated it must not run the tool and then drop the part
1030    /// of the answer the client could not have used.
1031    #[tokio::test]
1032    async fn a_tool_needing_an_absent_feature_is_not_run() -> Result<(), TestError> {
1033        use crate::domain::registry::Tool;
1034
1035        // The full `Tool`, not `simple_tool`: an output schema is what raises
1036        // the requirement in the first place, and `simple_tool` could not
1037        // express one. A test written against it would be testing a gap.
1038        let structured = Tool::new("echo", r#"{"type":"object"}"#)?
1039            .with_output_schema(r#"{"type":"object"}"#)?;
1040
1041        let server = Server::builder("demo")
1042            .tool(structured, answering(r#"{"ran":true}"#)?)
1043            .build();
1044
1045        let ctx = ctx_for(FeatureProfile::empty());
1046        match server.call(&ctx, "echo", JsonObject::new("{}")?).await {
1047            Ok(answer) => Err(Failure::from(format!(
1048                "a tool the session cannot call returned {answer}"
1049            ))),
1050            // `UnknownTool`, not `NotCallable`, and the distinction is the
1051            // point. A tool this session may not see is a tool this session is
1052            // not told about: answering "it exists but you may not have it"
1053            // would confirm it exists, and a caller could then enumerate the
1054            // registry by watching which names come back refused. The same
1055            // answer as a tool that is not there is what makes the refusal
1056            // probe-proof.
1057            Err(DispatchError::UnknownTool) => Ok(()),
1058            Err(other) => Err(Failure::from(format!(
1059                "refused for the wrong reason: {other}"
1060            ))),
1061        }
1062    }
1063}