use std::sync::Arc;
use axum::{
Router,
body::{Body, Bytes},
http::{HeaderMap, Method, StatusCode, header::HeaderName},
response::{IntoResponse, Response},
routing::any,
};
use crate::ServerState;
use super::caller::{self, AssistantMcpAuthError};
use super::runtime::AssistantMcpRuntime;
pub const ASSISTANT_MCP_PATH: &str = "/assistant/mcp";
pub(crate) fn assistant_mcp_router(runtime: Arc<AssistantMcpRuntime>) -> Router<ServerState> {
Router::new()
.route(ASSISTANT_MCP_PATH, any(handle))
.layer(axum::Extension(runtime))
}
async fn handle(
axum::Extension(runtime): axum::Extension<Arc<AssistantMcpRuntime>>,
axum::extract::State(state): axum::extract::State<ServerState>,
method: Method,
headers: HeaderMap,
body: Bytes,
) -> Response {
let pairs = header_pairs(&headers);
let caller = match caller::resolve(state.assistant_sessions(), &pairs).await {
Ok(caller) => caller,
Err(error) => return unauthorized(&error),
};
let request = aion_mcp::HttpRequest::new(
method.as_str().to_ascii_uppercase(),
aion_mcp::protocol::headers::HeaderView::new(pairs),
body.to_vec(),
);
into_axum(runtime.server().handle_http(&request, &caller).await)
}
fn unauthorized(error: &AssistantMcpAuthError) -> Response {
tracing::warn!(
target: "aion_server::assistant::mcp",
%error,
"a call on the assistant MCP route was not authorized"
);
(
StatusCode::UNAUTHORIZED,
axum::Json(
aion_proto::WireError::grant_denied(AssistantMcpAuthError::client_message())
.with_error_type(BEARER_REFUSED_TYPE),
),
)
.into_response()
}
pub(crate) const BEARER_REFUSED_TYPE: &str = "AssistantSessionBearerRefused";
fn header_pairs(headers: &HeaderMap) -> Vec<(String, String)> {
headers
.iter()
.filter_map(|(name, value)| {
value
.to_str()
.ok()
.map(|value| (name.as_str().to_owned(), value.to_owned()))
})
.collect()
}
fn into_axum(response: aion_mcp::HttpResponse) -> Response {
let status = StatusCode::from_u16(response.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let mut builder = Response::builder().status(status);
for (name, value) in response.headers {
match HeaderName::from_bytes(name.as_bytes()) {
Ok(name) => builder = builder.header(name, value),
Err(error) => {
tracing::error!(
target: "aion_server::assistant::mcp",
header = %name,
"an assistant MCP response header name was not valid and was dropped: {error}"
);
}
}
}
builder
.body(Body::from(response.body))
.unwrap_or_else(|error| {
tracing::error!(
target: "aion_server::assistant::mcp",
"an assistant MCP response could not be assembled: {error}"
);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
}