mcp-skill-framework
A small framework for building Model Context Protocol servers in Rust,
on top of rmcp, as a uniform layer of self-contained tools called
skills.
You write each tool as a Skill: a type with a name, a description, a
JSON-schema'd argument struct, and an async call body. The framework supplies
the cross-cutting machinery every tool ends up wanting — declarative input
validation, capability probes, family grouping, rich descriptions, and a
ready-made dispatcher — so your modules hold domain logic and nothing else.
use Arc;
use BoxFuture;
use ;
use ;
use ErrorData as McpError;
use Deserialize;
; // your shared server state
;
Why
Building an MCP server means writing a lot of tools, and every tool wants the
same supporting cast: argument parsing, input validation that a model can
actually correct from, a way to say "this tool needs ffmpeg and the host
doesn't have it", and a way to describe itself on demand. Hand-rolling that per
tool produces drift and copy-paste. This crate factors it into one contract.
What you get
| Piece | Module | What it does |
|---|---|---|
Skill<S> |
skill |
The tool contract: name / description / schema / call, plus optional examples, use_cases, validation_rules, check_capability. Generic over your server state S. |
| Validation DSL | validation |
Rule::{Range, OneOf, Regex, Length, ExactlyOne, AtLeastOne, All, Any, Not, Custom}. Evaluated before call; failures become a structured {"validation_failed": [...]} payload. |
SkillCapability |
capability |
Ready / Unavailable { reason, hint } — the result of a host probe you write, plus resolve to combine family + tool probes into a per-tool map. |
FamilyMeta |
family |
Group related skills, describe them, probe their shared host requirement as a unit. |
render_skill / render_family |
describe |
Plain-text rendering for an on-demand describe_skill / describe_family introspection tool. |
route_skill |
dispatch |
Adapt a Skill into an rmcp tool route with the validation gate wired in. |
Validation as data
Validation rules are values, not code, so they double as documentation. The dispatcher evaluates them after the arguments arrive and before your body runs. On failure the call returns a structured payload instead of a prose error:
A model reads that and retries correctly without you parsing English error
strings. Rules also render through [describe::render_skill], so the same
declaration powers both enforcement and self-description.
System requirements: capability gating
Validation answers "are these arguments well-formed?" Capabilities answer a different question — "can this host even run this tool?" — and keep it separate from whether the operator enabled it.
A tool — or a whole family via FamilyMeta — declares a probe. The probe
itself is yours to write (the framework defines the contract, not the host
checks); on_path below is application code:
use SkillCapability;
At startup, capability::resolve runs every probe once and combines each tool's
own probe with its family's — family Unavailable wins, because the family
hint ("install ffmpeg") is usually the actionable one. routes_gated then wires
the result into dispatch in one call:
let = routes_gated;
for in caps.unavailable_tools
// add `routes` to your rmcp ToolRouter<S>
A call to a blocked tool is refused at dispatch — before validation or your body
runs — with an invalid_request error like
tool 'media_probe' is unavailable on this host: \ffmpeg` not found on $PATH — install ffmpeg, so the calling model learns exactly what's missing and can choose another path. The returned Capabilities` map is also handy for a one-line startup log or a
status snapshot.
Wiring it into an rmcp server
route_skill turns each Skill into a ToolRoute<S> you add to an rmcp
ToolRouter<S>. The framework's dispatcher applies the validation gate and
then calls your body; application-specific concerns (capability gating,
backgrounding, recall, metrics) are intentionally left for you to wrap around
it. See dispatch for the extension points, and with_extra_property for
injecting a global argument (a background flag, say) into every tool's schema.
Examples
examples/duration.rs— one pure-compute skill end to end: declarative validation, on-demand description rendering, and calling the body the way the dispatcher does.examples/ffmpeg.rs— amediafamily whose probe rides onffmpegbeing on$PATH; showsresolve,routes_gated, and the blocked-tool report. The probe is application code, not a framework helper.
Status
Early (0.1). The skill/validation/capability/family surface is stable in
shape; the dispatcher is deliberately minimal and may grow opt-in extensions.
Targets rmcp 1.7.
License
MIT © Ely Erin Fox. See LICENSE.