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
//! Human-readable rendering of skills and families — "the description" layer.
//!
//! [`render_skill`] turns one [`Skill`] into a plain-text block: its
//! description, use cases, worked examples, validation rules, and pretty-
//! printed argument schema. [`render_family`] does the same for a
//! [`FamilyMeta`]. These power an on-demand introspection tool (commonly named
//! `describe_skill` / `describe_family`) so a model can look up one tool's
//! exact shape after the initial `tools/list` — handy when a host truncates
//! the catalog, or when the model wants to double-check arguments before a
//! call.
//!
//! Both renderers accept an optional `gating` line so the host can splice in
//! application-specific state ("`[filesystem].enabled = true`") without this
//! crate needing to know anything about configuration.

use std::fmt::Write;

use crate::capability::SkillCapability;
use crate::family::FamilyMeta;
use crate::skill::Skill;
use crate::validation::rules_to_json;

/// Render one skill as a plain-text description block.
///
/// `family` is the family name this tool belongs to, if known. `gating` is an
/// optional application-specific line (e.g. config state) spliced in near the
/// top. Pass `None` for either to omit it.
pub fn render_skill<S: 'static>(
    skill: &dyn Skill<S>,
    family: Option<&str>,
    gating: Option<&str>,
) -> String {
    let mut out = String::new();
    let _ = writeln!(out, "Tool: {}", skill.name());
    if let Some(f) = family {
        let _ = writeln!(out, "Family: {f}");
    }
    if let Some(g) = gating {
        let _ = writeln!(out, "{g}");
    }
    let _ = writeln!(out);
    let _ = writeln!(out, "Description:");
    let _ = writeln!(out, "  {}", skill.description());

    let use_cases = skill.use_cases();
    if !use_cases.is_empty() {
        let _ = writeln!(out);
        let _ = writeln!(out, "Use cases:");
        for uc in use_cases {
            let _ = writeln!(out, "  - {uc}");
        }
    }

    let examples = skill.examples();
    if !examples.is_empty() {
        let _ = writeln!(out);
        let _ = writeln!(out, "Examples:");
        for (i, ex) in examples.iter().enumerate() {
            let _ = writeln!(out, "  {}. {}", i + 1, ex.title);
            let _ = writeln!(out, "     args: {}", ex.args);
            if let Some(note) = ex.note {
                let _ = writeln!(out, "     note: {note}");
            }
        }
    }

    let rules = skill.validation_rules();
    if !rules.is_empty() {
        let _ = writeln!(out);
        let _ = writeln!(out, "Validation rules:");
        let rules_json = rules_to_json(rules);
        let rules_pretty = serde_json::to_string_pretty(&rules_json)
            .unwrap_or_else(|_| "<could not serialize rules>".into());
        let _ = writeln!(out, "{rules_pretty}");
    }

    let schema_json = serde_json::to_value(skill.schema().as_ref())
        .ok()
        .and_then(|v| serde_json::to_string_pretty(&v).ok())
        .unwrap_or_else(|| "<could not serialize schema>".into());
    let _ = writeln!(out);
    let _ = writeln!(out, "Argument schema (JSON):");
    let _ = writeln!(out, "{schema_json}");

    out
}

/// Render one family as a plain-text description block: its summary,
/// capability state, the tools it contributes, and a worked flow if it has
/// one. `gating` is an optional application-specific line spliced in after
/// the capability line.
pub fn render_family(fam: &dyn FamilyMeta, gating: Option<&str>) -> String {
    let mut out = String::new();
    let _ = writeln!(out, "Family: {}", fam.family());
    let _ = writeln!(out, "Description:");
    let _ = writeln!(out, "  {}", fam.description());

    let cap_line = match fam.check_capability() {
        SkillCapability::Ready => "Capability: Ready".to_string(),
        SkillCapability::Unavailable { reason, hint } => {
            let mut s = format!("Capability: Unavailable — {reason}");
            if let Some(h) = hint {
                s.push_str(&format!("\n  Hint: {h}"));
            }
            s
        }
    };
    let _ = writeln!(out, "{cap_line}");

    if let Some(g) = gating {
        let _ = writeln!(out, "{g}");
    }

    let tools = fam.tools();
    let _ = writeln!(out);
    let _ = writeln!(out, "Tools ({}):", tools.len());
    for t in &tools {
        let _ = writeln!(out, "  - {t}");
    }

    if let Some(flow) = fam.example_flow() {
        let _ = writeln!(out);
        let _ = writeln!(out, "Example flow:");
        for line in flow.lines() {
            let _ = writeln!(out, "  {line}");
        }
    }

    out
}