mcp-skill-framework 0.1.1

A small framework for building MCP (Model Context Protocol) servers as a uniform layer of self-contained tools ("skills"): a typed skill contract, declarative input validation, capability probes, family metadata, and a ready-made dispatcher.
Documentation
//! # 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 mod capability;
pub mod describe;
pub mod dispatch;
pub mod family;
pub mod skill;
pub mod validation;

pub use capability::{Capabilities, SkillCapability};
pub use dispatch::{route_skill, route_skill_gated, routes_gated, with_extra_property};
pub use family::FamilyMeta;
pub use skill::{schema_for, NoArgs, Skill, SkillCtx, SkillExample};
pub use validation::{evaluate, FieldViolation, Rule, ValidationResult};

// 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 futures::future::BoxFuture;

use rmcp::model::{CallToolResult, Content};
use rmcp::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)))
///         })
///     }
/// }
/// ```
pub mod prelude {
    pub use crate::{
        internal, invalid, route_skill, route_skill_gated, routes_gated, schema_for, text_result,
        BoxFuture, Capabilities, FamilyMeta, NoArgs, Rule, Skill, SkillCapability, SkillCtx,
        SkillExample, ValidationResult,
    };
    pub use rmcp::model::{CallToolResult, JsonObject};
    pub use rmcp::ErrorData as McpError;
}

/// 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`]).
pub fn text_result(s: impl Into<String>) -> CallToolResult {
    CallToolResult::success(vec![Content::text(s.into())])
}

/// Build an `invalid_params` MCP error from anything `Display`. Use for bad
/// caller input that the declarative [`validation`] layer didn't catch.
pub fn invalid(e: impl std::fmt::Display) -> McpError {
    McpError::invalid_params(e.to_string(), None)
}

/// 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).
pub fn internal(e: impl std::fmt::Display) -> McpError {
    McpError::internal_error(e.to_string(), None)
}