use rmcp::handler::server::wrapper::{Json, Parameters};
use rmcp::{schemars, tool, tool_router};
use serde::{Deserialize, Serialize};
use crate::config;
use crate::git;
use crate::mcp::query;
use crate::mcp::query::specs::DependencyGraph;
use crate::mcp::server::GitPawMcpServer;
use crate::skills::{self, GateCommands, Source};
use crate::specs::{self, SpecBackendKind};
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct GetSpecParams {
pub id: String,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct GetTasksParams {
pub spec: String,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct GetTaskParams {
pub spec: String,
pub id: String,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct GetSkillParams {
pub name: String,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct SpecsResponse {
pub specs: Vec<query::specs::SpecInfo>,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct SpecResponse {
pub spec: Option<query::specs::SpecDetail>,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct TasksResponse {
pub tasks: Vec<query::specs::TaskInfo>,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct TaskResponse {
pub task: Option<query::specs::TaskInfo>,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct SkillView {
pub name: String,
pub content: String,
pub source: String,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct SkillResponse {
pub skill: Option<SkillView>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[tool_router(router = project_router, vis = "pub(crate)")]
impl GitPawMcpServer {
#[tool(
description = "List discovered specs across OpenSpec, Markdown, and Spec Kit backends. \
Each carries id, backend, title, status, and path. Empty when none exist."
)]
pub(crate) fn get_specs(&self) -> Json<SpecsResponse> {
Json(SpecsResponse {
specs: query::specs::list_specs(&self.ctx),
})
}
#[tool(
description = "Return the discovered artifacts (proposal/design/tasks/specs for OpenSpec; \
spec/plan/tasks/checklists for Spec Kit; body for Markdown) of a named spec \
with their content, or { \"spec\": null } when not found."
)]
pub(crate) fn get_spec(&self, Parameters(p): Parameters<GetSpecParams>) -> Json<SpecResponse> {
Json(SpecResponse {
spec: query::specs::get_spec(&self.ctx, &p.id),
})
}
#[tool(
description = "List the tasks for a named spec: id, phase, parallel marker, description, \
and completion state. Empty when the spec has no tasks or is not found."
)]
pub(crate) fn get_tasks(
&self,
Parameters(p): Parameters<GetTasksParams>,
) -> Json<TasksResponse> {
Json(TasksResponse {
tasks: query::specs::get_tasks(&self.ctx, &p.spec),
})
}
#[tool(
description = "Return a single task by id within a spec, or { \"task\": null } when the \
spec or task id is not found."
)]
pub(crate) fn get_task(&self, Parameters(p): Parameters<GetTaskParams>) -> Json<TaskResponse> {
let task = query::specs::get_tasks(&self.ctx, &p.spec)
.into_iter()
.find(|t| t.id == p.id);
Json(TaskResponse { task })
}
#[tool(
description = "Return the spec dependency graph derived from [[other-spec]] cross-references \
in proposals, as { nodes, edges }."
)]
pub(crate) fn get_dependency_graph(&self) -> Json<DependencyGraph> {
Json(query::specs::dependency_graph(&self.ctx))
}
#[tool(
description = "Return the rendered content of a named agent skill (post {{...}} \
substitution) plus its source (standard | user_override | embedded). \
Read-only — no disk write. Unknown skills return { \"skill\": null } with a \
message, not a transport error."
)]
pub(crate) fn get_skill(
&self,
Parameters(p): Parameters<GetSkillParams>,
) -> Json<SkillResponse> {
let root = &self.ctx.root;
match skills::resolve(&p.name) {
Ok(template) => {
let cfg = config::load_config(root, None).unwrap_or_default();
let project = git::project_name(root);
let branch = git::current_branch(root).unwrap_or_else(|_| "main".to_string());
let broker_url = self
.ctx
.broker_url
.clone()
.unwrap_or_else(|| "http://127.0.0.1:9119".to_string());
let backends = match specs::resolved_spec_type(&cfg, root).as_deref() {
Some("speckit") => vec![SpecBackendKind::SpecKit],
Some("markdown") => vec![SpecBackendKind::Markdown],
Some("openspec") => vec![SpecBackendKind::OpenSpec],
_ => Vec::new(),
};
let gates = GateCommands {
test_command: None,
lint_command: None,
build_command: None,
doc_build_command: None,
spec_validate_command: None,
fmt_check_command: None,
security_audit_command: None,
doc_tool_command: None,
};
let content =
skills::render(&template, &branch, &broker_url, &project, &gates, &backends);
let source = match template.source {
Source::Embedded => "embedded",
Source::AgentsStandard => "standard",
Source::User => "user_override",
};
Json(SkillResponse {
skill: Some(SkillView {
name: template.name,
content,
source: source.to_string(),
}),
message: None,
})
}
Err(skills::SkillError::UnknownSkill { name }) => Json(SkillResponse {
skill: None,
message: Some(format!("unknown skill: {name}")),
}),
Err(e) => Json(SkillResponse {
skill: None,
message: Some(e.to_string()),
}),
}
}
}