Skip to content

Commit e62d46c

Browse files
committed
feat: Enhance MCP CLI with command handling and knowledge sources
- Added CLI command handling for help, version, tool listing, and validation. - Introduced CompositeKnowledgeSource to manage multiple knowledge sources. - Implemented FileSystemKnowledgeSource and EmbeddedKnowledgeSource for file and embedded resource access. - Updated AiKnowledgeRepository to utilize new knowledge sources. - Enhanced DocsRootResolver to return nullable path. - Created unit tests for AiKnowledgeRepository, CompositeKnowledgeSource, and EmbeddedKnowledgeSource. - Added GitHub Actions workflows for CI and release processes. - Updated README with installation instructions and CLI usage.
1 parent f4a5a73 commit e62d46c

18 files changed

Lines changed: 1027 additions & 86 deletions

.github/workflows/mcp-ci.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Codout.Framework.Mcp CI
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
paths:
7+
- 'Codout.Framework.Mcp/**'
8+
- '.github/workflows/mcp-ci.yml'
9+
pull_request:
10+
branches: [ master ]
11+
paths:
12+
- 'Codout.Framework.Mcp/**'
13+
- '.github/workflows/mcp-ci.yml'
14+
15+
env:
16+
DOTNET_NOLOGO: true
17+
DOTNET_CLI_TELEMETRY_OPTOUT: true
18+
PROJECT: Codout.Framework.Mcp/src/Tools/Codout.Framework.Mcp/Codout.Framework.Mcp.csproj
19+
TESTS: Codout.Framework.Mcp/src/Tools/Codout.Framework.Mcp.Tests/Codout.Framework.Mcp.Tests.csproj
20+
CONFIGURATION: Release
21+
22+
jobs:
23+
build:
24+
runs-on: ubuntu-latest
25+
26+
steps:
27+
- uses: actions/checkout@v4
28+
29+
- name: Setup .NET
30+
uses: actions/setup-dotnet@v4
31+
with:
32+
dotnet-version: 10.0.x
33+
34+
- name: Restore
35+
run: dotnet restore "$PROJECT"
36+
37+
- name: Build
38+
run: dotnet build "$PROJECT" --configuration "$CONFIGURATION" --no-restore
39+
40+
- name: Test
41+
run: |
42+
if [ -f "$TESTS" ]; then
43+
dotnet restore "$TESTS"
44+
dotnet test "$TESTS" --configuration "$CONFIGURATION" --verbosity normal
45+
else
46+
echo "No test project found, skipping."
47+
fi
48+
49+
- name: Smoke test (--validate)
50+
run: dotnet run --project "$PROJECT" --configuration "$CONFIGURATION" --no-build -- --validate
51+
52+
- name: Smoke test (--list-tools)
53+
run: dotnet run --project "$PROJECT" --configuration "$CONFIGURATION" --no-build -- --list-tools

