use crate::config::get_target_path;
use crate::resolver::resolve_skill;
use crate::{InitOptions, LintOptions, OutputFormat, QueryType, StatsOptions};
use rmcp::ErrorData as McpError;
use rmcp::handler::server::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{CallToolResult, Content, ServerCapabilities, ServerInfo};
use rmcp::transport::stdio;
use rmcp::{ServerHandler, ServiceExt, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
type McpResult<T> = core::result::Result<T, McpError>;
fn to_mcp_err(e: crate::SkillcError) -> McpError {
McpError::internal_error(e.to_string(), None)
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct OutlineParams {
pub skill: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub level: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ShowParams {
pub skill: String,
pub section: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_lines: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct OpenParams {
pub skill: String,
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_lines: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct SourcesParams {
pub skill: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub depth: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct SearchParams {
pub skill: String,
pub query: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct StatsParams {
pub skill: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub until: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BuildParams {
pub skill: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct InitParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default)]
pub global: bool,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct LintParams {
pub skill: String,
#[serde(default)]
pub force: bool,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(default)]
pub check_obsolete: bool,
}
#[derive(Clone)]
pub struct SkillcServer {
tool_router: ToolRouter<Self>,
}
impl SkillcServer {
pub fn new() -> Self {
Self {
tool_router: Self::tool_router(),
}
}
}
impl Default for SkillcServer {
fn default() -> Self {
Self::new()
}
}
#[tool_router]
impl SkillcServer {
#[tool(
description = "List all sections in a skill. Returns JSON array of {file, level, heading}. Use 'level' param to filter by max heading level (1-6).",
annotations(read_only_hint = true)
)]
async fn skc_outline(&self, params: Parameters<OutlineParams>) -> McpResult<CallToolResult> {
match crate::outline(¶ms.0.skill, params.0.level, OutputFormat::Json) {
Ok(json) => Ok(CallToolResult::success(vec![Content::text(json)])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
#[tool(
description = "Retrieve markdown section content by heading. Returns raw text. Use 'max_lines' to limit output.",
annotations(read_only_hint = true)
)]
async fn skc_show(&self, params: Parameters<ShowParams>) -> McpResult<CallToolResult> {
match crate::show(
¶ms.0.skill,
¶ms.0.section,
params.0.file.as_deref(),
params.0.max_lines,
OutputFormat::Text,
) {
Ok(content) => Ok(CallToolResult::success(vec![Content::text(content)])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
#[tool(
description = "Retrieve raw file content by path. Returns raw text. Use 'max_lines' to limit output.",
annotations(read_only_hint = true)
)]
async fn skc_open(&self, params: Parameters<OpenParams>) -> McpResult<CallToolResult> {
match crate::open(
¶ms.0.skill,
¶ms.0.path,
params.0.max_lines,
OutputFormat::Text,
) {
Ok(content) => Ok(CallToolResult::success(vec![Content::text(content)])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
#[tool(
description = "List source files as tree structure. Returns JSON array of {path, type, children?}.",
annotations(read_only_hint = true)
)]
async fn skc_sources(&self, params: Parameters<SourcesParams>) -> McpResult<CallToolResult> {
match crate::sources(
¶ms.0.skill,
params.0.depth,
params.0.dir.as_deref(),
params.0.limit.unwrap_or(100),
params.0.pattern.as_deref(),
OutputFormat::Json,
) {
Ok(json) => Ok(CallToolResult::success(vec![Content::text(json)])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
#[tool(
description = "Full-text search in skill content. Returns JSON array of {file, line, content, score}.",
annotations(read_only_hint = true)
)]
async fn skc_search(&self, params: Parameters<SearchParams>) -> McpResult<CallToolResult> {
match crate::search(
¶ms.0.skill,
¶ms.0.query,
params.0.limit.unwrap_or(10),
OutputFormat::Json,
) {
Ok(json) => Ok(CallToolResult::success(vec![Content::text(json)])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
#[tool(
description = "Usage analytics for a skill. Returns JSON with access counts, popular sections, etc. Use group_by: summary, files, sections, commands, projects, errors, or search.",
annotations(read_only_hint = true)
)]
async fn skc_stats(&self, params: Parameters<StatsParams>) -> McpResult<CallToolResult> {
let query_type = match params.0.group_by.as_deref() {
Some("files") => QueryType::Files,
Some("sections") => QueryType::Sections,
Some("commands") => QueryType::Commands,
Some("projects") => QueryType::Projects,
Some("errors") => QueryType::Errors,
Some("search") => QueryType::Search,
_ => QueryType::Summary, };
match crate::stats(
¶ms.0.skill,
StatsOptions {
query: query_type,
format: OutputFormat::Json,
since: params.0.since.clone(),
until: params.0.until.clone(),
projects: params.0.project.clone().unwrap_or_default(),
},
) {
Ok(json) => Ok(CallToolResult::success(vec![Content::text(json)])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
#[tool(
description = "Compile skill to target platform (claude, cursor). Returns {success, output_path}."
)]
async fn skc_build(&self, params: Parameters<BuildParams>) -> McpResult<CallToolResult> {
let resolved = resolve_skill(¶ms.0.skill).map_err(to_mcp_err)?;
let source = resolved.source_dir;
let target = params.0.target.as_deref().unwrap_or("claude");
let skill_name = source
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(¶ms.0.skill);
let runtime = get_target_path(target)
.map_err(to_mcp_err)?
.join(skill_name);
match crate::compile(&source, &runtime) {
Ok(()) => {
let result = serde_json::json!({
"success": true,
"output_path": runtime.to_string_lossy()
});
Ok(CallToolResult::success(vec![Content::text(
result.to_string(),
)]))
}
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
#[tool(
description = "Initialize skillc project or create a new skill. Without name: creates .skillc/ structure. With name: creates skill template. With name+global: creates in global store."
)]
async fn skc_init(&self, params: Parameters<InitParams>) -> McpResult<CallToolResult> {
let options = InitOptions {
name: params.0.name.clone(),
global: params.0.global,
};
match crate::init(options) {
Ok(message) => {
let result = serde_json::json!({
"success": true,
"message": message
});
Ok(CallToolResult::success(vec![Content::text(
result.to_string(),
)]))
}
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
#[tool(
description = "List all skillc-managed skills from source stores. Returns JSON with skills array containing name, scope, status, and paths.",
annotations(read_only_hint = true)
)]
async fn skc_list(&self, params: Parameters<ListParams>) -> McpResult<CallToolResult> {
use crate::list::{ListOptions, SkillScope, SkillStatus};
let scope = params.0.scope.as_deref().and_then(|s| match s {
"project" => Some(SkillScope::Project),
"global" => Some(SkillScope::Global),
_ => None,
});
let status = params.0.status.as_deref().and_then(|s| match s {
"normal" => Some(SkillStatus::Normal),
"not-built" => Some(SkillStatus::NotBuilt),
"obsolete" => Some(SkillStatus::Obsolete),
_ => None,
});
let options = ListOptions {
scope,
status,
limit: params.0.limit,
pattern: params.0.pattern.clone(),
check_obsolete: params.0.check_obsolete,
};
match crate::list::list(&options) {
Ok(result) => {
let json = serde_json::to_string(&result)
.unwrap_or_else(|e| format!(r#"{{"error": "serialization failed: {}"}}"#, e));
Ok(CallToolResult::success(vec![Content::text(json)]))
}
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
#[tool(
description = "Validate skill authoring quality. Returns diagnostics with rule IDs, severities, and messages.",
annotations(read_only_hint = true)
)]
async fn skc_lint(&self, params: Parameters<LintParams>) -> McpResult<CallToolResult> {
let resolved = resolve_skill(¶ms.0.skill).map_err(to_mcp_err)?;
let skill_path = resolved.source_dir;
let options = LintOptions {
force: params.0.force,
};
match crate::lint(&skill_path, options) {
Ok(result) => {
let json = serde_json::json!({
"skill": result.skill,
"path": result.path.to_string_lossy(),
"error_count": result.error_count,
"warning_count": result.warning_count,
"diagnostics": result.diagnostics
});
Ok(CallToolResult::success(vec![Content::text(
json.to_string(),
)]))
}
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"error: {}",
e
))])),
}
}
}
#[tool_handler]
impl ServerHandler for SkillcServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some(
"skillc - A development kit for Agent Skills. Use MCP tools to access skill content.".into(),
),
capabilities: ServerCapabilities::builder().enable_tools().build(),
..Default::default()
}
}
}
pub async fn run_server() -> crate::error::Result<()> {
let server = SkillcServer::new();
let service = server
.serve(stdio())
.await
.map_err(|e| crate::error::SkillcError::Internal(format!("MCP server error: {}", e)))?;
service
.waiting()
.await
.map_err(|e| crate::error::SkillcError::Internal(format!("MCP server error: {}", e)))?;
Ok(())
}