Skip to content

Commit 0ddf9bf

Browse files
MCP
1 parent 35ea394 commit 0ddf9bf

File tree

9 files changed

+2647
-64
lines changed

9 files changed

+2647
-64
lines changed

.github/workflows/pr-build.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,20 @@ jobs:
3131
- name: Build solution
3232
run: dotnet build RestClient.sln --configuration Release --no-restore /warnaserror
3333

34+
- name: Verify Docker is available
35+
run: |
36+
docker --version
37+
docker compose version
38+
3439
- name: Run all tests with code coverage
3540
run: dotnet test RestClient.sln --configuration Release --no-build --verbosity normal --logger "console;verbosity=detailed" --collect:"XPlat Code Coverage" -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Threshold=100 DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ThresholdType=line,branch,method DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ThresholdStat=total
3641

42+
- name: Cleanup Docker containers
43+
if: always()
44+
run: |
45+
cd Samples/NucliaDbClient
46+
docker compose down -v --remove-orphans || true
47+
3748
- name: Install Stryker Mutator
3849
run: dotnet tool install --global dotnet-stryker
3950

RestClient.Net.McpGenerator.Cli/Program.cs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,22 @@ static void PrintUsage()
2525
Console.WriteLine("Usage:");
2626
Console.WriteLine(" mcp-generator [options]\n");
2727
Console.WriteLine("Options:");
28-
Console.WriteLine(" -u, --openapi-url <url> (Required) URL or file path to OpenAPI spec");
29-
Console.WriteLine(" -o, --output-file <path> (Required) Output file path for generated code");
30-
Console.WriteLine(" -n, --namespace <namespace> MCP server namespace (default: 'McpServer')");
28+
Console.WriteLine(
29+
" -u, --openapi-url <url> (Required) URL or file path to OpenAPI spec"
30+
);
31+
Console.WriteLine(
32+
" -o, --output-file <path> (Required) Output file path for generated code"
33+
);
34+
Console.WriteLine(
35+
" -n, --namespace <namespace> MCP server namespace (default: 'McpServer')"
36+
);
3137
Console.WriteLine(" -s, --server-name <name> MCP server name (default: 'ApiMcp')");
32-
Console.WriteLine(" --ext-namespace <namespace> Extensions namespace (default: 'Generated')");
33-
Console.WriteLine(" --ext-class <class> Extensions class name (default: 'ApiExtensions')");
38+
Console.WriteLine(
39+
" --ext-namespace <namespace> Extensions namespace (default: 'Generated')"
40+
);
41+
Console.WriteLine(
42+
" --ext-class <class> Extensions class name (default: 'ApiExtensions')"
43+
);
3444
Console.WriteLine(" -h, --help Show this help message");
3545
}
3646

