use std::sync::Arc; use domain::{ ChannelService, ContentType, ProgrammingBlock, ScheduleConfig, ScheduleEngineService, }; use rmcp::{ ServerHandler, model::{Implementation, ProtocolVersion, ServerCapabilities, ServerInfo}, tool, }; use schemars::JsonSchema; use serde::Deserialize; use uuid::Uuid; use crate::tools::{channels, library, schedule}; #[derive(Clone)] pub struct KTvMcpServer { pub channel_service: Arc, pub schedule_engine: Arc, pub provider_registry: Arc, pub owner_id: Uuid, } // ============================================================================ // Parameter types — Uuid fields stored as String to satisfy JsonSchema bound. // ============================================================================ #[derive(Debug, Deserialize, JsonSchema)] pub struct GetChannelParams { /// Channel UUID (e.g. "550e8400-e29b-41d4-a716-446655440000") pub id: String, } #[derive(Debug, Deserialize, JsonSchema)] pub struct CreateChannelParams { pub name: String, /// IANA timezone, e.g. "America/New_York" pub timezone: String, pub description: Option, } #[derive(Debug, Deserialize, JsonSchema)] pub struct UpdateChannelParams { /// Channel UUID pub id: String, pub name: Option, pub timezone: Option, pub description: Option, } #[derive(Debug, Deserialize, JsonSchema)] pub struct DeleteChannelParams { /// Channel UUID pub id: String, } #[derive(Debug, Deserialize, JsonSchema)] pub struct SetScheduleConfigParams { /// Channel UUID pub channel_id: String, /// JSON object of the full ScheduleConfig shape: {"monday": [...], "tuesday": [...], ...} pub day_blocks_json: String, } #[derive(Debug, Deserialize, JsonSchema)] pub struct AddBlockParams { /// Channel UUID pub channel_id: String, /// Day of week: "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday" pub day: String, /// ProgrammingBlock serialized as JSON pub block_json: String, } #[derive(Debug, Deserialize, JsonSchema)] pub struct RemoveBlockParams { /// Channel UUID pub channel_id: String, /// Block UUID pub block_id: String, } #[derive(Debug, Deserialize, JsonSchema)] pub struct ChannelIdParam { /// Channel UUID pub channel_id: String, } #[derive(Debug, Deserialize, JsonSchema)] pub struct SearchMediaParams { /// "movie", "episode", or "short" pub content_type: Option, pub genres: Option>, pub search_term: Option, pub series_names: Option>, pub collections: Option>, } #[derive(Debug, Deserialize, JsonSchema)] pub struct ListGenresParams { /// Optional content type: "movie", "episode", or "short" pub content_type: Option, } // ============================================================================ // Tool implementations // ============================================================================ fn parse_uuid(s: &str) -> Result { s.parse::() .map_err(|_| serde_json::json!({"error": format!("invalid UUID: {s}")}).to_string()) } #[tool(tool_box)] impl KTvMcpServer { #[tool(description = "List all channels owned by the configured user")] async fn list_channels(&self) -> String { channels::list_channels(&self.channel_service, self.owner_id).await } #[tool(description = "Get a channel by UUID")] async fn get_channel(&self, #[tool(aggr)] p: GetChannelParams) -> String { match parse_uuid(&p.id) { Ok(id) => channels::get_channel(&self.channel_service, id).await, Err(e) => e, } } #[tool(description = "Create a new channel with a name and IANA timezone")] async fn create_channel(&self, #[tool(aggr)] p: CreateChannelParams) -> String { channels::create_channel( &self.channel_service, self.owner_id, &p.name, &p.timezone, p.description, ) .await } #[tool(description = "Update channel name, timezone, and/or description")] async fn update_channel(&self, #[tool(aggr)] p: UpdateChannelParams) -> String { match parse_uuid(&p.id) { Ok(id) => { channels::update_channel( &self.channel_service, id, p.name, p.timezone, p.description, ) .await } Err(e) => e, } } #[tool(description = "Delete a channel (must be owned by the configured user)")] async fn delete_channel(&self, #[tool(aggr)] p: DeleteChannelParams) -> String { match parse_uuid(&p.id) { Ok(id) => channels::delete_channel(&self.channel_service, id, self.owner_id).await, Err(e) => e, } } #[tool( description = "Replace a channel's entire schedule config. day_blocks_json is a JSON object of the ScheduleConfig shape: {\"monday\": [...], ...}" )] async fn set_schedule_config(&self, #[tool(aggr)] p: SetScheduleConfigParams) -> String { let channel_id = match parse_uuid(&p.channel_id) { Ok(id) => id, Err(e) => return e, }; let config: ScheduleConfig = match serde_json::from_str(&p.day_blocks_json) { Ok(c) => c, Err(e) => { return serde_json::json!({"error": format!("invalid day_blocks_json: {e}")}) .to_string() } }; channels::set_schedule_config(&self.channel_service, channel_id, config).await } #[tool( description = "Append a ProgrammingBlock to a channel's schedule for a specific day. day: monday|tuesday|wednesday|thursday|friday|saturday|sunday. block_json is a serialized ProgrammingBlock." )] async fn add_programming_block(&self, #[tool(aggr)] p: AddBlockParams) -> String { let channel_id = match parse_uuid(&p.channel_id) { Ok(id) => id, Err(e) => return e, }; let day: domain::Weekday = match serde_json::from_str(&format!("\"{}\"", p.day)) { Ok(d) => d, Err(e) => { return serde_json::json!({"error": format!("invalid day: {e}")}).to_string() } }; let block: ProgrammingBlock = match serde_json::from_str(&p.block_json) { Ok(b) => b, Err(e) => { return serde_json::json!({"error": format!("invalid block_json: {e}")}).to_string() } }; channels::add_programming_block(&self.channel_service, channel_id, day, block).await } #[tool(description = "Remove a programming block from a channel's schedule by block UUID")] async fn remove_programming_block(&self, #[tool(aggr)] p: RemoveBlockParams) -> String { let channel_id = match parse_uuid(&p.channel_id) { Ok(id) => id, Err(e) => return e, }; let block_id = match parse_uuid(&p.block_id) { Ok(id) => id, Err(e) => return e, }; channels::remove_programming_block(&self.channel_service, channel_id, block_id).await } #[tool(description = "Generate a fresh 48-hour schedule for the given channel")] async fn generate_schedule(&self, #[tool(aggr)] p: ChannelIdParam) -> String { match parse_uuid(&p.channel_id) { Ok(id) => schedule::generate_schedule(&self.schedule_engine, id).await, Err(e) => e, } } #[tool(description = "Get the currently active schedule for a channel (returns null if none)")] async fn get_active_schedule(&self, #[tool(aggr)] p: ChannelIdParam) -> String { match parse_uuid(&p.channel_id) { Ok(id) => schedule::get_active_schedule(&self.schedule_engine, id).await, Err(e) => e, } } #[tool( description = "Get what is currently broadcasting on a channel (returns null if in a gap or no schedule)" )] async fn get_current_broadcast(&self, #[tool(aggr)] p: ChannelIdParam) -> String { match parse_uuid(&p.channel_id) { Ok(id) => schedule::get_current_broadcast(&self.schedule_engine, id).await, Err(e) => e, } } #[tool(description = "List media collections/libraries available in the configured provider")] async fn list_collections(&self) -> String { library::list_collections(&self.provider_registry).await } #[tool( description = "List genres available in the provider, optionally filtered by content type (movie/episode/short)" )] async fn list_genres(&self, #[tool(aggr)] p: ListGenresParams) -> String { let ct = p.content_type.as_deref().and_then(parse_content_type); library::list_genres(&self.provider_registry, ct).await } #[tool( description = "Search media items. content_type: movie|episode|short. Returns JSON array of MediaItem." )] async fn search_media(&self, #[tool(aggr)] p: SearchMediaParams) -> String { let ct = p.content_type.as_deref().and_then(parse_content_type); library::search_media( &self.provider_registry, ct, p.genres.unwrap_or_default(), p.search_term, p.series_names.unwrap_or_default(), p.collections.unwrap_or_default(), ) .await } } fn parse_content_type(s: &str) -> Option { match s { "movie" => Some(ContentType::Movie), "episode" => Some(ContentType::Episode), "short" => Some(ContentType::Short), _ => None, } } // ============================================================================ // ServerHandler // ============================================================================ #[tool(tool_box)] impl ServerHandler for KTvMcpServer { fn get_info(&self) -> ServerInfo { ServerInfo { protocol_version: ProtocolVersion::V_2024_11_05, capabilities: ServerCapabilities::builder().enable_tools().build(), server_info: Implementation { name: "k-tv-mcp".into(), version: "0.1.0".into(), }, instructions: Some( "K-TV MCP server. Create channels, define programming blocks, generate schedules. \ All operations run as the user configured via MCP_USER_ID." .into(), ), } } }