1use std::collections::HashMap;
24use std::convert::Infallible;
25use std::future::Future;
26use std::pin::Pin;
27use std::sync::Arc;
28use std::task::{Context, Poll};
29
30use tower::{Layer, Service};
31use tower_mcp::router::{RouterRequest, RouterResponse};
32use tower_mcp_types::protocol::{McpRequest, McpResponse};
33
34#[derive(Clone)]
49pub struct ParamOverrideLayer {
50 overrides: Vec<ToolOverride>,
51}
52
53impl ParamOverrideLayer {
54 pub fn new(overrides: Vec<ToolOverride>) -> Self {
56 Self { overrides }
57 }
58}
59
60impl<S> Layer<S> for ParamOverrideLayer {
61 type Service = ParamOverrideService<S>;
62
63 fn layer(&self, inner: S) -> Self::Service {
64 ParamOverrideService::new(inner, self.overrides.clone())
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct ToolOverride {
71 namespaced_tool: String,
73 hide: Vec<String>,
75 defaults: serde_json::Map<String, serde_json::Value>,
77 rename_forward: HashMap<String, String>,
79 rename_reverse: HashMap<String, String>,
81}
82
83impl ToolOverride {
84 pub fn new(namespace: &str, config: &crate::config::ParamOverrideConfig) -> Self {
86 let rename_forward: HashMap<String, String> = config.rename.clone();
87 let rename_reverse: HashMap<String, String> = config
88 .rename
89 .iter()
90 .map(|(orig, new)| (new.clone(), orig.clone()))
91 .collect();
92
93 Self {
94 namespaced_tool: format!("{namespace}{}", config.tool),
95 hide: config.hide.clone(),
96 defaults: config.defaults.clone(),
97 rename_forward,
98 rename_reverse,
99 }
100 }
101}
102
103#[derive(Clone)]
109pub struct ParamOverrideService<S> {
110 inner: S,
111 overrides: Arc<Vec<ToolOverride>>,
112}
113
114impl<S> ParamOverrideService<S> {
115 pub fn new(inner: S, overrides: Vec<ToolOverride>) -> Self {
117 Self {
118 inner,
119 overrides: Arc::new(overrides),
120 }
121 }
122}
123
124fn rewrite_schema(
126 schema: &mut serde_json::Value,
127 hide: &[String],
128 rename_forward: &HashMap<String, String>,
129) {
130 let Some(obj) = schema.as_object_mut() else {
131 return;
132 };
133
134 if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
136 for param in hide {
137 props.remove(param);
138 }
139 for (original, renamed) in rename_forward {
140 if let Some(prop_schema) = props.remove(original) {
141 props.insert(renamed.clone(), prop_schema);
142 }
143 }
144 }
145
146 if let Some(required) = obj.get_mut("required").and_then(|v| v.as_array_mut()) {
148 required.retain(|v| {
149 v.as_str()
150 .map(|s| !hide.contains(&s.to_string()))
151 .unwrap_or(true)
152 });
153 for entry in required.iter_mut() {
154 if let Some(s) = entry.as_str()
155 && let Some(new_name) = rename_forward.get(s)
156 {
157 *entry = serde_json::Value::String(new_name.clone());
158 }
159 }
160 }
161}
162
163impl<S> Service<RouterRequest> for ParamOverrideService<S>
164where
165 S: Service<RouterRequest, Response = RouterResponse, Error = Infallible>
166 + Clone
167 + Send
168 + 'static,
169 S::Future: Send,
170{
171 type Response = RouterResponse;
172 type Error = Infallible;
173 type Future = Pin<Box<dyn Future<Output = Result<RouterResponse, Infallible>> + Send>>;
174
175 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
176 self.inner.poll_ready(cx)
177 }
178
179 fn call(&mut self, mut req: RouterRequest) -> Self::Future {
180 let overrides = Arc::clone(&self.overrides);
181
182 if let McpRequest::CallTool(ref mut params) = req.inner {
184 for tool_override in overrides.iter() {
185 if params.name != tool_override.namespaced_tool {
186 continue;
187 }
188
189 if let serde_json::Value::Object(ref mut args) = params.arguments {
191 for (key, value) in &tool_override.defaults {
192 if !args.contains_key(key) {
193 args.insert(key.clone(), value.clone());
194 }
195 }
196
197 let keys_to_rename: Vec<(String, String)> = args
199 .keys()
200 .filter_map(|k| {
201 tool_override
202 .rename_reverse
203 .get(k)
204 .map(|orig| (k.clone(), orig.clone()))
205 })
206 .collect();
207
208 for (new_name, original_name) in keys_to_rename {
209 if let Some(value) = args.remove(&new_name) {
210 args.insert(original_name, value);
211 }
212 }
213 }
214
215 break;
216 }
217 }
218
219 let fut = self.inner.call(req);
220
221 Box::pin(async move {
222 let mut resp = fut.await?;
223
224 if let Ok(McpResponse::ListTools(ref mut result)) = resp.inner {
226 for tool in &mut result.tools {
227 for tool_override in overrides.iter() {
228 if tool.name == tool_override.namespaced_tool {
229 rewrite_schema(
230 &mut tool.input_schema,
231 &tool_override.hide,
232 &tool_override.rename_forward,
233 );
234 break;
235 }
236 }
237 }
238 }
239
240 Ok(resp)
241 })
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::config::ParamOverrideConfig;
249 use crate::test_util::{MockService, call_service};
250 use tower_mcp_types::protocol::{CallToolParams, McpRequest, McpResponse};
251
252 fn mock_with_schema(name: &str, schema: serde_json::Value) -> MockService {
254 use tower_mcp_types::protocol::ToolDefinition;
255 MockService {
256 tools: vec![ToolDefinition {
257 name: name.to_string(),
258 title: None,
259 description: Some(format!("{name} tool")),
260 input_schema: schema,
261 output_schema: None,
262 icons: None,
263 annotations: None,
264 execution: None,
265 meta: None,
266 }],
267 }
268 }
269
270 fn list_dir_schema() -> serde_json::Value {
271 serde_json::json!({
272 "type": "object",
273 "properties": {
274 "path": { "type": "string" },
275 "recursive": { "type": "boolean" },
276 "pattern": { "type": "string" }
277 },
278 "required": ["path"]
279 })
280 }
281
282 fn make_overrides(namespace: &str, configs: Vec<ParamOverrideConfig>) -> Vec<ToolOverride> {
283 configs
284 .iter()
285 .map(|c| ToolOverride::new(namespace, c))
286 .collect()
287 }
288
289 #[tokio::test]
290 async fn test_hide_removes_param_from_schema() {
291 let mock = mock_with_schema("fs/list_directory", list_dir_schema());
292 let overrides = make_overrides(
293 "fs/",
294 vec![ParamOverrideConfig {
295 tool: "list_directory".to_string(),
296 hide: vec!["path".to_string()],
297 defaults: {
298 let mut m = serde_json::Map::new();
299 m.insert("path".to_string(), serde_json::json!("/home/docs"));
300 m
301 },
302 rename: HashMap::new(),
303 }],
304 );
305 let mut svc = ParamOverrideService::new(mock, overrides);
306
307 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
308 match resp.inner.unwrap() {
309 McpResponse::ListTools(result) => {
310 let tool = &result.tools[0];
311 let props = tool.input_schema["properties"].as_object().unwrap();
312 assert!(
313 !props.contains_key("path"),
314 "path should be hidden from schema"
315 );
316 assert!(props.contains_key("recursive"), "recursive should remain");
317 assert!(props.contains_key("pattern"), "pattern should remain");
318 let required = tool.input_schema["required"].as_array().unwrap();
320 let req_strs: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
321 assert!(!req_strs.contains(&"path"), "path should not be required");
322 }
323 other => panic!("expected ListTools, got: {:?}", other),
324 }
325 }
326
327 #[tokio::test]
328 async fn test_hide_injects_defaults_on_call() {
329 let mock = mock_with_schema("fs/list_directory", list_dir_schema());
330 let overrides = make_overrides(
331 "fs/",
332 vec![ParamOverrideConfig {
333 tool: "list_directory".to_string(),
334 hide: vec!["path".to_string()],
335 defaults: {
336 let mut m = serde_json::Map::new();
337 m.insert("path".to_string(), serde_json::json!("/home/docs"));
338 m
339 },
340 rename: HashMap::new(),
341 }],
342 );
343 let mut svc = ParamOverrideService::new(mock, overrides);
344
345 let resp = call_service(
346 &mut svc,
347 McpRequest::CallTool(CallToolParams {
348 name: "fs/list_directory".to_string(),
349 arguments: serde_json::json!({"recursive": true}),
350 input_responses: None,
351 request_state: None,
352 meta: None,
353 task: None,
354 }),
355 )
356 .await;
357
358 assert!(resp.inner.is_ok(), "call should succeed");
359 }
360
361 #[tokio::test]
362 async fn test_rename_rewrites_schema() {
363 let mock = mock_with_schema("fs/list_directory", list_dir_schema());
364 let overrides = make_overrides(
365 "fs/",
366 vec![ParamOverrideConfig {
367 tool: "list_directory".to_string(),
368 hide: vec![],
369 defaults: serde_json::Map::new(),
370 rename: {
371 let mut m = HashMap::new();
372 m.insert("recursive".to_string(), "deep_search".to_string());
373 m
374 },
375 }],
376 );
377 let mut svc = ParamOverrideService::new(mock, overrides);
378
379 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
380 match resp.inner.unwrap() {
381 McpResponse::ListTools(result) => {
382 let tool = &result.tools[0];
383 let props = tool.input_schema["properties"].as_object().unwrap();
384 assert!(
385 !props.contains_key("recursive"),
386 "recursive should be renamed"
387 );
388 assert!(
389 props.contains_key("deep_search"),
390 "deep_search should appear"
391 );
392 assert!(props.contains_key("path"), "path should remain");
393 }
394 other => panic!("expected ListTools, got: {:?}", other),
395 }
396 }
397
398 #[tokio::test]
399 async fn test_rename_reverse_maps_on_call() {
400 let mock = mock_with_schema("fs/list_directory", list_dir_schema());
401 let overrides = make_overrides(
402 "fs/",
403 vec![ParamOverrideConfig {
404 tool: "list_directory".to_string(),
405 hide: vec![],
406 defaults: serde_json::Map::new(),
407 rename: {
408 let mut m = HashMap::new();
409 m.insert("recursive".to_string(), "deep_search".to_string());
410 m
411 },
412 }],
413 );
414 let mut svc = ParamOverrideService::new(mock, overrides);
415
416 let resp = call_service(
418 &mut svc,
419 McpRequest::CallTool(CallToolParams {
420 name: "fs/list_directory".to_string(),
421 arguments: serde_json::json!({"path": "/tmp", "deep_search": true}),
422 input_responses: None,
423 request_state: None,
424 meta: None,
425 task: None,
426 }),
427 )
428 .await;
429
430 assert!(resp.inner.is_ok(), "call should succeed");
431 }
432
433 #[tokio::test]
434 async fn test_hide_and_rename_combined() {
435 let mock = mock_with_schema("fs/list_directory", list_dir_schema());
436 let overrides = make_overrides(
437 "fs/",
438 vec![ParamOverrideConfig {
439 tool: "list_directory".to_string(),
440 hide: vec!["path".to_string()],
441 defaults: {
442 let mut m = serde_json::Map::new();
443 m.insert("path".to_string(), serde_json::json!("/home/docs"));
444 m
445 },
446 rename: {
447 let mut m = HashMap::new();
448 m.insert("recursive".to_string(), "deep_search".to_string());
449 m
450 },
451 }],
452 );
453 let mut svc = ParamOverrideService::new(mock, overrides);
454
455 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
457 match resp.inner.unwrap() {
458 McpResponse::ListTools(result) => {
459 let props = result.tools[0].input_schema["properties"]
460 .as_object()
461 .unwrap();
462 assert!(!props.contains_key("path"));
463 assert!(!props.contains_key("recursive"));
464 assert!(props.contains_key("deep_search"));
465 assert!(props.contains_key("pattern"));
466 }
467 other => panic!("expected ListTools, got: {:?}", other),
468 }
469 }
470
471 #[tokio::test]
472 async fn test_non_matching_tool_passes_through() {
473 let mock = mock_with_schema("db/query", list_dir_schema());
474 let overrides = make_overrides(
475 "fs/",
476 vec![ParamOverrideConfig {
477 tool: "list_directory".to_string(),
478 hide: vec!["path".to_string()],
479 defaults: serde_json::Map::new(),
480 rename: HashMap::new(),
481 }],
482 );
483 let mut svc = ParamOverrideService::new(mock, overrides);
484
485 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
486 match resp.inner.unwrap() {
487 McpResponse::ListTools(result) => {
488 let props = result.tools[0].input_schema["properties"]
490 .as_object()
491 .unwrap();
492 assert!(props.contains_key("path"), "unmatched tool is untouched");
493 }
494 other => panic!("expected ListTools, got: {:?}", other),
495 }
496 }
497
498 #[tokio::test]
499 async fn test_non_call_tool_passes_through() {
500 let mock = MockService::with_tools(&["fs/list_directory"]);
501 let overrides = make_overrides(
502 "fs/",
503 vec![ParamOverrideConfig {
504 tool: "list_directory".to_string(),
505 hide: vec!["path".to_string()],
506 defaults: serde_json::Map::new(),
507 rename: HashMap::new(),
508 }],
509 );
510 let mut svc = ParamOverrideService::new(mock, overrides);
511
512 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
514 assert!(resp.inner.is_ok());
515 }
516
517 #[tokio::test]
518 async fn test_rename_updates_required_array() {
519 let schema = serde_json::json!({
520 "type": "object",
521 "properties": {
522 "path": { "type": "string" },
523 "recursive": { "type": "boolean" }
524 },
525 "required": ["path", "recursive"]
526 });
527 let mock = mock_with_schema("fs/list_directory", schema);
528 let overrides = make_overrides(
529 "fs/",
530 vec![ParamOverrideConfig {
531 tool: "list_directory".to_string(),
532 hide: vec![],
533 defaults: serde_json::Map::new(),
534 rename: {
535 let mut m = HashMap::new();
536 m.insert("recursive".to_string(), "deep_search".to_string());
537 m
538 },
539 }],
540 );
541 let mut svc = ParamOverrideService::new(mock, overrides);
542
543 let resp = call_service(&mut svc, McpRequest::ListTools(Default::default())).await;
544 match resp.inner.unwrap() {
545 McpResponse::ListTools(result) => {
546 let required = result.tools[0].input_schema["required"].as_array().unwrap();
547 let req_strs: Vec<&str> = required.iter().map(|v| v.as_str().unwrap()).collect();
548 assert!(req_strs.contains(&"path"));
549 assert!(req_strs.contains(&"deep_search"));
550 assert!(!req_strs.contains(&"recursive"));
551 }
552 other => panic!("expected ListTools, got: {:?}", other),
553 }
554 }
555
556 #[test]
557 fn test_rewrite_schema_no_properties() {
558 let mut schema = serde_json::json!({"type": "object"});
560 rewrite_schema(&mut schema, &["path".to_string()], &HashMap::new());
561 assert_eq!(schema, serde_json::json!({"type": "object"}));
562 }
563
564 #[test]
565 fn test_rewrite_schema_non_object() {
566 let mut schema = serde_json::json!("string");
568 rewrite_schema(&mut schema, &["path".to_string()], &HashMap::new());
569 assert_eq!(schema, serde_json::json!("string"));
570 }
571
572 #[test]
573 fn test_tool_override_construction() {
574 let config = ParamOverrideConfig {
575 tool: "list_directory".to_string(),
576 hide: vec!["path".to_string()],
577 defaults: {
578 let mut m = serde_json::Map::new();
579 m.insert("path".to_string(), serde_json::json!("/home"));
580 m
581 },
582 rename: {
583 let mut m = HashMap::new();
584 m.insert("recursive".to_string(), "deep_search".to_string());
585 m
586 },
587 };
588 let to = ToolOverride::new("fs/", &config);
589 assert_eq!(to.namespaced_tool, "fs/list_directory");
590 assert_eq!(to.hide, vec!["path"]);
591 assert_eq!(to.rename_forward.get("recursive").unwrap(), "deep_search");
592 assert_eq!(to.rename_reverse.get("deep_search").unwrap(), "recursive");
593 }
594
595 #[tokio::test]
596 async fn test_hidden_default_does_not_overwrite_explicit_arg() {
597 let _mock = mock_with_schema("fs/list_directory", list_dir_schema());
598 let overrides = make_overrides(
599 "fs/",
600 vec![ParamOverrideConfig {
601 tool: "list_directory".to_string(),
602 hide: vec!["path".to_string()],
603 defaults: {
604 let mut m = serde_json::Map::new();
605 m.insert("path".to_string(), serde_json::json!("/home/docs"));
606 m
607 },
608 rename: HashMap::new(),
609 }],
610 );
611
612 let mut req = RouterRequest {
615 id: tower_mcp::protocol::RequestId::Number(1),
616 inner: McpRequest::CallTool(CallToolParams {
617 name: "fs/list_directory".to_string(),
618 arguments: serde_json::json!({"path": "/custom"}),
619 input_responses: None,
620 request_state: None,
621 meta: None,
622 task: None,
623 }),
624 extensions: tower_mcp::router::Extensions::new(),
625 };
626
627 if let McpRequest::CallTool(ref mut params) = req.inner
629 && let serde_json::Value::Object(ref mut args) = params.arguments
630 {
631 let defaults = &overrides[0].defaults;
632 for (key, value) in defaults {
633 if !args.contains_key(key) {
634 args.insert(key.clone(), value.clone());
635 }
636 }
637 assert_eq!(args.get("path").unwrap(), "/custom");
639 }
640 }
641}