1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
//! # mcp-skill-framework
//!
//! A small framework for building [Model Context Protocol][mcp] servers on top
//! of [`rmcp`] as a uniform layer of self-contained tools called **skills**.
//!
//! Each tool you expose is a [`Skill`]: a type with a `name`, a `description`,
//! a JSON-schema'd argument struct, and an async `call` body. The framework
//! adds the cross-cutting machinery every skill ends up wanting:
//!
//! - **Declarative input validation** ([`validation`]) — assert ranges, enums,
//! mutual exclusion, regex, and length as data ([`Rule`]s). The dispatcher
//! evaluates them before your body runs and returns a structured
//! `{"validation_failed": [...]}` payload a calling model can correct from.
//! - **Capability probes** ([`SkillCapability`]) — answer "can this host
//! actually run this tool?" (a binary on `$PATH`, a reachable socket)
//! separately from whether the operator enabled it.
//! [`capability::resolve`] combines each tool's probe with its family's
//! ("family `Unavailable` wins"), and [`routes_gated`] blocks unavailable
//! tools at dispatch with a reason + hint the caller can act on.
//! - **Family metadata** ([`FamilyMeta`]) — group related skills, describe
//! them, and probe their shared host requirement as a unit.
//! - **Rich descriptions** ([`describe`]) — render a skill or family
//! (description, use cases, worked examples, validation rules, argument
//! schema) for an on-demand introspection tool.
//! - **A ready-made dispatcher** ([`route_skill`]) — adapt a [`Skill`] into an
//! [`rmcp`] tool route with the validation gate already wired in.
//!
//! The [`Skill`] trait is generic over a server-state type `S` — whatever
//! shared state your tools need (HTTP clients, DB handles, config). The
//! framework never inspects `S`; it just hands each call a `&S`.
//!
//! ## Quickstart
//!
//! ```no_run
//! use std::sync::Arc;
//! use futures::future::BoxFuture;
//! use mcp_skill_framework::{schema_for, text_result, Rule, Skill, SkillCtx, SkillExample};
//! use rmcp::model::{CallToolResult, JsonObject};
//! use rmcp::ErrorData as McpError;
//! use serde::Deserialize;
//!
//! struct App; // your shared server state
//!
//! #[derive(Deserialize, schemars::JsonSchema)]
//! struct FormatArgs {
//! /// Total seconds to format.
//! seconds: i64,
//! /// Output style: `human` or `hms`.
//! #[serde(default)]
//! style: Option<String>,
//! }
//!
//! struct Format;
//! impl Skill<App> for Format {
//! fn name(&self) -> &'static str { "duration_format" }
//! fn description(&self) -> &'static str { "Format seconds as `human` or `hms`." }
//! fn schema(&self) -> Arc<JsonObject> { schema_for::<FormatArgs>() }
//! fn validation_rules(&self) -> &'static [Rule] {
//! &[Rule::OneOf { field: "style", values: &["human", "hms"] }]
//! }
//! fn examples(&self) -> &'static [SkillExample] {
//! &[SkillExample { title: "HH:MM:SS", args: r#"{"seconds": 9045, "style": "hms"}"#, note: None }]
//! }
//! fn call<'a>(&self, ctx: SkillCtx<'a, App>) -> BoxFuture<'a, Result<CallToolResult, McpError>> {
//! Box::pin(async move {
//! let (_app, a) = ctx.parse::<FormatArgs>()?;
//! Ok(text_result(format!("{} seconds", a.seconds)))
//! })
//! }
//! }
//!
//! // Register with an rmcp ToolRouter:
//! // let mut router = rmcp::handler::server::router::tool::ToolRouter::new();
//! // router.add_route(mcp_skill_framework::route_skill(Box::new(Format)));
//! ```
//!
//! ## Relationship to `rmcp`
//!
//! This is a thin layer on top of [`rmcp`], not a wall around it. [`Skill`] and
//! [`route_skill`] are defined directly in terms of rmcp's own types
//! (`CallToolResult`, `ErrorData`, `JsonObject`, `ToolRoute`), so you keep full
//! access to everything rmcp offers: you build the `ToolRouter`, implement
//! `ServerHandler`, and choose a transport with rmcp, and just `add_route` the
//! routes this crate produces.
//!
//! rmcp is re-exported as [`mcp_skill_framework::rmcp`](rmcp) so you can name
//! those types and stay pinned to one compatible version without adding rmcp
//! yourself. If you want an rmcp transport *feature* for the server side (stdio,
//! streamable HTTP, …), add `rmcp` to your own `Cargo.toml` with that feature —
//! Cargo unifies it with the version re-exported here. Your argument structs
//! still derive `serde::Deserialize` + `schemars::JsonSchema` as in any
//! schema'd-serde crate, so add those two; [`schemars`](crate::schemars) is
//! re-exported for version reference. The [`prelude`] glob-imports the handful
//! of names a skill body needs.
//!
//! [mcp]: https://modelcontextprotocol.io
pub use ;
pub use ;
pub use FamilyMeta;
pub use ;
pub use ;
// Re-export the foundational crates whose types appear in this crate's public
// API, so consumers name them through a single, version-compatible path and
// don't have to add (and keep in lockstep) a separate dependency.
/// The [`rmcp`] this crate is built on, re-exported. Use it to name the types in
/// a [`Skill::call`] signature and to build your `ToolRouter` / `ServerHandler`
/// / transport against the exact version this crate expects.
pub use rmcp;
/// The [`schemars`] whose `JsonSchema` [`schema_for`] requires, re-exported so
/// you can pin a matching version — and point the derive at it with
/// `#[schemars(crate = "mcp_skill_framework::schemars")]` if you'd rather not
/// add `schemars` to your own manifest.
pub use schemars;
/// `BoxFuture`, the return type of [`Skill::call`], re-exported from `futures`.
pub use BoxFuture;
use ;
use ErrorData as McpError;
/// Everything a skill body typically needs, in one glob import.
///
/// ```
/// use std::sync::Arc;
/// use mcp_skill_framework::prelude::*;
///
/// struct App;
///
/// #[derive(serde::Deserialize, schemars::JsonSchema)]
/// struct Args {
/// /// Who to greet.
/// name: String,
/// }
///
/// struct Greet;
/// impl Skill<App> for Greet {
/// fn name(&self) -> &'static str { "greet" }
/// fn description(&self) -> &'static str { "Greet someone." }
/// fn schema(&self) -> Arc<JsonObject> { schema_for::<Args>() }
/// fn call<'a>(&self, ctx: SkillCtx<'a, App>) -> BoxFuture<'a, Result<CallToolResult, McpError>> {
/// Box::pin(async move {
/// let (_app, a) = ctx.parse::<Args>()?;
/// Ok(text_result(format!("Hello, {}!", a.name)))
/// })
/// }
/// }
/// ```
/// Wrap a string as a successful single-text-content tool result. The
/// idiomatic return for a skill body that produced one textual answer (often
/// a JSON string built with [`serde_json`]).
/// Build an `invalid_params` MCP error from anything `Display`. Use for bad
/// caller input that the declarative [`validation`] layer didn't catch.
/// Build an `internal_error` MCP error from anything `Display`. Use for
/// failures that aren't the caller's fault (an upstream API, a socket, a parse
/// of data you fetched).