Skip to main content

mcp_preview/
server.rs

1//! Preview server implementation
2
3use 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/// Configuration for the preview server
18#[derive(Debug, Clone)]
19pub struct PreviewConfig {
20    /// URL of the target MCP server
21    pub mcp_url: String,
22    /// Port for the preview server
23    pub port: u16,
24    /// Initial tool to select
25    pub initial_tool: Option<String>,
26    /// Initial theme (light/dark)
27    pub theme: String,
28    /// Initial locale
29    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
44/// Shared application state
45pub struct AppState {
46    pub config: PreviewConfig,
47    pub proxy: McpProxy,
48}
49
50/// MCP Preview Server
51pub struct PreviewServer;
52
53impl PreviewServer {
54    /// Start the preview server
55    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        // Build CORS layer
64        let cors = CorsLayer::new()
65            .allow_origin(Any)
66            .allow_methods(Any)
67            .allow_headers(Any);
68
69        // Build router
70        let app = Router::new()
71            // Main preview page
72            .route("/", get(handlers::page::index))
73            // API endpoints
74            .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            // Static assets
78            .route("/assets/{*path}", get(handlers::assets::serve))
79            // WebSocket for live updates
80            .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}