codex_config/
mcp_requirements.rs1use crate::mcp_types::McpServerConfig;
2use crate::mcp_types::McpServerTransportConfig;
3use regex_lite::Regex;
4use serde::Deserialize;
5
6#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
7#[serde(untagged)]
8pub enum McpServerIdentity {
9 Command { command: String },
10 Url { url: String },
11}
12
13#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
15#[serde(tag = "match", rename_all = "snake_case", deny_unknown_fields)]
16pub enum McpServerValueMatcher {
17 Exact { value: String },
18 Prefix { value: String },
19 Regex { expression: String },
20}
21
22impl McpServerValueMatcher {
23 fn compile_full_regex(expression: &str) -> Result<Regex, String> {
24 Regex::new(&format!(r"\A(?:{expression})\z")).map_err(|err| {
25 format!("regex `{expression}` cannot be used for full-value matching: {err}")
26 })
27 }
28
29 fn validate(&self) -> Result<(), String> {
30 let Self::Regex { expression } = self else {
31 return Ok(());
32 };
33
34 Regex::new(expression).map_err(|err| format!("invalid regex `{expression}`: {err}"))?;
35 Self::compile_full_regex(expression).map(|_| ())
36 }
37
38 fn matches(&self, candidate: &str) -> bool {
39 match self {
40 Self::Exact { value } => candidate == value,
41 Self::Prefix { value } => candidate.starts_with(value),
42 Self::Regex { expression } => Self::compile_full_regex(expression)
43 .ok()
44 .is_some_and(|regex| regex.is_match(candidate)),
45 }
46 }
47}
48
49#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
50#[serde(deny_unknown_fields)]
51pub struct McpServerCommandMatcher {
52 pub executable: String,
53 pub args: Vec<McpServerValueMatcher>,
54}
55
56#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
57#[serde(deny_unknown_fields)]
58struct RawMcpServerCommandIdentity {
59 command: McpServerCommandMatcher,
60}
61
62#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
63#[serde(deny_unknown_fields)]
64struct RawMcpServerUrlIdentity {
65 url: McpServerValueMatcher,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum McpServerRequirement {
75 Identity { identity: McpServerIdentity },
76 Command(McpServerCommandMatcher),
77 Url(McpServerValueMatcher),
78}
79
80#[derive(Deserialize)]
81struct RawMcpServerRequirement {
82 identity: RawMcpServerIdentity,
83}
84
85#[derive(Deserialize)]
86#[serde(untagged)]
87enum RawMcpServerIdentity {
88 Exact(McpServerIdentity),
89 Command(RawMcpServerCommandIdentity),
90 Url(RawMcpServerUrlIdentity),
91}
92
93impl<'de> Deserialize<'de> for McpServerRequirement {
94 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95 where
96 D: serde::Deserializer<'de>,
97 {
98 let RawMcpServerRequirement { identity } =
99 RawMcpServerRequirement::deserialize(deserializer)?;
100 match identity {
101 RawMcpServerIdentity::Exact(identity) => Ok(Self::Identity { identity }),
102 RawMcpServerIdentity::Command(matcher) => Ok(Self::Command(matcher.command)),
103 RawMcpServerIdentity::Url(matcher) => Ok(Self::Url(matcher.url)),
104 }
105 }
106}
107
108impl McpServerRequirement {
109 pub(crate) fn validate(&self) -> Result<(), String> {
110 match self {
111 Self::Identity { .. } => Ok(()),
112 Self::Command(matcher) => {
113 for (index, arg) in matcher.args.iter().enumerate() {
114 arg.validate().map_err(|err| {
115 format!("invalid argument matcher at index {index}: {err}")
116 })?;
117 }
118 Ok(())
119 }
120 Self::Url(matcher) => matcher.validate(),
121 }
122 }
123
124 pub fn matches(&self, server: &McpServerConfig) -> bool {
125 match (self, &server.transport) {
126 (
127 Self::Identity {
128 identity:
129 McpServerIdentity::Command {
130 command: want_command,
131 },
132 },
133 McpServerTransportConfig::Stdio {
134 command: got_command,
135 ..
136 },
137 ) => got_command == want_command,
138 (
139 Self::Identity {
140 identity: McpServerIdentity::Url { url: want_url },
141 },
142 McpServerTransportConfig::StreamableHttp { url: got_url, .. },
143 ) => got_url == want_url,
144 (Self::Command(matcher), McpServerTransportConfig::Stdio { command, args, .. }) => {
145 matcher.executable == *command
146 && matcher.args.len() == args.len()
147 && matcher
148 .args
149 .iter()
150 .zip(args)
151 .all(|(matcher, arg)| matcher.matches(arg))
152 }
153 (Self::Url(matcher), McpServerTransportConfig::StreamableHttp { url, .. }) => {
154 matcher.matches(url)
155 }
156 _ => false,
157 }
158 }
159}
160
161#[cfg(test)]
162#[path = "mcp_requirements_tests.rs"]
163mod tests;