use anyhow::Result;
use clap::{Parser, ValueEnum};
use rmcp::{
ServiceExt,
transport::{stdio, streamable_http_server::StreamableHttpService},
};
use std::sync::Arc;
use tokio::sync::Mutex;
use tower_governor::{
GovernorLayer, governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor,
};
use tower_http::cors::{Any, CorsLayer};
use tracing::Level;
mod api;
mod llm;
mod mcp;
mod transcriber;
mod utils;
use api::AppState;
use mcp::VideoTranscriberServer;
use transcriber::TranscriberEngine;
use video_transcriber_mcp::credits;
#[derive(Debug, Clone, ValueEnum)]
enum Transport {
Stdio,
Http,
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long, value_enum, default_value = "stdio")]
transport: Transport,
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(short, long, default_value = "8080")]
port: u16,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
tracing_subscriber::fmt()
.with_max_level(Level::INFO)
.with_writer(std::io::stderr)
.with_target(false)
.with_thread_ids(false)
.with_file(false)
.with_line_number(false)
.with_ansi(matches!(args.transport, Transport::Http)) .init();
tracing::info!(
"Video Transcriber MCP Server (Rust) - v{}",
env!("CARGO_PKG_VERSION")
);
tracing::info!("Powered by whisper.cpp - 6x faster than Python whisper!");
match args.transport {
Transport::Stdio => run_stdio_transport().await,
Transport::Http => run_http_transport(&args.host, args.port).await,
}
}
async fn run_stdio_transport() -> Result<()> {
tracing::info!("Starting stdio transport...");
let server = VideoTranscriberServer::new();
let service = server.serve(stdio()).await?;
service.waiting().await?;
Ok(())
}
async fn run_http_transport(host: &str, port: u16) -> Result<()> {
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
tracing::info!("Starting Streamable HTTP transport on {}:{}...", host, port);
let mcp_service = StreamableHttpService::new(
|| Ok(VideoTranscriberServer::new()),
LocalSessionManager::default().into(),
Default::default(),
);
let app_state = AppState {
jobs: api::new_store(),
engine: Arc::new(Mutex::new(TranscriberEngine::new())),
credits: credits::new_store(),
};
let api_router = api::router(app_state);
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
let governor_conf = Arc::new(
GovernorConfigBuilder::default()
.per_second(1)
.burst_size(20)
.key_extractor(SmartIpKeyExtractor)
.finish()
.expect("failed to build rate limit config"),
);
let governor_layer = GovernorLayer::new(governor_conf);
let router = axum::Router::new()
.nest("/api", api_router.layer(governor_layer))
.nest_service("/mcp", mcp_service)
.layer(cors);
let addr = format!("{}:{}", host, port);
let tcp_listener = tokio::net::TcpListener::bind(&addr).await?;
tracing::info!("=================================================");
tracing::info!("Server ready");
tracing::info!(" MCP: http://{}/mcp", addr);
tracing::info!(" REST: http://{}/api/jobs", addr);
tracing::info!("=================================================");
use std::net::SocketAddr;
axum::serve(
tcp_listener,
router.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}