feat(server): mcp-servers HTTP API — handlers, type reuse, TS client

Implement the server-managed MCP server catalog HTTP API on top of the
OpenAPI spec and the `fabro-mcp-store` foundation, mirroring the
automations feature end to end.

- fabro-api: reuse the domain model as the wire contract via
  `with_replacement` instead of parallel DTOs — `McpServer` →
  `McpServerDefinition`, `CreateMcpServerRequest` → `McpServerDraft`,
  `ReplaceMcpServerRequest` → `McpServerReplace`. The transport types
  (`McpTransport`, `McpHttpProtocol`) are already mapped to the domain
  `u64`/`u16` widths, so the spec's signed integer formats never leak.
  Add identity + JSON-parity tests.
- fabro-types: derive serde on `McpServerDefinition` so it can serve as
  the public response type.
- fabro-server: wire the `McpServerStore` into `AppState` (load from the
  active config's `mcps/` directory) and add the
  `list/create/get/replace/delete` handlers with ETag-on-201, required
  `If-Match` on PUT/DELETE, and the same store-error → status mapping
  automations uses (404/409/422/500). Register the routes.
- Regenerate the TypeScript Axios client against the merged spec.

Credential-literal validation (warn vs hard-reject for credential-looking
literal env/header values) is left intentionally unenforced behind a TODO;
that decision is still open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-06-23 16:37:08 -04:00
parent 164d9dcfbc
commit 20cde16b59
29 changed files with 1740 additions and 27 deletions

1
Cargo.lock generated
View file

@ -2928,6 +2928,7 @@ dependencies = [
"fabro-llm",
"fabro-macros",
"fabro-manifest",
"fabro-mcp-store",
"fabro-model",
"fabro-proc",
"fabro-redact",

View file

@ -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", &[]),

View file

@ -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::*;

View file

@ -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);
}

View file

@ -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" }

View file

@ -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<Database>,
pub(crate) automations: Arc<AutomationStore>,
pub(crate) environments: Arc<EnvironmentStore>,
pub(crate) mcp_servers: Arc<McpServerStore>,
pub(crate) vault: Arc<AsyncRwLock<Vault>>,
pub(crate) variables: Arc<VariableStore>,
}
@ -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<Arc<AppState>> {
let AppStateConfig {
resolved_settings,
@ -2329,6 +2342,12 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
.map_err(anyhow::Error::new)
.context("load environments")?,
);
let mcp_server_dir = mcp_server_dir_for_active_config(&active_config_path);
let mcp_server_store = Arc::new(
McpServerStore::load(mcp_server_dir)
.map_err(anyhow::Error::new)
.context("load mcp servers")?,
);
let variables = Arc::new(VariableStore::new(db_pool));
let vault = match preloaded_vault {
Some(vault) => vault,
@ -2439,6 +2458,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
runs: store,
automations: automation_store,
environments: environment_store,
mcp_servers: mcp_server_store,
vault,
variables,
},

View file

@ -0,0 +1,150 @@
use std::sync::Arc;
use axum::http::HeaderMap;
use fabro_mcp_store::McpServerStoreError;
use fabro_types::{McpServerDefinition, McpServerDraft, McpServerId, McpServerReplace};
use serde::Serialize;
use super::super::{
ApiError, AppState, IntoResponse, Json, Path, RequiredUser, Response, Router, State,
StatusCode, get,
};
use super::{json_with_etag_response, parse_required_if_match};
#[derive(Serialize)]
struct McpServerListResponse {
data: Vec<McpServerDefinition>,
meta: McpServerListMeta,
}
#[derive(Serialize)]
struct McpServerListMeta {
total: usize,
}
pub(super) fn routes() -> Router<Arc<AppState>> {
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<Arc<AppState>>) -> 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<Arc<AppState>>,
Json(draft): Json<McpServerDraft>,
) -> Result<Response, ApiError> {
// 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<Arc<AppState>>,
Path(id): Path<String>,
) -> Result<Response, ApiError> {
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<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
Json(replacement): Json<McpServerReplace>,
) -> Result<Response, ApiError> {
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<Arc<AppState>>,
headers: HeaderMap,
Path(id): Path<String>,
) -> Result<Response, ApiError> {
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, ApiError> {
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<McpServerStoreError> 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",
),
}
}
}

