mcp: MCP server calling application use cases

This commit is contained in:
2026-07-12 03:31:08 +02:00
parent 56d742a74c
commit c869e9ab84
10 changed files with 980 additions and 1 deletions

204
crates/mcp/src/server.rs Normal file
View File

@@ -0,0 +1,204 @@
use std::sync::Arc;
use application::{
channels::{ChannelCommandDeps, ChannelQueryDeps},
library::LibraryQueryDeps,
schedule::ScheduleDeps,
};
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_cmd_deps: Arc<ChannelCommandDeps>,
pub channel_query_deps: Arc<ChannelQueryDeps>,
pub schedule_deps: Arc<ScheduleDeps>,
pub library_query_deps: Arc<LibraryQueryDeps>,
pub owner_id: Uuid,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetChannelParams {
pub id: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateChannelParams {
pub name: String,
pub timezone: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct UpdateChannelParams {
pub id: String,
pub name: Option<String>,
pub timezone: Option<String>,
pub description: Option<String>,
pub schedule_config_json: Option<String>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DeleteChannelParams {
pub id: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ChannelIdParam {
pub channel_id: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct SearchMediaParams {
pub content_type: Option<String>,
pub genres: Option<Vec<String>>,
pub search_term: Option<String>,
pub series_names: Option<Vec<String>>,
pub collections: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListGenresParams {
pub content_type: Option<String>,
}
fn parse_uuid(s: &str) -> Result<Uuid, String> {
s.parse::<Uuid>()
.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_query_deps, 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_query_deps, 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_cmd_deps, self.owner_id, &p.name, &p.timezone).await
}
#[tool(description = "Update channel name, timezone, description, and/or schedule config")]
async fn update_channel(&self, #[tool(aggr)] p: UpdateChannelParams) -> String {
let id = match parse_uuid(&p.id) {
Ok(id) => id,
Err(e) => return e,
};
let schedule_config = match p.schedule_config_json {
Some(json) => match serde_json::from_str(&json) {
Ok(c) => Some(c),
Err(e) => {
return serde_json::json!({"error": format!("invalid schedule_config_json: {e}")})
.to_string()
}
},
None => None,
};
channels::update_channel(
&self.channel_cmd_deps,
id,
self.owner_id,
p.name,
p.timezone,
p.description,
schedule_config,
)
.await
}
#[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_cmd_deps, id, self.owner_id).await,
Err(e) => e,
}
}
#[tool(description = "Generate a fresh 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_deps, 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_deps, 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_deps, id).await,
Err(e) => e,
}
}
#[tool(description = "List media collections/libraries available in the library")]
async fn list_collections(&self) -> String {
library::list_collections(&self.library_query_deps).await
}
#[tool(
description = "List genres available in the library, optionally filtered by content type (movie/episode/short)"
)]
async fn list_genres(&self, #[tool(aggr)] p: ListGenresParams) -> String {
library::list_genres(&self.library_query_deps, p.content_type).await
}
#[tool(
description = "Search media items. content_type: movie|episode|short. Returns JSON array of LibraryItem."
)]
async fn search_media(&self, #[tool(aggr)] p: SearchMediaParams) -> String {
library::search_media(
&self.library_query_deps,
p.content_type,
p.genres.unwrap_or_default(),
p.search_term,
p.series_names.unwrap_or_default(),
p.collections.unwrap_or_default(),
)
.await
}
}
#[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(),
),
}
}
}