use axum::extract::DefaultBodyLimit;
use axum::{Router, routing::get};
use rmcp::transport::streamable_http_server::StreamableHttpService;
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer};
use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
use tower_http::trace::{DefaultOnResponse, TraceLayer};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::http::controllers::{health, whoami};
use crate::http::{controllers, mcp};
use crate::state::AppState;
pub fn build_app(state: AppState, web_dir: Option<String>) -> Router {
let mcp_service = StreamableHttpService::new(
{
let state = state.clone();
move || Ok(mcp::YorishiroMcpServer::new(state.clone()))
},
LocalSessionManager::default().into(),
Default::default(),
);
let router = Router::new()
.route("/up", get(health::up_check))
.route("/health", get(health::health_check))
.route("/whoami", get(whoami::whoami))
.nest_service("/mcp", mcp_service)
.merge(controllers::router())
.merge(
SwaggerUi::new("/docs").url("/api-docs/openapi.json", controllers::ApiDoc::openapi()),
)
.layer(DefaultBodyLimit::max(2 * 1024 * 1024))
.with_state(state);
apply_observability_layers(router).fallback_service(yorishiro_web::fallback_service(web_dir))
}
pub fn apply_observability_layers(router: Router) -> Router {
router
.layer(build_cors_layer())
.layer(PropagateRequestIdLayer::x_request_id())
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &axum::extract::Request| {
let request_id = request
.headers()
.get("x-request-id")
.and_then(|value| value.to_str().ok())
.unwrap_or_default();
tracing::info_span!(
"request",
%request_id,
method = %request.method(),
uri = %request.uri(),
)
})
.on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
)
.layer(SetRequestIdLayer::x_request_id(MakeRequestUuid))
}
fn build_cors_layer() -> CorsLayer {
let origins_str = std::env::var("YSR_CORS_ORIGINS").unwrap_or_default();
let layer = if !origins_str.is_empty() {
let origins: Vec<_> = origins_str
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
CorsLayer::new().allow_origin(origins)
} else if cfg!(debug_assertions) {
debug_local_origin_layer()
} else {
CorsLayer::new()
};
layer
.allow_methods([
http::Method::GET,
http::Method::POST,
http::Method::PUT,
http::Method::DELETE,
http::Method::OPTIONS,
])
.allow_headers(AllowHeaders::list([
"authorization".parse().unwrap(),
"content-type".parse().unwrap(),
]))
.expose_headers(["x-request-id".parse().unwrap()])
}
fn debug_local_origin_layer() -> CorsLayer {
CorsLayer::new().allow_origin(AllowOrigin::predicate(|origin, _parts| {
origin
.to_str()
.map(|s| s.starts_with("http://localhost:") || s.starts_with("http://127.0.0.1:"))
.unwrap_or(false)
}))
}