Expand description
§mcp-skill-framework
A small framework for building Model Context Protocol 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 (Rules). 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::resolvecombines each tool’s probe with its family’s (“familyUnavailablewins”), androutes_gatedblocks 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 aSkillinto anrmcptool 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
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 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 is
re-exported for version reference. The prelude glob-imports the handful
of names a skill body needs.
Re-exports§
pub use capability::Capabilities;pub use capability::SkillCapability;pub use dispatch::route_skill;pub use dispatch::route_skill_gated;pub use dispatch::routes_gated;pub use dispatch::with_extra_property;pub use family::FamilyMeta;pub use skill::schema_for;pub use skill::NoArgs;pub use skill::Skill;pub use skill::SkillCtx;pub use skill::SkillExample;pub use validation::evaluate;pub use validation::FieldViolation;pub use validation::Rule;pub use validation::ValidationResult;pub use rmcp;pub use schemars;
Modules§
- capability
- Capability probes — “can this host actually run this tool?”
- describe
- Human-readable rendering of skills and families — “the description” layer.
- dispatch
- Turning skills into rmcp tool routes.
- family
- Family metadata — group related skills and describe them as a unit.
- prelude
- Everything a skill body typically needs, in one glob import.
- skill
- The skill contract — the uniform capability unit of an MCP server.
- validation
- Structured input validation for the
Skillcontract.
Functions§
- internal
- Build an
internal_errorMCP error from anythingDisplay. Use for failures that aren’t the caller’s fault (an upstream API, a socket, a parse of data you fetched). - invalid
- Build an
invalid_paramsMCP error from anythingDisplay. Use for bad caller input that the declarativevalidationlayer didn’t catch. - text_
result - 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).
Type Aliases§
- BoxFuture
BoxFuture, the return type ofSkill::call, re-exported fromfutures. An owned dynamically typedFuturefor use in cases where you can’t statically type your result or need to add some indirection.