use anyhow::Result;
use axum::{
routing::{get, post},
Router,
};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::net::TcpListener;
use tower_http::cors::{Any, CorsLayer};
use tracing::info;
use crate::handlers;
use crate::proxy::McpProxy;
use crate::wasm_builder::{find_workspace_root, WasmBuilder};
#[derive(Debug, Clone, Default, PartialEq)]
pub enum PreviewMode {
#[default]
Standard,
ChatGpt,
}
impl std::fmt::Display for PreviewMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Standard => write!(f, "standard"),
Self::ChatGpt => write!(f, "chatgpt"),
}
}
}
#[derive(Debug, Clone)]
pub struct PreviewConfig {
pub mcp_url: String,
pub port: u16,
pub initial_tool: Option<String>,
pub theme: String,
pub locale: String,
pub widgets_dir: Option<PathBuf>,
pub mode: PreviewMode,
pub auth_header: Option<String>,
}
impl Default for PreviewConfig {
fn default() -> Self {
Self {
mcp_url: "http://localhost:3000".to_string(),
port: 8765,
initial_tool: None,
theme: "light".to_string(),
locale: "en-US".to_string(),
widgets_dir: None,
mode: PreviewMode::default(),
auth_header: None,
}
}
}
pub struct AppState {
pub config: PreviewConfig,
pub proxy: McpProxy,
pub wasm_builder: WasmBuilder,
}
pub struct PreviewServer;
impl PreviewServer {
pub async fn start(config: PreviewConfig) -> Result<()> {
let proxy = McpProxy::new_with_auth(&config.mcp_url, config.auth_header.clone());
let cwd = std::env::current_dir().unwrap_or_default();
let workspace_root = find_workspace_root(&cwd).unwrap_or_else(|| cwd.clone());
let wasm_source_dir = workspace_root.join("examples").join("wasm-client");
let wasm_cache_dir = workspace_root.join("target").join("wasm-bridge");
let wasm_builder = WasmBuilder::new(wasm_source_dir, wasm_cache_dir);
let state = Arc::new(AppState {
config: config.clone(),
proxy,
wasm_builder,
});
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
let app = Router::new()
.route("/", get(handlers::page::index))
.route("/api/config", get(handlers::api::get_config))
.route("/api/tools", get(handlers::api::list_tools))
.route("/api/tools/call", post(handlers::api::call_tool))
.route("/api/resources", get(handlers::api::list_resources))
.route("/api/resources/read", get(handlers::api::read_resource))
.route("/api/reconnect", post(handlers::api::reconnect))
.route("/api/status", get(handlers::api::status))
.route("/api/mcp", post(handlers::api::forward_mcp))
.route("/api/wasm/build", post(handlers::wasm::trigger_build))
.route("/api/wasm/status", get(handlers::wasm::build_status))
.route("/wasm/{*path}", get(handlers::wasm::serve_artifact))
.route("/assets/{*path}", get(handlers::assets::serve))
.route("/ws", get(handlers::websocket::handler))
.layer(cors)
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
println!();
println!("\x1b[1;36m╔══════════════════════════════════════════════════╗\x1b[0m");
println!("\x1b[1;36m║ MCP Apps Preview Server ║\x1b[0m");
println!("\x1b[1;36m╠══════════════════════════════════════════════════╣\x1b[0m");
println!(
"\x1b[1;36m║\x1b[0m Preview: \x1b[1;33mhttp://localhost:{:<5}\x1b[0m \x1b[1;36m║\x1b[0m",
config.port
);
println!(
"\x1b[1;36m║\x1b[0m MCP Server: \x1b[1;32m{:<30}\x1b[0m \x1b[1;36m║\x1b[0m",
truncate_url(&config.mcp_url, 30)
);
if let Some(ref widgets_dir) = config.widgets_dir {
println!(
"\x1b[1;36m║\x1b[0m Widgets: \x1b[1;35m{:<30}\x1b[0m \x1b[1;36m║\x1b[0m",
truncate_url(&widgets_dir.display().to_string(), 30)
);
info!(
"Widgets directory: {} (hot-reload enabled)",
widgets_dir.display()
);
}
println!(
"\x1b[1;36m║\x1b[0m Mode: {:<30} \x1b[1;36m║\x1b[0m",
match config.mode {
PreviewMode::ChatGpt => "\x1b[1;31mChatGPT Strict\x1b[0m",
PreviewMode::Standard => "\x1b[1;32mStandard MCP Apps\x1b[0m",
}
);
println!("\x1b[1;36m╠══════════════════════════════════════════════════╣\x1b[0m");
println!(
"\x1b[1;36m║\x1b[0m Press Ctrl+C to stop \x1b[1;36m║\x1b[0m"
);
println!("\x1b[1;36m╚══════════════════════════════════════════════════╝\x1b[0m");
println!();
info!("Preview server starting on http://{}", addr);
let listener = TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
}
fn truncate_url(url: &str, max_len: usize) -> String {
if url.len() <= max_len {
url.to_string()
} else {
format!("{}...", &url[..max_len - 3])
}
}