diff --git a/Cargo.lock b/Cargo.lock index eb74e3d93..26a08c8c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2928,6 +2928,7 @@ dependencies = [ "fabro-llm", "fabro-macros", "fabro-manifest", + "fabro-mcp-store", "fabro-model", "fabro-proc", "fabro-redact", diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index ea56ec278..73bb5e0a8 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -645,6 +645,18 @@ fn main() { "fabro_automation::AutomationReplace", &[], ), + // MCP server catalog wire types reuse the domain model rather than + // generating parallel DTOs. The integer formats in the spec generate as + // `i64`/`i32`, but the domain types use `u64`/`u16`; the replacements + // ensure the signed widths never leak into the public client. + // (`McpTransport`/`McpHttpProtocol` are already mapped above.) + ("McpServer", "fabro_types::McpServerDefinition", &[]), + ("CreateMcpServerRequest", "fabro_types::McpServerDraft", &[]), + ( + "ReplaceMcpServerRequest", + "fabro_types::McpServerReplace", + &[], + ), ("Environment", "fabro_environment::Environment", &[]), ("SessionId", "fabro_types::SessionId", &[]), ("TurnId", "fabro_types::TurnId", &[]), diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs index efbbc347c..f122007c7 100644 --- a/lib/crates/fabro-api/src/lib.rs +++ b/lib/crates/fabro-api/src/lib.rs @@ -24,8 +24,7 @@ pub mod types { ModelTestMode, Provider, ReasoningEffort, ReasoningEffortFeature, Speed as BillingSpeed, }; pub use fabro_types::run_event::AgentSessionActivatedProps; - pub use fabro_types::settings::ServerNamespace; - pub use fabro_types::settings::run::{McpHttpProtocol, McpTransport}; + pub use fabro_types::settings::run::McpHttpProtocol; pub use fabro_types::settings::server::{ GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings, LogDestination, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, @@ -35,6 +34,7 @@ pub mod types { ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, WebhookStrategy, }; + pub use fabro_types::settings::{McpTransport, ServerNamespace}; pub use fabro_types::status::{ BlockedReason, FailureReason, PendingReason, RunControlAction, RunStatus, SuccessReason, }; @@ -46,28 +46,29 @@ pub mod types { FailureCategory, FailureDetail, FailureSignature, GitContext, IdpIdentity, IntegrationConnectionKind, IntegrationConnectionState, IntegrationConnectionStatus, IntegrationProvider, IntegrationStatus, InterviewOption, InterviewQuestionRecord, - McpServerProjection, McpServerStatus, Message, PairId, PairMessageId, PairMessageRecord, - PairMessageRequest, PairRecord, PairStartRequest, PairStatus, PairTarget, - PairTranscriptEntry, PairTranscriptResponse, PendingInterviewRecord, PermissionLevel, - PreRunPushOutcome, Principal, PullRequest, PullRequestDetails, PullRequestDetailsStatus, - PullRequestDetailsUnavailableReason, PullRequestLink, PullRequestMeta, PullRequestResponse, - QuestionType, RepositoryRef, Role, Run, RunApproval, RunApprovalState, RunClientProvenance, - RunEvent, RunEventDetailContentKind, RunEventDetailResponse, RunFailure, - RunPairStatusResponse, RunProjection, RunProvenance, RunRunnableSource, RunSandbox, - RunSandboxFailure, RunSandboxInstance, RunSandboxKind, RunSandboxPlan, RunSandboxRuntime, - RunServerProvenance, RunSize, SandboxDetails, SandboxInfo, SandboxListMeta, - SandboxListResponse, SandboxNetwork, SandboxNetworkPolicy, SandboxNetworkPolicyMode, - SandboxProviderKind, SandboxProviderLookupError, SandboxResources, SandboxService, - SandboxServiceListResponse, SandboxState, SandboxTimestamps, SecretMetadata, SecretType, - ServerSettings, SessionDetail, SessionId, SessionMessage, SessionRecord, SessionStatus, - SessionSummary, SessionTurn, SkillsProjection, StageCompletion, StageContextWindow, - StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod, - StageContextWindowProjection, StageContextWindowStaleness, - StageContextWindowUnavailableReason, StageContextWindowWarning, StageHandler, - StageModelUsage, StageOutcome, StageProjection, StageState, SubAgentProjection, - SubAgentStatus, SystemActorKind, SystemIntegrationStatus, SystemIntegrationsResponse, - TodoListProjection, TurnId, UpdateVariableRequest, UserPrincipal, Variable, - VariableListResponse, WorkflowSettings, + McpServerDefinition as McpServer, McpServerDraft as CreateMcpServerRequest, + McpServerProjection, McpServerReplace as ReplaceMcpServerRequest, McpServerStatus, Message, + PairId, PairMessageId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest, + PairStatus, PairTarget, PairTranscriptEntry, PairTranscriptResponse, + PendingInterviewRecord, PermissionLevel, PreRunPushOutcome, Principal, PullRequest, + PullRequestDetails, PullRequestDetailsStatus, PullRequestDetailsUnavailableReason, + PullRequestLink, PullRequestMeta, PullRequestResponse, QuestionType, RepositoryRef, Role, + Run, RunApproval, RunApprovalState, RunClientProvenance, RunEvent, + RunEventDetailContentKind, RunEventDetailResponse, RunFailure, RunPairStatusResponse, + RunProjection, RunProvenance, RunRunnableSource, RunSandbox, RunSandboxFailure, + RunSandboxInstance, RunSandboxKind, RunSandboxPlan, RunSandboxRuntime, RunServerProvenance, + RunSize, SandboxDetails, SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxNetwork, + SandboxNetworkPolicy, SandboxNetworkPolicyMode, SandboxProviderKind, + SandboxProviderLookupError, SandboxResources, SandboxService, SandboxServiceListResponse, + SandboxState, SandboxTimestamps, SecretMetadata, SecretType, ServerSettings, SessionDetail, + SessionId, SessionMessage, SessionRecord, SessionStatus, SessionSummary, SessionTurn, + SkillsProjection, StageCompletion, StageContextWindow, StageContextWindowBreakdownItem, + StageContextWindowCategory, StageContextWindowCountMethod, StageContextWindowProjection, + StageContextWindowStaleness, StageContextWindowUnavailableReason, + StageContextWindowWarning, StageHandler, StageModelUsage, StageOutcome, StageProjection, + StageState, SubAgentProjection, SubAgentStatus, SystemActorKind, SystemIntegrationStatus, + SystemIntegrationsResponse, TodoListProjection, TurnId, UpdateVariableRequest, + UserPrincipal, Variable, VariableListResponse, WorkflowSettings, }; pub use crate::generated::types::*; diff --git a/lib/crates/fabro-api/tests/mcp_server_round_trip.rs b/lib/crates/fabro-api/tests/mcp_server_round_trip.rs new file mode 100644 index 000000000..91ce0ab3e --- /dev/null +++ b/lib/crates/fabro-api/tests/mcp_server_round_trip.rs @@ -0,0 +1,108 @@ +use fabro_api::types::{ + CreateMcpServerRequest as ApiCreateMcpServerRequest, McpHttpProtocol as ApiMcpHttpProtocol, + McpServer as ApiMcpServer, McpTransport as ApiMcpTransport, + ReplaceMcpServerRequest as ApiReplaceMcpServerRequest, +}; +use fabro_types::settings::McpTransport; +use fabro_types::settings::run::McpHttpProtocol; +use fabro_types::{McpServerDefinition, McpServerDraft, McpServerReplace}; +use serde_json::json; + +// Compile-time witnesses that the generated API types resolve to the same types +// as the `fabro-types` domain types via `with_replacement(...)`. If progenitor +// stops reusing the domain type, these functions stop type-checking and the +// build fails. This is what keeps the spec's signed integer formats (`i64` for +// the `u64` timeouts, `i32` for the `u16` sandbox port) from leaking into the +// public client. +const _: fn(ApiMcpServer) -> McpServerDefinition = |value| value; +const _: fn(ApiCreateMcpServerRequest) -> McpServerDraft = |value| value; +const _: fn(ApiReplaceMcpServerRequest) -> McpServerReplace = |value| value; +const _: fn(ApiMcpTransport) -> McpTransport = |value| value; +const _: fn(ApiMcpHttpProtocol) -> McpHttpProtocol = |value| value; + +#[test] +fn mcp_server_response_round_trips_http_transport_json_shape() { + let value = json!({ + "id": "sentry", + "revision": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "name": "Sentry", + "description": "Production Sentry MCP server.", + "transport": { + "type": "http", + "protocol": "streamable_http", + "url": "https://sentry.example.com/mcp", + "headers": { + "X-Org": "fabro" + } + }, + "startup_timeout_secs": 10, + "tool_timeout_secs": 60 + }); + + let api: ApiMcpServer = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(api).unwrap(), value); +} + +#[test] +fn mcp_server_response_round_trips_null_description() { + let value = json!({ + "id": "local", + "revision": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "name": "Local", + "description": null, + "transport": { + "type": "stdio", + "command": ["npx", "@modelcontextprotocol/server-filesystem"], + "env": {} + }, + "startup_timeout_secs": 10, + "tool_timeout_secs": 60 + }); + + let api: ApiMcpServer = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(api).unwrap(), value); +} + +#[test] +fn create_mcp_server_request_round_trips_sandbox_transport_json_shape() { + // The sandbox transport carries a `port`, whose spec format is `int32`. + // Reusing the domain type pins it to `u16`, so this round-trip also guards + // against the signed-width leak. + let value = json!({ + "id": "sandbox-mcp", + "name": "Sandbox MCP", + "transport": { + "type": "sandbox", + "protocol": "sse", + "command": ["./serve"], + "port": 8080, + "env": { + "NODE_ENV": "production" + } + }, + "startup_timeout_secs": 15, + "tool_timeout_secs": 90 + }); + + let api: ApiCreateMcpServerRequest = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(api).unwrap(), value); +} + +#[test] +fn replace_mcp_server_request_round_trips_http_transport_json_shape() { + let value = json!({ + "name": "Sentry v2", + "description": "Updated.", + "transport": { + "type": "http", + "protocol": "streamable_http", + "url": "https://sentry.example.com/mcp/v2", + "headers": {} + }, + "startup_timeout_secs": 20, + "tool_timeout_secs": 120 + }); + + let api: ApiReplaceMcpServerRequest = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(api).unwrap(), value); +} diff --git a/lib/crates/fabro-server/Cargo.toml b/lib/crates/fabro-server/Cargo.toml index 387298232..8627bebb9 100644 --- a/lib/crates/fabro-server/Cargo.toml +++ b/lib/crates/fabro-server/Cargo.toml @@ -38,6 +38,7 @@ fabro-github = { path = "../fabro-github" } fabro-agent = { path = "../fabro-agent" } fabro-llm = { path = "../fabro-llm" } fabro-manifest = { path = "../fabro-manifest" } +fabro-mcp-store = { path = "../fabro-mcp-store" } fabro-model = { path = "../fabro-model" } fabro-proc = { path = "../fabro-proc" } fabro-tool = { path = "../fabro-tool" } diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index d913eba22..c6aacb6ef 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -64,6 +64,7 @@ use fabro_llm::model_test::run_model_test; use fabro_llm::types::{ FinishReason, Message as LlmMessage, Request as LlmRequest, ToolChoice, ToolDefinition, }; +use fabro_mcp_store::McpServerStore; use fabro_model::catalog::LlmCatalogSettings; use fabro_model::{BilledTokenCounts, Catalog, ModelRef, ModelTestMode, ProviderId}; use fabro_redact::redact_jsonl_line; @@ -1107,6 +1108,7 @@ pub(crate) struct AppStores { pub(crate) runs: Arc, pub(crate) automations: Arc, pub(crate) environments: Arc, + pub(crate) mcp_servers: Arc, pub(crate) vault: Arc>, pub(crate) variables: Arc, } @@ -1122,6 +1124,10 @@ impl AppState { &self.stores.environments } + pub(crate) fn mcp_server_store(&self) -> &McpServerStore { + &self.stores.mcp_servers + } + pub(crate) async fn materialize_automation_run( &self, input: AutomationRunMaterializeInput, @@ -2285,6 +2291,13 @@ fn environment_dir_for_active_config(active_config_path: &std::path::Path) -> Pa .join("environments") } +fn mcp_server_dir_for_active_config(active_config_path: &std::path::Path) -> PathBuf { + active_config_path + .parent() + .unwrap_or_else(|| std::path::Path::new(".")) + .join("mcps") +} + pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result> { let AppStateConfig { resolved_settings, @@ -2329,6 +2342,12 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result vault, @@ -2439,6 +2458,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result, + meta: McpServerListMeta, +} + +#[derive(Serialize)] +struct McpServerListMeta { + total: usize, +} + +pub(super) fn routes() -> Router> { + Router::new() + .route( + "/mcp-servers", + get(list_mcp_servers).post(create_mcp_server), + ) + .route( + "/mcp-servers/{id}", + get(get_mcp_server) + .put(replace_mcp_server) + .delete(delete_mcp_server), + ) +} + +async fn list_mcp_servers(_auth: RequiredUser, State(state): State>) -> Response { + let data = state.mcp_server_store().list().await; + let total = data.len(); + ( + StatusCode::OK, + Json(McpServerListResponse { + data, + meta: McpServerListMeta { total }, + }), + ) + .into_response() +} + +async fn create_mcp_server( + _auth: RequiredUser, + State(state): State>, + Json(draft): Json, +) -> Result { + // TODO(mcp): credential-literal validation — warn vs hard-reject still open. + // The store keeps validation structural (id/name/transport); whether the API + // should reject credential-looking literal values in env/header fields and + // point the user at `{{ secrets.NAME }}` is undecided. Until then literals + // are accepted verbatim and secret references are stored as written. + let definition = state.mcp_server_store().create(draft).await?; + Ok(mcp_server_with_etag_response( + StatusCode::CREATED, + definition, + )) +} + +async fn get_mcp_server( + _auth: RequiredUser, + State(state): State>, + Path(id): Path, +) -> Result { + let id = parse_path_id(id)?; + match state.mcp_server_store().get(&id).await { + Some(definition) => Ok(mcp_server_with_etag_response(StatusCode::OK, definition)), + None => Err(ApiError::not_found(format!("mcp server not found: {id}"))), + } +} + +async fn replace_mcp_server( + _auth: RequiredUser, + State(state): State>, + headers: HeaderMap, + Path(id): Path, + Json(replacement): Json, +) -> Result { + let id = parse_path_id(id)?; + let expected = parse_required_if_match(&headers, "mcp server", &id)?; + // TODO(mcp): credential-literal validation — warn vs hard-reject still open. + // See `create_mcp_server`; validation stays structural here too. + let definition = state + .mcp_server_store() + .replace(&id, &expected, replacement) + .await?; + Ok(mcp_server_with_etag_response(StatusCode::OK, definition)) +} + +async fn delete_mcp_server( + _auth: RequiredUser, + State(state): State>, + headers: HeaderMap, + Path(id): Path, +) -> Result { + let id = parse_path_id(id)?; + let expected = parse_required_if_match(&headers, "mcp server", &id)?; + state.mcp_server_store().delete(&id, &expected).await?; + Ok(StatusCode::NO_CONTENT.into_response()) +} + +fn parse_path_id(id: String) -> Result { + McpServerId::new(id) + .map_err(|err| ApiError::bad_request(format!("invalid mcp server id: {err}"))) +} + +fn mcp_server_with_etag_response(status: StatusCode, definition: McpServerDefinition) -> Response { + let revision = definition.revision.clone(); + json_with_etag_response(status, "mcp server", &revision, definition) +} + +impl From for ApiError { + fn from(err: McpServerStoreError) -> Self { + match err { + McpServerStoreError::NotFound { id } => { + Self::not_found(format!("mcp server not found: {id}")) + } + McpServerStoreError::AlreadyExists { id } => Self::new( + StatusCode::CONFLICT, + format!("mcp server already exists: {id}"), + ), + McpServerStoreError::StaleRevision { id, .. } => Self::new( + StatusCode::CONFLICT, + format!("mcp server revision is stale: {id}"), + ), + McpServerStoreError::Validation { source } => { + Self::new(StatusCode::UNPROCESSABLE_ENTITY, source.to_string()) + } + // Remaining variants are persistence/parse faults that indicate an + // internal problem rather than a client one. + McpServerStoreError::InvalidFilename { .. } + | McpServerStoreError::Parse { .. } + | McpServerStoreError::InvalidUtf8 { .. } + | McpServerStoreError::Serialize { .. } + | McpServerStoreError::Io { .. } => Self::new( + StatusCode::INTERNAL_SERVER_ERROR, + "mcp server store operation failed", + ), + } + } +} diff --git a/lib/crates/fabro-server/src/server/handler/mod.rs b/lib/crates/fabro-server/src/server/handler/mod.rs index c1a5b35ce..75dd6507d 100644 --- a/lib/crates/fabro-server/src/server/handler/mod.rs +++ b/lib/crates/fabro-server/src/server/handler/mod.rs @@ -16,6 +16,7 @@ pub(in crate::server) mod events; pub(in crate::server) mod graph; pub(in crate::server) mod lifecycle; mod llm_sse; +mod mcp_servers; mod models; mod pair; mod playground; @@ -211,6 +212,7 @@ pub(super) fn real_routes() -> Router> { .merge(pull_requests::routes()) .merge(artifacts::routes()) .merge(automations::routes()) + .merge(mcp_servers::routes()) .merge(environments::routes()) .merge(sandbox::routes()) .merge(sandboxes::routes()) diff --git a/lib/crates/fabro-server/tests/it/api/mcp_servers.rs b/lib/crates/fabro-server/tests/it/api/mcp_servers.rs new file mode 100644 index 000000000..5ae238fdd --- /dev/null +++ b/lib/crates/fabro-server/tests/it/api/mcp_servers.rs @@ -0,0 +1,569 @@ +use std::path::{Path, PathBuf}; + +use axum::body::Body; +use axum::http::{Method, Request, StatusCode, header}; +use fabro_server::server::build_router; +use fabro_server::test_support::{TestAppStateBuilder, build_test_router, test_auth_mode}; +use serde_json::{Value, json}; +use tower::ServiceExt; + +use crate::helpers::{checked_response, response_json, response_status}; + +fn mcp_server_body(id: &str, name: &str) -> Value { + json!({ + "id": id, + "name": name, + "description": "Production MCP server.", + "transport": { + "type": "http", + "protocol": "streamable_http", + "url": "https://example.com/mcp", + "headers": { + "X-Org": "fabro" + } + }, + "startup_timeout_secs": 10, + "tool_timeout_secs": 60 + }) +} + +fn replacement_body(name: &str) -> Value { + json!({ + "name": name, + "description": null, + "transport": { + "type": "http", + "protocol": "sse", + "url": "https://example.com/mcp/v2", + "headers": {} + }, + "startup_timeout_secs": 15, + "tool_timeout_secs": 90 + }) +} + +fn mcp_server_app() -> (axum::Router, tempfile::TempDir, PathBuf) { + let temp_dir = tempfile::tempdir().expect("mcp server test tempdir should be created"); + let active_config_path = temp_dir.path().join("settings.toml"); + let mcp_dir = temp_dir.path().join("mcps"); + let state = TestAppStateBuilder::new() + .active_config_path(active_config_path) + .build(); + (build_test_router(state), temp_dir, mcp_dir) +} + +fn json_request(method: Method, path: &str, body: &Value) -> Request { + Request::builder() + .method(method) + .uri(crate::helpers::api(path)) + .header(header::CONTENT_TYPE, "application/json") + .body(Body::from( + serde_json::to_vec(&body).expect("mcp server fixture should serialize"), + )) + .expect("mcp server JSON request should build") +} + +fn empty_request(method: Method, path: &str) -> Request { + Request::builder() + .method(method) + .uri(crate::helpers::api(path)) + .body(Body::empty()) + .expect("mcp server request should build") +} + +fn request_with_if_match( + method: Method, + path: &str, + revision: &str, + body: Option, +) -> Request { + let mut builder = Request::builder() + .method(method) + .uri(crate::helpers::api(path)) + .header(header::IF_MATCH, revision); + let body = match body { + Some(value) => { + builder = builder.header(header::CONTENT_TYPE, "application/json"); + Body::from(serde_json::to_vec(&value).expect("mcp server fixture should serialize")) + } + None => Body::empty(), + }; + builder + .body(body) + .expect("mcp server If-Match request should build") +} + +async fn create_mcp_server(app: &axum::Router, id: &str, name: &str) -> Value { + create_mcp_server_with_body(app, &mcp_server_body(id, name)).await +} + +async fn create_mcp_server_with_body(app: &axum::Router, body: &Value) -> Value { + let response = app + .clone() + .oneshot(json_request(Method::POST, "/mcp-servers", body)) + .await + .expect("create mcp server should respond"); + response_json(response, StatusCode::CREATED, "POST /api/v1/mcp-servers").await +} + +fn revision_from(body: &Value) -> &str { + body["revision"] + .as_str() + .expect("mcp server response should include a revision") +} + +async fn persisted_mcp_server_toml(mcp_dir: &Path, id: &str) -> toml::Value { + let persisted = tokio::fs::read_to_string(mcp_dir.join(format!("{id}.toml"))) + .await + .expect("persisted mcp server TOML should be readable"); + toml::from_str(&persisted).expect("persisted mcp server TOML should parse") +} + +#[tokio::test] +async fn empty_mcp_server_list_returns_total_zero() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + + let response = app + .oneshot(empty_request(Method::GET, "/mcp-servers")) + .await + .expect("list mcp servers should respond"); + let body = response_json(response, StatusCode::OK, "GET /api/v1/mcp-servers").await; + + assert_eq!( + body, + json!({ + "data": [], + "meta": { "total": 0 } + }) + ); +} + +#[tokio::test] +async fn create_mcp_server_returns_etag_and_persists_sibling_toml_file() { + let (app, _temp_dir, mcp_dir) = mcp_server_app(); + + let response = app + .clone() + .oneshot(json_request( + Method::POST, + "/mcp-servers", + &mcp_server_body("sentry", "Sentry"), + )) + .await + .expect("create mcp server should respond"); + let response = + checked_response(response, StatusCode::CREATED, "POST /api/v1/mcp-servers").await; + let etag = response + .headers() + .get(header::ETAG) + .expect("create mcp server should include ETag") + .to_str() + .expect("ETag should be ASCII") + .to_string(); + let body = crate::helpers::body_json(response.into_body()).await; + + assert_eq!(body["id"], "sentry"); + assert_eq!(body["name"], "Sentry"); + assert_eq!(etag, format!("\"{}\"", revision_from(&body))); + assert!(mcp_dir.join("sentry.toml").exists()); + + let persisted = persisted_mcp_server_toml(&mcp_dir, "sentry").await; + assert_eq!( + persisted.get("name").and_then(toml::Value::as_str), + Some("Sentry") + ); + assert!(persisted.get("id").is_none()); + assert!(persisted.get("revision").is_none()); +} + +#[tokio::test] +async fn mcp_server_round_trips_through_create_get_and_toml() { + let (app, _temp_dir, mcp_dir) = mcp_server_app(); + + let created = create_mcp_server(&app, "sentry", "Sentry").await; + assert_eq!(created["transport"]["type"], "http"); + assert_eq!(created["transport"]["url"], "https://example.com/mcp"); + + let response = app + .clone() + .oneshot(empty_request(Method::GET, "/mcp-servers/sentry")) + .await + .expect("get mcp server should respond"); + let retrieved = response_json(response, StatusCode::OK, "GET /api/v1/mcp-servers/sentry").await; + assert_eq!(retrieved, created); + + let persisted = persisted_mcp_server_toml(&mcp_dir, "sentry").await; + assert_eq!( + persisted + .get("transport") + .and_then(|transport| transport.get("type")) + .and_then(toml::Value::as_str), + Some("http") + ); +} + +#[tokio::test] +async fn list_mcp_servers_returns_items_sorted_by_id() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + create_mcp_server(&app, "zulu", "Zulu").await; + create_mcp_server(&app, "alpha", "Alpha").await; + + let response = app + .oneshot(empty_request(Method::GET, "/mcp-servers")) + .await + .expect("list mcp servers should respond"); + let body = response_json(response, StatusCode::OK, "GET /api/v1/mcp-servers").await; + + assert_eq!(body["meta"]["total"], 2); + assert_eq!(body["data"][0]["id"], "alpha"); + assert_eq!(body["data"][1]["id"], "zulu"); +} + +#[tokio::test] +async fn duplicate_mcp_server_create_returns_conflict() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + create_mcp_server(&app, "sentry", "Sentry").await; + + let response = app + .oneshot(json_request( + Method::POST, + "/mcp-servers", + &mcp_server_body("sentry", "Duplicate"), + )) + .await + .expect("duplicate create should respond"); + + response_status( + response, + StatusCode::CONFLICT, + "POST /api/v1/mcp-servers duplicate", + ) + .await; +} + +#[tokio::test] +async fn get_mcp_server_returns_current_etag() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + let created = create_mcp_server(&app, "sentry", "Sentry").await; + let revision = revision_from(&created); + + let response = app + .oneshot(empty_request(Method::GET, "/mcp-servers/sentry")) + .await + .expect("get mcp server should respond"); + let response = + checked_response(response, StatusCode::OK, "GET /api/v1/mcp-servers/sentry").await; + + assert_eq!( + response + .headers() + .get(header::ETAG) + .expect("GET mcp server should include ETag"), + &format!("\"{revision}\"") + ); + let body = crate::helpers::body_json(response.into_body()).await; + assert_eq!(body["revision"], revision); +} + +#[tokio::test] +async fn get_missing_mcp_server_returns_not_found() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + + let response = app + .oneshot(empty_request(Method::GET, "/mcp-servers/missing")) + .await + .expect("get missing mcp server should respond"); + + response_status( + response, + StatusCode::NOT_FOUND, + "GET /api/v1/mcp-servers/missing", + ) + .await; +} + +#[tokio::test] +async fn replace_mcp_server_accepts_unquoted_if_match_and_returns_new_etag() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + let created = create_mcp_server(&app, "sentry", "Sentry").await; + let revision = revision_from(&created); + + let response = app + .oneshot(request_with_if_match( + Method::PUT, + "/mcp-servers/sentry", + revision, + Some(replacement_body("Sentry v2")), + )) + .await + .expect("replace mcp server should respond"); + let response = + checked_response(response, StatusCode::OK, "PUT /api/v1/mcp-servers/sentry").await; + let etag = response + .headers() + .get(header::ETAG) + .expect("PUT mcp server should include ETag") + .to_str() + .expect("ETag should be ASCII") + .to_string(); + let body = crate::helpers::body_json(response.into_body()).await; + + assert_eq!(body["name"], "Sentry v2"); + assert_eq!(body["transport"]["protocol"], "sse"); + assert_ne!(body["revision"], revision); + assert_eq!(etag, format!("\"{}\"", revision_from(&body))); +} + +#[tokio::test] +async fn replace_mcp_server_accepts_quoted_if_match() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + let created = create_mcp_server(&app, "sentry", "Sentry").await; + let revision = revision_from(&created); + + let response = app + .oneshot(request_with_if_match( + Method::PUT, + "/mcp-servers/sentry", + &format!("\"{revision}\""), + Some(replacement_body("Sentry v2")), + )) + .await + .expect("replace mcp server with quoted If-Match should respond"); + + response_status( + response, + StatusCode::OK, + "PUT /api/v1/mcp-servers/sentry quoted If-Match", + ) + .await; +} + +#[tokio::test] +async fn stale_mcp_server_replace_returns_conflict() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + let created = create_mcp_server(&app, "sentry", "Sentry").await; + let stale_revision = revision_from(&created).to_string(); + + let replaced = app + .clone() + .oneshot(request_with_if_match( + Method::PUT, + "/mcp-servers/sentry", + &stale_revision, + Some(replacement_body("Updated")), + )) + .await + .expect("first replace should respond"); + response_status( + replaced, + StatusCode::OK, + "PUT /api/v1/mcp-servers/sentry first replace", + ) + .await; + + let response = app + .oneshot(request_with_if_match( + Method::PUT, + "/mcp-servers/sentry", + &stale_revision, + Some(replacement_body("Stale")), + )) + .await + .expect("stale replace should respond"); + + response_status( + response, + StatusCode::CONFLICT, + "PUT /api/v1/mcp-servers/sentry stale", + ) + .await; +} + +#[tokio::test] +async fn replace_and_delete_mcp_server_require_if_match() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + create_mcp_server(&app, "sentry", "Sentry").await; + + let replace_response = app + .clone() + .oneshot(json_request( + Method::PUT, + "/mcp-servers/sentry", + &replacement_body("Updated"), + )) + .await + .expect("replace without If-Match should respond"); + response_status( + replace_response, + StatusCode::PRECONDITION_REQUIRED, + "PUT /api/v1/mcp-servers/sentry without If-Match", + ) + .await; + + let delete_response = app + .oneshot(empty_request(Method::DELETE, "/mcp-servers/sentry")) + .await + .expect("delete without If-Match should respond"); + response_status( + delete_response, + StatusCode::PRECONDITION_REQUIRED, + "DELETE /api/v1/mcp-servers/sentry without If-Match", + ) + .await; +} + +#[tokio::test] +async fn delete_mcp_server_removes_file_and_resource() { + let (app, _temp_dir, mcp_dir) = mcp_server_app(); + let created = create_mcp_server(&app, "sentry", "Sentry").await; + let revision = revision_from(&created); + + let response = app + .clone() + .oneshot(request_with_if_match( + Method::DELETE, + "/mcp-servers/sentry", + &format!("\"{revision}\""), + None, + )) + .await + .expect("delete mcp server should respond"); + response_status( + response, + StatusCode::NO_CONTENT, + "DELETE /api/v1/mcp-servers/sentry", + ) + .await; + + assert!(!mcp_dir.join("sentry.toml").exists()); + let response = app + .oneshot(empty_request(Method::GET, "/mcp-servers/sentry")) + .await + .expect("get deleted mcp server should respond"); + response_status( + response, + StatusCode::NOT_FOUND, + "GET /api/v1/mcp-servers/sentry after delete", + ) + .await; +} + +#[tokio::test] +async fn empty_mcp_server_name_is_unprocessable() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + let mut body = mcp_server_body("sentry", "Sentry"); + body["name"] = json!(" "); + + let response = app + .oneshot(json_request(Method::POST, "/mcp-servers", &body)) + .await + .expect("empty mcp server name create should respond"); + + response_status( + response, + StatusCode::UNPROCESSABLE_ENTITY, + "POST /api/v1/mcp-servers empty name", + ) + .await; +} + +#[tokio::test] +async fn empty_transport_command_is_unprocessable() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + let mut body = mcp_server_body("local", "Local"); + body["transport"] = json!({ + "type": "stdio", + "command": [], + "env": {} + }); + + let response = app + .oneshot(json_request(Method::POST, "/mcp-servers", &body)) + .await + .expect("empty transport command create should respond"); + + response_status( + response, + StatusCode::UNPROCESSABLE_ENTITY, + "POST /api/v1/mcp-servers empty transport command", + ) + .await; +} + +#[tokio::test] +async fn unknown_transport_type_is_unprocessable() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + let mut body = mcp_server_body("sentry", "Sentry"); + body["transport"] = json!({ + "type": "carrier-pigeon", + "url": "https://example.com/mcp" + }); + + let response = app + .oneshot(json_request(Method::POST, "/mcp-servers", &body)) + .await + .expect("unknown transport type create should respond"); + + response_status( + response, + StatusCode::UNPROCESSABLE_ENTITY, + "POST /api/v1/mcp-servers unknown transport type", + ) + .await; +} + +#[tokio::test] +async fn invalid_mcp_server_id_is_bad_request() { + let (app, _temp_dir, _mcp_dir) = mcp_server_app(); + + let response = app + .oneshot(empty_request(Method::GET, "/mcp-servers/Bad_Id")) + .await + .expect("invalid id get should respond"); + + response_status( + response, + StatusCode::BAD_REQUEST, + "GET /api/v1/mcp-servers/Bad_Id", + ) + .await; +} + +#[tokio::test] +async fn mcp_server_store_malformed_persisted_toml_fails_startup() { + let temp_dir = tempfile::tempdir().expect("mcp server test tempdir should be created"); + let mcp_dir = temp_dir.path().join("mcps"); + tokio::fs::create_dir_all(&mcp_dir) + .await + .expect("mcp server dir should be created"); + tokio::fs::write(mcp_dir.join("broken.toml"), "not valid toml =") + .await + .expect("broken mcp server fixture should be written"); + + let result = TestAppStateBuilder::new() + .active_config_path(temp_dir.path().join("settings.toml")) + .try_build(); + + assert!(result.is_err()); +} + +#[tokio::test] +async fn mcp_servers_routes_require_authenticated_user() { + let temp_dir = tempfile::tempdir().expect("mcp server test tempdir should be created"); + let state = TestAppStateBuilder::new() + .active_config_path(temp_dir.path().join("settings.toml")) + .build(); + let app = build_router(state, test_auth_mode()); + + let response = app + .oneshot(empty_request(Method::GET, "/mcp-servers")) + .await + .expect("unauthenticated mcp server list should respond"); + + response_status( + response, + StatusCode::UNAUTHORIZED, + "GET /api/v1/mcp-servers without auth", + ) + .await; +} diff --git a/lib/crates/fabro-server/tests/it/api/mod.rs b/lib/crates/fabro-server/tests/it/api/mod.rs index 98bf2a4f8..9fe4bcbc0 100644 --- a/lib/crates/fabro-server/tests/it/api/mod.rs +++ b/lib/crates/fabro-server/tests/it/api/mod.rs @@ -6,6 +6,7 @@ mod environments; mod events; mod install; mod install_openai_compatible; +mod mcp_servers; mod routing; mod run_files; mod runs; diff --git a/lib/crates/fabro-types/src/mcp_store.rs b/lib/crates/fabro-types/src/mcp_store.rs index 3efe0ac34..8b2ff256d 100644 --- a/lib/crates/fabro-types/src/mcp_store.rs +++ b/lib/crates/fabro-types/src/mcp_store.rs @@ -22,8 +22,13 @@ use crate::settings::McpTransport; /// A server-managed MCP server definition. /// /// `id` and `revision` are derived (filename stem + content hash of the -/// persisted TOML bytes) and are not stored in the file body. -#[derive(Debug, Clone, PartialEq)] +/// persisted TOML bytes) and are not stored in the persisted TOML body. It +/// derives serde because it is the public wire type for `GET`/`POST`/`PUT` MCP +/// server responses (reused by `fabro-api` via `with_replacement` rather than a +/// parallel DTO). The serialized shape is the struct's own fields — `id` and +/// `revision` are present on the wire but not in the persisted TOML body. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct McpServerDefinition { pub id: McpServerId, pub revision: McpServerRevision, diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index c3eb73fba..e9a380137 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -9,6 +9,7 @@ api/human-in-the-loop-api.ts api/insights-api.ts api/install-api.ts api/integrations-api.ts +api/mcpservers-api.ts api/models-api.ts api/playground-api.ts api/repos-api.ts @@ -98,6 +99,7 @@ models/cost-source.ts models/create-automation-request.ts models/create-completion-request.ts models/create-environment-request.ts +models/create-mcp-server-request.ts models/create-playground-chat-request.ts models/create-run-pull-request-request.ts models/create-run-session-request.ts @@ -203,14 +205,22 @@ models/manifest-goal.ts models/manifest-target.ts models/manifest-workflow-config.ts models/manifest-workflow.ts +models/mcp-http-protocol.ts +models/mcp-server-list-meta.ts +models/mcp-server-list-response.ts models/mcp-server-projection.ts models/mcp-server-settings.ts models/mcp-server-status-failed.ts models/mcp-server-status-ready.ts models/mcp-server-status.ts +models/mcp-server.ts models/mcp-transport-http.ts models/mcp-transport-sandbox.ts models/mcp-transport-stdio.ts +models/mcp-transport-view-http.ts +models/mcp-transport-view-sandbox.ts +models/mcp-transport-view-stdio.ts +models/mcp-transport-view.ts models/mcp-transport.ts models/merge-method.ts models/merge-run-pull-request-request.ts @@ -309,6 +319,7 @@ models/render-workflow-graph-format.ts models/render-workflow-graph-request.ts models/replace-automation-request.ts models/replace-environment-request.ts +models/replace-mcp-server-request.ts models/repo-check-response-permissions.ts models/repo-check-response.ts models/repository-ref.ts diff --git a/lib/packages/fabro-api-client/src/api.ts b/lib/packages/fabro-api-client/src/api.ts index a7bc99437..02fa11e7b 100644 --- a/lib/packages/fabro-api-client/src/api.ts +++ b/lib/packages/fabro-api-client/src/api.ts @@ -24,6 +24,7 @@ export * from './api/human-in-the-loop-api'; export * from './api/insights-api'; export * from './api/install-api'; export * from './api/integrations-api'; +export * from './api/mcpservers-api'; export * from './api/models-api'; export * from './api/playground-api'; export * from './api/repos-api'; diff --git a/lib/packages/fabro-api-client/src/api/mcpservers-api.ts b/lib/packages/fabro-api-client/src/api/mcpservers-api.ts new file mode 100644 index 000000000..f72066148 --- /dev/null +++ b/lib/packages/fabro-api-client/src/api/mcpservers-api.ts @@ -0,0 +1,453 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from '../configuration'; +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction, replaceWithSerializableTypeIfNeeded } from '../common'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +// @ts-ignore +import type { CreateMcpServerRequest } from '../models'; +// @ts-ignore +import type { ErrorResponse } from '../models'; +// @ts-ignore +import type { McpServer } from '../models'; +// @ts-ignore +import type { McpServerListResponse } from '../models'; +// @ts-ignore +import type { ReplaceMcpServerRequest } from '../models'; +/** + * MCPServersApi - axios parameter creator + */ +export const MCPServersApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * Creates a new MCP server definition. The id is the runtime MCP server name used in qualified tool names. + * @summary Create MCP server + * @param {CreateMcpServerRequest} createMcpServerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createMcpServer: async (createMcpServerRequest: CreateMcpServerRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'createMcpServerRequest' is not null or undefined + assertParamExists('createMcpServer', 'createMcpServerRequest', createMcpServerRequest) + const localVarPath = `/api/v1/mcp-servers`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication SessionCookie required + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + localVarHeaderParameter['Content-Type'] = 'application/json'; + localVarHeaderParameter['Accept'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createMcpServerRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Deletes an MCP server definition when `If-Match` matches the current MCP server revision. + * @summary Delete MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {string} ifMatch Current resource revision used for optimistic concurrency, as returned in the `ETag` response header. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteMcpServer: async (id: string, ifMatch: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('deleteMcpServer', 'id', id) + // verify required parameter 'ifMatch' is not null or undefined + assertParamExists('deleteMcpServer', 'ifMatch', ifMatch) + const localVarPath = `/api/v1/mcp-servers/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication SessionCookie required + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + localVarHeaderParameter['Accept'] = 'application/json'; + + if (ifMatch != null) { + localVarHeaderParameter['If-Match'] = String(ifMatch); + } + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Returns all server-managed MCP server definitions with transport env/header values omitted. + * @summary List MCP servers + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listMcpServers: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/v1/mcp-servers`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication SessionCookie required + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + localVarHeaderParameter['Accept'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Replaces an MCP server definition when `If-Match` matches the current MCP server revision. + * @summary Replace MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {string} ifMatch Current resource revision used for optimistic concurrency, as returned in the `ETag` response header. + * @param {ReplaceMcpServerRequest} replaceMcpServerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + replaceMcpServer: async (id: string, ifMatch: string, replaceMcpServerRequest: ReplaceMcpServerRequest, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('replaceMcpServer', 'id', id) + // verify required parameter 'ifMatch' is not null or undefined + assertParamExists('replaceMcpServer', 'ifMatch', ifMatch) + // verify required parameter 'replaceMcpServerRequest' is not null or undefined + assertParamExists('replaceMcpServer', 'replaceMcpServerRequest', replaceMcpServerRequest) + const localVarPath = `/api/v1/mcp-servers/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication SessionCookie required + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + localVarHeaderParameter['Content-Type'] = 'application/json'; + localVarHeaderParameter['Accept'] = 'application/json'; + + if (ifMatch != null) { + localVarHeaderParameter['If-Match'] = String(ifMatch); + } + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(replaceMcpServerRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Returns one MCP server definition by id with transport env/header values omitted. + * @summary Retrieve MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + retrieveMcpServer: async (id: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('retrieveMcpServer', 'id', id) + const localVarPath = `/api/v1/mcp-servers/{id}` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication SessionCookie required + + // authentication BearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + localVarHeaderParameter['Accept'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * MCPServersApi - functional programming interface + */ +export const MCPServersApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = MCPServersApiAxiosParamCreator(configuration) + return { + /** + * Creates a new MCP server definition. The id is the runtime MCP server name used in qualified tool names. + * @summary Create MCP server + * @param {CreateMcpServerRequest} createMcpServerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createMcpServer(createMcpServerRequest: CreateMcpServerRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createMcpServer(createMcpServerRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MCPServersApi.createMcpServer']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Deletes an MCP server definition when `If-Match` matches the current MCP server revision. + * @summary Delete MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {string} ifMatch Current resource revision used for optimistic concurrency, as returned in the `ETag` response header. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async deleteMcpServer(id: string, ifMatch: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.deleteMcpServer(id, ifMatch, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MCPServersApi.deleteMcpServer']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Returns all server-managed MCP server definitions with transport env/header values omitted. + * @summary List MCP servers + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async listMcpServers(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listMcpServers(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MCPServersApi.listMcpServers']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Replaces an MCP server definition when `If-Match` matches the current MCP server revision. + * @summary Replace MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {string} ifMatch Current resource revision used for optimistic concurrency, as returned in the `ETag` response header. + * @param {ReplaceMcpServerRequest} replaceMcpServerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async replaceMcpServer(id: string, ifMatch: string, replaceMcpServerRequest: ReplaceMcpServerRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.replaceMcpServer(id, ifMatch, replaceMcpServerRequest, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MCPServersApi.replaceMcpServer']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * Returns one MCP server definition by id with transport env/header values omitted. + * @summary Retrieve MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async retrieveMcpServer(id: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveMcpServer(id, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['MCPServersApi.retrieveMcpServer']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * MCPServersApi - factory interface + */ +export const MCPServersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = MCPServersApiFp(configuration) + return { + /** + * Creates a new MCP server definition. The id is the runtime MCP server name used in qualified tool names. + * @summary Create MCP server + * @param {CreateMcpServerRequest} createMcpServerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createMcpServer(createMcpServerRequest: CreateMcpServerRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.createMcpServer(createMcpServerRequest, options).then((request) => request(axios, basePath)); + }, + /** + * Deletes an MCP server definition when `If-Match` matches the current MCP server revision. + * @summary Delete MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {string} ifMatch Current resource revision used for optimistic concurrency, as returned in the `ETag` response header. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + deleteMcpServer(id: string, ifMatch: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.deleteMcpServer(id, ifMatch, options).then((request) => request(axios, basePath)); + }, + /** + * Returns all server-managed MCP server definitions with transport env/header values omitted. + * @summary List MCP servers + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listMcpServers(options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.listMcpServers(options).then((request) => request(axios, basePath)); + }, + /** + * Replaces an MCP server definition when `If-Match` matches the current MCP server revision. + * @summary Replace MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {string} ifMatch Current resource revision used for optimistic concurrency, as returned in the `ETag` response header. + * @param {ReplaceMcpServerRequest} replaceMcpServerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + replaceMcpServer(id: string, ifMatch: string, replaceMcpServerRequest: ReplaceMcpServerRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.replaceMcpServer(id, ifMatch, replaceMcpServerRequest, options).then((request) => request(axios, basePath)); + }, + /** + * Returns one MCP server definition by id with transport env/header values omitted. + * @summary Retrieve MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + retrieveMcpServer(id: string, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.retrieveMcpServer(id, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * MCPServersApi - object-oriented interface + */ +export class MCPServersApi extends BaseAPI { + /** + * Creates a new MCP server definition. The id is the runtime MCP server name used in qualified tool names. + * @summary Create MCP server + * @param {CreateMcpServerRequest} createMcpServerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public createMcpServer(createMcpServerRequest: CreateMcpServerRequest, options?: RawAxiosRequestConfig) { + return MCPServersApiFp(this.configuration).createMcpServer(createMcpServerRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Deletes an MCP server definition when `If-Match` matches the current MCP server revision. + * @summary Delete MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {string} ifMatch Current resource revision used for optimistic concurrency, as returned in the `ETag` response header. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public deleteMcpServer(id: string, ifMatch: string, options?: RawAxiosRequestConfig) { + return MCPServersApiFp(this.configuration).deleteMcpServer(id, ifMatch, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Returns all server-managed MCP server definitions with transport env/header values omitted. + * @summary List MCP servers + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public listMcpServers(options?: RawAxiosRequestConfig) { + return MCPServersApiFp(this.configuration).listMcpServers(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Replaces an MCP server definition when `If-Match` matches the current MCP server revision. + * @summary Replace MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {string} ifMatch Current resource revision used for optimistic concurrency, as returned in the `ETag` response header. + * @param {ReplaceMcpServerRequest} replaceMcpServerRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public replaceMcpServer(id: string, ifMatch: string, replaceMcpServerRequest: ReplaceMcpServerRequest, options?: RawAxiosRequestConfig) { + return MCPServersApiFp(this.configuration).replaceMcpServer(id, ifMatch, replaceMcpServerRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Returns one MCP server definition by id with transport env/header values omitted. + * @summary Retrieve MCP server + * @param {string} id Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public retrieveMcpServer(id: string, options?: RawAxiosRequestConfig) { + return MCPServersApiFp(this.configuration).retrieveMcpServer(id, options).then((request) => request(this.axios, this.basePath)); + } +} diff --git a/lib/packages/fabro-api-client/src/models/create-mcp-server-request.ts b/lib/packages/fabro-api-client/src/models/create-mcp-server-request.ts new file mode 100644 index 000000000..058a3f7e3 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/create-mcp-server-request.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { McpTransport } from './mcp-transport'; + +/** + * Request body for creating an MCP server definition. + */ +export interface CreateMcpServerRequest { + /** + * Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + */ + 'id': string; + /** + * Human-readable label for display in management UIs. + */ + 'display_name': string; + 'description'?: string | null; + 'transport': McpTransport; + 'startup_timeout_secs': number; + 'tool_timeout_secs': number; +} diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index dc392e2bc..40cf68be5 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -70,6 +70,7 @@ export * from './cost-source'; export * from './create-automation-request'; export * from './create-completion-request'; export * from './create-environment-request'; +export * from './create-mcp-server-request'; export * from './create-playground-chat-request'; export * from './create-run-pull-request-request'; export * from './create-run-session-request'; @@ -174,6 +175,10 @@ export * from './manifest-goal'; export * from './manifest-target'; export * from './manifest-workflow'; export * from './manifest-workflow-config'; +export * from './mcp-http-protocol'; +export * from './mcp-server'; +export * from './mcp-server-list-meta'; +export * from './mcp-server-list-response'; export * from './mcp-server-projection'; export * from './mcp-server-settings'; export * from './mcp-server-status'; @@ -183,6 +188,10 @@ export * from './mcp-transport'; export * from './mcp-transport-http'; export * from './mcp-transport-sandbox'; export * from './mcp-transport-stdio'; +export * from './mcp-transport-view'; +export * from './mcp-transport-view-http'; +export * from './mcp-transport-view-sandbox'; +export * from './mcp-transport-view-stdio'; export * from './merge-method'; export * from './merge-run-pull-request-request'; export * from './merge-run-pull-request-response'; @@ -280,6 +289,7 @@ export * from './render-workflow-graph-format'; export * from './render-workflow-graph-request'; export * from './replace-automation-request'; export * from './replace-environment-request'; +export * from './replace-mcp-server-request'; export * from './repo-check-response'; export * from './repo-check-response-permissions'; export * from './repository-ref'; diff --git a/lib/packages/fabro-api-client/src/models/mcp-http-protocol.ts b/lib/packages/fabro-api-client/src/models/mcp-http-protocol.ts new file mode 100644 index 000000000..c03287667 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-http-protocol.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Wire protocol used by HTTP and sandbox MCP transports. + */ + +export const McpHttpProtocol = { + STREAMABLE_HTTP: 'streamable_http', + SSE: 'sse' +} as const; + +export type McpHttpProtocol = typeof McpHttpProtocol[keyof typeof McpHttpProtocol]; diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-list-meta.ts b/lib/packages/fabro-api-client/src/models/mcp-server-list-meta.ts new file mode 100644 index 000000000..a90b708eb --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-server-list-meta.ts @@ -0,0 +1,25 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Metadata for MCP server list responses. + */ +export interface McpServerListMeta { + /** + * Total number of server-managed MCP server definitions. + */ + 'total': number; +} diff --git a/lib/packages/fabro-api-client/src/models/mcp-server-list-response.ts b/lib/packages/fabro-api-client/src/models/mcp-server-list-response.ts new file mode 100644 index 000000000..f1a6ffe39 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-server-list-response.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { McpServer } from './mcp-server'; +// May contain unused imports in some cases +// @ts-ignore +import type { McpServerListMeta } from './mcp-server-list-meta'; + +/** + * List envelope for MCP server definitions. + */ +export interface McpServerListResponse { + 'data': Array; + 'meta': McpServerListMeta; +} diff --git a/lib/packages/fabro-api-client/src/models/mcp-server.ts b/lib/packages/fabro-api-client/src/models/mcp-server.ts new file mode 100644 index 000000000..e791b7feb --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-server.ts @@ -0,0 +1,46 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { McpTransportView } from './mcp-transport-view'; + +/** + * Public server-managed MCP server definition. Transport env/header values are never returned. + */ +export interface McpServer { + /** + * Stable MCP server identifier, used as the runtime MCP server name in qualified tool names. + */ + 'id': string; + /** + * Stable revision used with `If-Match` for optimistic concurrency. + */ + 'revision': string; + /** + * Human-readable label for display in management UIs. + */ + 'display_name': string; + 'description': string | null; + 'transport': McpTransportView; + /** + * Seconds to wait for the MCP server to become ready at connect time. + */ + 'startup_timeout_secs': number; + /** + * Seconds to allow each MCP tool call before timing out. + */ + 'tool_timeout_secs': number; +} diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-http.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-http.ts index 341b4617d..af9b8991c 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-http.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-http.ts @@ -13,9 +13,16 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { McpHttpProtocol } from './mcp-http-protocol'; +/** + * HTTP transport that connects to a remote MCP server URL. + */ export interface McpTransportHttp { 'type': McpTransportHttpTypeEnum; + 'protocol'?: McpHttpProtocol; 'url': string; 'headers': { [key: string]: string; }; } diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-sandbox.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-sandbox.ts index c96f48dc4..e534632b9 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-sandbox.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-sandbox.ts @@ -13,9 +13,19 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { McpHttpProtocol } from './mcp-http-protocol'; +/** + * Sandbox transport that launches the MCP server inside the run sandbox and connects over HTTP. + */ export interface McpTransportSandbox { 'type': McpTransportSandboxTypeEnum; + 'protocol'?: McpHttpProtocol; + /** + * Command and arguments used to launch the in-sandbox MCP server. + */ 'command': Array; 'port': number; 'env': { [key: string]: string; }; diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-stdio.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-stdio.ts index 4c7039879..27a3d7270 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport-stdio.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-stdio.ts @@ -14,8 +14,14 @@ +/** + * Stdio transport that launches a local MCP server subprocess. + */ export interface McpTransportStdio { 'type': McpTransportStdioTypeEnum; + /** + * Command and arguments used to launch the MCP server. + */ 'command': Array; 'env': { [key: string]: string; }; } diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-view-http.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-view-http.ts new file mode 100644 index 000000000..724f99612 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-view-http.ts @@ -0,0 +1,37 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { McpHttpProtocol } from './mcp-http-protocol'; + +/** + * HTTP transport view. Header values are omitted. + */ +export interface McpTransportViewHttp { + 'type': McpTransportViewHttpTypeEnum; + 'protocol'?: McpHttpProtocol; + 'url': string; + /** + * HTTP header names configured for this transport. + */ + 'header_keys': Array; +} + +export const McpTransportViewHttpTypeEnum = { + HTTP: 'http' +} as const; + +export type McpTransportViewHttpTypeEnum = typeof McpTransportViewHttpTypeEnum[keyof typeof McpTransportViewHttpTypeEnum]; diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-view-sandbox.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-view-sandbox.ts new file mode 100644 index 000000000..ea01a6cab --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-view-sandbox.ts @@ -0,0 +1,41 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { McpHttpProtocol } from './mcp-http-protocol'; + +/** + * Sandbox transport view. Environment variable values are omitted. + */ +export interface McpTransportViewSandbox { + 'type': McpTransportViewSandboxTypeEnum; + 'protocol'?: McpHttpProtocol; + /** + * Command and arguments used to launch the in-sandbox MCP server. + */ + 'command': Array; + 'port': number; + /** + * Environment variable names configured for this transport. + */ + 'env_keys': Array; +} + +export const McpTransportViewSandboxTypeEnum = { + SANDBOX: 'sandbox' +} as const; + +export type McpTransportViewSandboxTypeEnum = typeof McpTransportViewSandboxTypeEnum[keyof typeof McpTransportViewSandboxTypeEnum]; diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-view-stdio.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-view-stdio.ts new file mode 100644 index 000000000..03fd5665f --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-view-stdio.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Stdio transport view. Environment variable values are omitted. + */ +export interface McpTransportViewStdio { + 'type': McpTransportViewStdioTypeEnum; + /** + * Command and arguments used to launch the MCP server. + */ + 'command': Array; + /** + * Environment variable names configured for this transport. + */ + 'env_keys': Array; +} + +export const McpTransportViewStdioTypeEnum = { + STDIO: 'stdio' +} as const; + +export type McpTransportViewStdioTypeEnum = typeof McpTransportViewStdioTypeEnum[keyof typeof McpTransportViewStdioTypeEnum]; diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport-view.ts b/lib/packages/fabro-api-client/src/models/mcp-transport-view.ts new file mode 100644 index 000000000..f9761b5a8 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/mcp-transport-view.ts @@ -0,0 +1,33 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { McpHttpProtocol } from './mcp-http-protocol'; +// May contain unused imports in some cases +// @ts-ignore +import type { McpTransportViewHttp } from './mcp-transport-view-http'; +// May contain unused imports in some cases +// @ts-ignore +import type { McpTransportViewSandbox } from './mcp-transport-view-sandbox'; +// May contain unused imports in some cases +// @ts-ignore +import type { McpTransportViewStdio } from './mcp-transport-view-stdio'; + +/** + * @type McpTransportView + * MCP server transport configuration returned by catalog read APIs. + */ +export type McpTransportView = { type: 'http' } & McpTransportViewHttp | { type: 'sandbox' } & McpTransportViewSandbox | { type: 'stdio' } & McpTransportViewStdio; diff --git a/lib/packages/fabro-api-client/src/models/mcp-transport.ts b/lib/packages/fabro-api-client/src/models/mcp-transport.ts index 7191c0125..611215bdd 100644 --- a/lib/packages/fabro-api-client/src/models/mcp-transport.ts +++ b/lib/packages/fabro-api-client/src/models/mcp-transport.ts @@ -13,6 +13,9 @@ */ +// May contain unused imports in some cases +// @ts-ignore +import type { McpHttpProtocol } from './mcp-http-protocol'; // May contain unused imports in some cases // @ts-ignore import type { McpTransportHttp } from './mcp-transport-http'; @@ -25,5 +28,6 @@ import type { McpTransportStdio } from './mcp-transport-stdio'; /** * @type McpTransport + * MCP server transport configuration. The `type` field selects stdio, HTTP, or sandbox transport. Unknown `type` discriminator values are reported as domain validation errors with HTTP 422. */ -export type McpTransport = McpTransportHttp | McpTransportSandbox | McpTransportStdio; +export type McpTransport = { type: 'http' } & McpTransportHttp | { type: 'sandbox' } & McpTransportSandbox | { type: 'stdio' } & McpTransportStdio; diff --git a/lib/packages/fabro-api-client/src/models/replace-mcp-server-request.ts b/lib/packages/fabro-api-client/src/models/replace-mcp-server-request.ts new file mode 100644 index 000000000..3efb9cf8b --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/replace-mcp-server-request.ts @@ -0,0 +1,32 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { McpTransport } from './mcp-transport'; + +/** + * Request body for replacing an MCP server definition. The path id is authoritative. + */ +export interface ReplaceMcpServerRequest { + /** + * Human-readable label for display in management UIs. + */ + 'display_name': string; + 'description'?: string | null; + 'transport': McpTransport; + 'startup_timeout_secs': number; + 'tool_timeout_secs': number; +}