1use hashbrown::HashMap;
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
8#[derive(Debug, Clone, Deserialize, Serialize)]
9pub struct McpClientConfig {
10 #[serde(default = "default_mcp_enabled")]
12 pub enabled: bool,
13
14 #[serde(default)]
16 pub ui: McpUiConfig,
17
18 #[serde(default)]
20 pub providers: Vec<McpProviderConfig>,
21
22 #[serde(default)]
24 pub server: McpServerConfig,
25
26 #[serde(default)]
28 pub allowlist: McpAllowListConfig,
29
30 #[serde(default = "default_max_concurrent_connections")]
32 pub max_concurrent_connections: usize,
33
34 #[serde(default = "default_request_timeout_seconds")]
36 pub request_timeout_seconds: u64,
37
38 #[serde(default = "default_retry_attempts")]
40 pub retry_attempts: u32,
41
42 #[serde(default)]
44 pub startup_timeout_seconds: Option<u64>,
45
46 #[serde(default)]
48 pub tool_timeout_seconds: Option<u64>,
49
50 #[serde(default = "default_experimental_use_rmcp_client")]
52 pub experimental_use_rmcp_client: bool,
53
54 #[serde(default = "default_connection_pooling_enabled")]
56 pub connection_pooling_enabled: bool,
57
58 #[serde(default = "default_tool_cache_capacity")]
60 pub tool_cache_capacity: usize,
61
62 #[serde(default = "default_connection_timeout_seconds")]
64 pub connection_timeout_seconds: u64,
65
66 #[serde(default)]
68 pub security: McpSecurityConfig,
69}
70
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct McpSecurityConfig {
75 #[serde(default = "default_mcp_auth_enabled")]
77 pub auth_enabled: bool,
78
79 #[serde(default)]
81 pub api_key_env: Option<String>,
82
83 #[serde(default)]
85 pub rate_limit: McpRateLimitConfig,
86
87 #[serde(default)]
89 pub validation: McpValidationConfig,
90}
91
92impl Default for McpSecurityConfig {
93 fn default() -> Self {
94 Self {
95 auth_enabled: default_mcp_auth_enabled(),
96 api_key_env: None,
97 rate_limit: McpRateLimitConfig::default(),
98 validation: McpValidationConfig::default(),
99 }
100 }
101}
102
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[derive(Debug, Clone, Deserialize, Serialize)]
106pub struct McpRateLimitConfig {
107 #[serde(default = "default_requests_per_minute")]
109 pub requests_per_minute: u32,
110
111 #[serde(default = "default_concurrent_requests")]
113 pub concurrent_requests: u32,
114}
115
116impl Default for McpRateLimitConfig {
117 fn default() -> Self {
118 Self {
119 requests_per_minute: default_requests_per_minute(),
120 concurrent_requests: default_concurrent_requests(),
121 }
122 }
123}
124
125#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
127#[derive(Debug, Clone, Deserialize, Serialize)]
128pub struct McpValidationConfig {
129 #[serde(default = "default_schema_validation_enabled")]
131 pub schema_validation_enabled: bool,
132
133 #[serde(default = "default_path_traversal_protection_enabled")]
135 pub path_traversal_protection: bool,
136
137 #[serde(default = "default_max_argument_size")]
139 pub max_argument_size: u32,
140}
141
142impl Default for McpValidationConfig {
143 fn default() -> Self {
144 Self {
145 schema_validation_enabled: default_schema_validation_enabled(),
146 path_traversal_protection: default_path_traversal_protection_enabled(),
147 max_argument_size: default_max_argument_size(),
148 }
149 }
150}
151
152impl Default for McpClientConfig {
153 fn default() -> Self {
154 Self {
155 enabled: default_mcp_enabled(),
156 ui: McpUiConfig::default(),
157 providers: Vec::new(),
158 server: McpServerConfig::default(),
159 allowlist: McpAllowListConfig::default(),
160 max_concurrent_connections: default_max_concurrent_connections(),
161 request_timeout_seconds: default_request_timeout_seconds(),
162 retry_attempts: default_retry_attempts(),
163 startup_timeout_seconds: None,
164 tool_timeout_seconds: None,
165 experimental_use_rmcp_client: default_experimental_use_rmcp_client(),
166 security: McpSecurityConfig::default(),
167 connection_pooling_enabled: default_connection_pooling_enabled(),
168 connection_timeout_seconds: default_connection_timeout_seconds(),
169 tool_cache_capacity: default_tool_cache_capacity(),
170 }
171 }
172}
173
174#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
176#[derive(Debug, Clone, Deserialize, Serialize)]
177pub struct McpUiConfig {
178 #[serde(default = "default_mcp_ui_mode")]
180 pub mode: McpUiMode,
181
182 #[serde(default = "default_max_mcp_events")]
184 pub max_events: usize,
185
186 #[serde(default = "default_show_provider_names")]
188 pub show_provider_names: bool,
189
190 #[serde(default)]
192 pub renderers: HashMap<String, McpRendererProfile>,
193}
194
195impl Default for McpUiConfig {
196 fn default() -> Self {
197 Self {
198 mode: default_mcp_ui_mode(),
199 max_events: default_max_mcp_events(),
200 show_provider_names: default_show_provider_names(),
201 renderers: HashMap::new(),
202 }
203 }
204}
205
206impl McpUiConfig {
207 pub fn renderer_for_identifier(&self, identifier: &str) -> Option<McpRendererProfile> {
209 let normalized_identifier = normalize_mcp_identifier(identifier);
210 if normalized_identifier.is_empty() {
211 return None;
212 }
213
214 self.renderers.iter().find_map(|(key, profile)| {
215 let normalized_key = normalize_mcp_identifier(key);
216 if normalized_identifier.starts_with(&normalized_key) {
217 Some(*profile)
218 } else {
219 None
220 }
221 })
222 }
223
224 pub fn renderer_for_tool(&self, tool_name: &str) -> Option<McpRendererProfile> {
226 let identifier = tool_name.strip_prefix("mcp_").unwrap_or(tool_name);
227 self.renderer_for_identifier(identifier)
228 }
229}
230
231#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
234#[serde(rename_all = "snake_case")]
235#[derive(Default)]
236pub enum McpUiMode {
237 #[default]
239 Compact,
240 Full,
242}
243
244impl std::fmt::Display for McpUiMode {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 match self {
247 McpUiMode::Compact => write!(f, "compact"),
248 McpUiMode::Full => write!(f, "full"),
249 }
250 }
251}
252
253#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
256#[serde(rename_all = "kebab-case")]
257pub enum McpRendererProfile {
258 Context7,
260 SequentialThinking,
262}
263
264#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
266#[derive(Debug, Clone, Deserialize, Serialize)]
267pub struct McpProviderConfig {
268 pub name: String,
270
271 #[serde(flatten)]
273 pub transport: McpTransportConfig,
274
275 #[serde(default)]
277 pub env: HashMap<String, String>,
278
279 #[serde(default = "default_provider_enabled")]
281 pub enabled: bool,
282
283 #[serde(default = "default_provider_max_concurrent")]
285 pub max_concurrent_requests: usize,
286
287 #[serde(default)]
289 pub startup_timeout_ms: Option<u64>,
290}
291
292impl Default for McpProviderConfig {
293 fn default() -> Self {
294 Self {
295 name: String::new(),
296 transport: McpTransportConfig::Stdio(McpStdioServerConfig::default()),
297 env: HashMap::new(),
298 enabled: default_provider_enabled(),
299 max_concurrent_requests: default_provider_max_concurrent(),
300 startup_timeout_ms: None,
301 }
302 }
303}
304
305#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
307#[derive(Debug, Clone, Deserialize, Serialize)]
308pub struct McpAllowListConfig {
309 #[serde(default = "default_allowlist_enforced")]
311 pub enforce: bool,
312
313 #[serde(default)]
315 pub default: McpAllowListRules,
316
317 #[serde(default)]
319 pub providers: BTreeMap<String, McpAllowListRules>,
320}
321
322impl Default for McpAllowListConfig {
323 fn default() -> Self {
324 Self {
325 enforce: default_allowlist_enforced(),
326 default: McpAllowListRules::default(),
327 providers: BTreeMap::new(),
328 }
329 }
330}
331
332impl McpAllowListConfig {
333 pub fn is_tool_allowed(&self, provider: &str, tool_name: &str) -> bool {
335 if !self.enforce {
336 return true;
337 }
338
339 self.resolve_match(provider, tool_name, |rules| &rules.tools)
340 }
341
342 pub fn is_resource_allowed(&self, provider: &str, resource: &str) -> bool {
344 if !self.enforce {
345 return true;
346 }
347
348 self.resolve_match(provider, resource, |rules| &rules.resources)
349 }
350
351 pub fn is_prompt_allowed(&self, provider: &str, prompt: &str) -> bool {
353 if !self.enforce {
354 return true;
355 }
356
357 self.resolve_match(provider, prompt, |rules| &rules.prompts)
358 }
359
360 pub fn is_logging_channel_allowed(&self, provider: Option<&str>, channel: &str) -> bool {
362 if !self.enforce {
363 return true;
364 }
365
366 if let Some(name) = provider
367 && let Some(rules) = self.providers.get(name)
368 && let Some(patterns) = &rules.logging
369 {
370 return pattern_matches(patterns, channel);
371 }
372
373 if let Some(patterns) = &self.default.logging
374 && pattern_matches(patterns, channel)
375 {
376 return true;
377 }
378
379 false
380 }
381
382 pub fn is_configuration_allowed(
384 &self,
385 provider: Option<&str>,
386 category: &str,
387 key: &str,
388 ) -> bool {
389 if !self.enforce {
390 return true;
391 }
392
393 if let Some(name) = provider
394 && let Some(rules) = self.providers.get(name)
395 && let Some(result) = configuration_allowed(rules, category, key)
396 {
397 return result;
398 }
399
400 if let Some(result) = configuration_allowed(&self.default, category, key) {
401 return result;
402 }
403
404 false
405 }
406
407 fn resolve_match<'a, F>(&'a self, provider: &str, candidate: &str, accessor: F) -> bool
408 where
409 F: Fn(&'a McpAllowListRules) -> &'a Option<Vec<String>>,
410 {
411 if let Some(rules) = self.providers.get(provider)
412 && let Some(patterns) = accessor(rules)
413 {
414 return pattern_matches(patterns, candidate);
415 }
416
417 if let Some(patterns) = accessor(&self.default)
418 && pattern_matches(patterns, candidate)
419 {
420 return true;
421 }
422
423 false
424 }
425}
426
427fn configuration_allowed(rules: &McpAllowListRules, category: &str, key: &str) -> Option<bool> {
428 rules.configuration.as_ref().and_then(|entries| {
429 entries
430 .get(category)
431 .map(|patterns| pattern_matches(patterns, key))
432 })
433}
434
435fn pattern_matches(patterns: &[String], candidate: &str) -> bool {
436 patterns
437 .iter()
438 .any(|pattern| wildcard_match(pattern, candidate))
439}
440
441fn wildcard_match(pattern: &str, candidate: &str) -> bool {
442 if pattern == "*" {
443 return true;
444 }
445
446 let mut regex_pattern = String::from("^");
447 let mut literal_buffer = String::new();
448
449 for ch in pattern.chars() {
450 match ch {
451 '*' => {
452 if !literal_buffer.is_empty() {
453 regex_pattern.push_str(®ex::escape(&literal_buffer));
454 literal_buffer.clear();
455 }
456 regex_pattern.push_str(".*");
457 }
458 '?' => {
459 if !literal_buffer.is_empty() {
460 regex_pattern.push_str(®ex::escape(&literal_buffer));
461 literal_buffer.clear();
462 }
463 regex_pattern.push('.');
464 }
465 _ => literal_buffer.push(ch),
466 }
467 }
468
469 if !literal_buffer.is_empty() {
470 regex_pattern.push_str(®ex::escape(&literal_buffer));
471 }
472
473 regex_pattern.push('$');
474
475 Regex::new(®ex_pattern)
476 .map(|regex| regex.is_match(candidate))
477 .unwrap_or(false)
478}
479
480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
482#[derive(Debug, Clone, Deserialize, Serialize, Default)]
483pub struct McpAllowListRules {
484 #[serde(default)]
486 pub tools: Option<Vec<String>>,
487
488 #[serde(default)]
490 pub resources: Option<Vec<String>>,
491
492 #[serde(default)]
494 pub prompts: Option<Vec<String>>,
495
496 #[serde(default)]
498 pub logging: Option<Vec<String>>,
499
500 #[serde(default)]
502 pub configuration: Option<BTreeMap<String, Vec<String>>>,
503}
504
505#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
507#[derive(Debug, Clone, Deserialize, Serialize)]
508pub struct McpServerConfig {
509 #[serde(default = "default_mcp_server_enabled")]
511 pub enabled: bool,
512
513 #[serde(default = "default_mcp_server_bind")]
515 pub bind_address: String,
516
517 #[serde(default = "default_mcp_server_port")]
519 pub port: u16,
520
521 #[serde(default = "default_mcp_server_transport")]
523 pub transport: McpServerTransport,
524
525 #[serde(default = "default_mcp_server_name")]
527 pub name: String,
528
529 #[serde(default = "default_mcp_server_version")]
531 pub version: String,
532
533 #[serde(default)]
535 pub exposed_tools: Vec<String>,
536}
537
538impl Default for McpServerConfig {
539 fn default() -> Self {
540 Self {
541 enabled: default_mcp_server_enabled(),
542 bind_address: default_mcp_server_bind(),
543 port: default_mcp_server_port(),
544 transport: default_mcp_server_transport(),
545 name: default_mcp_server_name(),
546 version: default_mcp_server_version(),
547 exposed_tools: Vec::new(),
548 }
549 }
550}
551
552#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
554#[derive(Debug, Clone, Deserialize, Serialize)]
555#[serde(rename_all = "snake_case")]
556#[derive(Default)]
557pub enum McpServerTransport {
558 #[default]
560 Sse,
561 Http,
563}
564
565#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
567#[derive(Debug, Clone, Deserialize, Serialize)]
568#[serde(untagged)]
569pub enum McpTransportConfig {
570 Stdio(McpStdioServerConfig),
572 Http(McpHttpServerConfig),
574}
575
576#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
578#[derive(Debug, Clone, Deserialize, Serialize, Default)]
579pub struct McpStdioServerConfig {
580 pub command: String,
582
583 pub args: Vec<String>,
585
586 #[serde(default)]
588 pub working_directory: Option<String>,
589}
590
591#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
597#[derive(Debug, Clone, Deserialize, Serialize)]
598pub struct McpHttpServerConfig {
599 pub endpoint: String,
601
602 #[serde(default)]
604 pub api_key_env: Option<String>,
605
606 #[serde(default = "default_mcp_protocol_version")]
608 pub protocol_version: String,
609
610 #[serde(default, alias = "headers")]
612 pub http_headers: HashMap<String, String>,
613
614 #[serde(default)]
617 pub env_http_headers: HashMap<String, String>,
618}
619
620impl Default for McpHttpServerConfig {
621 fn default() -> Self {
622 Self {
623 endpoint: String::new(),
624 api_key_env: None,
625 protocol_version: default_mcp_protocol_version(),
626 http_headers: HashMap::new(),
627 env_http_headers: HashMap::new(),
628 }
629 }
630}
631
632fn default_mcp_enabled() -> bool {
634 false
635}
636
637fn default_mcp_ui_mode() -> McpUiMode {
638 McpUiMode::Compact
639}
640
641fn default_max_mcp_events() -> usize {
642 50
643}
644
645fn default_show_provider_names() -> bool {
646 true
647}
648
649fn default_max_concurrent_connections() -> usize {
650 5
651}
652
653fn default_request_timeout_seconds() -> u64 {
654 30
655}
656
657fn default_retry_attempts() -> u32 {
658 3
659}
660
661fn default_experimental_use_rmcp_client() -> bool {
662 true
663}
664
665fn default_provider_enabled() -> bool {
666 true
667}
668
669fn default_provider_max_concurrent() -> usize {
670 3
671}
672
673fn default_allowlist_enforced() -> bool {
674 false
675}
676
677fn default_mcp_protocol_version() -> String {
678 "2024-11-05".into()
679}
680
681fn default_mcp_server_enabled() -> bool {
682 false
683}
684
685fn default_connection_pooling_enabled() -> bool {
686 true
687}
688
689fn default_tool_cache_capacity() -> usize {
690 100
691}
692
693fn default_connection_timeout_seconds() -> u64 {
694 30
695}
696
697fn default_mcp_server_bind() -> String {
698 "127.0.0.1".into()
699}
700
701fn default_mcp_server_port() -> u16 {
702 3000
703}
704
705fn default_mcp_server_transport() -> McpServerTransport {
706 McpServerTransport::Sse
707}
708
709fn default_mcp_server_name() -> String {
710 "vtcode-mcp-server".into()
711}
712
713fn default_mcp_server_version() -> String {
714 env!("CARGO_PKG_VERSION").into()
715}
716
717fn normalize_mcp_identifier(value: &str) -> String {
718 value
719 .chars()
720 .filter(|ch| ch.is_ascii_alphanumeric())
721 .map(|ch| ch.to_ascii_lowercase())
722 .collect()
723}
724
725fn default_mcp_auth_enabled() -> bool {
726 false
727}
728
729fn default_requests_per_minute() -> u32 {
730 100
731}
732
733fn default_concurrent_requests() -> u32 {
734 10
735}
736
737fn default_schema_validation_enabled() -> bool {
738 true
739}
740
741fn default_path_traversal_protection_enabled() -> bool {
742 true
743}
744
745fn default_max_argument_size() -> u32 {
746 1024 * 1024 }
748
749#[cfg(test)]
750mod tests {
751 use super::*;
752 use crate::constants::mcp as mcp_constants;
753 use std::collections::BTreeMap;
754
755 #[test]
756 fn test_mcp_config_defaults() {
757 let config = McpClientConfig::default();
758 assert!(!config.enabled);
759 assert_eq!(config.ui.mode, McpUiMode::Compact);
760 assert_eq!(config.ui.max_events, 50);
761 assert!(config.ui.show_provider_names);
762 assert!(config.ui.renderers.is_empty());
763 assert_eq!(config.max_concurrent_connections, 5);
764 assert_eq!(config.request_timeout_seconds, 30);
765 assert_eq!(config.retry_attempts, 3);
766 assert!(config.providers.is_empty());
767 assert!(!config.server.enabled);
768 assert!(!config.allowlist.enforce);
769 assert!(config.allowlist.default.tools.is_none());
770 }
771
772 #[test]
773 fn test_allowlist_pattern_matching() {
774 let patterns = vec!["get_*".to_string(), "convert_timezone".to_string()];
775 assert!(pattern_matches(&patterns, "get_current_time"));
776 assert!(pattern_matches(&patterns, "convert_timezone"));
777 assert!(!pattern_matches(&patterns, "delete_timezone"));
778 }
779
780 #[test]
781 fn test_allowlist_provider_override() {
782 let mut config = McpAllowListConfig {
783 enforce: true,
784 default: McpAllowListRules {
785 tools: Some(vec!["get_*".to_string()]),
786 ..Default::default()
787 },
788 ..Default::default()
789 };
790
791 let provider_rules = McpAllowListRules {
792 tools: Some(vec!["list_*".to_string()]),
793 ..Default::default()
794 };
795 config
796 .providers
797 .insert("context7".to_string(), provider_rules);
798
799 assert!(config.is_tool_allowed("context7", "list_documents"));
800 assert!(!config.is_tool_allowed("context7", "get_current_time"));
801 assert!(config.is_tool_allowed("other", "get_timezone"));
802 assert!(!config.is_tool_allowed("other", "list_documents"));
803 }
804
805 #[test]
806 fn test_allowlist_configuration_rules() {
807 let mut config = McpAllowListConfig {
808 enforce: true,
809 default: McpAllowListRules {
810 configuration: Some(BTreeMap::from([(
811 "ui".to_string(),
812 vec!["mode".to_string(), "max_events".to_string()],
813 )])),
814 ..Default::default()
815 },
816 ..Default::default()
817 };
818
819 let provider_rules = McpAllowListRules {
820 configuration: Some(BTreeMap::from([(
821 "provider".to_string(),
822 vec!["max_concurrent_requests".to_string()],
823 )])),
824 ..Default::default()
825 };
826 config.providers.insert("time".to_string(), provider_rules);
827
828 assert!(config.is_configuration_allowed(None, "ui", "mode"));
829 assert!(!config.is_configuration_allowed(None, "ui", "show_provider_names"));
830 assert!(config.is_configuration_allowed(
831 Some("time"),
832 "provider",
833 "max_concurrent_requests"
834 ));
835 assert!(!config.is_configuration_allowed(Some("time"), "provider", "retry_attempts"));
836 }
837
838 #[test]
839 fn test_allowlist_resource_override() {
840 let mut config = McpAllowListConfig {
841 enforce: true,
842 default: McpAllowListRules {
843 resources: Some(vec!["docs/**/*".to_string()]),
844 ..Default::default()
845 },
846 ..Default::default()
847 };
848
849 let provider_rules = McpAllowListRules {
850 resources: Some(vec!["journals/*".to_string()]),
851 ..Default::default()
852 };
853 config
854 .providers
855 .insert("context7".to_string(), provider_rules);
856
857 assert!(config.is_resource_allowed("context7", "journals/2024"));
858 assert!(config.is_resource_allowed("other", "docs/config/config.md"));
859 assert!(config.is_resource_allowed("other", "docs/guides/zed-acp.md"));
860 assert!(!config.is_resource_allowed("other", "journals/2023"));
861 }
862
863 #[test]
864 fn test_allowlist_logging_override() {
865 let mut config = McpAllowListConfig {
866 enforce: true,
867 default: McpAllowListRules {
868 logging: Some(vec!["info".to_string(), "debug".to_string()]),
869 ..Default::default()
870 },
871 ..Default::default()
872 };
873
874 let provider_rules = McpAllowListRules {
875 logging: Some(vec!["audit".to_string()]),
876 ..Default::default()
877 };
878 config
879 .providers
880 .insert("sequential".to_string(), provider_rules);
881
882 assert!(config.is_logging_channel_allowed(Some("sequential"), "audit"));
883 assert!(!config.is_logging_channel_allowed(Some("sequential"), "info"));
884 assert!(config.is_logging_channel_allowed(Some("other"), "info"));
885 assert!(!config.is_logging_channel_allowed(Some("other"), "trace"));
886 }
887
888 #[test]
889 fn test_mcp_ui_renderer_resolution() {
890 let mut config = McpUiConfig::default();
891 config.renderers.insert(
892 mcp_constants::RENDERER_CONTEXT7.to_string(),
893 McpRendererProfile::Context7,
894 );
895 config.renderers.insert(
896 mcp_constants::RENDERER_SEQUENTIAL_THINKING.to_string(),
897 McpRendererProfile::SequentialThinking,
898 );
899
900 assert_eq!(
901 config.renderer_for_tool("mcp_context7_lookup"),
902 Some(McpRendererProfile::Context7)
903 );
904 assert_eq!(
905 config.renderer_for_tool("mcp_context7lookup"),
906 Some(McpRendererProfile::Context7)
907 );
908 assert_eq!(
909 config.renderer_for_tool("mcp_sequentialthinking_run"),
910 Some(McpRendererProfile::SequentialThinking)
911 );
912 assert_eq!(
913 config.renderer_for_identifier("sequential-thinking-analyze"),
914 Some(McpRendererProfile::SequentialThinking)
915 );
916 assert_eq!(config.renderer_for_tool("mcp_unknown"), None);
917 }
918}