Skip to content

Commit 5ecade8

Browse files
Merge pull request #17 from YuliiaKovalova/feature/v1.1.0-profiles-and-validate
feat: v1.1.0 — template_validate tool, tool profiles, improved descriptions
2 parents c7e4425 + ee2008d commit 5ecade8

25 files changed

Lines changed: 865 additions & 24 deletions

AGENTS.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# AGENTS.md — AI Agent Instructions for dotnet-template-mcp
2+
3+
## Project Overview
4+
5+
MCP server wrapping the .NET Template Engine for AI-driven template discovery, inspection, and instantiation. Ships as a dotnet tool (`DotnetTemplateMCP`) on NuGet.
6+
7+
## Build & Test
8+
9+
```bash
10+
dotnet build
11+
dotnet test
12+
```
13+
14+
- Targets **net10.0** (see `global.json` for SDK version).
15+
- Uses **Central Package Management** — all versions in `Directory.Packages.props`.
16+
- CI runs on **ubuntu-latest** and **windows-latest** (see `.github/workflows/ci.yml`).
17+
- When the MCP server is running locally (e.g., as a tool provider), `dotnet build` may fail with a file lock on `bin/Debug/net10.0/Microsoft.TemplateEngine.MCP.exe`. Use `-o <tempdir>` to build to an alternate output path.
18+
19+
## Architecture
20+
21+
### Tool Registration Pattern
22+
23+
Each MCP tool is a `static async` method in a sealed class under `src/Microsoft.TemplateEngine.MCP/Tools/`:
24+
25+
```csharp
26+
[McpServerToolType]
27+
internal sealed class MyTool
28+
{
29+
[McpServerTool(Name = "tool_name")]
30+
[Description("Description shown to AI agents — lead with the pain point, not the feature name.")]
31+
public static async Task<string> MyMethodAsync(
32+
TemplateEngineService engineService, // DI-injected service
33+
McpFeatureFlags featureFlags, // DI-injected feature flags
34+
[Description("...")] string param1, // User-facing params with [Description]
35+
CancellationToken cancellationToken = default)
36+
{
37+
// 1. Telemetry
38+
using var activity = McpTelemetry.StartToolActivity("tool_name");
39+
var sw = Stopwatch.StartNew();
40+
try
41+
{
42+
// 2. Profile check (for non-lite tools)
43+
if (!featureFlags.IsToolEnabled("tool_name"))
44+
{
45+
return ToolProfileResponse.DisabledMessage("tool_name", "Hint for the user.");
46+
}
47+
48+
// 3. Tool logic
49+
// ...
50+
}
51+
finally
52+
{
53+
McpTelemetry.RecordDuration("tool_name", sw.Elapsed.TotalMilliseconds);
54+
}
55+
}
56+
}
57+
```
58+
59+
### Key Rules
60+
61+
1. **DI parameters come first**`TemplateEngineService`, `McpFeatureFlags`, `McpServer` (if elicitation is needed) are injected by the MCP framework. User-facing parameters follow, each with a `[Description]` attribute.
62+
63+
2. **When you add or change a DI parameter on a tool method, you MUST update all test call sites.** Tests in `test/Microsoft.TemplateEngine.MCP.Tests/` call tool methods directly (not through DI), so they must pass all parameters explicitly. Example: `new McpFeatureFlags()` for the default (Full profile).
64+
65+
3. **Tool profiles** — Tools are either "lite" (5 core tools always available) or "full" (all tools). Non-lite tools must include a `featureFlags.IsToolEnabled()` check at the start. The lite tools are: `template_from_intent`, `template_instantiate`, `template_inspect`, `template_search`, `template_dry_run`.
66+
67+
4. **Telemetry** — Every tool must call `McpTelemetry.StartToolActivity()` and `McpTelemetry.RecordDuration()`. Use `McpTelemetry.RecordError()` for failures.
68+
69+
5. **Return format** — Tools return JSON strings via `JsonSerializer.Serialize(new { ... }, new JsonSerializerOptions { WriteIndented = true })`. Errors use `{ error, hint }` shape.
70+
71+
6. **File header** — Every `.cs` file starts with:
72+
```csharp
73+
// Licensed to the .NET Foundation under one or more agreements.
74+
// The .NET Foundation licenses this file to you under the MIT license.
75+
```
76+
77+
## PR Workflow
78+
79+
1. **Always create PRs on a feature branch**, not directly to `main`.
80+
2. **After pushing, monitor the CI run** — check GitHub Actions status. If build or tests fail, fix and push again before considering the PR ready.
81+
3. **Version bumps** require updating three files: `Microsoft.TemplateEngine.MCP.csproj` (`<Version>`), `server.json` (both `version` fields), and `README.md` (install commands).
82+
83+
## Testing
84+
85+
- Unit tests use **xUnit** + **FakeItEasy** for mocking.
86+
- `TemplateEngineService` is mocked via `A.Fake<TemplateEngineService>()` in unit tests.
87+
- Integration tests (in `IntegrationTests.cs`, `EndToEndTests.cs`) use a real template engine instance.
88+
- Test naming: `MethodName_Scenario_ExpectedBehavior`.
89+
90+
## Key Files
91+
92+
| File | Purpose |
93+
|------|---------|
94+
| `src/.../McpFeatureFlags.cs` | Environment-based feature flags (transport, profiles, elicitation) |
95+
| `src/.../Tools/ToolProfileResponse.cs` | Consistent "tool disabled" JSON responses |
96+
| `src/.../Host/TemplateEngineService.cs` | Core service wrapping the template engine |
97+
| `src/.../Telemetry/McpTelemetry.cs` | ActivitySource + Meter for observability |
98+
| `server.json` | MCP Registry manifest |
99+
| `.github/copilot-instructions.md` | Instructions for AI agents *using* this tool (not developing it) |

