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::idempotency::IdempotencyError;
29use crate::domain::ir::{IrErrorCode, JsonObject};
30use crate::domain::registry::{
31    Primitive, PrimitiveId, PrimitiveKind, Prompt, Registered, Resource, ResourceTemplate, Tool,
32};
33
34/// A tool handler, and what it answers with.
35///
36/// The boxed form rather than a generic parameter, deliberately: a server holds
37/// a heterogeneous set of these behind one name, so there is nothing to
38/// monomorphise over. The alternative — a generic `Server<H>` — buys nothing
39/// here and makes the struct unnameable in a field.
40///
41/// The [`Send`] bound is load-bearing. A handler runs on the runtime, and a
42/// handler that is not `Send` cannot be sent to it.
43pub type ToolHandler = Arc<dyn Fn(RequestContext, JsonObject) -> BoxFuture + Send + Sync>;
44
45/// The future a [`ToolHandler`] returns.
46///
47/// Its own alias because spelling `Pin<Box<dyn Future<...> + Send>>` at every
48/// call site is how one of them ends up without the `Send`.
49pub type BoxFuture = std::pin::Pin<Box<dyn Future<Output = JsonObject> + Send>>;
50
51/// The future a [`ResourceHandler`] or [`PromptHandler`] returns.
52///
53/// Fallsible where [`BoxFuture`] is not, because a read is allowed to fail for
54/// a reason the peer should see as a refusal: a resource whose backing store
55/// is unreachable is not an empty resource.
56pub type BoxReadFuture =
57    std::pin::Pin<Box<dyn Future<Output = Result<JsonObject, DispatchError>> + Send>>;
58
59/// Serves one `resources/read`, given the URI the peer asked for.
60///
61/// Takes the URI rather than closing over it, because the URI is the request.
62/// A handler that closed over its own registration would have to be one per
63/// resource, and the point of the lookup table is that the dispatcher finds the
64/// right one from the request the peer actually sent.
65///
66/// Takes the URI by value rather than by reference. A reference would put a
67/// lifetime on the handler type, and a closure whose signature carries one
68/// cannot be inferred as higher-ranked — so the coercion to this type fails
69/// with a lifetime error aimed at the `#[server]` attribute. The dispatcher
70/// already owns the string it parsed out of the request, so this costs one
71/// clone and buys a handler type with no lifetimes in it at all.
72pub type ResourceHandler = Arc<dyn Fn(RequestContext, String) -> BoxReadFuture + Send + Sync>;
73
74/// Serves one `prompts/get`, given the `arguments` the peer sent.
75///
76/// Takes the whole argument object rather than a decoded one: prompt arguments
77/// are free-form by revision — a template like `{diff}` or `{focus}` is the
78/// specification's own idiom — so there is no fixed struct to decode into
79/// without turning a user's template into this crate's invention.
80/// Owned arguments, for the same reason as [`ResourceHandler`].
81pub type PromptHandler = Arc<dyn Fn(RequestContext, JsonObject) -> BoxReadFuture + Send + Sync>;
82
83/// A server, built.
84///
85/// # Examples
86///
87/// ```
88/// use std::sync::Arc;
89///
90/// use protocol_droid::application::server::Server;
91/// use protocol_droid::domain::ir::JsonObject;
92///
93/// let server = Server::builder("demo")
94///     .simple_tool(
95///         "echo",
96///         r#"{"type":"object","properties":{"text":{"type":"string"}}}"#,
97///         Arc::new(|_ctx, args| {
98///             Box::pin(async move {
99///                 // A handler answers with the tool's result object, not with
100///                 // a `Result`: MCP reports a tool failure as a successful
101///                 // call carrying `isError: true`, so "the tool failed" is
102///                 // something the handler *returns* rather than propagates.
103///
104///                 // The result is built from a `serde_json::Value` so the
105///                 // peer's text is escaped by the serialiser. Hand-writing
106///                 // the document would make injection a matter of how many
107///                 // quotes a caller remembered.
108///                 let echoed = args.as_str().to_string();
109///                 let result = JsonObject::from_value(serde_json::json!({
110///                     "content": [{ "type": "text", "text": echoed }],
111///                 }));
112///                 result.unwrap_or_default()
113///             })
114///         }),
115///     )
116///     .expect("the schema above is valid")
117///     .build();
118///
119/// assert_eq!(server.tool_count(), 1);
120/// assert_eq!(server.name(), "demo");
121/// assert!(server.handler("echo").is_some());
122/// assert!(server.handler("missing").is_none());
123/// ```
124// Manual `Debug` rather than derived, for the reason `Dispatcher` has one: the
125// store is a `dyn` port with no readable state, so the derived form would
126// print `idempotency: <unknown>` on every line of a post-mortem and say
127// nothing. What a reader wants is the name, the tool count, and whether a
128// store was ever supplied — the third because a server still on the default
129// has an in-memory bound, and that is the question to ask first when a
130// recorded result has gone missing across a restart.
131pub struct Server {
132    name: String,
133    version: String,
134    /// The registry, the handler table, and who is told when they change —
135    /// shared with every [`ServerHandle`] so a change made while serving lands
136    /// in the same place requests read from.
137    live: Arc<Live>,
138    /// Deduplicates `tools/call` by request.
139    ///
140    /// A trait object rather than a concrete store because a caller serving
141    /// over HTTP wants a bound that outlives one process — a retried call may
142    /// arrive at a different replica — and a concrete `InMemoryIdempotencyStore`
143    /// in this field would be the kind of type that compiles everywhere and is
144    /// only correct on the one host. The default is the in-memory store, which
145    /// is correct for a single process and honest about being so.
146    idempotency: Arc<dyn IdempotencyStore<Result = JsonObject>>,
147    /// The type-keyed state handlers read through
148    /// [`RequestContext::state`](crate::application::server::context::RequestContext::state).
149    ///
150    /// `Arc` so a `Server` is cheap to clone into a `Dispatcher`, and
151    /// the dispatcher is what the transport's per-request handler
152    /// actually reaches into. An empty map is the default; a server
153    /// that never called [`ServerBuilder::with_state`] gets an empty
154    /// map and `state::<T>()` returns `None` for every `T`.
155    state: Arc<crate::application::server::context::StateMap>,
156    /// What SEP-2322 `requestState` is sealed under. See
157    /// [`Server::request_state_key`].
158    state_key: Option<StateKey>,
159}
160
161impl core::fmt::Debug for Server {
162    /// Prints the name, the version, and the tool count.
163    ///
164    /// Not the store, and the omission is deliberate: a built server always
165    /// has one, so printing that would say nothing. Whether a store was
166    /// *supplied* is a question about the builder, and the builder's own
167    /// `Debug` is where it is answerable.
168    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
169        f.debug_struct("Server")
170            .field("name", &self.name)
171            .field("version", &self.version)
172            .field("tool_count", &self.tool_count())
173            .finish_non_exhaustive()
174    }
175}
176
177impl Server {
178    /// Serves this server on stdin and stdout until the peer closes stdin.
179    ///
180    /// The shortest complete serving path in the crate, and the one the
181    /// documentation elsewhere assumes exists. The revision is settled from the
182    /// peer's own `initialize`, so this neither takes a version nor forces the
183    /// caller to pick one — which is the whole reason the crate exists.
184    ///
185    /// The caller does not have to know that a [`Dispatcher`] and a
186    /// [`PeerSession`] are involved, or that the session's profile is resolved
187    /// before the loop starts. Both stay reachable: this is a façade over the
188    /// types above, not a replacement for them.
189    ///
190    /// [`Dispatcher`]: crate::application::server::dispatch::Dispatcher
191    /// [`PeerSession`]: crate::application::server::context::PeerSession
192    ///
193    /// # Errors
194    ///
195    /// Propagates a [`ServeError`] for a read, write, or diagnostics failure.
196    /// A protocol error is not one of those: a peer whose frame this server
197    /// cannot serve is answered on the wire rather than raised here.
198    ///
199    /// [`ServeError`]: crate::adapters::primary::stdio::ServeError
200    ///
201    /// # Examples
202    ///
203    /// ```no_run
204    /// # use protocol_droid::application::server::Server;
205    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
206    /// let server = Server::builder("greeter").build();
207    /// let report = server.serve_stdio().await?;
208    /// eprintln!("{report:?}");
209    /// # Ok(())
210    /// # }
211    /// ```
212    #[cfg(feature = "stdio")]
213    pub async fn serve_stdio(
214        self,
215    ) -> Result<
216        crate::adapters::primary::stdio::ServeReport,
217        crate::adapters::primary::stdio::ServeError,
218    > {
219        let mut loop_ = crate::application::server::serve::StdioBuilder::new(self).build();
220        loop_.serve().await
221    }
222
223    /// Serves this server over Streamable HTTP, on loopback, until it stops.
224    ///
225    /// The same one-call shape as [`serve_stdio`](Self::serve_stdio), for the
226    /// same reason. The alternative — building an `McpRouter`, an
227    /// `axum::Router`, and a listener — moves the work onto the consumer
228    /// rather than removing it, and the pieces a consumer would have to reach
229    /// for are a `SessionStore` with no production `IdSource` in it and a
230    /// `RequestHandler` with no implementation for this `Dispatcher` anywhere
231    /// but the tests.
232    ///
233    /// Binds `127.0.0.1:3000` and serves plain HTTP. That is a development
234    /// shape, not a deployment one: a non-loopback address and TLS in front of
235    /// it are the caller's to arrange, and [`HttpBuilder`] is how.
236    ///
237    /// # Errors
238    ///
239    /// [`ServeError`] from building the route table, from binding, or from the
240    /// accept loop. A protocol error is not one of those: a peer whose request
241    /// this server cannot serve is answered on the wire rather than raised
242    /// here.
243    ///
244    /// # Examples
245    ///
246    /// ```no_run
247    /// # use protocol_droid::application::server::Server;
248    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
249    /// let server = Server::builder("greeter").build();
250    /// let report = server.serve_http().await?;
251    /// eprintln!("listening on {}", report.addr);
252    /// # Ok(())
253    /// # }
254    /// ```
255    ///
256    /// [`HttpBuilder`]: crate::adapters::primary::transport::http::HttpBuilder
257    /// [`ServeError`]: crate::adapters::primary::transport::http::ServeError
258    #[cfg(feature = "http")]
259    pub async fn serve_http(
260        self,
261    ) -> Result<
262        crate::adapters::primary::transport::http::ServeReport,
263        crate::adapters::primary::transport::http::ServeError,
264    > {
265        crate::adapters::primary::transport::http::HttpBuilder::new(self)
266            .serve()
267            .await
268    }
269
270    /// Starts a server with a name and nothing else.
271    ///
272    /// Every method on the returned [`ServerBuilder`] is optional, so this
273    /// alone is a valid — if useless — server. That is the honest minimum: a
274    /// server with no tools is a real state, not a mistake to prevent at the
275    /// type level.
276    #[must_use]
277    pub fn builder(name: impl Into<String>) -> ServerBuilder {
278        ServerBuilder {
279            name: name.into(),
280            version: env!("CARGO_PKG_VERSION").to_string(),
281            registry: RegistryStore::new(),
282            tools: Vec::new(),
283            resource_reads: Vec::new(),
284            prompt_gets: Vec::new(),
285            idempotency: None,
286            state: Vec::new(),
287            state_key: None,
288        }
289    }
290
291    /// The name this server was built with.
292    #[must_use]
293    pub fn name(&self) -> &str {
294        &self.name
295    }
296
297    /// The version this server reports in `serverInfo`.
298    ///
299    /// Free-form on the wire — the schema types it as a plain `string` — so
300    /// nothing constrains the format, only that a peer can read it. It lives
301    /// beside [`name`](Self::name) because the two are the same fact: they are
302    /// the two fields of the `serverInfo` object, they are both about who this
303    /// server is rather than what it does, and splitting them would mean a
304    /// server whose `name` came from the builder and whose `version` came from
305    /// somewhere else.
306    #[must_use]
307    pub fn version(&self) -> &str {
308        &self.version
309    }
310
311    /// How many tools are registered, hidden or not.
312    #[must_use]
313    pub fn tool_count(&self) -> usize {
314        self.live.tool_count()
315    }
316
317    /// The key `requestState` is sealed under, if one was set.
318    ///
319    /// Set by [`ServerBuilder::request_state_key`], or filled from the OS
320    /// random source by a transport when the server is served without one. A
321    /// server with no key can still answer every request; it cannot hand out a
322    /// `requestState`, and a retry that carries one is refused as tampered,
323    /// because there is nothing to verify it against.
324    #[must_use]
325    pub const fn request_state_key(&self) -> Option<&StateKey> {
326        self.state_key.as_ref()
327    }
328
329    /// Installs `key` if the server has none, for a transport that is about to
330    /// serve it.
331    pub fn ensure_request_state_key(&mut self, key: impl FnOnce() -> Option<StateKey>) {
332        if self.state_key.is_none() {
333            self.state_key = key();
334        }
335    }
336
337    /// A handle that adds and removes tools, resources and prompts while this
338    /// server is serving, announcing each change to the sessions that
339    /// negotiated the matching `listChanged` capability.
340    #[must_use]
341    pub fn handle(&self) -> ServerHandle {
342        ServerHandle::new(Arc::clone(&self.live))
343    }
344
345    /// The store `tools/call` is deduplicated through.
346    ///
347    /// Exposed so a transport can answer "have you seen this id before?"
348    /// without running the tool, which is what a resuming peer wants: it has
349    /// dropped a connection and wants to know whether the call it never saw
350    /// answered actually happened.
351    #[must_use]
352    pub const fn idempotency(&self) -> &Arc<dyn IdempotencyStore<Result = JsonObject>> {
353        &self.idempotency
354    }
355
356    /// Registers a state value after the server is built.
357    ///
358    /// The builder API is `ServerBuilder::with_state`; this method
359    /// exists for callers that built a server through a path that did
360    /// not surface the builder (the `#[server]` macro's
361    /// `into_server` is the canonical one). Each call appends to
362    /// the existing map, so a server that already had state for one
363    /// type can grow state for a second.
364    ///
365    /// `Send + Sync + 'static` is what every other port in this crate
366    /// asks for; a pool of non-`Send` handles cannot live in this
367    /// map.
368    pub fn with_state<T: Send + Sync + 'static>(&mut self, value: T) {
369        Arc::make_mut(&mut self.state).insert(
370            std::any::TypeId::of::<T>(),
371            Arc::new(value) as Arc<dyn std::any::Any + Send + Sync>,
372        );
373    }
374
375    /// Adds a tool to a server that was already built.
376    ///
377    /// [`ServerBuilder::tool`] is the builder API and takes precedence. This
378    /// exists for the same reason [`with_state`](Self::with_state) does: a
379    /// caller who went through `#[server]`'s `into_server` never saw a
380    /// builder, and until this there was nothing they could do about it.
381    ///
382    /// It is not a convenience for schemas a Rust type can derive. `#[tool]`
383    /// builds `inputSchema` through `schemars` from the handler's parameters,
384    /// and `schemars` cannot express what MCP asks a tool to be able to say:
385    /// `$schema`, `$defs` with an `$anchor`, `allOf`/`anyOf`, or `if`/`then`/
386    /// `else`. Those are SEP-1613 and SEP-2106 — a tool whose schema another
387    /// toolchain generated and shipped as JSON has exactly one way to reach a
388    /// peer through this crate, and before this method it had none through
389    /// the front door.
390    ///
391    /// Registering and publishing together, as on the builder: a tool in
392    /// `tools/list` with no handler is one a client can see and not call, and
393    /// a handler with no entry is one that never runs. There is no state in
394    /// between for a caller to be wrong in.
395    ///
396    /// It announces nothing: `&mut self` means nothing can be connected yet.
397    /// To change a server that is already serving, use [`Self::handle`].
398    ///
399    /// Infallible, unlike [`Tool::new`]: a [`Tool`] has already had its
400    /// `inputSchema` parsed and validated, so there is no second parse here to
401    /// fail.
402    pub fn add_tool(&mut self, tool: Tool, handler: ToolHandler) {
403        let name = tool.name.clone();
404        let _published = self
405            .live
406            .add(Registered::from(Primitive::from(tool)), |table| {
407                table.tools.retain(|entry| entry.name != name);
408                table.tools.push(ToolEntry { name, handler });
409            });
410    }
411
412    /// The state map handlers read through
413    /// [`RequestContext::state`](crate::application::server::context::RequestContext::state).
414    ///
415    /// A transport hands this to every [`RequestContext`]
416    /// it builds. A handler that never calls `with_state` on the
417    /// builder gets an empty map here, and `state::<T>()` returns
418    /// `None` for every `T`.
419    #[must_use]
420    pub fn state(&self) -> &Arc<crate::application::server::context::StateMap> {
421        &self.state
422    }
423
424    /// The registry, for the transport that serves it.
425    ///
426    /// Takes `&self` and hands out an owned snapshot later, so a handler
427    /// publishing a new tool does not need the server borrowed mutably while
428    /// a request is in flight.
429    #[must_use]
430    pub fn registry(&self) -> &RegistryStore {
431        self.live.registry()
432    }
433
434    /// The handler for `name`, if there is one.
435    ///
436    /// Owned, because the table it came from can be replaced the moment this
437    /// returns: a handle may remove the tool while the caller still holds it.
438    #[must_use]
439    pub fn handler(&self, name: &str) -> Option<ToolHandler> {
440        self.live.tool(name)
441    }
442
443    /// The reader for `uri`, if one is registered.
444    ///
445    /// `None` for a resource that is listed but has no reader, which is a
446    /// state [`ServerBuilder::resource_with`] cannot produce — registration
447    /// takes both together — and which therefore means the URI is simply not
448    /// this server's.
449    #[must_use]
450    pub fn reader(&self, uri: &str) -> Option<ResourceHandler> {
451        self.live.reader(uri)
452    }
453
454    /// The getter for `name`, if one is registered.
455    ///
456    /// `None` under the same reasoning as [`reader`](Self::reader).
457    #[must_use]
458    pub fn getter(&self, name: &str) -> Option<PromptHandler> {
459        self.live.getter(name)
460    }
461
462    /// Finds `name` and runs it.
463    ///
464    /// The lookup and the call together rather than a `handler()` the caller
465    /// has to remember to use: a transport and an in-process test that each
466    /// wrote their own lookup would drift, and the drift would show up as a
467    /// tool that is in the registry and not callable.
468    ///
469    /// Visibility is decided by the **registry**, and only by the registry. It
470    /// used to be a second, cheaper check over a cached `required` set held by
471    /// each tool entry — which covered features and not scopes. So a
472    /// scope-restricted tool was absent from `tools/list` and still ran on
473    /// `tools/call`, which is the existence oracle
474    /// [`UnknownTool`](DispatchError::UnknownTool) exists to prevent.
475    ///
476    /// The cached set is gone rather than kept as a belt-and-braces second
477    /// check. Once the registry gate runs first, that check can only ever
478    /// agree with it, so it decides nothing — and a check that decides nothing
479    /// is a check that can drift into deciding the wrong thing without anyone
480    /// noticing. One predicate for the whole surface is the property worth
481    /// having.
482    ///
483    /// # Errors
484    ///
485    /// [`DispatchError::UnknownTool`] when no tool of that name is registered
486    /// **or** when this session may not see it — because it requires a
487    /// feature the profile lacks, or a scope the caller does not hold. The two
488    /// are deliberately the same answer: a different one for each would let a
489    /// caller read the existence of a tool it is not allowed to know about.
490    /// The server still logs which it was.
491    pub async fn call(
492        &self,
493        ctx: &RequestContext,
494        name: &str,
495        args: JsonObject,
496    ) -> Result<JsonObject, DispatchError> {
497        let handler = self.handler(name).ok_or(DispatchError::UnknownTool)?;
498
499        // The gate, once, for both halves of the surface. After this the tool
500        // is known to exist *and* to be visible to this session, so the
501        // handler is reached with no further checking to disagree with.
502        if self
503            .registry()
504            .get_visible(
505                ctx.peer().auth(),
506                &ctx.peer().profile(),
507                PrimitiveKind::Tool,
508                name,
509            )
510            .is_none()
511        {
512            return Err(DispatchError::UnknownTool);
513        }
514
515        Ok(handler(ctx.clone(), args).await)
516    }
517}
518
519/// Why a tool call did not run.
520#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
521pub enum DispatchError {
522    /// No tool of that name is registered.
523    ///
524    /// Deliberately the same answer for a tool that exists but is hidden from
525    /// this session: the difference would let a client probe for the existence
526    /// of a tool it may not see. The server still logs which it was.
527    #[error("no such tool")]
528    UnknownTool,
529
530    /// The tool is registered, but the session did not negotiate the features
531    /// it requires.
532    #[error("the session did not negotiate what this tool requires")]
533    NotCallable,
534
535    /// No method of that name exists.
536    ///
537    /// Refused on the method, not on a tool, because the caller named a method
538    /// and there is no tool to say anything about.
539    #[error("no such method")]
540    UnknownMethod,
541    /// No resource of that URI is registered, or it is listed without a
542    /// reader.
543    ///
544    /// Two causes share one answer because a peer cannot tell them apart: a
545    /// listing is a promise, so a resource that appears in `resources/list`
546    /// and then refuses to be read is the same failure as one that was never
547    /// there, and naming which of the two happened tells the peer nothing it
548    /// can act on.
549    #[error("no such resource")]
550    UnknownResource,
551    /// No prompt of that name is registered, or it is listed without a getter.
552    #[error("no such prompt")]
553    UnknownPrompt,
554    /// A registered reader or getter refused, naming why.
555    ///
556    /// Carries the handler's own message rather than a category, because the
557    /// peer is a client and the only thing it can do with a category is show it
558    /// to a user who would rather read what went wrong.
559    #[error("the handler refused: {0}")]
560    HandlerFailed(String),
561
562    /// `tools/call` params were not the shape a call has to be: not an object,
563    /// no string `name`, or an `arguments` that is not an object.
564    ///
565    /// A distinct variant from `UnknownTool` because a call with no `name` at
566    /// all has not failed to find a tool — it never named one, and answering
567    /// "no such tool" would send the caller looking for a tool that works.
568    #[error("the call parameters are not a tool call")]
569    MalformedParams,
570
571    /// `initialize` params were not the shape the handshake has to be.
572    ///
573    /// Separate from [`MalformedParams`](Self::MalformedParams) because the
574    /// two failures are not the same event: that one says a tool call named
575    /// nothing, and this one says the one request that establishes a session
576    /// arrived in a shape that would have to be guessed at. A peer sending it
577    /// has a bug worth naming, not a tool to go looking for.
578    ///
579    /// Covers a body that is not an object, a missing or non-string
580    /// `protocolVersion`, a string that is not a protocol version, a missing or
581    /// non-object `capabilities`, and a `clientInfo` with no `name`.
582    #[error("the initialize parameters are not a handshake")]
583    MalformedInitialize,
584
585    /// The session is on a revision this build has no wire types for.
586    ///
587    /// Distinct from a refused *method* ([`UnknownMethod`](Self::UnknownMethod))
588    /// because nothing about the request was wrong: the method exists, the
589    /// params parse, and the answer cannot be built because the revision the
590    /// session settled on is one [`negotiate_server`](crate::domain::negotiate_server) would have to counter-
591    /// offer. The session should never be in this state — it is reported
592    /// rather than papered over, because the alternative is emitting a result
593    /// whose shape the revision cannot express.
594    #[error("the session is on a revision this build cannot serve")]
595    UnnegotiableVersion,
596
597    /// A SEP-2575 request arrived without the `_meta` the schema requires.
598    ///
599    /// `2026-07-28` makes `RequestParams._meta` required on every request and
600    /// requires two keys in it — the revision and the client's capabilities —
601    /// because there is no handshake to carry them and no session to remember
602    /// them on. A request missing either is a well-formed message with a
603    /// missing argument, which is `-32602` and not a transport refusal.
604    ///
605    /// Its own variant rather than a reuse of
606    /// [`MalformedInitialize`](Self::MalformedInitialize) because the two are
607    /// different failures with different fixes: one says the handshake arrived
608    /// in a shape that would have to be guessed at, this says the shape is
609    /// right and a required key is not there.
610    #[error("the request `_meta` is missing a required key")]
611    MalformedRequestMeta,
612
613    /// A SEP-2575 request named a revision this build does not implement.
614    ///
615    /// Not [`MalformedParams`](Self::MalformedParams), and the difference is
616    /// the whole point of the code the specification assigns: `-32022` with
617    /// `data.supported` is what lets a client retry against a version this
618    /// server does speak, where `-32602` would send it looking for a bug in
619    /// its own request.
620    #[error("this server does not implement the requested protocol version")]
621    UnsupportedVersion {
622        /// The revision the client asked for, as it wrote it.
623        requested: String,
624        /// Every revision this build speaks, for the client to choose from.
625        supported: Vec<String>,
626    },
627
628    /// A second request arrived with an id this session is still working on.
629    ///
630    /// The JSON-RPC reading, and the reason it is an error rather than a
631    /// queue: the first request is a real, running, side-effecting call, and
632    /// the second carries no way to say what should happen to it. Running the
633    /// tool twice is the answer a retry gets by accident, and this is the
634    /// answer it gets on purpose. A client that wants the first result waits
635    /// for it and retries later — at which point the id is `Completed` and
636    /// comes straight back.
637    #[error("request {} is already in flight on this session", .0)]
638    DuplicateInFlight(String),
639
640    /// Serving the request needs client capabilities the request did not
641    /// declare (SEP-2575, `-32021`).
642    ///
643    /// Raised when a handler answers with an `InputRequiredResult` asking for
644    /// something the client said it cannot do: sending it anyway would ask a
645    /// client to sample or elicit after it declined to, and the specification
646    /// is explicit that a server "MUST NOT rely on capabilities the client has
647    /// not declared".
648    #[error("the request needs client capabilities it did not declare")]
649    MissingClientCapability {
650        /// The capability names, as `ClientCapabilities` keys.
651        required: Vec<&'static str>,
652    },
653
654    /// A SEP-2322 `requestState` did not verify.
655    ///
656    /// `-32602`, because the state is a request parameter and this one is not a
657    /// value this server issued. Refused before the handler runs, so no
658    /// handler can be made to act on a state a client wrote.
659    #[error("requestState failed its integrity check")]
660    TamperedRequestState,
661
662    /// The store holding recorded results could not be reached.
663    ///
664    /// Reported rather than swallowed because the alternative is the one thing
665    /// this whole path exists to prevent: a store that errors, a caller that
666    /// treats the error as "no claim", and a tool that runs a second time.
667    /// Failing the call is the only answer that does not charge a client twice
668    /// behind its back.
669    #[error("recorded results are unavailable: {0}")]
670    IdempotencyUnavailable(String),
671}
672
673impl DispatchError {
674    /// The JSON-RPC code this refusal is reported as.
675    ///
676    /// Lives here rather than in a transport adapter because it is a property
677    /// of the error, not of the way it is written down. It sat in the stdio
678    /// adapter until the HTTP router needed the same mapping, and the two
679    /// options at that point were both wrong: copy it and let the copies drift
680    /// apart, or reach into another adapter and break the layering.
681    ///
682    /// Every refusal gets an answer. The alternative is to drop it, which
683    /// leaves the peer waiting on a response that is never coming — and a
684    /// server that hangs is worse than one that says no. The codes are the
685    /// closest the standard offers: a method nobody serves is `-32601`, a call
686    /// that does not name something callable is `-32602`, and a session that
687    /// did not negotiate a feature — or one settled on a revision this build
688    /// cannot serve — is a server-side condition rather than either.
689    ///
690    /// A malformed `initialize` is `-32602` like any other malformed params:
691    /// the peer sent a request whose body it could not have meant, which is the
692    /// same condition whatever the method was.
693    ///
694    /// # Examples
695    ///
696    /// ```
697    /// # use protocol_droid::application::server::DispatchError;
698    /// # use protocol_droid::domain::ir::IrErrorCode;
699    /// assert_eq!(
700    ///     DispatchError::UnknownMethod.jsonrpc_code(),
701    ///     IrErrorCode::MethodNotFound,
702    /// );
703    /// assert_eq!(
704    ///     DispatchError::MalformedParams.jsonrpc_code(),
705    ///     IrErrorCode::InvalidParams,
706    /// );
707    /// ```
708    #[must_use]
709    pub const fn jsonrpc_code(&self) -> IrErrorCode {
710        match self {
711            Self::UnknownMethod => IrErrorCode::MethodNotFound,
712            // A URI or a prompt name that is not registered is the same
713            // failure as a tool name that is not, and answers the same way. The
714            // specification is explicit that an unknown tool is reported as an
715            // error rather than an empty result, and a client cannot act on a
716            // distinction between "no such thing" and "this thing is empty".
717            Self::MalformedParams
718            | Self::MalformedInitialize
719            | Self::UnknownTool
720            | Self::UnknownResource
721            | Self::UnknownPrompt
722            | Self::MalformedRequestMeta
723            | Self::TamperedRequestState => IrErrorCode::InvalidParams,
724            Self::MissingClientCapability { .. } => IrErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY,
725            // A request that names an id this session is already working on is
726            // a frame that violates JSON-RPC, so it takes the code for a frame
727            // the specification does not permit — not the code for a tool it
728            // cannot run, which is a different failure with a different fix.
729            Self::DuplicateInFlight(_) => IrErrorCode::InvalidRequest,
730            // A store that cannot be reached is this server's problem, whatever
731            // the peer sent, so it is a server-side code. Reporting it as the
732            // caller's fault would send a client looking for a bug in its own
733            // request when the recording of results is what failed.
734            Self::NotCallable | Self::UnnegotiableVersion | Self::IdempotencyUnavailable(_) => {
735                IrErrorCode::InternalError
736            }
737            // SEP-2575 assigns this its own code precisely so a client can
738            // distinguish "you asked for something real that I do not speak"
739            // from "your request was wrong", and act on the first by retrying.
740            Self::UnsupportedVersion { .. } => IrErrorCode::UNSUPPORTED_PROTOCOL_VERSION,
741            // A handler that refused is this server's problem whatever the peer
742            // sent. The peer asked for a resource or a prompt, named it
743            // correctly, and the answer is that the backing store is
744            // unreachable — reporting that as the caller's fault would send a
745            // client hunting in its own request for something it got right.
746            Self::HandlerFailed(_) => IrErrorCode::InternalError,
747        }
748    }
749
750    /// The `data` member this refusal's code requires, if it has one.
751    ///
752    /// `-32022` carries the revision the client asked for and every revision
753    /// this build speaks; `-32021` carries `requiredCapabilities` as a
754    /// `ClientCapabilities` object keyed by what is missing — not an array of
755    /// names, because the schema types it as the object a client declares.
756    /// Every other refusal carries none. One definition, so stdio and HTTP
757    /// cannot disagree about it.
758    #[must_use]
759    pub fn wire_data(&self) -> Option<JsonObject> {
760        let data = match self {
761            Self::UnsupportedVersion {
762                requested,
763                supported,
764            } => serde_json::json!({ "requested": requested, "supported": supported }),
765            Self::MissingClientCapability { required } => {
766                let capabilities: serde_json::Map<String, serde_json::Value> = required
767                    .iter()
768                    .map(|name| {
769                        (
770                            (*name).to_owned(),
771                            serde_json::Value::Object(serde_json::Map::new()),
772                        )
773                    })
774                    .collect();
775                serde_json::json!({ "requiredCapabilities": capabilities })
776            }
777            _ => return None,
778        };
779        JsonObject::from_value(data).ok()
780    }
781
782    /// The code and message pair a transport writes to the wire.
783    ///
784    /// The message is [`thiserror`]'s own for the variant, never the method
785    /// name the peer chose: a method name is a JSON string a peer controls, and
786    /// JSON permits a newline inside one, so echoing it into a log line would
787    /// let a peer close that line and open one of its own.
788    #[must_use]
789    pub fn as_wire(&self) -> (IrErrorCode, String) {
790        (self.jsonrpc_code(), self.to_string())
791    }
792}
793
794impl From<crate::application::server::result::OutcomeError> for DispatchError {
795    /// A reader or getter that could not produce a document failed.
796    ///
797    /// Every variant becomes [`DispatchError::HandlerFailed`] carrying the
798    /// outcome's own message. None of them is a bad request — the peer named a
799    /// resource or a prompt that exists and sent well-formed params — so
800    /// reporting any of them as `InvalidParams` would send a client looking in
801    /// its own request for something it got right.
802    fn from(error: crate::application::server::result::OutcomeError) -> Self {
803        Self::HandlerFailed(error.to_string())
804    }
805}
806
807impl From<IdempotencyError> for DispatchError {
808    /// Keeps a poisoned or absent store from reading as an absent claim.
809    fn from(error: IdempotencyError) -> Self {
810        Self::IdempotencyUnavailable(error.to_string())
811    }
812}
813
814/// The builder behind [`Server::builder`].
815pub struct ServerBuilder {
816    name: String,
817    version: String,
818    registry: RegistryStore,
819    tools: Vec<ToolEntry>,
820    /// Resources with their readers, in registration order.
821    ///
822    /// A `Vec` rather than a map because a server registers a handful of
823    /// resources and a linear scan over a few entries beats a hash of a
824    /// `PrimitiveId` that has to be computed first.
825    resource_reads: Vec<(PrimitiveId, ResourceHandler)>,
826    /// Prompts with their getters, in registration order.
827    prompt_gets: Vec<(PrimitiveId, PromptHandler)>,
828    idempotency: Option<Arc<dyn IdempotencyStore<Result = JsonObject>>>,
829    /// The state entries built up through `with_state` calls. Held as a
830    /// `Vec<(TypeId, Arc<dyn Any>)>` and re-keyed at build time because
831    /// the `TypeId` of `T` is only known to `with_state`, and pushing
832    /// into a `HashMap` keyed by a value the caller cannot see is the
833    /// wrong shape for a builder.
834    state: Vec<(std::any::TypeId, Arc<dyn std::any::Any + Send + Sync>)>,
835    state_key: Option<StateKey>,
836}
837
838impl core::fmt::Debug for ServerBuilder {
839    /// Prints the same three facts as [`Server`], plus whether a store was
840    /// supplied.
841    ///
842    /// The last is the one worth having here, and it is the question to ask
843    /// first when a recorded result has gone missing across a restart: a
844    /// server still on the default has an in-process bound, and no way to see
845    /// that from the built server it became.
846    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
847        f.debug_struct("ServerBuilder")
848            .field("name", &self.name)
849            .field("version", &self.version)
850            .field("tool_count", &self.tools.len())
851            .field("idempotency_supplied", &self.idempotency.is_some())
852            .finish_non_exhaustive()
853    }
854}
855
856impl ServerBuilder {
857    /// Sets the key SEP-2322 `requestState` is sealed under.
858    ///
859    /// Optional: a transport fills one from the OS random source when it is
860    /// absent. Set it explicitly when several replicas serve one endpoint —
861    /// they must share it, or a retry that lands on a different replica finds
862    /// its state refused.
863    #[must_use]
864    pub const fn request_state_key(mut self, key: StateKey) -> Self {
865        self.state_key = Some(key);
866        self
867    }
868
869    /// Sets the version this server reports in `serverInfo`.
870    ///
871    /// Defaults to the version of the crate that built it, which is right for
872    /// a server that *is* this crate — the examples, the conformance runner —
873    /// and wrong for one that merely links it. A library user shipping their
874    /// own server should say so here rather than let the dependency's version
875    /// stand in for theirs, which would have every downstream server claiming
876    /// to be whatever `protocol-droid` happened to be pinned at.
877    #[must_use]
878    pub fn version(mut self, version: impl Into<String>) -> Self {
879        self.version = version.into();
880        self
881    }
882
883    /// Replaces the store `tools/call` is deduplicated through.
884    ///
885    /// For a store that outlives the process — a shared cache, a table — or
886    /// for a test that wants to assert on what was recorded. A server that
887    /// keeps its own `IdSource` across restarts has a gap the in-memory
888    /// default cannot close: a call recorded before a restart is unknown
889    /// after it, and the retry runs the tool again.
890    #[must_use]
891    pub fn idempotency(mut self, store: Arc<dyn IdempotencyStore<Result = JsonObject>>) -> Self {
892        self.idempotency = Some(store);
893        self
894    }
895
896    /// Registers a value handlers can read through
897    /// [`RequestContext::state`](crate::application::server::context::RequestContext::state).
898    ///
899    /// `T` is keyed by `TypeId`, so two `with_state` calls with the same
900    /// `T` keep the later value (the registry is a `HashMap`, and the
901    /// later write is the one the lookup will see). The `Send + Sync`
902    /// bounds are the same every other port in this crate asks for; a
903    /// pool of non-`Send` handles cannot live in this map.
904    ///
905    /// The point of this method is the `#[tool]` problem: a macro that
906    /// generates module-scope items cannot see `self`, so the handler
907    /// it generates has no captured environment. Storing the state
908    /// here and reading it through the request context is the
909    /// standard answer, and the only one that keeps the macro
910    /// out of the receiver business.
911    #[must_use]
912    pub fn with_state<T: Send + Sync + 'static>(mut self, value: T) -> Self {
913        self.state.push((
914            std::any::TypeId::of::<T>(),
915            Arc::new(value) as Arc<dyn std::any::Any + Send + Sync>,
916        ));
917        self
918    }
919
920    /// Adds a tool, and publishes it to the registry in the same step.
921    ///
922    /// Takes a whole [`Tool`] rather than a name and a schema string, because
923    /// a schema string can only ever express the plainest tool there is. An
924    /// output schema, annotations and icons all change what the tool
925    /// _requires_ of a session, and a builder that cannot name them would
926    /// register a tool whose requirement nothing had computed.
927    ///
928    /// Registering and publishing together is the point: a tool in the list
929    /// but not in the registry is a tool a client can see and not call, and
930    /// one in neither is a handler that never runs. There is no state in
931    /// between for a caller to be wrong in.
932    #[must_use]
933    pub fn tool(self, tool: Tool, handler: ToolHandler) -> Self {
934        self.restricted_tool(Registered::from(Primitive::from(tool)), handler)
935    }
936
937    /// Adds a tool that carries its own requirements.
938    ///
939    /// [`tool`](Self::tool) takes a [`Tool`], and a `Tool` cannot say
940    /// "callers need the `files:read` scope" — that lives on [`Registered`],
941    /// which wraps a primitive with the policy it must satisfy. So without this
942    /// method the scope half of `is_visible` is reachable from no builder
943    /// call, `auth.rs` is dead code, and a server can be built that has no way
944    /// to express an authorisation rule at all. That is the same dead-gate
945    /// shape T11 found, one layer up.
946    ///
947    /// A `Registered` carrying something other than a tool is registered
948    /// under its own id and left out of the call table: it stays visible to
949    /// `tools/list` and the listing is the truth, so nothing is silently
950    /// dropped, but there is no handler to name for a `tools/call` of it.
951    #[must_use]
952    pub fn restricted_tool(mut self, registered: Registered, handler: ToolHandler) -> Self {
953        if let Primitive::Tool(tool) = registered.primitive() {
954            self.tools.push(ToolEntry {
955                name: tool.name.clone(),
956                handler,
957            });
958        }
959        self.registry.register(registered);
960        self
961    }
962
963    /// Adds a plain tool: a name, an input schema, and a handler.
964    ///
965    /// The common case, and the one a tool without an output schema or
966    /// annotations fits. It exists so the common case is one line rather than
967    /// a `Tool::new(...)?` at every call site, and it returns a `Result`
968    /// because the schema is the one fallible step.
969    /// # Errors
970    ///
971    /// The [`JsonObject`](crate::domain::ir::JsonObjectError) from parsing
972    /// `input_schema`. The schema is validated here, once, so a malformed one
973    /// fails at build time rather than on the first call.
974    pub fn simple_tool(
975        self,
976        name: impl Into<PrimitiveId>,
977        input_schema: &str,
978        handler: ToolHandler,
979    ) -> Result<Self, crate::domain::ir::JsonObjectError> {
980        Ok(self.tool(Tool::new(name, input_schema)?, handler))
981    }
982
983    /// Adds a resource.
984    ///
985    /// Adds a resource together with the reader that answers `resources/read`
986    /// for it.
987    ///
988    /// Both at once, and there is deliberately no `resource(resource)` beside
989    /// it. A resource that is listed by `resources/list` and then refused by
990    /// `resources/read` is a server telling a peer something false, and the
991    /// dispatcher has to answer `InvalidParams` for the read because it has no
992    /// other way to distinguish that from a URI the server never had. Making
993    /// the reader a separate opt-in would put that state one method call away,
994    /// and the failure would surface to a client rather than at the call site
995    /// that built it.
996    #[must_use]
997    pub fn resource_with(self, resource: Resource, reader: ResourceHandler) -> Self {
998        let mut builder = self;
999        builder.resource_reads.push((resource.uri.clone(), reader));
1000        builder
1001            .registry
1002            .register(Registered::from(Primitive::from(resource)));
1003        builder
1004    }
1005
1006    /// Adds a resource template.
1007    ///
1008    /// Templates are listed by their own method, and they are a distinct
1009    /// [`PrimitiveKind`] in the registry, so a template needs its own entry
1010    /// point rather than sharing `resource`'s.
1011    #[must_use]
1012    pub fn resource_template(self, template: ResourceTemplate) -> Self {
1013        self.registry
1014            .register(Registered::from(Primitive::from(template)));
1015        self
1016    }
1017
1018    /// Adds a prompt together with the getter that answers `prompts/get` for it.
1019    ///
1020    /// Required together for the reason [`resource_with`](Self::resource_with)
1021    /// takes both.
1022    #[must_use]
1023    pub fn prompt_with(self, prompt: Prompt, getter: PromptHandler) -> Self {
1024        let mut builder = self;
1025        builder.prompt_gets.push((prompt.name.clone(), getter));
1026        builder
1027            .registry
1028            .register(Registered::from(Primitive::from(prompt)));
1029        builder
1030    }
1031
1032    /// Finishes the server.
1033    #[must_use]
1034    pub fn build(self) -> Server {
1035        // Deduplicating by default rather than on request. A caller who has
1036        // never heard of idempotency is the caller most likely to be serving
1037        // a retriable HTTP transport, where a dropped connection turns one
1038        // call into two, and a store that has to be opted into is a store
1039        // nobody will think to opt into. The default is the bound that fails
1040        // safe: a server that forgets runs a tool once rather than twice.
1041        let idempotency = self.idempotency.unwrap_or_else(|| {
1042            // 4096 keys is generous for a single process and small enough to
1043            // bound memory. The capacity is a policy number, not a constant
1044            // with a meaning, so it is named rather than inlined.
1045            const DEFAULT_KEYS: NonZeroUsize = match NonZeroUsize::new(4096) {
1046                Some(value) => value,
1047                // Unreachable: 4096 is not zero. A `match` rather than an
1048                // `unwrap` because the crate denies that.
1049                None => NonZeroUsize::MIN,
1050            };
1051            Arc::new(InMemoryIdempotencyStore::with_capacity(DEFAULT_KEYS))
1052        });
1053        Server {
1054            name: self.name,
1055            version: self.version,
1056            live: Arc::new(Live::new(
1057                self.registry,
1058                Handlers {
1059                    tools: self.tools,
1060                    resources: self.resource_reads,
1061                    prompts: self.prompt_gets,
1062                },
1063            )),
1064            idempotency,
1065            state: Arc::new(
1066                self.state
1067                    .into_iter()
1068                    .collect::<std::collections::HashMap<_, _>>(),
1069            ),
1070            state_key: self.state_key,
1071        }
1072    }
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077    use std::sync::Arc;
1078
1079    use super::{DispatchError, Server};
1080    use crate::application::server::context::{PeerSession, RequestContext};
1081    use crate::application::session::SinkError;
1082    use crate::domain::feature::FeatureProfile;
1083    use crate::domain::ir::{JsonObject, JsonObjectError};
1084    use crate::domain::registry::PrimitiveKind;
1085    use crate::domain::session::{CancellationToken, SessionId};
1086    use crate::domain::{ProtocolVersion, RequestId, TransportKind};
1087
1088    /// What a test here can fail with.
1089    #[derive(Debug)]
1090    enum Failure {
1091        /// The test's own complaint.
1092        Message(String),
1093        /// A fixture was not a JSON object.
1094        Fixture(JsonObjectError),
1095    }
1096
1097    impl core::fmt::Display for Failure {
1098        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1099            match self {
1100                Self::Message(text) => f.write_str(text),
1101                Self::Fixture(error) => write!(f, "bad fixture: {error}"),
1102            }
1103        }
1104    }
1105
1106    impl std::error::Error for Failure {}
1107
1108    impl From<String> for Failure {
1109        fn from(text: String) -> Self {
1110            Self::Message(text)
1111        }
1112    }
1113
1114    impl From<&str> for Failure {
1115        fn from(text: &str) -> Self {
1116            Self::Message(text.to_string())
1117        }
1118    }
1119
1120    impl From<JsonObjectError> for Failure {
1121        fn from(error: JsonObjectError) -> Self {
1122            Self::Fixture(error)
1123        }
1124    }
1125
1126    type TestError = Failure;
1127
1128    /// A handler that answers with a fixed object, so a test can tell one tool
1129    /// from another without every case writing its own closure.
1130    fn answering(answer: &str) -> Result<super::ToolHandler, TestError> {
1131        let answer = JsonObject::new(answer)?;
1132        Ok(Arc::new(move |_ctx: RequestContext, _args: JsonObject| {
1133            let answer = answer.clone();
1134            Box::pin(async move { answer })
1135        }))
1136    }
1137
1138    /// A port pair that answers nothing and records nothing.
1139    ///
1140    /// The builder tests never elicit, sample, or emit progress — they are
1141    /// about registration and dispatch — so the ports exist only to satisfy
1142    /// `PeerSession`. A `RefCell` is safe here precisely because nothing
1143    /// touches them, and a real channel would be a second thing to get wrong
1144    /// in a test that is not about channels.
1145    struct Inert;
1146
1147    impl crate::application::session::FrameSink for Inert {
1148        fn send(&self, _frame: &str) -> impl Future<Output = Result<(), SinkError>> + Send {
1149            std::future::ready(Ok(()))
1150        }
1151    }
1152
1153    impl crate::application::server::context::CallbackExchange for Inert {
1154        fn exchange(
1155            &self,
1156            _method: &str,
1157            _params: &JsonObject,
1158        ) -> impl Future<Output = Result<JsonObject, crate::application::server::ExchangeError>> + Send
1159        {
1160            std::future::ready(Err(crate::application::server::ExchangeError::new(
1161                "inert in these tests",
1162            )))
1163        }
1164    }
1165
1166    fn peer_at(profile: FeatureProfile) -> PeerSession {
1167        PeerSession::new(
1168            ProtocolVersion::V2025_06_18,
1169            profile,
1170            SessionId::new(1),
1171            TransportKind::Stdio,
1172            Arc::new(Inert),
1173            Arc::new(Inert),
1174        )
1175    }
1176
1177    fn ctx_for(profile: FeatureProfile) -> RequestContext {
1178        RequestContext::for_request(
1179            &peer_at(profile),
1180            RequestId::number(1),
1181            CancellationToken::new(),
1182            None,
1183        )
1184    }
1185
1186    /// A tool added to the builder is in the registry in the same step.
1187    ///
1188    /// The builder's claim is that there is no state in between — no tool that
1189    /// is listable but not callable. A registry the builder has to be told
1190    /// about separately cannot make that claim, so this is the test of the
1191    /// claim and not a restatement of the code.
1192    #[test]
1193    fn a_registered_tool_is_in_the_registry_immediately() -> Result<(), TestError> {
1194        let server = Server::builder("demo")
1195            .simple_tool("echo", r#"{"type":"object"}"#, answering("{}")?)?
1196            .build();
1197
1198        assert_eq!(server.tool_count(), 1);
1199        assert_eq!(server.registry().snapshot().len(PrimitiveKind::Tool), 1);
1200        Ok(())
1201    }
1202
1203    /// A tool added after `into_server` lists *and* runs.
1204    ///
1205    /// Both halves, because the registry and the handler table are two places
1206    /// a method like this could write to only one of. A tool in `tools/list`
1207    /// with no handler is one a client can see and not call — the failure mode
1208    /// the builder's own doc calls out — and it would pass a listing-only test.
1209    #[test]
1210    fn a_tool_added_after_the_build_lists_and_runs() -> Result<(), TestError> {
1211        use crate::domain::registry::{Primitive, Tool};
1212
1213        let mut server = Server::builder("demo")
1214            .simple_tool("echo", r#"{"type":"object"}"#, answering("{}")?)?
1215            .build();
1216
1217        // The schema is the one thing `add_tool` exists for: `$defs`, an
1218        // `$anchor` and `if`/`then`/`else` are JSON Schema 2020-12 keywords no
1219        // Rust type derives, so this is the whole reason the method is here.
1220        let raw = Tool::new(
1221            "json_schema_2020_12",
1222            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"]}}"#,
1223        )?;
1224        server.add_tool(raw, answering(r#"{"ok":true}"#)?);
1225
1226        assert_eq!(server.tool_count(), 2);
1227        assert_eq!(server.registry().snapshot().len(PrimitiveKind::Tool), 2);
1228        assert!(server.handler("json_schema_2020_12").is_some());
1229
1230        // The listing, not just the count: the schema has to reach `tools/list`
1231        // with the keywords intact, since that is the wire the suite reads.
1232        let snapshot = server.registry().snapshot();
1233        let advertised = snapshot
1234            .list(PrimitiveKind::Tool)
1235            .iter()
1236            .find(|tool| tool.id().as_str() == "json_schema_2020_12")
1237            .ok_or("the added tool is not in the registry")?;
1238        let Primitive::Tool(advertised) = advertised.primitive() else {
1239            return Err("a tool listing held something that is not a tool".into());
1240        };
1241        let text = advertised.input_schema.as_str();
1242        for keyword in ["$schema", "$defs", "$anchor", "if", "then"] {
1243            if !text.contains(keyword) {
1244                return Err(format!("{keyword} did not reach the listing").into());
1245            }
1246        }
1247        Ok(())
1248    }
1249
1250    /// A malformed input schema fails at build time, not on the first call.
1251    #[test]
1252    fn a_malformed_input_schema_is_rejected_at_build_time() -> Result<(), TestError> {
1253        // The assertion is on the `Result` itself rather than on an error
1254        // message: the error's text is not the contract, the timing of the
1255        // failure is. A test that unwrapped and matched on a string would pass
1256        // against a builder that validated for the wrong reason.
1257        let handler = answering("{}")?;
1258        let built = Server::builder("demo").simple_tool("bad", "{not json", handler);
1259
1260        match built {
1261            Ok(_) => Err(Failure::from(
1262                "a malformed input schema was accepted at build time",
1263            )),
1264            Err(_) => Ok(()),
1265        }
1266    }
1267
1268    /// Two tools with one name is one tool, not two.
1269    ///
1270    /// T11's store treats a re-registration as a replacement, and the builder
1271    /// has to agree or the registry would list a name the dispatcher cannot
1272    /// resolve to a single handler.
1273    #[test]
1274    fn a_second_tool_of_the_same_name_replaces_the_first() -> Result<(), TestError> {
1275        let server = Server::builder("demo")
1276            .simple_tool(
1277                "echo",
1278                r#"{"type":"object"}"#,
1279                answering(r#"{"first":true}"#)?,
1280            )?
1281            .simple_tool(
1282                "echo",
1283                r#"{"type":"object"}"#,
1284                answering(r#"{"second":true}"#)?,
1285            )?
1286            .build();
1287
1288        assert_eq!(server.tool_count(), 2, "both handlers are kept");
1289        assert_eq!(
1290            server.registry().snapshot().len(PrimitiveKind::Tool),
1291            1,
1292            "but the registry holds one name"
1293        );
1294        Ok(())
1295    }
1296
1297    /// An unknown tool is refused without reaching any handler.
1298    #[tokio::test]
1299    async fn an_unknown_tool_is_refused() -> Result<(), TestError> {
1300        let server = Server::builder("demo")
1301            .simple_tool("echo", r#"{"type":"object"}"#, answering("{}")?)?
1302            .build();
1303        let ctx = ctx_for(FeatureProfile::all());
1304
1305        match server.call(&ctx, "nope", JsonObject::new("{}")?).await {
1306            Ok(answer) => Err(Failure::from(format!("expected a refusal, got {answer}"))),
1307            Err(DispatchError::UnknownTool) => Ok(()),
1308            Err(other) => Err(Failure::from(format!(
1309                "refused for the wrong reason: {other}"
1310            ))),
1311        }
1312    }
1313
1314    /// A registered tool whose required features the session lacks is refused
1315    /// by `call`, not run and refused afterwards.
1316    ///
1317    /// A tool with an output schema requires `ToolStructuredOutput`. A session
1318    /// that never negotiated it must not run the tool and then drop the part
1319    /// of the answer the client could not have used.
1320    #[tokio::test]
1321    async fn a_tool_needing_an_absent_feature_is_not_run() -> Result<(), TestError> {
1322        use crate::domain::registry::Tool;
1323
1324        // The full `Tool`, not `simple_tool`: an output schema is what raises
1325        // the requirement in the first place, and `simple_tool` could not
1326        // express one. A test written against it would be testing a gap.
1327        let structured = Tool::new("echo", r#"{"type":"object"}"#)?
1328            .with_output_schema(r#"{"type":"object"}"#)?;
1329
1330        let server = Server::builder("demo")
1331            .tool(structured, answering(r#"{"ran":true}"#)?)
1332            .build();
1333
1334        let ctx = ctx_for(FeatureProfile::empty());
1335        match server.call(&ctx, "echo", JsonObject::new("{}")?).await {
1336            Ok(answer) => Err(Failure::from(format!(
1337                "a tool the session cannot call returned {answer}"
1338            ))),
1339            // `UnknownTool`, not `NotCallable`, and the distinction is the
1340            // point. A tool this session may not see is a tool this session is
1341            // not told about: answering "it exists but you may not have it"
1342            // would confirm it exists, and a caller could then enumerate the
1343            // registry by watching which names come back refused. The same
1344            // answer as a tool that is not there is what makes the refusal
1345            // probe-proof.
1346            Err(DispatchError::UnknownTool) => Ok(()),
1347            Err(other) => Err(Failure::from(format!(
1348                "refused for the wrong reason: {other}"
1349            ))),
1350        }
1351    }
1352}