@@ -47,16 +57,20 @@ static void PrintUsage()
4757
{
4858
switch (args[i])
4959
{
50-
case "-u" or "--openapi-url":
60+
case "-u"
61+
or "--openapi-url":
5162
openApiUrl = GetNextArg(args, i++, "openapi-url");
5263
break;
53-
case "-o" or "--output-file":
64+
case "-o"
65+
or "--output-file":
5466
outputFile = GetNextArg(args, i++, "output-file");
5567
break;
56-
case "-n" or "--namespace":
68+
case "-n"
69+
or "--namespace":
5770
namespaceName = GetNextArg(args, i++, "namespace") ?? namespaceName;
5871
break;
59-
case "-s" or "--server-name":
72+
case "-s"
73+
or "--server-name":
6074
serverName = GetNextArg(args, i++, "server-name") ?? serverName;
6175
break;
6276
case "--ext-namespace":
@@ -146,16 +160,15 @@ static async Task GenerateCode(Config config)
146160
openApiSpec,
147161
@namespace: config.Namespace,
148162
serverName: config.ServerName,
149-
extensionsNamespace: config.ExtensionsNamespace,
150-
extensionsClassName: config.ExtensionsClass
163+
extensionsNamespace: config.ExtensionsNamespace
151164
);
152165

153166
#pragma warning disable IDE0010
154167
switch (result)
155168
#pragma warning restore IDE0010
156169
{
157170
case Outcome.Result<string, string>.Ok<string, string>(var code):
158-
File.WriteAllText(config.OutputFile, code);
171+
await File.WriteAllTextAsync(config.OutputFile, code).ConfigureAwait(false);
159172
Console.WriteLine($"Generated {code.Length} characters of MCP tools code");
160173
Console.WriteLine($"\nSaved to: {config.OutputFile}");
161174
Console.WriteLine("\nGeneration completed successfully!");
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
global using Microsoft.OpenApi;
22
global using Microsoft.OpenApi.Reader;
3+
global using RestClient.Net.OpenApiGenerator;

RestClient.Net.McpGenerator/McpServerGenerator.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,13 @@ public static class McpServerGenerator
1212
/// <param name="namespace">The namespace for generated MCP tools.</param>
1313
/// <param name="serverName">The MCP server name.</param>
1414
/// <param name="extensionsNamespace">The namespace of the pre-generated extensions.</param>
15-
/// <param name="extensionsClassName">The class name of the pre-generated extensions.</param>
1615
/// <returns>A Result containing the generated C# code or error message.</returns>
1716
#pragma warning disable CA1054
1817
public static Result<string, string> Generate(
1918
string openApiContent,
2019
string @namespace,
2120
string serverName,
22-
string extensionsNamespace,
23-
string extensionsClassName
21+
string extensionsNamespace
2422
)
2523
#pragma warning restore CA1054
2624
{
@@ -43,8 +41,7 @@ string extensionsClassName
4341
document,
4442
@namespace,
4543
serverName,
46-
extensionsNamespace,
47-
extensionsClassName
44+
extensionsNamespace
4845
)
4946
);
5047
}

RestClient.Net.McpGenerator/McpToolGenerator.cs

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using RestClient.Net.OpenApiGenerator;
2-
31
namespace RestClient.Net.McpGenerator;
42

