Skip to main content

vtcode_core/tool_policy/
mcp.rs

1use std::collections::BTreeMap;
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5
6use super::ToolPolicy;
7use crate::config::mcp::{McpAllowListConfig, McpAllowListRules};
8use crate::utils::tool_name_parsing::parse_canonical_mcp_tool_name;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct McpPolicyStore {
12    /// Active MCP allow list configuration
13    #[serde(default = "default_secure_mcp_allowlist")]
14    pub allowlist: McpAllowListConfig,
15    /// Provider-specific tool policies (allow/prompt/deny)
16    #[serde(default)]
17    pub providers: IndexMap<String, McpProviderPolicy>,
18}
19
20impl Default for McpPolicyStore {
21    fn default() -> Self {
22        Self {
23            allowlist: default_secure_mcp_allowlist(),
24            providers: IndexMap::new(),
25        }
26    }
27}
28
29/// MCP provider policy entry containing per-tool permissions
30#[derive(Debug, Clone, Serialize, Deserialize, Default)]
31pub struct McpProviderPolicy {
32    #[serde(default)]
33    pub tools: IndexMap<String, ToolPolicy>,
34}
35
36// Helper constants to reduce allocations in MCP allowlist configuration
37const MCP_LOGGING_EVENTS: &[&str] = &[
38    "mcp.tool_execution",
39    "mcp.tool_failed",
40    "mcp.tool_denied",
41    "mcp.tool_filtered",
42    "mcp.provider_initialized",
43];
44
45const MCP_DEFAULT_LOGGING_EVENTS: &[&str] = &[
46    "mcp.provider_initialized",
47    "mcp.provider_initialization_failed",
48    "mcp.tool_filtered",
49    "mcp.tool_execution",
50    "mcp.tool_failed",
51    "mcp.tool_denied",
52];
53
54/// Helper to create standard MCP logging configuration
55#[inline]
56pub(crate) fn mcp_standard_logging() -> Vec<String> {
57    MCP_LOGGING_EVENTS.iter().map(|s| (*s).into()).collect()
58}
59
60/// Helper to create provider configuration with max_concurrent_requests
61#[inline]
62fn mcp_provider_config_with(extra: (&str, Vec<&str>)) -> BTreeMap<String, Vec<String>> {
63    BTreeMap::from([
64        ("provider".into(), vec!["max_concurrent_requests".into()]),
65        (extra.0.into(), extra.1.into_iter().map(Into::into).collect()),
66    ])
67}
68
69fn default_secure_mcp_allowlist() -> McpAllowListConfig {
70    let default_logging = Some(MCP_DEFAULT_LOGGING_EVENTS.iter().map(|s| (*s).into()).collect());
71
72    let default_configuration = Some(BTreeMap::from([
73        (
74            "client".into(),
75            vec![
76                "max_concurrent_connections".into(),
77                "request_timeout_seconds".into(),
78                "retry_attempts".into(),
79                "startup_timeout_seconds".into(),
80                "tool_timeout_seconds".into(),
81                "experimental_use_rmcp_client".into(),
82            ],
83        ),
84        ("ui".into(), vec!["mode".into(), "max_events".into(), "show_provider_names".into()]),
85        (
86            "server".into(),
87            vec![
88                "enabled".into(),
89                "bind_address".into(),
90                "port".into(),
91                "transport".into(),
92                "name".into(),
93                "version".into(),
94            ],
95        ),
96    ]));
97
98    let time_rules = McpAllowListRules {
99        tools: Some(vec![
100            "get_*".into(),
101            "list_*".into(),
102            "convert_timezone".into(),
103            "describe_timezone".into(),
104            "time_*".into(),
105        ]),
106        resources: Some(vec!["timezone:*".into(), "location:*".into()]),
107        logging: Some(mcp_standard_logging()),
108        configuration: Some(mcp_provider_config_with(("time", vec!["local_timezone_override"]))),
109        ..Default::default()
110    };
111
112    let context_rules = McpAllowListRules {
113        tools: Some(vec![
114            "search_*".into(),
115            "fetch_*".into(),
116            "list_*".into(),
117            "context7_*".into(),
118            "get_*".into(),
119        ]),
120        resources: Some(vec![
121            "docs::*".into(),
122            "snippets::*".into(),
123            "repositories::*".into(),
124            "context7::*".into(),
125        ]),
126        prompts: Some(vec!["context7::*".into(), "support::*".into(), "docs::*".into()]),
127        logging: Some(mcp_standard_logging()),
128        configuration: Some(mcp_provider_config_with(("context7", vec!["workspace", "search_scope", "max_results"]))),
129    };
130
131    let seq_rules = McpAllowListRules {
132        tools: Some(vec![
133            "plan".into(),
134            "critique".into(),
135            "reflect".into(),
136            "decompose".into(),
137            "sequential_*".into(),
138        ]),
139        resources: None,
140        prompts: Some(vec![
141            "sequential-thinking::*".into(),
142            "plan".into(),
143            "reflect".into(),
144            "critique".into(),
145        ]),
146        logging: Some(mcp_standard_logging()),
147        configuration: Some(mcp_provider_config_with(("sequencing", vec!["max_depth", "max_branches"]))),
148    };
149
150    let mut allowlist = McpAllowListConfig {
151        enforce: true,
152        default: McpAllowListRules {
153            logging: default_logging,
154            configuration: default_configuration,
155            ..Default::default()
156        },
157        ..Default::default()
158    };
159
160    allowlist.providers.insert("time".into(), time_rules);
161    allowlist.providers.insert("context7".into(), context_rules);
162    allowlist.providers.insert("sequential-thinking".into(), seq_rules);
163
164    allowlist
165}
166
167pub(crate) fn parse_mcp_policy_key(tool_name: &str) -> Option<(String, String)> {
168    parse_canonical_mcp_tool_name(tool_name).map(|(provider, tool)| (provider.to_string(), tool.to_string()))
169}