1use std::collections::HashMap;
2
3use chrono::{DateTime, Utc};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use utoipa::ToSchema;
7use uuid::Uuid;
8
9use crate::McpClientTransport;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, ToSchema)]
12#[serde(rename_all = "snake_case")]
13pub enum ConnectionAuthType {
14 OAuth2,
15 Secret,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, ToSchema)]
19#[serde(rename_all = "snake_case")]
20pub enum ConnectionStatus {
21 Connected,
22 Disconnected,
23 Expired,
24 NeedsSetup,
25 Partial,
26 Pending,
27 Error,
28}
29
30impl std::fmt::Display for ConnectionStatus {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 ConnectionStatus::Connected => write!(f, "connected"),
34 ConnectionStatus::Disconnected => write!(f, "disconnected"),
35 ConnectionStatus::Expired => write!(f, "expired"),
36 ConnectionStatus::NeedsSetup => write!(f, "needs_setup"),
37 ConnectionStatus::Partial => write!(f, "partial"),
38 ConnectionStatus::Pending => write!(f, "pending"),
39 ConnectionStatus::Error => write!(f, "error"),
40 }
41 }
42}
43
44impl std::str::FromStr for ConnectionStatus {
45 type Err = anyhow::Error;
46
47 fn from_str(s: &str) -> Result<Self, Self::Err> {
48 match s {
49 "connected" => Ok(ConnectionStatus::Connected),
50 "disconnected" => Ok(ConnectionStatus::Disconnected),
51 "expired" => Ok(ConnectionStatus::Expired),
52 "needs_setup" => Ok(ConnectionStatus::NeedsSetup),
53 "partial" => Ok(ConnectionStatus::Partial),
54 "pending" => Ok(ConnectionStatus::Pending),
55 "error" => Ok(ConnectionStatus::Error),
56 _ => Err(anyhow::anyhow!("unknown connection status: {}", s)),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, ToSchema)]
67#[serde(rename_all = "snake_case")]
68pub enum AuthScope {
69 Public,
70 Workspace,
71 User,
72}
73
74impl std::fmt::Display for AuthScope {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::Public => write!(f, "public"),
78 Self::Workspace => write!(f, "workspace"),
79 Self::User => write!(f, "user"),
80 }
81 }
82}
83
84impl std::str::FromStr for AuthScope {
85 type Err = anyhow::Error;
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 match s {
88 "public" => Ok(Self::Public),
89 "workspace" => Ok(Self::Workspace),
90 "user" => Ok(Self::User),
91 _ => Err(anyhow::anyhow!("unknown auth_scope: {}", s)),
92 }
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema, PartialEq)]
98pub struct CustomField {
99 pub key: String,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub label: Option<String>,
104 #[serde(default)]
106 pub is_secret: bool,
107 #[serde(default = "default_required")]
109 pub required: bool,
110}
111
112fn default_required() -> bool {
113 true
114}
115
116pub type ProviderGroup = String;
123
124#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema, PartialEq)]
145pub struct OAuthProviderConfig {
146 pub name: String,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub display_name: Option<String>,
152 pub authorization_url: String,
153 pub token_url: String,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub refresh_url: Option<String>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub registration_endpoint: Option<String>,
160 #[serde(default)]
161 pub scopes_supported: Vec<String>,
162 #[serde(default)]
163 pub default_scopes: Vec<String>,
164 #[serde(default)]
167 pub default_auth_params: HashMap<String, String>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub auth_params_schema: Option<serde_json::Value>,
172 #[serde(default)]
173 pub pkce_required: bool,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub env_client_id: Option<String>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub env_client_secret: Option<String>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub icon_url: Option<String>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
196#[serde(tag = "kind", rename_all = "snake_case")]
197pub enum CatalogProvider {
198 Rest {
199 #[serde(flatten)]
200 oauth: OAuthProviderConfig,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 group: Option<ProviderGroup>,
203 },
204 Mcp {
205 #[serde(flatten)]
206 oauth: OAuthProviderConfig,
207 transport_url: String,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 group: Option<ProviderGroup>,
212 },
213}
214
215impl CatalogProvider {
216 pub fn oauth(&self) -> &OAuthProviderConfig {
218 match self {
219 Self::Rest { oauth, .. } | Self::Mcp { oauth, .. } => oauth,
220 }
221 }
222
223 pub fn group(&self) -> Option<&ProviderGroup> {
225 match self {
226 Self::Rest { group, .. } | Self::Mcp { group, .. } => group.as_ref(),
227 }
228 }
229
230 pub fn transport_url(&self) -> Option<&str> {
232 match self {
233 Self::Mcp { transport_url, .. } => Some(transport_url.as_str()),
234 _ => None,
235 }
236 }
237}
238
239impl OAuthProviderConfig {
240 pub fn display(&self) -> String {
242 self.display_name.clone().unwrap_or_else(|| {
243 let mut chars = self.name.chars();
244 match chars.next() {
245 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
246 None => String::new(),
247 }
248 })
249 }
250
251 pub fn to_auth_type(&self, scopes: Vec<String>) -> crate::auth::AuthType {
255 crate::auth::AuthType::OAuth2 {
256 flow_type: crate::auth::OAuth2FlowType::AuthorizationCode,
257 authorization_url: self.authorization_url.clone(),
258 token_url: self.token_url.clone(),
259 refresh_url: self.refresh_url.clone(),
260 scopes,
261 send_redirect_uri: true,
262 }
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema, PartialEq)]
271#[serde(tag = "type", rename_all = "snake_case")]
272pub enum ConnectionAuth {
273 None,
275 Oauth {
280 provider: OAuthProviderConfig,
281 #[serde(default)]
282 scopes: Vec<String>,
283 },
284 Custom {
287 #[serde(default)]
288 fields: Vec<CustomField>,
289 },
290 DistriNative,
292}
293
294impl ConnectionAuth {
295 pub fn provider_name(&self) -> &str {
296 match self {
297 Self::None => "none",
298 Self::Oauth { provider, .. } => provider.name.as_str(),
299 Self::Custom { .. } => "custom",
300 Self::DistriNative => "distri",
301 }
302 }
303
304 pub fn oauth_config(&self) -> Option<&OAuthProviderConfig> {
306 match self {
307 Self::Oauth { provider, .. } => Some(provider),
308 _ => None,
309 }
310 }
311
312 pub fn is_oauth(&self) -> bool {
313 matches!(self, Self::Oauth { .. })
314 }
315
316 pub fn is_custom(&self) -> bool {
317 matches!(self, Self::Custom { .. })
318 }
319
320 pub fn is_distri_native(&self) -> bool {
321 matches!(self, Self::DistriNative)
322 }
323
324 pub fn custom_fields(&self) -> &[CustomField] {
325 match self {
326 Self::Custom { fields } => fields,
327 _ => &[],
328 }
329 }
330
331 pub fn custom_required_fields(&self) -> Vec<&CustomField> {
332 match self {
333 Self::Custom { fields } => fields.iter().filter(|f| f.required).collect(),
334 _ => vec![],
335 }
336 }
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
342pub struct ConnectionToken {
343 pub access_token: String,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub refresh_token: Option<String>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub expires_at: Option<DateTime<Utc>>,
348 #[serde(default = "default_token_type")]
349 pub token_type: String,
350 #[serde(default)]
351 pub scopes: Vec<String>,
352}
353
354fn default_token_type() -> String {
355 "Bearer".to_string()
356}
357
358impl ConnectionToken {
359 pub fn is_expired(&self) -> bool {
360 self.expires_at.map(|exp| exp < Utc::now()).unwrap_or(false)
361 }
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema, PartialEq)]
378#[serde(tag = "type", rename_all = "snake_case")]
379pub enum ConnectionKind {
380 Default {
384 #[serde(default, skip_serializing_if = "Option::is_none")]
389 skill_content: Option<String>,
390 },
391 Mcp {
395 #[serde(flatten)]
396 mcp: McpConnectionSpec,
397 },
398}
399
400impl Default for ConnectionKind {
401 fn default() -> Self {
402 Self::Default {
403 skill_content: None,
404 }
405 }
406}
407
408impl ConnectionKind {
409 pub fn is_mcp(&self) -> bool {
410 matches!(self, Self::Mcp { .. })
411 }
412 pub fn as_mcp(&self) -> Option<&McpConnectionSpec> {
413 match self {
414 Self::Mcp { mcp } => Some(mcp),
415 _ => None,
416 }
417 }
418 pub fn skill_content(&self) -> Option<&str> {
422 match self {
423 Self::Default { skill_content } => skill_content.as_deref(),
424 Self::Mcp { .. } => None,
425 }
426 }
427 pub fn kind_str(&self) -> &'static str {
428 match self {
429 Self::Default { .. } => "default",
430 Self::Mcp { .. } => "mcp",
431 }
432 }
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema, PartialEq)]
442#[serde(rename_all = "snake_case")]
443pub struct McpConnectionSpec {
444 pub transport: McpClientTransport,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
447 pub description: Option<String>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub tool_filter: Option<McpToolFilter>,
451 #[serde(default = "default_true")]
453 pub enabled: bool,
454}
455
456fn default_true() -> bool {
457 true
458}
459
460#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, ToSchema, PartialEq)]
461#[serde(rename_all = "snake_case")]
462pub struct McpToolFilter {
463 #[serde(default, skip_serializing_if = "Vec::is_empty")]
464 pub include: Vec<String>,
465 #[serde(default, skip_serializing_if = "Vec::is_empty")]
466 pub exclude: Vec<String>,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
470pub struct Connection {
471 pub id: Uuid,
472 pub workspace_id: Uuid,
473 pub skill_id: Uuid,
474 pub name: String,
475 pub status: ConnectionStatus,
476 pub config: serde_json::Value,
477 pub connected_by: Option<Uuid>,
478 pub created_at: DateTime<Utc>,
479 pub updated_at: DateTime<Utc>,
480 pub auth_scope: AuthScope,
483 pub auth: ConnectionAuth,
486 #[serde(default)]
488 pub kind: ConnectionKind,
489 #[serde(default)]
492 pub is_system: bool,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
496pub struct NewConnection {
497 pub workspace_id: Uuid,
498 pub skill_id: Uuid,
499 pub name: String,
500 pub status: ConnectionStatus,
501 pub config: serde_json::Value,
502 pub connected_by: Option<Uuid>,
503 pub auth_scope: AuthScope,
504 pub auth: ConnectionAuth,
505 #[serde(default)]
506 pub kind: ConnectionKind,
507 #[serde(default)]
508 pub is_system: bool,
509}
510
511impl Connection {
512 pub fn is_mcp(&self) -> bool {
513 self.kind.is_mcp()
514 }
515 pub fn mcp_spec(&self) -> Option<&McpConnectionSpec> {
516 self.kind.as_mcp()
517 }
518}
519
520#[derive(Debug, Clone, Deserialize)]
530pub struct VerifyRequest {
531 pub url: String,
532 pub method: String,
533 #[serde(default)]
534 pub headers: HashMap<String, String>,
535}
536
537impl Connection {
538 pub fn verify_request(&self) -> Option<VerifyRequest> {
540 self.config
541 .get("verify_request")
542 .and_then(|v| serde_json::from_value(v.clone()).ok())
543 }
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema, Default)]
555pub struct ConnectionRequirement {
556 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub provider: Option<String>,
561
562 #[serde(default, skip_serializing_if = "Option::is_none")]
564 pub connection_id: Option<Uuid>,
565
566 #[serde(default, skip_serializing_if = "Vec::is_empty")]
570 pub scopes: Vec<String>,
571
572 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub env_var: Option<String>,
576
577 #[serde(default)]
581 pub required: bool,
582}