53
internal readonly record struct McpParameterInfo(
@@ -20,14 +18,12 @@ internal static class McpToolGenerator
2018
/// <param name="namespace">The namespace for the MCP server.</param>
2119
/// <param name="serverName">The MCP server name.</param>
2220
/// <param name="extensionsNamespace">The namespace of the extensions.</param>
23-
/// <param name="extensionsClassName">The class name of the extensions.</param>
2421
/// <returns>The generated MCP tools code.</returns>
2522
public static string GenerateTools(
2623
OpenApiDocument document,
2724
string @namespace,
2825
string serverName,
29-
string extensionsNamespace,
30-
string extensionsClassName
26+
string extensionsNamespace
3127
)
3228
{
3329
var tools = new List<string>();
@@ -47,8 +43,6 @@ string extensionsClassName
4743
operation.Key,
4844
operation.Value,
4945
document.Components?.Schemas,
50-
extensionsNamespace,
51-
extensionsClassName,
5246
methodNameCounts
5347
);
5448

@@ -71,6 +65,7 @@ string extensionsClassName
7165
7266
namespace {{@namespace}};
7367
68+
/// <summary>MCP server tools for {{serverName}} API.</summary>
7469
[McpServerToolType]
7570
public class {{serverName}}Tools(IHttpClientFactory httpClientFactory)
7671
{
@@ -90,8 +85,6 @@ private static string GenerateTool(
9085
HttpMethod operationType,
9186
OpenApiOperation operation,
9287
IDictionary<string, IOpenApiSchema>? schemas,
93-
string extensionsNamespace,
94-
string extensionsClassName,
9588
Dictionary<string, int> methodNameCounts
9689
)
9790
{
@@ -123,7 +116,6 @@ Dictionary<string, int> methodNameCounts
123116
return GenerateToolMethod(
124117
mcpToolName,
125118
extensionMethodName,
126-
extensionsClassName,
127119
summary,
128120
parameters,
129121
hasBody,
@@ -135,7 +127,6 @@ Dictionary<string, int> methodNameCounts
135127
private static string GenerateToolMethod(
136128
string toolName,
137129
string extensionMethodName,
138-
string extensionsClassName,
139130
string summary,
140131
List<McpParameterInfo> parameters,
141132
bool hasBody,
@@ -177,6 +168,9 @@ string responseType
177168
? string.Join(", ", extensionCallArgs) + ", "
178169
: string.Empty;
179170

171+
var okAlias = $"Ok{responseType}";
172+
var errorAlias = $"Error{responseType}";
173+
180174
return $$"""
181175
/// <summary>{{SanitizeDescription(summary)}}</summary>
182176
{{paramDescriptions}}
@@ -189,9 +183,9 @@ string responseType
189183
190184
return result switch
191185
{
192-
Result<{{responseType}}, HttpError<string>>.Ok(var success) =>
186+
{{okAlias}}(var success) =>
193187
JsonSerializer.Serialize(success, JsonOptions),
194-
Result<{{responseType}}, HttpError<string>>.Error(var error) =>
188+
{{errorAlias}}(var error) =>
195189
$"Error: {error.StatusCode} - {error.Body}",
196190
_ => "Unknown error"
197191
};
@@ -201,11 +195,13 @@ string responseType
201195

202196
private static string FormatParameter(McpParameterInfo param)
203197
{
198+
var isNullable = param.Type.Contains('?', StringComparison.Ordinal);
199+
204200
var defaultPart =
205-
param.DefaultValue != null
201+
isNullable ? " = null"
202+
: param.DefaultValue != null
206203
? param.Type switch
207204
{
208-
var t when t.Contains('?', StringComparison.Ordinal) => " = null",
209205
var t when t.StartsWith("string", StringComparison.Ordinal) =>
210206
$" = \"{param.DefaultValue}\"",
211207
var t when t.StartsWith("bool", StringComparison.Ordinal) =>
@@ -214,7 +210,6 @@ var t when t.StartsWith("bool", StringComparison.Ordinal) =>
214210
: " = false",
215211
_ => $" = {param.DefaultValue}",
216212
}
217-
: param.Type.Contains('?', StringComparison.Ordinal) ? " = null"
218213
: string.Empty;
219214

220215
return $"{param.Type} {param.Name}{defaultPart}";
@@ -245,9 +240,31 @@ private static List<McpParameterInfo> GetParameters(
245240
var description = param.Description ?? sanitizedName;
246241
var isPath = param.In == ParameterLocation.Path;
247242
var isHeader = param.In == ParameterLocation.Header;
243+
var isQuery = param.In == ParameterLocation.Query;
244+
245+
// Extract default value - match the extension generator logic
246+
var rawDefaultValue = param.Schema?.Default?.ToString();
247+
var isSimpleType =
248+
baseType
249+
is "string"
250+
or "int"
251+
or "long"
252+
or "float"
253+
or "double"
254+
or "decimal"
255+
or "bool";
256+
var defaultValue =
257+
isSimpleType && !string.IsNullOrEmpty(rawDefaultValue) ? rawDefaultValue : null;
258+
259+
// For optional string query parameters without a schema default, use empty string
260+
var hasNoDefault = defaultValue == null;
261+
if (!required && baseType == "string" && isQuery && hasNoDefault)
262+
{
263+
defaultValue = "";
264+
}
248265

249-
var defaultValue = param.Schema?.Default?.ToString();
250-
var makeNullable = !required && defaultValue == null && !baseType.EndsWith('?');
266+
// Make nullable if not required and no default value
267+
var makeNullable = !required && hasNoDefault && !baseType.EndsWith('?');
251268
var type = makeNullable ? $"{baseType}?" : baseType;
252269

253270
parameters.Add(

RestClient.Net.McpGenerator/RestClient.Net.McpGenerator.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
<ItemGroup>
1212
<ProjectReference Include="..\RestClient.Net.OpenApiGenerator\RestClient.Net.OpenApiGenerator.csproj" />
1313
<ProjectReference Include="..\RestClient.Net\RestClient.Net.csproj" />
14+
<PackageReference Include="Outcome" Version="1.0.0" />
1415
</ItemGroup>
1516
</Project>

0 commit comments

Comments
 (0)