Skip to content

Commit 87d380e

Browse files
authored
Merge pull request #113 from codervisor/copilot/implement-spec-196
Embed spec template body in MCP create tool description
2 parents aac60ef + a4ce132 commit 87d380e

File tree

3 files changed

+153
-23
lines changed

3 files changed

+153
-23
lines changed

rust/leanspec-mcp/src/tools.rs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use leanspec_core::{
99
};
1010
use serde_json::{json, Value};
1111
use std::path::{Path, PathBuf};
12+
use std::sync::OnceLock;
1213

1314
/// Get all tool definitions
1415
pub fn get_tool_definitions() -> Vec<ToolDefinition> {
@@ -84,7 +85,7 @@ pub fn get_tool_definitions() -> Vec<ToolDefinition> {
8485
},
8586
"content": {
8687
"type": "string",
87-
"description": "Full markdown content to use instead of template"
88+
"description": create_content_description()
8889
},
8990
"tags": {
9091
"type": "array",
@@ -984,6 +985,87 @@ fn tool_stats(specs_dir: &str) -> Result<String, String> {
984985

985986
// Helper functions
986987

988+
fn create_content_description() -> String {
989+
static DESCRIPTION: OnceLock<String> = OnceLock::new();
990+
991+
DESCRIPTION
992+
.get_or_init(|| {
993+
build_template_body_description().unwrap_or_else(|e| {
994+
eprintln!(
995+
"Warning: failed to load spec template for create tool description: {}",
996+
e
997+
);
998+
CREATE_CONTENT_FALLBACK.to_string()
999+
})
1000+
})
1001+
.clone()
1002+
}
1003+
1004+
fn build_template_body_description() -> Result<String, String> {
1005+
let specs_dir = std::env::var("LEANSPEC_SPECS_DIR").unwrap_or_else(|_| "specs".to_string());
1006+
let project_root = resolve_project_root(&specs_dir)?;
1007+
let config = load_config(&project_root);
1008+
let loader = TemplateLoader::with_config(&project_root, config);
1009+
let template = loader
1010+
.load(None)
1011+
.map_err(|e| format!("Failed to load template: {}", e))?;
1012+
1013+
let template_body = extract_template_body(&template);
1014+
1015+
Ok(format!(
1016+
"{}{}{}",
1017+
CONTENT_DESCRIPTION_PREFIX, template_body, CONTENT_DESCRIPTION_SUFFIX
1018+
))
1019+
}
1020+
1021+
fn extract_template_body(template: &str) -> String {
1022+
let parser = FrontmatterParser::new();
1023+
let body = match parser.parse(template) {
1024+
Ok((_, body)) => body,
1025+
Err(_) => template.to_string(),
1026+
};
1027+
1028+
let mut lines = body.lines().peekable();
1029+
let mut skip_empty = |iter: &mut std::iter::Peekable<std::str::Lines<'_>>| {
1030+
while matches!(iter.peek(), Some(line) if line.trim().is_empty()) {
1031+
iter.next();
1032+
}
1033+
};
1034+
1035+
skip_empty(&mut lines);
1036+
1037+
if matches!(lines.peek(), Some(line) if line.trim_start().starts_with('#')) {
1038+
lines.next();
1039+
skip_empty(&mut lines);
1040+
}
1041+
1042+
if matches!(
1043+
lines.peek(),
1044+
Some(line) if line.trim_start().starts_with("> **Status**")
1045+
) {
1046+
lines.next();
1047+
skip_empty(&mut lines);
1048+
}
1049+
1050+
let mut collected = String::with_capacity(body.len());
1051+
for (idx, line) in lines.enumerate() {
1052+
if idx > 0 {
1053+
collected.push('\n');
1054+
}
1055+
collected.push_str(line);
1056+
}
1057+
1058+
collected.trim().to_string()
1059+
}
1060+
1061+
const CREATE_CONTENT_FALLBACK: &str =
1062+
"Body content only (markdown sections). Frontmatter and title are auto-generated.";
1063+
1064+
const CONTENT_DESCRIPTION_PREFIX: &str = "Body content only (markdown sections). DO NOT include frontmatter or title - these are auto-generated from other parameters (name, title, status, priority, tags).\n\nTEMPLATE STRUCTURE (body sections only):\n\n";
1065+
1066+
const CONTENT_DESCRIPTION_SUFFIX: &str =
1067+
"\n\nKeep specs <2000 tokens optimal, <3500 max. Consider sub-specs (IMPLEMENTATION.md) if >400 lines.";
1068+
9871069
fn get_next_spec_number(specs_dir: &str) -> Result<u32, String> {
9881070
let specs_path = std::path::Path::new(specs_dir);
9891071

rust/leanspec-mcp/tests/tools/create.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
mod helpers;
55

66
use helpers::*;
7-
use leanspec_mcp::tools::call_tool;
7+
use leanspec_mcp::tools::{call_tool, get_tool_definitions};
88
use serde_json::json;
99

1010
#[tokio::test]
@@ -251,3 +251,36 @@ async fn test_create_spec_with_content_override_includes_frontmatter() {
251251
assert!(content.contains("Custom Title"));
252252
assert!(content.contains("Body text."));
253253
}
254+
255+
#[test]
256+
fn test_create_tool_description_includes_template_body() {
257+
let temp = create_empty_project();
258+
set_specs_dir_env(&temp);
259+
260+
let tools = get_tool_definitions();
261+
let create_tool = tools
262+
.iter()
263+
.find(|tool| tool.name == "create")
264+
.expect("create tool definition should exist");
265+
266+
let description = create_tool
267+
.input_schema
268+
.get("properties")
269+
.and_then(|props| props.get("content"))
270+
.and_then(|content| content.get("description"))
271+
.and_then(|desc| desc.as_str())
272+
.expect("content description should be present");
273+
274+
assert!(
275+
description.contains("TEMPLATE STRUCTURE"),
276+
"description should include explanatory heading"
277+
);
278+
assert!(
279+
description.contains("## Overview") && description.contains("## Plan"),
280+
"template body should be embedded in description"
281+
);
282+
assert!(
283+
!description.contains("status: planned"),
284+
"frontmatter should be stripped from template body"
285+
);
286+
}

specs/196-mcp-create-content-field-documentation/README.md

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
---
2-
status: planned
3-
created: 2025-12-21
2+
status: complete
3+
created: '2025-12-21'
44
priority: medium
55
tags:
6-
- mcp
7-
- dx
8-
- ai-agents
9-
- templates
10-
- documentation
11-
created_at: 2025-12-21T14:53:17.954876Z
12-
updated_at: 2025-12-21T14:53:17.954876Z
6+
- mcp
7+
- dx
8+
- ai-agents
9+
- templates
10+
- documentation
11+
created_at: '2025-12-21T14:53:17.954876Z'
12+
updated_at: '2025-12-21T15:24:08.504Z'
13+
transitions:
14+
- status: in-progress
15+
at: '2025-12-21T15:07:43.301Z'
16+
- status: complete
17+
at: '2025-12-21T15:24:08.504Z'
18+
completed_at: '2025-12-21T15:24:08.504Z'
19+
completed: '2025-12-21'
1320
---
1421

1522
# Embed Spec Template in MCP Create Tool Content Field Description
1623

17-
> **Status**: planned · **Priority**: medium · **Created**: {date}
24+
> **Status**: ✅ Complete · **Priority**: Medium · **Created**: 2025-12-21 · **Tags**: mcp, dx, ai-agents, templates, documentation
1825
1926
## Problem & Motivation
2027

@@ -164,16 +171,16 @@ Keep specs <2000 tokens optimal, <3500 max. Consider sub-specs (IMPLEMENTATION.m
164171

165172
## Acceptance Criteria
166173

167-
- [ ] MCP server loads `.lean-spec/templates/spec-template.md` at startup
168-
- [ ] Template loading uses existing `TemplateLoader` infrastructure
169-
- [ ] Frontmatter (YAML block) is stripped from loaded template
170-
- [ ] Title line (`# {name}` or similar) is stripped from loaded template
171-
- [ ] Template body is embedded into `content` field description
172-
- [ ] Description prepends explanatory text about body-only content
173-
- [ ] If template load fails, falls back to minimal static description
174-
- [ ] Tool schema reflects actual template (test by viewing schema)
175-
- [ ] AI agents can see template structure in MCP tool descriptions
176-
- [ ] Changes to template file automatically reflect in tool schema after MCP restart
174+
- [x] MCP server loads `.lean-spec/templates/spec-template.md` at startup
175+
- [x] Template loading uses existing `TemplateLoader` infrastructure
176+
- [x] Frontmatter (YAML block) is stripped from loaded template
177+
- [x] Title line (`# {name}` or similar) is stripped from loaded template
178+
- [x] Template body is embedded into `content` field description
179+
- [x] Description prepends explanatory text about body-only content
180+
- [x] If template load fails, falls back to minimal static description
181+
- [x] Tool schema reflects actual template (test by viewing schema)
182+
- [x] AI agents can see template structure in MCP tool descriptions
183+
- [x] Changes to template file automatically reflect in tool schema after MCP restart
177184

178185
## Out of Scope
179186

@@ -232,6 +239,14 @@ Keep specs <2000 tokens optimal, <3500 max. Consider sub-specs (IMPLEMENTATION.m
232239
- Verify fallback works if template missing
233240
- Confirm template changes reflect after restart
234241

242+
### Implementation Notes (2025-12-21)
243+
- Create tool schema now builds its `content` description by loading the default template via `TemplateLoader`, stripping frontmatter/title/status lines, and caching the processed body with `OnceLock` for reuse.
244+
- Added a graceful fallback description and warning when template loading fails.
245+
- Added a regression test asserting the create tool description includes the template body (without frontmatter) so AI agents see the expected structure.
246+
247+
### Testing
248+
- `RUSTFLAGS="-Awarnings" cargo test -p leanspec-mcp test_create_tool_description_includes_template_body -- --nocapture`
249+
235250
## Open Questions
236251

237252
1. Should we cache the loaded template or reload on every schema request?
@@ -301,4 +316,4 @@ If LeanSpec adds multiple templates (standard, detailed, minimal), we could:
301316
- Show merged/combined structure in description
302317
- Or dynamically show template based on `template` parameter
303318

304-
For now, **YAGNI** - just load the default `spec-template.md`.
319+
For now, **YAGNI** - just load the default `spec-template.md`.

0 commit comments

Comments
 (0)