Files
k-tv/k-tv-backend/mcp/src/main.rs

178 lines
5.7 KiB
Rust

use std::sync::Arc;
use std::time::Duration as StdDuration;
use domain::{
ChannelService, DomainError, DomainResult, IMediaProvider, IProviderRegistry, MediaFilter,
MediaItemId, ProviderCapabilities, ScheduleEngineService, StreamQuality, StreamingProtocol,
UserService,
};
use infra::factory::{build_channel_repository, build_schedule_repository, build_user_repository};
use infra::run_migrations;
use tracing::info;
use uuid::Uuid;
mod error;
mod server;
mod tools;
use server::KTvMcpServer;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let _ = dotenvy::dotenv();
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("mcp=info".parse().unwrap()),
)
.init();
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite:data.db?mode=rwc".to_string());
let owner_id: Uuid = std::env::var("MCP_USER_ID")
.map_err(|_| anyhow::anyhow!("MCP_USER_ID env var is required (UUID of the user)"))?
.parse()
.map_err(|_| anyhow::anyhow!("MCP_USER_ID must be a valid UUID"))?;
info!("Connecting to database: {}", database_url);
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
let db_type = k_core::db::DbType::Sqlite;
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
let db_type = k_core::db::DbType::Postgres;
#[cfg(all(feature = "sqlite", feature = "postgres"))]
let db_type = if database_url.starts_with("postgres") {
k_core::db::DbType::Postgres
} else {
k_core::db::DbType::Sqlite
};
let db_config = k_core::db::DatabaseConfig {
db_type,
url: database_url.clone(),
max_connections: 5,
min_connections: 1,
acquire_timeout: StdDuration::from_secs(30),
};
let db_pool = k_core::db::connect(&db_config).await?;
run_migrations(&db_pool).await?;
let user_repo = build_user_repository(&db_pool).await?;
let channel_repo = build_channel_repository(&db_pool).await?;
let schedule_repo = build_schedule_repository(&db_pool).await?;
let _user_service = UserService::new(user_repo);
let channel_service = ChannelService::new(channel_repo.clone());
let mut registry = infra::ProviderRegistry::new();
#[cfg(feature = "jellyfin")]
{
let base_url = std::env::var("JELLYFIN_BASE_URL").ok();
let api_key = std::env::var("JELLYFIN_API_KEY").ok();
let user_id = std::env::var("JELLYFIN_USER_ID").ok();
if let (Some(base_url), Some(api_key), Some(user_id)) = (base_url, api_key, user_id) {
info!("Media provider: Jellyfin at {}", base_url);
registry.register("jellyfin", Arc::new(infra::JellyfinMediaProvider::new(
infra::JellyfinConfig { base_url, api_key, user_id },
)));
}
}
#[cfg(feature = "local-files")]
if let Some(dir) = std::env::var("LOCAL_FILES_DIR").ok().map(std::path::PathBuf::from) {
let k_core::db::DatabasePool::Sqlite(ref sqlite_pool) = db_pool;
let base_url = std::env::var("BASE_URL")
.unwrap_or_else(|_| "http://localhost:3000".to_string());
let lf_cfg = infra::LocalFilesConfig {
root_dir: dir,
base_url,
transcode_dir: None,
cleanup_ttl_hours: 24,
};
let idx = Arc::new(infra::LocalIndex::new(&lf_cfg, sqlite_pool.clone()).await);
let scan_idx = Arc::clone(&idx);
tokio::spawn(async move { scan_idx.rescan().await; });
registry.register("local", Arc::new(infra::LocalFilesProvider::new(idx, lf_cfg, None)));
}
if registry.is_empty() {
tracing::warn!("No media provider configured. Set JELLYFIN_BASE_URL or LOCAL_FILES_DIR.");
registry.register("noop", Arc::new(NoopMediaProvider));
}
let registry = Arc::new(registry);
let schedule_engine = ScheduleEngineService::new(
Arc::clone(&registry) as Arc<dyn IProviderRegistry>,
channel_repo,
schedule_repo,
);
let server = KTvMcpServer {
channel_service: Arc::new(channel_service),
schedule_engine: Arc::new(schedule_engine),
provider_registry: registry,
owner_id,
};
info!("K-TV MCP server starting (stdio transport), owner_id={}", owner_id);
use rmcp::ServiceExt;
let service = server
.serve(rmcp::transport::stdio())
.await
.inspect_err(|e| tracing::error!("MCP server error: {e}"))?;
service.waiting().await?;
Ok(())
}
struct NoopMediaProvider;
#[async_trait::async_trait]
impl IMediaProvider for NoopMediaProvider {
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities {
collections: false,
series: false,
genres: false,
tags: false,
decade: false,
search: false,
streaming_protocol: StreamingProtocol::DirectFile,
rescan: false,
transcode: false,
}
}
async fn fetch_items(&self, _: &MediaFilter) -> DomainResult<Vec<domain::MediaItem>> {
Err(DomainError::InfrastructureError(
"No media provider configured.".into(),
))
}
async fn fetch_by_id(&self, _: &MediaItemId) -> DomainResult<Option<domain::MediaItem>> {
Err(DomainError::InfrastructureError(
"No media provider configured.".into(),
))
}
async fn get_stream_url(
&self,
_: &MediaItemId,
_: &StreamQuality,
) -> DomainResult<String> {
Err(DomainError::InfrastructureError(
"No media provider configured.".into(),
))
}
}