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}