Skip to main content

Crate mcp_skill_framework

Crate mcp_skill_framework 

Source
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::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

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 Skill contract.

Functions§

internal
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).
invalid
Build an invalid_params MCP error from anything Display. Use for bad caller input that the declarative validation layer 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 of Skill::call, re-exported from futures. An owned dynamically typed Future for use in cases where you can’t statically type your result or need to add some indirection.