Skip to main content

mcp_skill_framework/
lib.rs

1//! # mcp-skill-framework
2//!
3//! A small framework for building [Model Context Protocol][mcp] servers on top
4//! of [`rmcp`] as a uniform layer of self-contained tools called **skills**.
5//!
6//! Each tool you expose is a [`Skill`]: a type with a `name`, a `description`,
7//! a JSON-schema'd argument struct, and an async `call` body. The framework
8//! adds the cross-cutting machinery every skill ends up wanting:
9//!
10//! - **Declarative input validation** ([`validation`]) — assert ranges, enums,
11//!   mutual exclusion, regex, and length as data ([`Rule`]s). The dispatcher
12//!   evaluates them before your body runs and returns a structured
13//!   `{"validation_failed": [...]}` payload a calling model can correct from.
14//! - **Capability probes** ([`SkillCapability`]) — answer "can this host
15//!   actually run this tool?" (a binary on `$PATH`, a reachable socket)
16//!   separately from whether the operator enabled it.
17//!   [`capability::resolve`] combines each tool's probe with its family's
18//!   ("family `Unavailable` wins"), and [`routes_gated`] blocks unavailable
19//!   tools at dispatch with a reason + hint the caller can act on.
20//! - **Family metadata** ([`FamilyMeta`]) — group related skills, describe
21//!   them, and probe their shared host requirement as a unit.
22//! - **Rich descriptions** ([`describe`]) — render a skill or family
23//!   (description, use cases, worked examples, validation rules, argument
24//!   schema) for an on-demand introspection tool.
25//! - **A ready-made dispatcher** ([`route_skill`]) — adapt a [`Skill`] into an
26//!   [`rmcp`] tool route with the validation gate already wired in.
27//!
28//! The [`Skill`] trait is generic over a server-state type `S` — whatever
29//! shared state your tools need (HTTP clients, DB handles, config). The
30//! framework never inspects `S`; it just hands each call a `&S`.
31//!
32//! ## Quickstart
33//!
34//! ```no_run
35//! use std::sync::Arc;
36//! use futures::future::BoxFuture;
37//! use mcp_skill_framework::{schema_for, text_result, Rule, Skill, SkillCtx, SkillExample};
38//! use rmcp::model::{CallToolResult, JsonObject};
39//! use rmcp::ErrorData as McpError;
40//! use serde::Deserialize;
41//!
42//! struct App; // your shared server state
43//!
44//! #[derive(Deserialize, schemars::JsonSchema)]
45//! struct FormatArgs {
46//!     /// Total seconds to format.
47//!     seconds: i64,
48//!     /// Output style: `human` or `hms`.
49//!     #[serde(default)]
50//!     style: Option<String>,
51//! }
52//!
53//! struct Format;
54//! impl Skill<App> for Format {
55//!     fn name(&self) -> &'static str { "duration_format" }
56//!     fn description(&self) -> &'static str { "Format seconds as `human` or `hms`." }
57//!     fn schema(&self) -> Arc<JsonObject> { schema_for::<FormatArgs>() }
58//!     fn validation_rules(&self) -> &'static [Rule] {
59//!         &[Rule::OneOf { field: "style", values: &["human", "hms"] }]
60//!     }
61//!     fn examples(&self) -> &'static [SkillExample] {
62//!         &[SkillExample { title: "HH:MM:SS", args: r#"{"seconds": 9045, "style": "hms"}"#, note: None }]
63//!     }
64//!     fn call<'a>(&self, ctx: SkillCtx<'a, App>) -> BoxFuture<'a, Result<CallToolResult, McpError>> {
65//!         Box::pin(async move {
66//!             let (_app, a) = ctx.parse::<FormatArgs>()?;
67//!             Ok(text_result(format!("{} seconds", a.seconds)))
68//!         })
69//!     }
70//! }
71//!
72//! // Register with an rmcp ToolRouter:
73//! // let router = rmcp::handler::server::router::tool::ToolRouter::new()
74//! //     .with_route(mcp_skill_framework::route_skill(Box::new(Format)));
75//! ```
76//!
77//! [mcp]: https://modelcontextprotocol.io
78
79pub mod capability;
80pub mod describe;
81pub mod dispatch;
82pub mod family;
83pub mod skill;
84pub mod validation;
85
86pub use capability::{Capabilities, SkillCapability};
87pub use dispatch::{route_skill, route_skill_gated, routes_gated, with_extra_property};
88pub use family::FamilyMeta;
89pub use skill::{schema_for, NoArgs, Skill, SkillCtx, SkillExample};
90pub use validation::{evaluate, FieldViolation, Rule, ValidationResult};
91
92use rmcp::model::{CallToolResult, Content};
93use rmcp::ErrorData as McpError;
94
95/// Wrap a string as a successful single-text-content tool result. The
96/// idiomatic return for a skill body that produced one textual answer (often
97/// a JSON string built with [`serde_json`]).
98pub fn text_result(s: impl Into<String>) -> CallToolResult {
99    CallToolResult::success(vec![Content::text(s.into())])
100}
101
102/// Build an `invalid_params` MCP error from anything `Display`. Use for bad
103/// caller input that the declarative [`validation`] layer didn't catch.
104pub fn invalid(e: impl std::fmt::Display) -> McpError {
105    McpError::invalid_params(e.to_string(), None)
106}
107
108/// Build an `internal_error` MCP error from anything `Display`. Use for
109/// failures that aren't the caller's fault (an upstream API, a socket, a parse
110/// of data you fetched).
111pub fn internal(e: impl std::fmt::Display) -> McpError {
112    McpError::internal_error(e.to_string(), None)
113}