embacle_server/router.rs
1// ABOUTME: Axum router wiring OpenAI-compatible and MCP endpoints
2// ABOUTME: Mounts completions, models, health, and MCP routes with optional auth middleware
3//
4// SPDX-License-Identifier: Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7use std::sync::Arc;
8
9use axum::middleware;
10use axum::routing::{get, post};
11use axum::Router;
12use dravr_tronc::mcp::transport::http::mcp_router as build_mcp_router;
13use dravr_tronc::McpServer;
14
15use crate::auth;
16use crate::completions;
17use crate::health;
18use crate::models;
19use crate::state::AppState;
20use crate::uhp;
21
22/// Build the application router with all endpoints
23///
24/// Routes:
25/// - `POST /v1/chat/completions` — Chat completion (streaming and non-streaming)
26/// - `GET /v1/models` — List available models
27/// - `GET /health` — Provider health check
28/// - `POST /mcp` — MCP Streamable HTTP (JSON-RPC 2.0, via dravr-tronc)
29/// - `{UHP_BASE_PATH}/v1/*` — Unified Harness Protocol, default base `/uhp`
30///
31/// The auth middleware is applied to all routes. It only enforces
32/// authentication when `EMBACLE_API_KEY` is set.
33///
34/// UHP is nested under its own base path rather than merged, because both
35/// surfaces define `GET /v1/models` with incompatible bodies. It carries its
36/// own auth and version layers: its refusals must be the UHP error envelope,
37/// and its discovery endpoint must answer without a credential.
38pub fn build(state: AppState) -> Router {
39 let mcp_server = Arc::new(McpServer::new(
40 "embacle-mcp",
41 env!("CARGO_PKG_VERSION"),
42 embacle_mcp::build_tool_registry(),
43 Arc::clone(&state.shared),
44 ));
45
46 let mcp_router = build_mcp_router(mcp_server);
47
48 let openai = Router::new()
49 .route("/v1/chat/completions", post(completions::handle))
50 .route("/v1/models", get(models::handle))
51 .route("/health", get(health::handle))
52 .with_state(state.clone())
53 .merge(mcp_router)
54 .layer(middleware::from_fn(auth::require_auth));
55
56 let base = uhp::base_path();
57 let uhp_routes = uhp::router(state);
58
59 if base.is_empty() {
60 openai.merge(uhp_routes)
61 } else {
62 openai.nest(&base, uhp_routes)
63 }
64}