use std::sync::Arc;
use futures::future::BoxFuture;
use mcp_skill_framework::{
describe, routes_gated, schema_for, text_result, FamilyMeta, NoArgs, Skill, SkillCapability,
SkillCtx,
};
use rmcp::model::{CallToolResult, JsonObject};
use rmcp::ErrorData as McpError;
struct App;
fn on_path(bin: &str) -> bool {
let Some(paths) = std::env::var_os("PATH") else {
return false;
};
let candidates: Vec<String> = if cfg!(windows) {
vec![format!("{bin}.exe"), format!("{bin}.cmd"), bin.to_string()]
} else {
vec![bin.to_string()]
};
std::env::split_paths(&paths)
.any(|dir| candidates.iter().any(|name| dir.join(name).is_file()))
}
struct MediaProbe;
impl Skill<App> for MediaProbe {
fn name(&self) -> &'static str {
"media_probe"
}
fn description(&self) -> &'static str {
"Probe a media file's streams (would shell out to ffmpeg)."
}
fn schema(&self) -> Arc<JsonObject> {
schema_for::<NoArgs>()
}
fn call<'a>(&self, _ctx: SkillCtx<'a, App>) -> BoxFuture<'a, Result<CallToolResult, McpError>> {
Box::pin(async move { Ok(text_result("(would run `ffmpeg -i ...`)")) })
}
}
struct MediaThumbnail;
impl Skill<App> for MediaThumbnail {
fn name(&self) -> &'static str {
"media_thumbnail"
}
fn description(&self) -> &'static str {
"Extract a thumbnail frame (would shell out to ffmpeg)."
}
fn schema(&self) -> Arc<JsonObject> {
schema_for::<NoArgs>()
}
fn call<'a>(&self, _ctx: SkillCtx<'a, App>) -> BoxFuture<'a, Result<CallToolResult, McpError>> {
Box::pin(async move { Ok(text_result("(would run `ffmpeg -ss ... -vframes 1`)")) })
}
}
struct MediaFamily;
impl FamilyMeta for MediaFamily {
fn family(&self) -> &'static str {
"media"
}
fn tools(&self) -> Vec<&'static str> {
vec!["media_probe", "media_thumbnail"]
}
fn description(&self) -> &'static str {
"Media inspection. Requires `ffmpeg` on the host's $PATH."
}
fn check_capability(&self) -> SkillCapability {
if on_path("ffmpeg") {
SkillCapability::Ready
} else {
SkillCapability::unavailable("`ffmpeg` not found on $PATH", "install ffmpeg")
}
}
fn example_flow(&self) -> Option<&'static str> {
Some("1. `media_probe { ... }` then 2. `media_thumbnail { ... }`.")
}
}
fn families() -> Vec<Box<dyn FamilyMeta>> {
vec![Box::new(MediaFamily)]
}
fn all_skills() -> Vec<Box<dyn Skill<App>>> {
vec![Box::new(MediaProbe), Box::new(MediaThumbnail)]
}
fn main() {
println!("ffmpeg on $PATH: {}\n", on_path("ffmpeg"));
let (routes, caps) = routes_gated(&families(), all_skills());
println!("built {} tool route(s) for App", routes.len());
let blocked = caps.unavailable_tools();
if blocked.is_empty() {
println!("all tools are runnable on this host");
} else {
println!("{} tool(s) blocked on this host:", blocked.len());
for (tool, cap) in blocked {
if let SkillCapability::Unavailable { reason, hint } = cap {
match hint {
Some(h) => println!(" - {tool}: {reason} ({h})"),
None => println!(" - {tool}: {reason}"),
}
}
}
}
println!("\n{}", describe::render_family(&MediaFamily, None));
}