View file

@ -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<Arc<AppState>> {
.merge(pull_requests::routes())
.merge(artifacts::routes())
.merge(automations::routes())
.merge(mcp_servers::routes())
.merge(environments::routes())
.merge(sandbox::routes())
.merge(sandboxes::routes())

View file

@ -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<Body> {
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<Body> {
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<Value>,
) -> Request<Body> {
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;
}

View file

@ -6,6 +6,7 @@ mod environments;
mod events;
mod install;
mod install_openai_compatible;
mod mcp_servers;
mod routing;
mod run_files;
mod runs;

View file

@ -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,

View file

@ -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

View file

@ -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';

View file

@ -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<RequestArgs> => {
// 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 &#x60;ETag&#x60; response header.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteMcpServer: async (id: string, ifMatch: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// 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<RequestArgs> => {
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 &#x60;ETag&#x60; response header.
* @param {ReplaceMcpServerRequest} replaceMcpServerRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
replaceMcpServer: async (id: string, ifMatch: string, replaceMcpServerRequest: ReplaceMcpServerRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// 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<RequestArgs> => {
// 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<McpServer>> {
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 &#x60;ETag&#x60; response header.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async deleteMcpServer(id: string, ifMatch: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
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<McpServerListResponse>> {
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 &#x60;ETag&#x60; 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<McpServer>> {
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<McpServer>> {
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<McpServer> {
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 &#x60;ETag&#x60; response header.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
deleteMcpServer(id: string, ifMatch: string, options?: RawAxiosRequestConfig): AxiosPromise<void> {
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<McpServerListResponse> {
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 &#x60;ETag&#x60; response header.
* @param {ReplaceMcpServerRequest} replaceMcpServerRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
replaceMcpServer(id: string, ifMatch: string, replaceMcpServerRequest: ReplaceMcpServerRequest, options?: RawAxiosRequestConfig): AxiosPromise<McpServer> {
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<McpServer> {
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 &#x60;ETag&#x60; 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 &#x60;ETag&#x60; 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));
}
}

View file

@ -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;
}

View file

@ -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';

View file

@ -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];

View file

@ -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;
}

View file

@ -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<McpServer>;
'meta': McpServerListMeta;
}

View file

@ -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;
}

View file

@ -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; };
}

View file

@ -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<string>;
'port': number;
'env': { [key: string]: string; };

View file

@ -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<string>;
'env': { [key: string]: string; };
}

View file

@ -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<string>;
}
export const McpTransportViewHttpTypeEnum = {
HTTP: 'http'
} as const;
export type McpTransportViewHttpTypeEnum = typeof McpTransportViewHttpTypeEnum[keyof typeof McpTransportViewHttpTypeEnum];

View file

@ -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<string>;
'port': number;
/**
* Environment variable names configured for this transport.
*/
'env_keys': Array<string>;
}
export const McpTransportViewSandboxTypeEnum = {
SANDBOX: 'sandbox'
} as const;
export type McpTransportViewSandboxTypeEnum = typeof McpTransportViewSandboxTypeEnum[keyof typeof McpTransportViewSandboxTypeEnum];

View file

@ -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<string>;
/**
* Environment variable names configured for this transport.
*/
'env_keys': Array<string>;
}
export const McpTransportViewStdioTypeEnum = {
STDIO: 'stdio'
} as const;
export type McpTransportViewStdioTypeEnum = typeof McpTransportViewStdioTypeEnum[keyof typeof McpTransportViewStdioTypeEnum];

View file

@ -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;

View file

@ -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;

View file

@ -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;
}