mod guard;
mod oauth;
mod run;
mod server;
pub use guard::FailureLimiter;
pub use oauth::well_known_split;
use std::net::SocketAddr;
use std::sync::Arc;
use axum::body::Bytes;
use axum::extract::{ConnectInfo, Path, State};
use axum::http::{Extensions, HeaderMap, Method, StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde_json::json;
use super::{PlatformAppState, ProjectHost};
use oauth::{Issuer, Op};
use server::{PublishedServer, serve};
use crate::infra::execution::placement::ProjectRuntimePlacementTarget;
use crate::pipeline::nodes::basic::trigger::mcp_trigger;
use crate::platform::services::addressing::Surface;
use crate::platform::services::pipeline_runtime::{CompiledPipeline, McpTriggerSpec};
const WORKER_PREFIX: &str = "/api/internal/runtime/mcp/";
const BASE_HEADER: &str = "x-zebflow-mcp-base";
#[derive(Clone)]
struct PublishedTool {
compiled: CompiledPipeline,
spec: McpTriggerSpec,
}
enum Arrival {
Client { peer: Option<SocketAddr> },
Controller,
}
fn tools_on_route(state: &PlatformAppState, owner: &str, project: &str, route: &str) -> Vec<PublishedTool> {
let mut tools: Vec<PublishedTool> = state
.platform
.pipeline_runtime
.list_project(owner, project)
.into_iter()
.flat_map(|compiled| {
compiled
.mcp_triggers
.iter()
.filter(|spec| spec.route == route)
.map(|spec| PublishedTool { compiled: compiled.clone(), spec: spec.clone() })
.collect::<Vec<_>>()
})
.collect();
tools.sort_by(|a, b| {
a.spec.tool_name.cmp(&b.spec.tool_name).then(a.compiled.file_rel_path.cmp(&b.compiled.file_rel_path))
});
tools
}
fn refusal(status: StatusCode, code: &str, message: &str) -> Response {
(status, Json(json!({ "ok": false, "error": { "code": code, "message": message } }))).into_response()
}
fn peer_of(extensions: &Extensions) -> Option<SocketAddr> {
extensions.get::<ConnectInfo<SocketAddr>>().map(|ConnectInfo(addr)| *addr)
}
struct Request {
method: Method,
uri: Uri,
headers: HeaderMap,
body: Bytes,
host: Option<ProjectHost>,
arrival: Arrival,
}
fn from_client(method: Method, uri: Uri, headers: HeaderMap, extensions: &Extensions, body: Bytes) -> Request {
let host = extensions.get::<ProjectHost>().cloned();
Request { method, uri, headers, body, host, arrival: Arrival::Client { peer: peer_of(extensions) } }
}
pub(super) async fn ingress_root(
State(state): State<PlatformAppState>,
Path((owner, project)): Path<(String, String)>,
method: Method,
uri: Uri,
headers: HeaderMap,
extensions: Extensions,
body: Bytes,
) -> Response {
let request = from_client(method, uri, headers, &extensions, body);
answer(state, owner, project, String::new(), None, request).await
}
pub(super) async fn ingress(
State(state): State<PlatformAppState>,
Path((owner, project, tail)): Path<(String, String, String)>,
method: Method,
uri: Uri,
headers: HeaderMap,
extensions: Extensions,
body: Bytes,
) -> Response {
let request = from_client(method, uri, headers, &extensions, body);
answer(state, owner, project, tail, None, request).await
}
macro_rules! well_known {
($root:ident, $named:ident, $op:expr, $what:literal) => {
#[doc = concat!("`/.well-known/", $what, "/mcp/{owner}/{project}` — a route of `/`.")]
pub(super) async fn $root(
State(state): State<PlatformAppState>,
Path((owner, project)): Path<(String, String)>,
method: Method,
uri: Uri,
headers: HeaderMap,
extensions: Extensions,
body: Bytes,
) -> Response {
let request = from_client(method, uri, headers, &extensions, body);
answer(state, owner, project, String::new(), Some($op), request).await
}
#[doc = concat!("`/.well-known/", $what, "/mcp/{owner}/{project}/{route}` — the platform form; a project host's path-inserted document is rewritten here.")]
pub(super) async fn $named(
State(state): State<PlatformAppState>,
Path((owner, project, route)): Path<(String, String, String)>,
method: Method,
uri: Uri,
headers: HeaderMap,
extensions: Extensions,
body: Bytes,
) -> Response {
let request = from_client(method, uri, headers, &extensions, body);
answer(state, owner, project, route, Some($op), request).await
}
};
}
well_known!(protected_resource_root, protected_resource, Op::ProtectedResource, "oauth-protected-resource");
well_known!(authorization_server_root, authorization_server, Op::AuthorizationServer, "oauth-authorization-server");
pub(super) async fn internal_ingress_root(
State(state): State<PlatformAppState>,
Path((owner, project)): Path<(String, String)>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Bytes,
) -> Response {
if let Err(response) = super::require_controller_call(&state, &headers) {
return response;
}
let request = Request { method, uri, headers, body, host: None, arrival: Arrival::Controller };
answer(state, owner, project, String::new(), None, request).await
}
pub(super) async fn internal_ingress(
State(state): State<PlatformAppState>,
Path((owner, project, tail)): Path<(String, String, String)>,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Bytes,
) -> Response {
if let Err(response) = super::require_controller_call(&state, &headers) {
return response;
}
let request = Request { method, uri, headers, body, host: None, arrival: Arrival::Controller };
answer(state, owner, project, tail, None, request).await
}
fn base_of(owner: &str, project: &str, request: &Request) -> Option<(String, String)> {
match request.arrival {
Arrival::Client { .. } => {
oauth::arrival_base(owner, project, request.host.as_ref(), &request.headers, oauth::platform_base().as_deref())
}
Arrival::Controller => {
let value = request.headers.get(BASE_HEADER)?.to_str().ok()?;
let (base, pages) = value.split_once(' ')?;
Some((base.to_string(), pages.to_string()))
}
}
}
async fn answer(state: PlatformAppState, owner: String, project: String, tail: String, op_hint: Option<Op>, request: Request) -> Response {
let owner = crate::platform::model::slug_segment(&owner);
let project = crate::platform::model::slug_segment(&project);
let (route_tail, op) = match op_hint {
Some(op) => (tail, op),
None => oauth::split_tail(&tail),
};
if op_hint.is_none() && op.internal() && !matches!(request.arrival, Arrival::Controller) {
return refusal(StatusCode::NOT_FOUND, "not_found", "no MCP server is published on this route");
}
let route = mcp_trigger::normalize_route(&route_tail);
let base = base_of(&owner, &project, &request);
let limit = match request.arrival {
Arrival::Controller => None,
Arrival::Client { peer } => {
let enabled = state
.platform
.addressing
.read(&owner, &project)
.map(|a| a.is_enabled(Surface::Mcp))
.unwrap_or(false);
if !enabled {
return refusal(StatusCode::NOT_FOUND, "not_found", "the mcp surface is switched off for this project (Settings → Addressing)");
}
let key = guard::limit_key(&owner, &project, &route, &guard::client_address(peer, &request.headers));
if let Some(wait) = state.mcp_failures.blocked(&key) {
return guard::too_many(wait);
}
if let Some(worker_id) = worker_for(&state, &owner, &project) {
if state.platform.addressing.sections(&owner, &project).is_some() {
return refusal(StatusCode::SERVICE_UNAVAILABLE, "unavailable", "a project with sections runs on its own office only");
}
let path = match op.word().filter(|_| op_hint.is_some()) {
Some(word) => Some(format!("{WORKER_PREFIX}{owner}/{project}{}/_oauth/{word}", if route == "/" { "" } else { route.as_str() })),
None => worker_path_and_query(&request.uri),
};
let response = forward(&state, &request, path, base.as_ref(), &worker_id).await;
if matches!(response.status(), StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN) {
state.mcp_failures.fail(&key);
}
return response;
}
Some(key)
}
};
let tools = tools_on_route(&state, &owner, &project, &route);
let Some(first) = tools.first() else {
return refusal(StatusCode::NOT_FOUND, "not_found", "no MCP server is published on this route");
};
let guard_of_route = first.spec.guard();
if tools.iter().any(|t| t.spec.guard() != guard_of_route) {
return refusal(StatusCode::INTERNAL_SERVER_ERROR, "mcp_route_auth_mixed", "the tools on this route declare different --auth; activate them again with one");
}
let oauth_route = first.spec.auth_type == "oauth";
let section_root = request.host.as_ref().and_then(|h| h.root.clone());
let visible_route = match §ion_root {
None => Some(route.clone()),
Some(root) => {
let prefix = format!("/{root}");
if route == prefix {
Some("/".to_string())
} else {
route.strip_prefix(&prefix).filter(|rest| rest.starts_with('/')).map(str::to_string)
}
}
};
let issuer = base
.as_ref()
.filter(|_| oauth_route)
.zip(visible_route.as_deref())
.and_then(|((base, pages), visible)| Issuer::new(base, visible, pages))
.map(|issuer| match (&request.host, state.platform.addressing.sections(&owner, &project)) {
(Some(host), Some(table)) => issuer.in_section(table, host.host.clone()),
_ => issuer,
});
if op != Op::Mcp && !oauth_route {
return refusal(StatusCode::NOT_FOUND, "not_found", "this MCP server does not sign people in (--auth is not oauth)");
}
if oauth_route && issuer.is_none() {
return refusal(StatusCode::NOT_FOUND, "not_found", "this MCP server signs people in on the project's host; connect there");
}
if op != Op::Mcp {
let spec = first.spec.clone();
let issuer = issuer.expect("checked above");
let ctx = oauth::Ctx { state: &state, owner: &owner, project: &project, route: &route, spec: &spec, issuer: &issuer, limit: limit.as_deref() };
return oauth::dispatch(ctx, op, &request.method, &request.uri, &request.headers, &request.body).await;
}
let door = guard::Door {
state: &state,
owner: &owner,
project: &project,
route: &route,
tools: &tools,
headers: &request.headers,
body: &request.body,
issuer: issuer.as_ref(),
limit: limit.as_deref(),
};
if let Err(response) = door.check() {
return response;
}
let server = PublishedServer { state: state.clone(), owner, project, route, tools: Arc::new(tools) };
serve(server, request.method, request.uri, request.headers, request.body).await
}
fn worker_for(state: &PlatformAppState, owner: &str, project: &str) -> Option<String> {
let placement = state.platform.cluster_placement.get(owner, project).ok()??;
if placement.target != ProjectRuntimePlacementTarget::Worker {
return None;
}
placement.worker_id.filter(|id| *id != state.platform.cluster_bootstrap.node_id())
}
fn worker_path_and_query(uri: &Uri) -> Option<String> {
let suffix = uri.path().strip_prefix("/mcp/")?;
let query = uri.query().map(|q| format!("?{q}")).unwrap_or_default();
Some(format!("{WORKER_PREFIX}{suffix}{query}"))
}
async fn forward(state: &PlatformAppState, request: &Request, path: Option<String>, base: Option<&(String, String)>, worker_id: &str) -> Response {
let Some(path_and_query) = path else {
return refusal(StatusCode::NOT_FOUND, "not_found", "not a published MCP route");
};
let mut headers = request.headers.clone();
headers.remove(BASE_HEADER);
if let Some((base, pages)) = base
&& let Ok(value) = axum::http::HeaderValue::from_str(&format!("{base} {pages}"))
{
headers.insert(BASE_HEADER, value);
}
match super::forward_to_worker(state, &request.method, &path_and_query, &headers, &request.body, worker_id).await {
Ok(response) => response,
Err(err) => super::internal_error(err),
}
}
#[cfg(test)]
mod tests {
use super::worker_path_and_query;
#[test]
fn a_worker_hop_changes_only_the_prefix() {
let uri = "/mcp/site-a/demo/shop/?x=1".parse().unwrap();
assert_eq!(worker_path_and_query(&uri).as_deref(), Some("/api/internal/runtime/mcp/site-a/demo/shop/?x=1"));
let root = "/mcp/site-a/demo".parse().unwrap();
assert_eq!(worker_path_and_query(&root).as_deref(), Some("/api/internal/runtime/mcp/site-a/demo"));
let other = "/wh/site-a/demo/shop".parse().unwrap();
assert_eq!(worker_path_and_query(&other), None);
}
}