ceylon_runtime/config/
mcp_config.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
9#[serde(rename_all = "lowercase")]
10pub enum McpTransportType {
11 #[default]
13 Stdio,
14 Http,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, Default)]
48pub struct McpToolConfig {
49 #[serde(rename = "type", default)]
51 pub transport_type: McpTransportType,
52
53 #[serde(default)]
56 pub command: Option<String>,
57
58 #[serde(default)]
60 pub args: Vec<String>,
61
62 #[serde(default)]
65 pub env: Option<Vec<(String, String)>>,
66
67 #[serde(default)]
70 pub url: Option<String>,
71
72 #[serde(default)]
75 pub auth_token: Option<String>,
76
77 #[serde(default)]
79 pub name: Option<String>,
80}
81
82impl McpToolConfig {
83 pub fn stdio(command: impl Into<String>, args: Vec<String>) -> Self {
89 Self {
90 transport_type: McpTransportType::Stdio,
91 command: Some(command.into()),
92 args,
93 ..Default::default()
94 }
95 }
96
97 pub fn npx(package: impl Into<String>, extra_args: Vec<String>) -> Self {
103 let mut args = vec!["-y".to_string(), package.into()];
104 args.extend(extra_args);
105 Self::stdio("npx", args)
106 }
107
108 pub fn http(url: impl Into<String>) -> Self {
113 Self {
114 transport_type: McpTransportType::Http,
115 url: Some(url.into()),
116 ..Default::default()
117 }
118 }
119
120 pub fn with_name(mut self, name: impl Into<String>) -> Self {
122 self.name = Some(name.into());
123 self
124 }
125
126 pub fn with_env(mut self, env: Vec<(String, String)>) -> Self {
128 self.env = Some(env);
129 self
130 }
131
132 pub fn with_auth_token(mut self, token: impl Into<String>) -> Self {
134 self.auth_token = Some(token.into());
135 self
136 }
137
138 pub fn validate(&self) -> Result<(), String> {
140 match self.transport_type {
141 McpTransportType::Stdio => {
142 if self.command.is_none() {
143 return Err("STDIO transport requires 'command' field".to_string());
144 }
145 }
146 McpTransportType::Http => {
147 if self.url.is_none() {
148 return Err("HTTP transport requires 'url' field".to_string());
149 }
150 }
151 }
152 Ok(())
153 }
154}
155
156#[cfg(feature = "mcp")]
158impl McpToolConfig {
159 pub fn to_transport(&self) -> Result<ceylon_mcp::McpTransport, String> {
163 self.validate()?;
164
165 match self.transport_type {
166 McpTransportType::Stdio => Ok(ceylon_mcp::McpTransport::Stdio {
167 command: self.command.clone().unwrap(),
168 args: self.args.clone(),
169 env: self.env.clone(),
170 }),
171 McpTransportType::Http => Ok(ceylon_mcp::McpTransport::Http {
172 url: self.url.clone().unwrap(),
173 auth_token: self.auth_token.clone(),
174 }),
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_parse_stdio_config() {
185 let toml = r#"
186 type = "stdio"
187 command = "npx"
188 args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
189 "#;
190
191 let config: McpToolConfig = toml::from_str(toml).unwrap();
192 assert_eq!(config.transport_type, McpTransportType::Stdio);
193 assert_eq!(config.command, Some("npx".to_string()));
194 assert_eq!(config.args.len(), 3);
195 assert!(config.validate().is_ok());
196 }
197
198 #[test]
199 fn test_parse_http_config() {
200 let toml = r#"
201 type = "http"
202 url = "http://localhost:3000/mcp"
203 auth_token = "secret-token"
204 "#;
205
206 let config: McpToolConfig = toml::from_str(toml).unwrap();
207 assert_eq!(config.transport_type, McpTransportType::Http);
208 assert_eq!(config.url, Some("http://localhost:3000/mcp".to_string()));
209 assert_eq!(config.auth_token, Some("secret-token".to_string()));
210 assert!(config.validate().is_ok());
211 }
212
213 #[test]
214 fn test_builder_methods() {
215 let config = McpToolConfig::npx(
216 "@modelcontextprotocol/server-filesystem",
217 vec!["/home".into()],
218 )
219 .with_name("filesystem-tools");
220
221 assert_eq!(config.transport_type, McpTransportType::Stdio);
222 assert_eq!(config.command, Some("npx".to_string()));
223 assert_eq!(
224 config.args,
225 vec!["-y", "@modelcontextprotocol/server-filesystem", "/home"]
226 );
227 assert_eq!(config.name, Some("filesystem-tools".to_string()));
228 }
229
230 #[test]
231 fn test_validation_fails_without_command() {
232 let config = McpToolConfig {
233 transport_type: McpTransportType::Stdio,
234 command: None,
235 ..Default::default()
236 };
237 assert!(config.validate().is_err());
238 }
239
240 #[test]
241 fn test_validation_fails_without_url() {
242 let config = McpToolConfig {
243 transport_type: McpTransportType::Http,
244 url: None,
245 ..Default::default()
246 };
247 assert!(config.validate().is_err());
248 }
249}