.github/workflows/mcp-release.yml

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
name: Codout.Framework.Mcp Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'mcp-v*'
7+
workflow_dispatch:
8+
inputs:
9+
version:
10+
description: 'Override package version (optional, e.g. 6.2.3)'
11+
required: false
12+
13+
env:
14+
DOTNET_NOLOGO: true
15+
DOTNET_CLI_TELEMETRY_OPTOUT: true
16+
PROJECT: Codout.Framework.Mcp/src/Tools/Codout.Framework.Mcp/Codout.Framework.Mcp.csproj
17+
TESTS: Codout.Framework.Mcp/src/Tools/Codout.Framework.Mcp.Tests/Codout.Framework.Mcp.Tests.csproj
18+
CONFIGURATION: Release
19+
20+
jobs:
21+
build-test-pack:
22+
runs-on: ubuntu-latest
23+
permissions:
24+
contents: read
25+
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@v4
29+
30+
- name: Setup .NET
31+
uses: actions/setup-dotnet@v4
32+
with:
33+
dotnet-version: 10.0.x
34+
35+
- name: Resolve version
36+
id: version
37+
run: |
38+
if [ -n "${{ github.event.inputs.version }}" ]; then
39+
VERSION="${{ github.event.inputs.version }}"
40+
elif [[ "${GITHUB_REF}" == refs/tags/mcp-v* ]]; then
41+
VERSION="${GITHUB_REF#refs/tags/mcp-v}"
42+
else
43+
VERSION=""
44+
fi
45+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
46+
echo "Resolved version: ${VERSION:-<inherit from Directory.Build.props>}"
47+
48+
- name: Restore
49+
run: dotnet restore "$PROJECT"
50+
51+
- name: Restore tests
52+
run: |
53+
if [ -f "$TESTS" ]; then
54+
dotnet restore "$TESTS"
55+
fi
56+
57+
- name: Build
58+
run: dotnet build "$PROJECT" --configuration "$CONFIGURATION" --no-restore
59+
60+
- name: Test
61+
run: |
62+
if [ -f "$TESTS" ]; then
63+
dotnet test "$TESTS" --configuration "$CONFIGURATION" --no-restore --verbosity normal
64+
else
65+
echo "No test project found, skipping."
66+
fi
67+
68+
- name: Validate MCP CLI
69+
run: dotnet run --project "$PROJECT" --configuration "$CONFIGURATION" --no-build -- --validate
70+
71+
- name: Pack
72+
run: |
73+
if [ -n "${{ steps.version.outputs.version }}" ]; then
74+
dotnet pack "$PROJECT" --configuration "$CONFIGURATION" --no-build \
75+
-p:Version=${{ steps.version.outputs.version }} \
76+
-p:PackageVersion=${{ steps.version.outputs.version }} \
77+
--output ./artifacts
78+
else
79+
dotnet pack "$PROJECT" --configuration "$CONFIGURATION" --no-build --output ./artifacts
80+
fi
81+
82+
- name: Upload artifacts
83+
uses: actions/upload-artifact@v4
84+
with:
85+
name: codout-framework-mcp-nupkg
86+
path: ./artifacts/*.nupkg
87+
if-no-files-found: error
88+
89+
publish:
90+
needs: build-test-pack
91+
runs-on: ubuntu-latest
92+
if: startsWith(github.ref, 'refs/tags/mcp-v')
93+
permissions:
94+
contents: read
95+
96+
steps:
97+
- name: Setup .NET
98+
uses: actions/setup-dotnet@v4
99+
with:
100+
dotnet-version: 10.0.x
101+
102+
- name: Download artifacts
103+
uses: actions/download-artifact@v4
104+
with:
105+
name: codout-framework-mcp-nupkg
106+
path: ./artifacts
107+
108+
- name: Push to NuGet.org
109+
env:
110+
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
111+
run: |
112+
if [ -z "$NUGET_API_KEY" ]; then
113+
echo "NUGET_API_KEY secret is not set; aborting."
114+
exit 1
115+
fi
116+
dotnet nuget push "./artifacts/*.nupkg" \
117+
--api-key "$NUGET_API_KEY" \
118+
--source https://api.nuget.org/v3/index.json \
119+
--skip-duplicate
Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,78 @@
11
# Integration Guide
22

3-
## 1. Codout.Club
4-
Copie a pasta `docs/ai` para a raiz do repositório consumidor.
3+
O servidor MCP é distribuído como **.NET global tool** com o knowledge pack embutido. Em projetos novos, normalmente basta instalar o tool e apontar o cliente MCP para `codout-mcp`.
54

6-
## 2. Codout.Framework
7-
Copie `src/Tools/Codout.Framework.Mcp` para o repositório do framework.
5+
## 1. Instalação do tool
86

9-
## 3. Solution
10-
Adicione o projeto `src/Tools/Codout.Framework.Mcp/Codout.Framework.Mcp.csproj` à solution do framework.
7+
```bash
8+
dotnet tool install -g Codout.Framework.Mcp
9+
```
1110

12-
## 4. Configuração
13-
Defina `CODOUT_MCP_CodoutAi__DocsRoot` se os docs estiverem em outro repositório, por exemplo `D:/source/Codout/Codout.Club/docs/ai`.
11+
Confirme:
1412

15-
## 5. Execução
16-
Rode o projeto MCP com `dotnet run --project src/Tools/Codout.Framework.Mcp/Codout.Framework.Mcp.csproj`.
13+
```bash
14+
codout-mcp --validate
15+
codout-mcp --list-tools
16+
```
17+
18+
## 2. Registrar no cliente MCP
19+
20+
### Claude Code
21+
22+
```bash
23+
claude mcp add codout-framework -- codout-mcp
24+
```
25+
26+
### Claude Desktop / VSCode / Cursor
27+
28+
Configuração mínima (ver detalhes específicos por cliente em [src/Tools/Codout.Framework.Mcp/README.md](src/Tools/Codout.Framework.Mcp/README.md)):
29+
30+
```json
31+
{
32+
"mcpServers": {
33+
"codout-framework": {
34+
"command": "codout-mcp"
35+
}
36+
}
37+
}
38+
```
39+
40+
## 3. (Opcional) Apontar para `docs/ai` externo
41+
42+
Útil para iterar a constituição em `Codout.Club` sem republicar o tool:
43+
44+
```json
45+
{
46+
"mcpServers": {
47+
"codout-framework": {
48+
"command": "codout-mcp",
49+
"env": {
50+
"CODOUT_MCP_CodoutAi__DocsRoot": "D:/source/Codout/Codout.Club/docs/ai"
51+
}
52+
}
53+
}
54+
}
55+
```
56+
57+
## 4. Modo dev (sem instalar o tool)
58+
59+
```bash
60+
git clone https://github.com/Codout/Codout.Framework
61+
cd Codout.Framework
62+
dotnet run --project Codout.Framework.Mcp/src/Tools/Codout.Framework.Mcp/Codout.Framework.Mcp.csproj -- --validate
63+
```
64+
65+
Aponte o cliente MCP para `dotnet run --project <csproj>` em vez de `codout-mcp`.
66+
67+
## 5. Release
68+
69+
Push de tag `mcp-vX.Y.Z` dispara o workflow [`mcp-release.yml`](.github/workflows/mcp-release.yml) que:
70+
71+
1. Faz build, testes e `--validate`.
72+
2. Empacota o `.nupkg`.
73+
3. Publica em [nuget.org](https://www.nuget.org) usando o secret `NUGET_API_KEY`.
74+
75+
```bash
76+
git tag mcp-v6.2.3
77+
git push origin mcp-v6.2.3
78+
```
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using Codout.Framework.Mcp.Services;
2+
using Xunit;
3+
4+
namespace Codout.Framework.Mcp.Tests;
5+
6+
public class AiKnowledgeRepositoryTests
7+
{
8+
private static AiKnowledgeRepository CreateRepository()
9+
{
10+
var embedded = new EmbeddedKnowledgeSource(typeof(EmbeddedKnowledgeSource).Assembly);
11+
var composite = new CompositeKnowledgeSource(new IKnowledgeSource[] { embedded });
12+
return new AiKnowledgeRepository(composite);
13+
}
14+
15+
[Theory]
16+
[InlineData("ui-constitution")]
17+
[InlineData("components")]
18+
[InlineData("screen-patterns")]
19+
[InlineData("anti-patterns")]
20+
[InlineData("decision-flow")]
21+
[InlineData("crud-recipe")]
22+
[InlineData("grid-recipe")]
23+
[InlineData("form-simple-recipe")]
24+
[InlineData("form-complex-recipe")]
25+
[InlineData("details-actions-recipe")]
26+
[InlineData("authorization-recipe")]
27+
[InlineData("layout-recipe")]
28+
public async Task Static_documents_resolve_with_content(string key)
29+
{
30+
var repo = CreateRepository();
31+
var doc = await repo.GetDocumentAsync(key);
32+
33+
Assert.Equal(key, doc.Key);
34+
Assert.False(string.IsNullOrWhiteSpace(doc.Content), $"Document '{key}' is empty.");
35+
Assert.False(string.IsNullOrWhiteSpace(doc.RelativePath));
36+
}
37+
38+
[Fact]
39+
public async Task Gold_references_list_is_non_empty()
40+
{
41+
var repo = CreateRepository();
42+
var names = await repo.ListGoldReferenceNamesAsync();
43+
Assert.NotEmpty(names);
44+
}
45+
46+
[Fact]
47+
public async Task Each_gold_reference_loads()
48+
{
49+
var repo = CreateRepository();
50+
var names = await repo.ListGoldReferenceNamesAsync();
51+
52+
foreach (var name in names)
53+
{
54+
var doc = await repo.GetDocumentAsync($"gold:{name}");
55+
Assert.False(string.IsNullOrWhiteSpace(doc.Content), $"Gold reference '{name}' is empty.");
56+
}
57+
}
58+
59+
[Fact]
60+
public async Task Status_reports_resolved_with_counts()
61+
{
62+
var repo = CreateRepository();
63+
var status = await repo.GetStatusAsync();
64+
65+
Assert.True(status.DocsRootResolved);
66+
Assert.True(status.StaticDocumentCount > 0);
67+
Assert.True(status.GoldReferenceCount > 0);
68+
}
69+
70+
[Fact]
71+
public async Task Search_returns_matches_for_known_term()
72+
{
73+
var repo = CreateRepository();
74+
var results = await repo.SearchAsync("crud", take: 5);
75+
Assert.NotEmpty(results);
76+
}
77+
78+
[Fact]
79+
public async Task FindGoldReferences_returns_matches_for_known_query()
80+
{
81+
var repo = CreateRepository();
82+
var results = await repo.FindGoldReferencesAsync("financeiro", take: 3);
83+
Assert.NotNull(results);
84+
}
85+
86+
[Fact]
87+
public async Task Unknown_key_throws()
88+
{
89+
var repo = CreateRepository();
90+
await Assert.ThrowsAsync<FileNotFoundException>(
91+
() => repo.GetDocumentAsync("does-not-exist"));
92+
}
93+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
<IsPublishable>false</IsPublishable>
9+
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
14+
<PackageReference Include="xunit" Version="2.9.2" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\Codout.Framework.Mcp\Codout.Framework.Mcp.csproj" />
20+
</ItemGroup>
21+
22+
</Project>

0 commit comments

Comments
 (0)