README.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Microsoft.TemplateEngine.MCP
22

3+
<!-- mcp-name: io.github.YuliiaKovalova/dotnet-template-mcp -->
4+
35
An MCP server that lets AI agents work with `dotnet new` templates — search, inspect, preview, and create projects through natural conversation instead of memorizing CLI flags.
46

57
Instead of this:
@@ -27,6 +29,8 @@ Your AI agent just says: *"I need a web API with authentication and controllers"
2729
| `template_create_from_existing` | Analyze a .csproj → generate a reusable template matching repo conventions |
2830
| `template_compose` | Execute a sequence of templates (project + items) in one workflow |
2931
| `template_suggest_parameters` | Suggest parameter values with rationale based on cross-parameter relationships |
32+
| `template_validate` | Validate a local template directory for authoring issues before publishing |
33+
| `solution_analyze` | Analyze a solution/workspace — project structure, frameworks, CPM status |
3034

3135
📖 [Full tool reference →](docs/tool-reference.md)
3236

@@ -35,13 +39,13 @@ Your AI agent just says: *"I need a web API with authentication and controllers"
3539
### Zero-install with `dnx` (.NET 10+)
3640

3741
```bash
38-
dnx -y DotnetTemplateMCP --version 1.0.0
42+
dnx -y DotnetTemplateMCP --version 1.1.0
3943
```
4044

4145
### Global tool
4246

4347
```bash
44-
dotnet tool install --global DotnetTemplateMCP --version 1.0.0
48+
dotnet tool install --global DotnetTemplateMCP --version 1.1.0
4549
```
4650

4751
### VS Code / GitHub Copilot
@@ -54,7 +58,7 @@ Add to `mcp.json`:
5458
"dotnet-templates": {
5559
"type": "stdio",
5660
"command": "dnx",
57-
"args": ["-y", "DotnetTemplateMCP", "--version", "1.0.0"]
61+
"args": ["-y", "DotnetTemplateMCP", "--version", "1.1.0"]
5862
}
5963
}
6064
}
@@ -163,6 +167,32 @@ Chain multiple templates in one call with `template_compose`:
163167

