Skip to content

Commit dd2cf24

Browse files
committed
feat(anthropic-ext): test_support::request_context_for_test()
Public test helper for unit-testing custom ServerHandler::call_tool overrides from outside the rmcp crate. Workspace MCPs that wrap their tool_router in Arc<RwLock<ToolRouter<Self>>> (to support runtime mutation via load_tool_group) cannot use #[tool_handler] and must hand-roll call_tool — that dispatch path bypasses ToolRouter::call and therefore the post-dispatch normalize_call_tool_result hook, so each consumer must call the normalizer manually inside its hand-rolled call_tool. This helper makes it possible to drive the custom dispatch path from a unit test without spinning up a full serve_server / transport stack. Internally constructs a throwaway Peer<RoleServer> via the crate-internal Peer::new constructor; the receiving channel is dropped, so any peer-bound notifications the tool body emits silently no-op. Gated on the existing server feature.
1 parent 3d17418 commit dd2cf24

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

crates/rmcp/src/anthropic_ext.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,73 @@ pub fn normalize_call_tool_result(result: &mut CallToolResult) {
522522
}
523523
}
524524

525+
// ---------------------------------------------------------------------------
526+
// Test support — public helpers for unit-testing custom `ServerHandler` impls
527+
// ---------------------------------------------------------------------------
528+
529+
/// Public test helpers for unit-testing custom [`ServerHandler`](crate::ServerHandler)
530+
/// impls — in particular hand-rolled `call_tool` overrides — from outside the
531+
/// `rmcp` crate.
532+
///
533+
/// Workspace MCPs that wrap their `tool_router` in
534+
/// `Arc<RwLock<ToolRouter<Self>>>` (to support runtime mutation, e.g.
535+
/// `load_tool_group`) cannot use rmcp's `#[tool_handler]` macro and must
536+
/// hand-roll [`ServerHandler::call_tool`](crate::ServerHandler::call_tool).
537+
/// That dispatch path bypasses [`ToolRouter::call`](crate::handler::server::router::tool::ToolRouter)
538+
/// and therefore the post-dispatch [`normalize_call_tool_result`] hook —
539+
/// each consumer must call the normalizer manually inside its hand-rolled
540+
/// `call_tool`. The helpers in this module make it possible to drive that
541+
/// custom dispatch path from a unit test without spinning up a full
542+
/// `serve_server` / transport stack.
543+
///
544+
/// All helpers are gated on the `server` feature; the throwaway peer's
545+
/// outbound channel is dropped immediately, so any peer-bound notifications
546+
/// the tool body emits silently no-op.
547+
#[cfg(feature = "server")]
548+
pub mod test_support {
549+
use std::sync::Arc;
550+
551+
use crate::{
552+
RoleServer,
553+
model::NumberOrString,
554+
service::{AtomicU32RequestIdProvider, Peer, RequestContext, RequestIdProvider},
555+
};
556+
557+
/// Build a [`RequestContext<RoleServer>`] suitable for unit-testing
558+
/// [`ServerHandler::call_tool`](crate::ServerHandler::call_tool) (and
559+
/// other server methods that take a request context).
560+
///
561+
/// Internally constructs a throwaway [`Peer<RoleServer>`] via the
562+
/// crate-internal constructor; the receiving channel is dropped, so any
563+
/// peer-bound notifications the tool body emits silently no-op. The
564+
/// request id is fixed to `1`.
565+
///
566+
/// # Example
567+
///
568+
/// ```ignore
569+
/// use rmcp::{
570+
/// ServerHandler,
571+
/// anthropic_ext::test_support::request_context_for_test,
572+
/// model::CallToolRequestParams,
573+
/// };
574+
///
575+
/// # async fn smoke<S: ServerHandler>(server: &S) {
576+
/// let req: CallToolRequestParams =
577+
/// serde_json::from_value(serde_json::json!({ "name": "ping" })).unwrap();
578+
/// let ctx = request_context_for_test();
579+
/// let result = server.call_tool(req, ctx).await.expect("call_tool");
580+
/// // ... assertions ...
581+
/// # }
582+
/// ```
583+
#[must_use]
584+
pub fn request_context_for_test() -> RequestContext<RoleServer> {
585+
let id_provider: Arc<dyn RequestIdProvider> =
586+
Arc::new(AtomicU32RequestIdProvider::default());
587+
let (peer, _rx) = Peer::<RoleServer>::new(id_provider, None);
588+
RequestContext::new(NumberOrString::Number(1), peer)
589+
}
590+
}
591+
525592
// ---------------------------------------------------------------------------
526593
// Lint helpers
527594
// ---------------------------------------------------------------------------
@@ -814,6 +881,21 @@ mod tests {
814881
);
815882
}
816883

884+
#[cfg(feature = "server")]
885+
#[test]
886+
fn test_support_request_context_constructs() {
887+
// Sanity check: the public test helper produces a usable
888+
// RequestContext<RoleServer> that consumer crates can pass into a
889+
// hand-rolled ServerHandler::call_tool from unit tests.
890+
let ctx = test_support::request_context_for_test();
891+
match ctx.id {
892+
crate::model::NumberOrString::Number(n) => assert_eq!(n, 1),
893+
crate::model::NumberOrString::String(_) => {
894+
panic!("request_context_for_test() should produce a numeric id")
895+
}
896+
}
897+
}
898+
817899
// ----- permission relay -------------------------------------------------
818900

819901
#[test]

0 commit comments

Comments
 (0)