1use anyhow::Result;
4use axum::{
5 routing::{get, post},
6 Router,
7};
8use std::net::SocketAddr;
9use std::path::PathBuf;
10use std::sync::Arc;
11use tokio::net::TcpListener;
12use tower_http::cors::{Any, CorsLayer};
13use tracing::info;
14
15use crate::handlers;
16use crate::proxy::McpProxy;
17use crate::wasm_builder::{find_workspace_root, WasmBuilder};
18
19#[derive(Debug, Clone, Default, PartialEq)]
21pub enum PreviewMode {
22 #[default]
24 Standard,
25 ChatGpt,
27}
28
29impl std::fmt::Display for PreviewMode {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Self::Standard => write!(f, "standard"),
33 Self::ChatGpt => write!(f, "chatgpt"),
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct PreviewConfig {
41 pub mcp_url: String,
43 pub port: u16,
45 pub initial_tool: Option<String>,
47 pub theme: String,
49 pub locale: String,
51 pub widgets_dir: Option<PathBuf>,
58 pub mode: PreviewMode,
60}
61
62impl Default for PreviewConfig {
63 fn default() -> Self {
64 Self {
65 mcp_url: "http://localhost:3000".to_string(),
66 port: 8765,
67 initial_tool: None,
68 theme: "light".to_string(),
69 locale: "en-US".to_string(),
70 widgets_dir: None,
71 mode: PreviewMode::default(),
72 }
73 }
74}
75
76pub struct AppState {
78 pub config: PreviewConfig,
79 pub proxy: McpProxy,
80 pub wasm_builder: WasmBuilder,
81}
82
83pub struct PreviewServer;
85
86impl PreviewServer {
87 pub async fn start(config: PreviewConfig) -> Result<()> {
89 let proxy = McpProxy::new(&config.mcp_url);
90
91 let cwd = std::env::current_dir().unwrap_or_default();
93 let workspace_root = find_workspace_root(&cwd).unwrap_or_else(|| cwd.clone());
94 let wasm_source_dir = workspace_root.join("examples").join("wasm-client");
95 let wasm_cache_dir = workspace_root.join("target").join("wasm-bridge");
96 let wasm_builder = WasmBuilder::new(wasm_source_dir, wasm_cache_dir);
97
98 let state = Arc::new(AppState {
99 config: config.clone(),
100 proxy,
101 wasm_builder,
102 });
103
104 let cors = CorsLayer::new()
106 .allow_origin(Any)
107 .allow_methods(Any)
108 .allow_headers(Any);
109
110 let app = Router::new()
112 .route("/", get(handlers::page::index))
114 .route("/api/config", get(handlers::api::get_config))
116 .route("/api/tools", get(handlers::api::list_tools))
117 .route("/api/tools/call", post(handlers::api::call_tool))
118 .route("/api/resources", get(handlers::api::list_resources))
120 .route("/api/resources/read", get(handlers::api::read_resource))
121 .route("/api/reconnect", post(handlers::api::reconnect))
123 .route("/api/status", get(handlers::api::status))
124 .route("/api/wasm/build", post(handlers::wasm::trigger_build))
126 .route("/api/wasm/status", get(handlers::wasm::build_status))
127 .route("/wasm/{*path}", get(handlers::wasm::serve_artifact))
129 .route("/assets/{*path}", get(handlers::assets::serve))
131 .route("/ws", get(handlers::websocket::handler))
133 .layer(cors)
134 .with_state(state);
135
136 let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
137
138 println!();
139 println!("\x1b[1;36m╔══════════════════════════════════════════════════╗\x1b[0m");
140 println!("\x1b[1;36m║ MCP Apps Preview Server ║\x1b[0m");
141 println!("\x1b[1;36m╠══════════════════════════════════════════════════╣\x1b[0m");
142 println!(
143 "\x1b[1;36m║\x1b[0m Preview: \x1b[1;33mhttp://localhost:{:<5}\x1b[0m \x1b[1;36m║\x1b[0m",
144 config.port
145 );
146 println!(
147 "\x1b[1;36m║\x1b[0m MCP Server: \x1b[1;32m{:<30}\x1b[0m \x1b[1;36m║\x1b[0m",
148 truncate_url(&config.mcp_url, 30)
149 );
150 if let Some(ref widgets_dir) = config.widgets_dir {
151 println!(
152 "\x1b[1;36m║\x1b[0m Widgets: \x1b[1;35m{:<30}\x1b[0m \x1b[1;36m║\x1b[0m",
153 truncate_url(&widgets_dir.display().to_string(), 30)
154 );
155 info!(
156 "Widgets directory: {} (hot-reload enabled)",
157 widgets_dir.display()
158 );
159 }
160 println!(
161 "\x1b[1;36m║\x1b[0m Mode: {:<30} \x1b[1;36m║\x1b[0m",
162 match config.mode {
163 PreviewMode::ChatGpt => "\x1b[1;31mChatGPT Strict\x1b[0m",
164 PreviewMode::Standard => "\x1b[1;32mStandard\x1b[0m",
165 }
166 );
167 println!("\x1b[1;36m╠══════════════════════════════════════════════════╣\x1b[0m");
168 println!(
169 "\x1b[1;36m║\x1b[0m Press Ctrl+C to stop \x1b[1;36m║\x1b[0m"
170 );
171 println!("\x1b[1;36m╚══════════════════════════════════════════════════╝\x1b[0m");
172 println!();
173
174 info!("Preview server starting on http://{}", addr);
175
176 let listener = TcpListener::bind(addr).await?;
177 axum::serve(listener, app).await?;
178
179 Ok(())
180 }
181}
182
183fn truncate_url(url: &str, max_len: usize) -> String {
184 if url.len() <= max_len {
185 url.to_string()
186 } else {
187 format!("{}...", &url[..max_len - 3])
188 }
189}