164168
📖 [Architecture & smart behaviors →](docs/architecture.md)
165169

170+
### Tool Profiles (Lite vs Full)
171+
172+
By default, all 13 tools are available. If your agent works better with fewer tools, set the `MCP_TEMPLATE_TOOL_PROFILE` environment variable:
173+
174+
| Profile | Tools | When to use |
175+
|---------|-------|-------------|
176+
| `full` (default) | All 13 tools | Full control — advanced workflows, composition, custom templates |
177+
| `lite` | 5 core tools | Simpler agents that just need to find and create projects |
178+
179+
**Lite profile tools:** `template_from_intent`, `template_instantiate`, `template_inspect`, `template_search`, `template_dry_run`
180+
181+
```json
182+
{
183+
"servers": {
184+
"dotnet-template-mcp": {
185+
"command": "dotnet-template-mcp",
186+
"env": {
187+
"MCP_TEMPLATE_TOOL_PROFILE": "lite"
188+
}
189+
}
190+
}
191+
}
192+
```
193+
194+
Non-lite tools will return a helpful message explaining they're disabled and how to enable them.
195+
166196
## Documentation
167197

168198
| Doc | What's in it |

docs/tool-reference.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,27 @@ Example:
168168

169169
---
170170

171+
## `template_validate`
172+
173+
Validate a local template directory for authoring issues before publishing. Checks schema compliance, parameter definitions, constraints, post-actions, and common mistakes.
174+
175+
| Parameter | Type | Required | Description |
176+
|-----------|------|----------|-------------|
177+
| `path` | string | Yes | Path to the template directory (containing `.template.config/template.json`), or direct path to `template.json` |
178+
179+
**Validation checks:**
180+
- Required fields (`identity`, `name`, `shortName`)
181+
- Identity format and namespace conventions
182+
- Short name conflicts with dotnet CLI commands
183+
- Parameter issues: missing datatypes, empty choices, invalid defaults, prefix collisions
184+
- Computed/generated symbol completeness
185+
- Post-action and constraint configuration
186+
- Tag recommendations (language, type)
187+
188+
Returns: `{ valid, errors, warnings, suggestions }`
189+
190+
---
191+
171192
## `template_suggest_parameters`
172193

173194
Given a template and partial parameter values, suggest reasonable defaults with rationale. Example: `EnableAot=true` → suggests `Framework=net9.0` with explanation.

server.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3+
"name": "io.github.YuliiaKovalova/dotnet-template-mcp",
4+
"description": "Search, inspect, preview, and create .NET projects from dotnet new templates via AI agents.",
5+
"version": "1.1.0",
6+
"repository": {
7+
"url": "https://github.com/YuliiaKovalova/dotnet-template-mcp",
8+
"source": "github",
9+
"id": "1159945491"
10+
},
11+
"packages": [
12+
{
13+
"registryType": "nuget",
14+
"identifier": "DotnetTemplateMCP",
15+
"version": "1.1.0",
16+
"runtimeHint": "dnx",
17+
"runtimeArguments": [
18+
{
19+
"type": "positional",
20+
"description": "Auto-confirm package installation",
21+
"isRequired": true,
22+
"format": "string",
23+
"value": "-y"
24+
}
25+
],
26+
"transport": {
27+
"type": "stdio"
28+
}
29+
}
30+
]
31+
}

src/Microsoft.TemplateEngine.MCP/McpFeatureFlags.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ internal sealed class McpFeatureFlags
3333
/// </summary>
3434
public const string ElicitationEnvVar = "MCP_TEMPLATE_ELICITATION";
3535

