Skip to content

Commit 63583b1

Browse files
authored
feat(router): support runtime disabling of tools (modelcontextprotocol#809)
* feat(router): support runtime disabling of tools Add methods to disable/enable tools at runtime. Disabled tools are hidden from listing, lookup, and execution, including in composed routers. Closes modelcontextprotocol#477 * fix(router): simplify disable tool api * feat(router): auto-send tools/list_changed on disable/enable * refactor(router): simplify disable_route and notifier call
1 parent 8f696e6 commit 63583b1

4 files changed

Lines changed: 750 additions & 15 deletions

File tree

crates/rmcp/src/handler/server/router.rs

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use tool::{IntoToolRoute, ToolRoute};
66
use super::ServerHandler;
77
use crate::{
88
RoleServer, Service,
9-
model::{ClientRequest, ListPromptsResult, ListToolsResult, ServerResult},
9+
model::{ClientNotification, ClientRequest, ListPromptsResult, ListToolsResult, ServerResult},
1010
service::NotificationContext,
1111
};
1212

@@ -18,17 +18,22 @@ pub struct Router<S> {
1818
pub tool_router: tool::ToolRouter<S>,
1919
pub prompt_router: prompt::PromptRouter<S>,
2020
pub service: Arc<S>,
21+
peer_slot: Arc<std::sync::OnceLock<crate::service::Peer<RoleServer>>>,
2122
}
2223

2324
impl<S> Router<S>
2425
where
2526
S: ServerHandler,
2627
{
2728
pub fn new(service: S) -> Self {
29+
let (notifier, peer_slot) = tool::ToolRouter::<S>::deferred_peer_notifier();
30+
let mut tool_router = tool::ToolRouter::new();
31+
tool_router.set_notifier(notifier);
2832
Self {
29-
tool_router: tool::ToolRouter::new(),
33+
tool_router,
3034
prompt_router: prompt::PromptRouter::new(),
3135
service: Arc::new(service),
36+
peer_slot,
3237
}
3338
}
3439

@@ -72,6 +77,12 @@ where
7277
notification: <RoleServer as crate::service::ServiceRole>::PeerNot,
7378
context: NotificationContext<RoleServer>,
7479
) -> Result<(), crate::ErrorData> {
80+
if matches!(
81+
&notification,
82+
ClientNotification::InitializedNotification(_)
83+
) {
84+
let _ = self.peer_slot.set(context.peer.clone());
85+
}
7586
self.service
7687
.handle_notification(notification, context)
7788
.await
@@ -83,7 +94,10 @@ where
8394
) -> Result<<RoleServer as crate::service::ServiceRole>::Resp, crate::ErrorData> {
8495
match request {
8596
ClientRequest::CallToolRequest(request) => {
86-
if self.tool_router.has_route(request.params.name.as_ref())
97+
if self
98+
.tool_router
99+
.map
100+
.contains_key(request.params.name.as_ref())
87101
|| !self.tool_router.transparent_when_not_found
88102
{
89103
let tool_call_context = crate::handler::server::tool::ToolCallContext::new(
@@ -134,6 +148,81 @@ where
134148
}
135149

136150
fn get_info(&self) -> <RoleServer as crate::service::ServiceRole>::Info {
137-
ServerHandler::get_info(&self.service)
151+
let mut info = ServerHandler::get_info(&self.service);
152+
info.capabilities
153+
.tools
154+
.get_or_insert_with(Default::default)
155+
.list_changed = Some(true);
156+
info
157+
}
158+
}
159+
160+
#[cfg(test)]
161+
mod tests {
162+
use std::sync::Arc;
163+
164+
use super::*;
165+
use crate::{
166+
model::{CallToolResult, ClientNotification, ServerNotification, Tool},
167+
service::{AtomicU32RequestIdProvider, Peer, PeerSinkMessage, RequestIdProvider},
168+
};
169+
170+
struct DummyHandler;
171+
impl ServerHandler for DummyHandler {}
172+
173+
async fn recv_notification(
174+
rx: &mut tokio::sync::mpsc::Receiver<PeerSinkMessage<RoleServer>>,
175+
) -> ServerNotification {
176+
let msg = tokio::time::timeout(std::time::Duration::from_secs(1), rx.recv())
177+
.await
178+
.expect("timed out")
179+
.expect("channel closed");
180+
match msg {
181+
PeerSinkMessage::Notification {
182+
notification,
183+
responder,
184+
} => {
185+
let _ = responder.send(Ok(()));
186+
notification
187+
}
188+
other => panic!("expected notification, got {other:?}"),
189+
}
190+
}
191+
192+
#[tokio::test]
193+
async fn test_router_deferred_notifier_e2e() {
194+
let mut router = Router::new(DummyHandler).with_tool(tool::ToolRoute::new_dyn(
195+
Tool::new("my_tool", "test", Arc::new(Default::default())),
196+
|_ctx| Box::pin(async { Ok(CallToolResult::default()) }),
197+
));
198+
199+
let id_provider: Arc<dyn RequestIdProvider> =
200+
Arc::new(AtomicU32RequestIdProvider::default());
201+
let (peer, mut rx) = Peer::<RoleServer>::new(id_provider, None);
202+
203+
let context = crate::service::NotificationContext {
204+
peer: peer.clone(),
205+
meta: Default::default(),
206+
extensions: Default::default(),
207+
};
208+
router
209+
.handle_notification(
210+
ClientNotification::InitializedNotification(Default::default()),
211+
context,
212+
)
213+
.await
214+
.unwrap();
215+
216+
router.tool_router.disable_route("my_tool");
217+
assert!(matches!(
218+
recv_notification(&mut rx).await,
219+
ServerNotification::ToolListChangedNotification(_)
220+
));
221+
222+
router.tool_router.enable_route("my_tool");
223+
assert!(matches!(
224+
recv_notification(&mut rx).await,
225+
ServerNotification::ToolListChangedNotification(_)
226+
));
138227
}
139228
}

0 commit comments

Comments
 (0)