Skip to content

Commit 4666f47

Browse files
committed
Add integration tests
1 parent 7a8eda3 commit 4666f47

File tree

6 files changed

+267
-14
lines changed

6 files changed

+267
-14
lines changed

.github/workflows/nuget-publish.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,23 @@ defaults:
1818
shell: pwsh
1919

2020
jobs:
21+
test:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
with:
26+
fetch-depth: 0
27+
- name: Setup .NET
28+
uses: actions/setup-dotnet@v4
29+
- name: Restore dependencies
30+
run: dotnet restore TALXIS.CLI.sln
31+
- name: Build
32+
run: dotnet build TALXIS.CLI.sln --configuration Release --no-restore
33+
- name: Run tests (including integration tests)
34+
run: dotnet test TALXIS.CLI.sln --configuration Release --no-build --logger "trx;LogFileName=test_results.trx"
35+
2136
create_nuget:
37+
needs: [ test ]
2238
runs-on: windows-latest
2339
steps:
2440
- uses: actions/checkout@v4

TALXIS.CLI.sln

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TALXIS.CLI.MCP", "src\TALXI
1515
EndProject
1616
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TALXIS.CLI.Docs", "src\TALXIS.CLI.Docs\TALXIS.CLI.Docs.csproj", "{0914E284-15E7-4215-B72F-7195F0EB8EEA}"
1717
EndProject
18+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
19+
EndProject
20+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TALXIS.CLI.MCP.Tests", "tests\TALXIS.CLI.MCP.Tests\TALXIS.CLI.MCP.Tests.csproj", "{4F3EA22D-B57A-496D-8385-29E41B57DAFB}"
21+
EndProject
1822
Global
1923
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2024
Debug|Any CPU = Debug|Any CPU
@@ -85,6 +89,18 @@ Global
8589
{0914E284-15E7-4215-B72F-7195F0EB8EEA}.Release|x64.Build.0 = Release|Any CPU
8690
{0914E284-15E7-4215-B72F-7195F0EB8EEA}.Release|x86.ActiveCfg = Release|Any CPU
8791
{0914E284-15E7-4215-B72F-7195F0EB8EEA}.Release|x86.Build.0 = Release|Any CPU
92+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
93+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Debug|Any CPU.Build.0 = Debug|Any CPU
94+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Debug|x64.ActiveCfg = Debug|Any CPU
95+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Debug|x64.Build.0 = Debug|Any CPU
96+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Debug|x86.ActiveCfg = Debug|Any CPU
97+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Debug|x86.Build.0 = Debug|Any CPU
98+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
99+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Release|Any CPU.Build.0 = Release|Any CPU
100+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Release|x64.ActiveCfg = Release|Any CPU
101+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Release|x64.Build.0 = Release|Any CPU
102+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Release|x86.ActiveCfg = Release|Any CPU
103+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB}.Release|x86.Build.0 = Release|Any CPU
88104
EndGlobalSection
89105
GlobalSection(SolutionProperties) = preSolution
90106
HideSolutionNode = FALSE
@@ -95,6 +111,7 @@ Global
95111
{047E218E-A6A2-1C66-58E1-AFEF0AD34E7F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
96112
{DFE5EC2E-21E2-42D6-B9C6-3111CE00FD0B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
97113
{0914E284-15E7-4215-B72F-7195F0EB8EEA} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
114+
{4F3EA22D-B57A-496D-8385-29E41B57DAFB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
98115
EndGlobalSection
99116
GlobalSection(ExtensibilityGlobals) = postSolution
100117
SolutionGuid = {53733BD6-A32A-41B7-9472-E377AF68151F}

src/TALXIS.CLI.MCP/README.md

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,28 +70,28 @@ When developing the MCP server locally, you can run it directly from source and
7070
- Adjust the path in `args` to match your local project location if needed.
7171
- This setup allows you to test changes without reinstalling the global tool.
7272

73-
You can also use the [Model Context Protocol Inspector](https://www.npmjs.com/package/@modelcontextprotocol/inspector) for interactive inspection:
73+
## Testing and Debugging
74+
75+
### Interactive Manual Testing
76+
77+
You can use the [Model Context Protocol Inspector](https://www.npmjs.com/package/@modelcontextprotocol/inspector) for interactive inspection:
7478

7579
```sh
7680
npx @modelcontextprotocol/inspector dotnet run --project src/TALXIS.CLI.MCP
7781
```
7882

79-
**Note:** The Inspector is an interactive web browser application designed for manual testing and exploration. It is not suitable for automated testing scenarios.
80-
81-
### Command Line Debugging
83+
> **Note:** The Inspector is an interactive web browser application designed for manual testing and exploration. It is not suitable for automated testing scenarios.
8284
83-
For debugging MCP server tool invocations directly from the command line, you can interact with the server using the stdio transport. The MCP protocol uses JSON-RPC messages over stdin/stdout.
85+
### Command Line Debugging & Automated Testing
8486

85-
**Manual Testing with JSON-RPC:**
86-
87-
You can test individual tool calls by sending JSON-RPC messages directly to the server:
87+
For debugging or automated testing, you can interact with the MCP server using JSON-RPC messages over stdin/stdout:
8888

8989
```sh
9090
# Start the server
9191
dotnet run --project src/TALXIS.CLI.MCP
9292

93-
# Then send JSON-RPC messages via stdin:
94-
# 1. Initialize the connection
93+
# Then send JSON-RPC messages via stdin (one per line):
94+
# 1. Initialize the connection (required by MCP protocol)
9595
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "test-client", "version": "1.0.0"}}}
9696

9797
# 2. List available tools
@@ -101,11 +101,20 @@ dotnet run --project src/TALXIS.CLI.MCP
101101
{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "tool-name", "arguments": {}}}
102102
```
103103

104-
**Note:** Each JSON-RPC message must be on a single line and followed by a newline character. The server will respond with JSON-RPC responses to stdout.
105104

106-
## Features
107-
- Dynamic discovery of CLI commands and subcommands using reflection
108-
- Implements MCP ListTools and CallTool handlers
105+
#### Example JSON-RPC Messages
106+
107+
To list available tools:
108+
109+
```json
110+
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
111+
```
112+
113+
To call a tool (replace arguments as needed):
114+
115+
```json
116+
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "workspace_component_create", "arguments": {"ShortName": "pp-entity", "name": "TestEntity", "Param": ["EntityType=InvalidType"]}}}
117+
```
109118

110119
---
111120

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.IO;
4+
using System.Text.Json;
5+
using System.Threading.Tasks;
6+
using Xunit;
7+
8+
namespace TALXIS.CLI.MCP.Tests
9+
{
10+
public class McpServerIntegrationTests
11+
{
12+
private static readonly string ProjectPath = GetProjectPath();
13+
14+
private static string GetProjectPath()
15+
{
16+
// Start from test output dir, traverse up to repo root
17+
var dir = new DirectoryInfo(AppContext.BaseDirectory);
18+
while (dir != null && !File.Exists(Path.Combine(dir.FullName, "TALXIS.CLI.sln")))
19+
{
20+
dir = dir.Parent;
21+
}
22+
if (dir == null)
23+
throw new DirectoryNotFoundException("Could not find repo root (TALXIS.CLI.sln)");
24+
var projectPath = Path.Combine(dir.FullName, "src", "TALXIS.CLI.MCP", "TALXIS.CLI.MCP.csproj");
25+
Console.WriteLine($"Resolved MCP project path: {projectPath}");
26+
if (!File.Exists(projectPath))
27+
throw new FileNotFoundException($"MCP project file not found at: {projectPath}");
28+
return projectPath;
29+
}
30+
31+
static McpServerIntegrationTests()
32+
{
33+
Console.WriteLine($"Resolved MCP project path: {ProjectPath}");
34+
if (!File.Exists(ProjectPath))
35+
{
36+
throw new FileNotFoundException($"MCP project file not found at: {ProjectPath}");
37+
}
38+
}
39+
40+
41+
[Fact]
42+
public async Task Initialize_And_ListTools_Works()
43+
{
44+
await WithMcpServer(async process =>
45+
{
46+
await InitializeMcp(process);
47+
var response = await ReadResponse(process);
48+
Assert.Contains("result", response);
49+
50+
await SendJsonRpc(process, new { jsonrpc = "2.0", id = 2, method = "tools/list", @params = new { } });
51+
response = await ReadResponse(process);
52+
Assert.Contains("result", response);
53+
Assert.Contains("workspace_component_create", response);
54+
});
55+
}
56+
57+
58+
[Fact]
59+
public async Task InvalidToolName_ReturnsError()
60+
{
61+
await WithMcpServer(async process =>
62+
{
63+
await InitializeMcp(process);
64+
await ReadResponse(process);
65+
66+
await SendJsonRpc(process, new { jsonrpc = "2.0", id = 2, method = "tools/call", @params = new { name = "nonexistent_tool", arguments = new { } } });
67+
var response = await ReadResponse(process);
68+
Assert.Contains("error", response, StringComparison.OrdinalIgnoreCase);
69+
});
70+
}
71+
72+
73+
[Fact]
74+
public async Task WorkspaceComponentCreate_MissingRequiredParam_ReturnsError()
75+
{
76+
await WithMcpServer(async process =>
77+
{
78+
await InitializeMcp(process);
79+
await ReadResponse(process);
80+
81+
// Missing required parameters (e.g. name, template)
82+
await SendJsonRpc(process, new { jsonrpc = "2.0", id = 2, method = "tools/call", @params = new { name = "workspace_component_create", arguments = new { } } });
83+
var response = await ReadResponse(process);
84+
Assert.Contains("error", response, StringComparison.OrdinalIgnoreCase);
85+
Assert.Contains("required", response, StringComparison.OrdinalIgnoreCase);
86+
});
87+
}
88+
89+
90+
[Fact]
91+
public async Task WorkspaceComponentCreate_InvalidTemplate_ReturnsError()
92+
{
93+
await WithMcpServer(async process =>
94+
{
95+
await InitializeMcp(process);
96+
await ReadResponse(process);
97+
98+
// Invalid template name
99+
await SendJsonRpc(process, new { jsonrpc = "2.0", id = 2, method = "tools/call", @params = new { name = "workspace_component_create", arguments = new { name = "TestComponent", template = "invalid-template" } } });
100+
var response = await ReadResponse(process);
101+
Assert.Contains("error", response, StringComparison.OrdinalIgnoreCase);
102+
Assert.Contains("template", response, StringComparison.OrdinalIgnoreCase);
103+
});
104+
}
105+
/// <summary>
106+
/// Helper to start the MCP server, run a test, and ensure cleanup.
107+
/// </summary>
108+
private static async Task WithMcpServer(Func<Process, Task> test)
109+
{
110+
var process = StartMcpServer();
111+
try
112+
{
113+
await test(process);
114+
}
115+
finally
116+
{
117+
process.Kill();
118+
}
119+
}
120+
121+
/// <summary>
122+
/// Helper to send the MCP initialize message.
123+
/// </summary>
124+
private static Task InitializeMcp(Process process)
125+
{
126+
return SendJsonRpc(process, new
127+
{
128+
jsonrpc = "2.0",
129+
id = 1,
130+
method = "initialize",
131+
@params = new
132+
{
133+
protocolVersion = "2025-06-18",
134+
capabilities = new { },
135+
clientInfo = new { name = "test-client", version = "1.0.0" }
136+
}
137+
});
138+
}
139+
140+
private static Process StartMcpServer()
141+
{
142+
var psi = new ProcessStartInfo("dotnet", $"run --project {ProjectPath}")
143+
{
144+
RedirectStandardInput = true,
145+
RedirectStandardOutput = true,
146+
RedirectStandardError = true,
147+
UseShellExecute = false,
148+
CreateNoWindow = true
149+
};
150+
return Process.Start(psi);
151+
}
152+
153+
private static async Task SendJsonRpc(Process process, object message)
154+
{
155+
var json = JsonSerializer.Serialize(message);
156+
await process.StandardInput.WriteLineAsync(json);
157+
await process.StandardInput.FlushAsync();
158+
}
159+
160+
private static async Task<string> ReadResponse(Process process)
161+
{
162+
// Read a single line response
163+
return await process.StandardOutput.ReadLineAsync();
164+
}
165+
}
166+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# TALXIS CLI MCP Integration Tests
2+
3+
This project contains integration tests for the TALXIS CLI MCP server. It uses xUnit and launches the MCP server as a subprocess, sending JSON-RPC messages to validate behavior.
4+
5+
## Running the Tests
6+
7+
```sh
8+
cd tests/TALXIS.CLI.MCP.Tests
9+
dotnet test
10+
```
11+
12+
## What is Tested
13+
- MCP server startup and initialization
14+
- Listing available tools
15+
- (Extend with more tests for error handling, template validation, etc.)
16+
17+
## Adding More Tests
18+
Add more `[Fact]` or `[Theory]` methods to `McpServerIntegrationTests.cs` to cover additional scenarios, such as:
19+
- Invalid template names
20+
- Missing/invalid parameters
21+
- Error message propagation
22+
- Valid/invalid tool invocations
23+
24+
---
25+
26+
For more details, see the main [TALXIS CLI README](../../README.md).
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
10+
<PackageReference Include="xunit" Version="2.6.6" />
11+
<PackageReference Include="xunit.runner.visualstudio" Version="2.6.6" />
12+
<PackageReference Include="coverlet.collector" Version="6.0.0" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<ProjectReference Include="../../src/TALXIS.CLI.MCP/TALXIS.CLI.MCP.csproj" />
17+
</ItemGroup>
18+
19+
</Project>

0 commit comments

Comments
 (0)