36+
/// <summary>
37+
/// Environment variable to select the tool profile.
38+
/// Values: "full" (default, all 13 tools), "lite" (5 core tools only).
39+
/// Lite mode reduces tool count to minimize agent confusion and context overhead.
40+
/// </summary>
41+
public const string ToolProfileEnvVar = "MCP_TEMPLATE_TOOL_PROFILE";
42+
3643
/// <summary>
3744
/// Whether intent resolution tools (template_from_intent, create_from_description) are enabled.
3845
/// </summary>
@@ -53,6 +60,16 @@ internal sealed class McpFeatureFlags
5360
/// </summary>
5461
public bool ElicitationEnabled { get; init; } = true;
5562

63+
/// <summary>
64+
/// The active tool profile. Controls which tools are exposed to the MCP client.
65+
/// </summary>
66+
public ToolProfile Profile { get; init; } = ToolProfile.Full;
67+
68+
/// <summary>
69+
/// Returns true if the given tool is enabled in the current profile.
70+
/// </summary>
71+
public bool IsToolEnabled(string toolName) => Profile == ToolProfile.Full || IsLiteProfileTool(toolName);
72+
5673
/// <summary>
5774
/// Load feature flags from environment variables and command-line arguments.
5875
/// </summary>
@@ -64,6 +81,7 @@ public static McpFeatureFlags FromEnvironment(string[] args)
6481
Transport = GetTransportMode(args),
6582
HttpUrl = Environment.GetEnvironmentVariable(HttpUrlEnvVar) ?? "http://localhost:5005",
6683
ElicitationEnabled = IsEnabled(ElicitationEnvVar, defaultValue: true),
84+
Profile = GetToolProfile(),
6785
};
6886
}
6987

@@ -75,6 +93,28 @@ public static McpFeatureFlags FromEnvironment()
7593
return FromEnvironment([]);
7694
}
7795

96+
/// <summary>
97+
/// Lite profile tools: the 5 most essential tools for typical AI agent workflows.
98+
/// </summary>
99+
private static bool IsLiteProfileTool(string toolName)
100+
=> toolName is "template_from_intent"
101+
or "template_instantiate"
102+
or "template_inspect"
103+
or "template_search"
104+
or "template_dry_run";
105+
106+
private static ToolProfile GetToolProfile()
107+
{
108+
var value = Environment.GetEnvironmentVariable(ToolProfileEnvVar);
109+
if (!string.IsNullOrEmpty(value) &&
110+
value.Equals("lite", StringComparison.OrdinalIgnoreCase))
111+
{
112+
return ToolProfile.Lite;
113+
}
114+
115+
return ToolProfile.Full;
116+
}
117+
78118
private static TransportMode GetTransportMode(string[] args)
79119
{
80120
// Check command-line: --transport http
@@ -125,3 +165,15 @@ internal enum TransportMode
125165
/// <summary>HTTP transport with streamable HTTP support (for remote, cloud, and multi-tenant deployment).</summary>
126166
Http,
127167
}
168+
169+
/// <summary>
170+
/// Tool profile modes controlling which tools are exposed to the MCP client.
171+
/// </summary>
172+
internal enum ToolProfile
173+
{
174+
/// <summary>All 13 tools exposed (default).</summary>
175+
Full,
176+
177+
/// <summary>5 core tools only: template_from_intent, template_instantiate, template_inspect, template_search, template_dry_run.</summary>
178+
Lite,
179+
}

src/Microsoft.TemplateEngine.MCP/Microsoft.TemplateEngine.MCP.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<PackageId>DotnetTemplateMCP</PackageId>
1111
<ToolCommandName>template-engine-mcp</ToolCommandName>
1212
<IsPackable>true</IsPackable>
13-
<Version>1.0.0</Version>
13+
<Version>1.1.0</Version>
1414
<Authors>YuliiaKovalova</Authors>
1515
<PackageProjectUrl>https://github.com/YuliiaKovalova/dotnet-template-mcp</PackageProjectUrl>
1616
<RepositoryUrl>https://github.com/YuliiaKovalova/dotnet-template-mcp</RepositoryUrl>

