1use anyhow::Result;
4use axum::{
5 routing::{get, post},
6 Router,
7};
8use std::net::SocketAddr;
9use std::sync::Arc;
10use tokio::net::TcpListener;
11use tower_http::cors::{Any, CorsLayer};
12use tracing::info;
13
14use crate::handlers;
15use crate::proxy::McpProxy;
16
17#[derive(Debug, Clone)]
19pub struct PreviewConfig {
20 pub mcp_url: String,
22 pub port: u16,
24 pub initial_tool: Option<String>,
26 pub theme: String,
28 pub locale: String,
30}
31
32impl Default for PreviewConfig {
33 fn default() -> Self {
34 Self {
35 mcp_url: "http://localhost:3000".to_string(),
36 port: 8765,
37 initial_tool: None,
38 theme: "light".to_string(),
39 locale: "en-US".to_string(),
40 }
41 }
42}
43
44pub struct AppState {
46 pub config: PreviewConfig,
47 pub proxy: McpProxy,
48}
49
50pub struct PreviewServer;
52
53impl PreviewServer {
54 pub async fn start(config: PreviewConfig) -> Result<()> {
56 let proxy = McpProxy::new(&config.mcp_url);
57
58 let state = Arc::new(AppState {
59 config: config.clone(),
60 proxy,
61 });
62
63 let cors = CorsLayer::new()
65 .allow_origin(Any)
66 .allow_methods(Any)
67 .allow_headers(Any);
68
69 let app = Router::new()
71 .route("/", get(handlers::page::index))
73 .route("/api/config", get(handlers::api::get_config))
75 .route("/api/tools", get(handlers::api::list_tools))
76 .route("/api/tools/call", post(handlers::api::call_tool))
77 .route("/assets/{*path}", get(handlers::assets::serve))
79 .route("/ws", get(handlers::websocket::handler))
81 .layer(cors)
82 .with_state(state);
83
84 let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
85
86 println!();
87 println!("\x1b[1;36m╔══════════════════════════════════════════════════╗\x1b[0m");
88 println!("\x1b[1;36m║ MCP Apps Preview Server ║\x1b[0m");
89 println!("\x1b[1;36m╠══════════════════════════════════════════════════╣\x1b[0m");
90 println!(
91 "\x1b[1;36m║\x1b[0m Preview: \x1b[1;33mhttp://localhost:{:<5}\x1b[0m \x1b[1;36m║\x1b[0m",
92 config.port
93 );
94 println!(
95 "\x1b[1;36m║\x1b[0m MCP Server: \x1b[1;32m{:<30}\x1b[0m \x1b[1;36m║\x1b[0m",
96 truncate_url(&config.mcp_url, 30)
97 );
98 println!("\x1b[1;36m╠══════════════════════════════════════════════════╣\x1b[0m");
99 println!(
100 "\x1b[1;36m║\x1b[0m Press Ctrl+C to stop \x1b[1;36m║\x1b[0m"
101 );
102 println!("\x1b[1;36m╚══════════════════════════════════════════════════╝\x1b[0m");
103 println!();
104
105 info!("Preview server starting on http://{}", addr);
106
107 let listener = TcpListener::bind(addr).await?;
108 axum::serve(listener, app).await?;
109
110 Ok(())
111 }
112}
113
114fn truncate_url(url: &str, max_len: usize) -> String {
115 if url.len() <= max_len {
116 url.to_string()
117 } else {
118 format!("{}...", &url[..max_len - 3])
119 }
120}