mcp: MCP server calling application use cases
This commit is contained in:
204
crates/mcp/src/server.rs
Normal file
204
crates/mcp/src/server.rs
Normal 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(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user