src/Microsoft.TemplateEngine.MCP/Tools/CreateFromExistingTool.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ internal sealed class CreateFromExistingTool
2020
"and content items. Solves the problem of 'dotnet new' creating generic projects that don't match repo conventions.")]
2121
public static async Task<string> CreateFromExistingAsync(
2222
TemplateEngineService engineService,
23+
McpFeatureFlags featureFlags,
2324
[Description("Full path to the .csproj file to analyze and use as a template source")] string projectPath,
2425
[Description("Human-readable name for the generated template (e.g., 'Repo Unit Test Project')")] string templateName,
2526
[Description("Short name for the template (e.g., 'repo-unittest'). Used with 'dotnet new <shortname>'.")] string? shortName = null,
@@ -28,9 +29,15 @@ public static async Task<string> CreateFromExistingAsync(
2829
CancellationToken cancellationToken = default)
2930
{
3031
using var activity = McpTelemetry.StartToolActivity("template_create_from_existing");
32+
3133
var sw = Stopwatch.StartNew();
3234
try
3335
{
36+
if (!featureFlags.IsToolEnabled("template_create_from_existing"))
37+
{
38+
return ToolProfileResponse.DisabledMessage("template_create_from_existing", "Set MCP_TEMPLATE_TOOL_PROFILE=full to generate templates from existing projects.");
39+
}
40+
3441
// 1. Analyze the project
3542
ProjectAnalysis analysis;
3643
try

src/Microsoft.TemplateEngine.MCP/Tools/SolutionAnalyzeTool.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@ internal sealed class SolutionAnalyzeTool
1515
[McpServerTool(Name = "solution_analyze")]
1616
[Description("Analyze a solution or workspace directory. Returns project structure, target frameworks, CPM status, and NuGet configuration — essential context for template creation decisions.")]
1717
public static async Task<string> AnalyzeSolutionAsync(
18+
McpFeatureFlags featureFlags,
1819
[Description("Path to a .sln/.slnx file or a directory to scan. Defaults to current directory.")] string? path = null,
1920
CancellationToken cancellationToken = default)
2021
{
2122
using var activity = McpTelemetry.StartToolActivity("solution_analyze");
2223
var sw = Stopwatch.StartNew();
2324
try
2425
{
26+
if (!featureFlags.IsToolEnabled("solution_analyze"))
27+
{
28+
return ToolProfileResponse.DisabledMessage("solution_analyze", "Set MCP_TEMPLATE_TOOL_PROFILE=full to analyze solution structure.");
29+
}
30+
2531
string resolvedPath = path ?? Environment.CurrentDirectory;
2632

2733
// Find .sln file

src/Microsoft.TemplateEngine.MCP/Tools/TemplateComposeTool.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,19 @@ internal sealed class TemplateComposeTool
1616
[Description("Execute a sequence of template operations (project + item templates) in order. For example, create a MAUI app then add specific pages/views. Each step can reference a different template. If a template is not installed, it will be auto-resolved from NuGet.")]
1717
public static async Task<string> ComposeTemplatesAsync(
1818
TemplateEngineService engineService,
19+
McpFeatureFlags featureFlags,
1920
[Description("JSON array of steps. Each step: {\"templateName\": \"...\", \"name\": \"...\", \"outputPath\": \"...\", \"target\": \"relative/path\", \"parametersJson\": \"{...}\"}. The first step creates the project; subsequent steps add items. If 'target' is set on later steps, it's resolved relative to the first step's output.")] string stepsJson,
2021
CancellationToken cancellationToken = default)
2122
{
2223
using var activity = McpTelemetry.StartToolActivity("template_compose");
2324
var sw = Stopwatch.StartNew();
2425
try
2526
{
27+
if (!featureFlags.IsToolEnabled("template_compose"))
28+
{
29+
return ToolProfileResponse.DisabledMessage("template_compose", "Use template_instantiate to create one project at a time.");
30+
}
31+
2632
List<ComposeStep>? steps;
2733
try
2834
{

0 commit comments

Comments
 (0)