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
//! A complete, runnable MCP server over stdio.
//!
//! `cargo run --example server` starts a real MCP server speaking the protocol
//! on stdin/stdout; point any MCP client at it (or drive it by hand). It proves
//! the end-to-end path a consumer takes:
//!
//! 1. Define skills (here over a `Server` state that carries a greeting).
//! 2. Adapt them to routes with [`route_skill`] and add them to an rmcp
//!    [`ToolRouter`].
//! 3. Implement rmcp's [`ServerHandler`] — `#[tool_handler(router = ...)]`
//!    delegates `tools/list` and `tools/call` to the router.
//! 4. Serve over a transport ([`stdio`]).
//!
//! Everything rmcp-side is reached through this crate's `rmcp` re-export, so the
//! only direct dependency a consumer needs for this file is
//! `mcp-skill-framework` (plus `serde` for their argument structs, and `rmcp`
//! with the `transport-io` feature for the stdio transport).

use std::sync::Arc;

use mcp_skill_framework::prelude::*;
use mcp_skill_framework::rmcp::{
    handler::server::router::tool::ToolRouter,
    model::{Implementation, ProtocolVersion, ServerCapabilities, ServerInfo},
    tool_handler,
    transport::stdio,
    ServerHandler, ServiceExt,
};
use serde::Deserialize;
use serde_json::json;

/// The server: shared state plus the tool router. It is both the `S` our skills
/// are generic over (each call gets `&Server`) and the rmcp [`ServerHandler`].
#[derive(Clone)]
struct Server {
    tool_router: ToolRouter<Server>,
    greeting: String,
}

// ---- a skill that reads server state ---------------------------------------

#[derive(Deserialize, schemars::JsonSchema)]
struct GreetArgs {
    /// Who to greet.
    name: String,
}

struct Greet;
impl Skill<Server> for Greet {
    fn name(&self) -> &'static str {
        "greet"
    }
    fn description(&self) -> &'static str {
        "Greet someone using the server's configured greeting."
    }
    fn schema(&self) -> Arc<JsonObject> {
        schema_for::<GreetArgs>()
    }
    fn call<'a>(
        &self,
        ctx: SkillCtx<'a, Server>,
    ) -> BoxFuture<'a, Result<CallToolResult, McpError>> {
        Box::pin(async move {
            let (server, a) = ctx.parse::<GreetArgs>()?;
            Ok(text_result(format!("{}, {}!", server.greeting, a.name)))
        })
    }
}

// ---- a pure-compute skill with validation ----------------------------------

#[derive(Deserialize, schemars::JsonSchema)]
struct RoundArgs {
    /// Value to round.
    value: f64,
    /// Rounding mode.
    #[serde(default)]
    mode: Option<String>,
}

struct Round;
impl Skill<Server> for Round {
    fn name(&self) -> &'static str {
        "round"
    }
    fn description(&self) -> &'static str {
        "Round a number `nearest` (default), `up`, or `down`."
    }
    fn schema(&self) -> Arc<JsonObject> {
        schema_for::<RoundArgs>()
    }
    fn validation_rules(&self) -> &'static [Rule] {
        &[Rule::OneOf {
            field: "mode",
            values: &["nearest", "up", "down"],
        }]
    }
    fn call<'a>(
        &self,
        ctx: SkillCtx<'a, Server>,
    ) -> BoxFuture<'a, Result<CallToolResult, McpError>> {
        Box::pin(async move {
            let (_server, a) = ctx.parse::<RoundArgs>()?;
            let out = match a.mode.as_deref().unwrap_or("nearest") {
                "up" => a.value.ceil(),
                "down" => a.value.floor(),
                _ => a.value.round(),
            };
            Ok(text_result(json!({ "result": out }).to_string()))
        })
    }
}

fn skills() -> Vec<Box<dyn Skill<Server>>> {
    vec![Box::new(Greet), Box::new(Round)]
}

// ---- the rmcp ServerHandler: delegate tool listing/dispatch to the router --

#[tool_handler(router = self.tool_router)]
impl ServerHandler for Server {
    fn get_info(&self) -> ServerInfo {
        let mut implementation = Implementation::from_build_env();
        implementation.name = "mcp-skill-framework-example".to_string();
        implementation.version = env!("CARGO_PKG_VERSION").to_string();
        ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
            .with_server_info(implementation)
            .with_protocol_version(ProtocolVersion::V_2024_11_05)
            .with_instructions(
                "Example MCP server built with mcp-skill-framework. Tools: greet, round.",
            )
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1-2. Build the router from our skills.
    let mut router = ToolRouter::new();
    for route in skills().into_iter().map(route_skill) {
        router.add_route(route);
    }

    // 3. Compose the server (state + router).
    let server = Server {
        tool_router: router,
        greeting: "Hello".to_string(),
    };
    eprintln!(
        "mcp-skill-framework example: serving {} tools over stdio…",
        server.tool_router.list_all().len()
    );

    // 4. Serve the MCP protocol on stdin/stdout until the client disconnects.
    let service = server.serve(stdio()).await?;
    service.waiting().await?;
    Ok(())
}