1pub use super::connector::{ConnectorConfig, ConnectorIdentity};
13use crate::ai::ToolModelConfig;
14use crate::auth::{JwtAudience, Permission};
15use crate::errors::ConfigValidationError;
16use crate::mcp::capabilities::ToolVisibility;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use systemprompt_identifiers::ClientId;
20
21#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
22pub enum McpServerType {
23 #[default]
24 #[serde(rename = "internal")]
25 Internal,
26 #[serde(rename = "external")]
27 External,
28}
29
30impl McpServerType {
31 pub const fn as_str(&self) -> &'static str {
32 match self {
33 Self::Internal => "internal",
34 Self::External => "external",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, Default)]
40pub struct ToolUiConfig {
41 #[serde(default = "default_resource_uri_template")]
42 pub resource_uri_template: String,
43 #[serde(default = "default_visibility_enum")]
44 pub visibility: Vec<ToolVisibility>,
45}
46
47fn default_resource_uri_template() -> String {
48 "ui://systemprompt/{artifact_id}".to_owned()
49}
50
51fn default_visibility_enum() -> Vec<ToolVisibility> {
52 vec![ToolVisibility::Model, ToolVisibility::App]
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56pub struct ToolMetadata {
57 #[serde(default)]
58 pub terminal_on_success: bool,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub model_config: Option<ToolModelConfig>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub ui: Option<ToolUiConfig>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct DeploymentConfig {
67 pub deployments: HashMap<String, Deployment>,
68 pub settings: Settings,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Deployment {
82 #[serde(default, alias = "type")]
83 pub server_type: McpServerType,
84 pub binary: String,
85 pub package: Option<String>,
86 pub port: u16,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub endpoint: Option<String>,
89 pub enabled: bool,
90 pub display_in_web: bool,
91 #[serde(default)]
92 pub dev_only: bool,
93 #[serde(default)]
94 pub schemas: Vec<SchemaDefinition>,
95 pub oauth: OAuthRequirement,
96 #[serde(default)]
97 pub tools: HashMap<String, ToolMetadata>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub model_config: Option<ToolModelConfig>,
100 #[serde(default)]
101 pub env_vars: Vec<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub external_auth: Option<ExternalAuth>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub connector: Option<ConnectorConfig>,
106 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
107 pub headers: HashMap<String, String>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub tool_policy: Option<crate::bridge::ids::ToolPolicy>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ExternalAuth {
122 pub token_endpoint: String,
123 #[serde(default = "default_auth_header")]
124 pub header: String,
125 #[serde(default = "default_auth_scheme")]
126 pub scheme: String,
127}
128
129fn default_auth_header() -> String {
130 "Authorization".to_owned()
131}
132
133fn default_auth_scheme() -> String {
134 "Bearer".to_owned()
135}
136
137impl ExternalAuth {
138 pub fn header_value(&self, bearer: &str) -> String {
139 if self.scheme.trim().is_empty() {
140 bearer.to_owned()
141 } else {
142 format!("{} {bearer}", self.scheme)
143 }
144 }
145}
146
147impl Deployment {
148 pub fn validate(&self, name: &str) -> Result<(), ConfigValidationError> {
149 if matches!(self.server_type, McpServerType::Internal) {
150 if let Some(ep) = self.endpoint.as_deref()
151 && (ep.starts_with("http://") || ep.starts_with("https://"))
152 {
153 return Err(ConfigValidationError::invalid_field(format!(
154 "MCP server '{name}': endpoint must be a relative path (e.g. \
155 /api/v1/mcp/{name}/mcp) or omitted; the host is derived from \
156 server.api_external_url. Remove the scheme+host prefix."
157 )));
158 }
159 if self.external_auth.is_some() || self.connector.is_some() || !self.headers.is_empty()
160 {
161 return Err(ConfigValidationError::invalid_field(format!(
162 "MCP server '{name}': external_auth and headers are only valid on \
163 external servers; internal servers are reached through the gateway \
164 with the systemprompt credential."
165 )));
166 }
167 }
168
169 if let Some(connector) = self.connector.as_ref() {
170 if connector.adapter != "generic"
171 || !self
172 .endpoint
173 .as_deref()
174 .is_some_and(|endpoint| endpoint.starts_with("https://"))
175 {
176 return Err(ConfigValidationError::invalid_field(format!(
177 "MCP server '{name}': generic connector requires an HTTPS resource"
178 )));
179 }
180 if connector.client_secret.is_some() && connector.client_id_secret.is_none() {
181 return Err(ConfigValidationError::invalid_field(format!(
182 "MCP server '{name}': connector client secret requires a client ID"
183 )));
184 }
185 connector.validate(name)?;
186 }
187 if let Some(ext) = self.external_auth.as_ref() {
188 if ext.token_endpoint.starts_with("http://")
189 || ext.token_endpoint.starts_with("https://")
190 {
191 return Err(ConfigValidationError::invalid_field(format!(
192 "MCP server '{name}': external_auth.token_endpoint must be a relative \
193 path (e.g. /api/public/<provider>/token); the host is derived from \
194 server.api_external_url. Remove the scheme+host prefix."
195 )));
196 }
197 if !ext.token_endpoint.starts_with('/') {
198 return Err(ConfigValidationError::invalid_field(format!(
199 "MCP server '{name}': external_auth.token_endpoint must be an absolute \
200 path beginning with '/'."
201 )));
202 }
203 if ext.header.trim().is_empty() {
204 return Err(ConfigValidationError::invalid_field(format!(
205 "MCP server '{name}': external_auth.header must not be empty."
206 )));
207 }
208 }
209
210 Ok(())
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct SchemaDefinition {
216 pub file: String,
217 pub table: String,
218 pub required_columns: Vec<String>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct OAuthRequirement {
223 pub required: bool,
224 pub scopes: Vec<Permission>,
225 pub audience: JwtAudience,
226 pub client_id: Option<ClientId>,
227 #[serde(default)]
228 pub ema: bool,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct Settings {
233 pub auto_build: bool,
234 pub build_timeout: u64,
235 pub health_check_timeout: u64,
236 #[serde(default = "default_base_port")]
237 pub base_port: u16,
238 #[serde(default = "default_working_dir")]
239 pub working_dir: String,
240}
241
242const fn default_base_port() -> u16 {
243 5000
244}
245
246fn default_working_dir() -> String {
247 "/app".to_owned()
248}