Skip to content

Commit 35ea394

Browse files
Add MCP
1 parent 14af39f commit 35ea394

File tree

15 files changed

+1314
-635
lines changed

15 files changed

+1314
-635
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#pragma warning disable CA1502
2+
3+
using RestClient.Net.McpGenerator;
4+
5+
if (args.Length == 0 || args.Contains("--help") || args.Contains("-h"))
6+
{
7+
PrintUsage();
8+
return 0;
9+
}
10+
11+
var config = ParseArgs(args);
12+
if (config is null)
13+
{
14+
return 1;
15+
}
16+
17+
await GenerateCode(config).ConfigureAwait(false);
18+
return 0;
19+
20+
static void PrintUsage()
21+
{
22+
Console.WriteLine("RestClient.Net MCP Server Generator");
23+
Console.WriteLine("====================================\n");
24+
Console.WriteLine("Generates MCP tool code that wraps RestClient.Net extension methods.\n");
25+
Console.WriteLine("Usage:");
26+
Console.WriteLine(" mcp-generator [options]\n");
27+
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')");
31+
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')");
34+
Console.WriteLine(" -h, --help Show this help message");
35+
}
36+
37+
static Config? ParseArgs(string[] args)
38+
{
39+
string? openApiUrl = null;
40+
string? outputFile = null;
41+
var namespaceName = "McpServer";
42+
var serverName = "ApiMcp";
43+
var extensionsNamespace = "Generated";
44+
var extensionsClass = "ApiExtensions";
45+
46+
for (var i = 0; i < args.Length; i++)
47+
{
48+
switch (args[i])
49+
{
50+
case "-u" or "--openapi-url":
51+
openApiUrl = GetNextArg(args, i++, "openapi-url");
52+
break;
53+
case "-o" or "--output-file":
54+
outputFile = GetNextArg(args, i++, "output-file");
55+
break;
56+
case "-n" or "--namespace":
57+
namespaceName = GetNextArg(args, i++, "namespace") ?? namespaceName;
58+
break;
59+
case "-s" or "--server-name":
60+
serverName = GetNextArg(args, i++, "server-name") ?? serverName;
61+
break;
62+
case "--ext-namespace":
63+
extensionsNamespace = GetNextArg(args, i++, "ext-namespace") ?? extensionsNamespace;
64+
break;
65+
case "--ext-class":
66+
extensionsClass = GetNextArg(args, i++, "ext-class") ?? extensionsClass;
67+
break;
68+
default:
69+
break;
70+
}
71+
}
72+
73+
if (string.IsNullOrEmpty(openApiUrl))
74+
{
75+
Console.WriteLine("Error: --openapi-url is required");
76+
PrintUsage();
77+
return null;
78+
}
79+
80+
if (string.IsNullOrEmpty(outputFile))
81+
{
82+
Console.WriteLine("Error: --output-file is required");
83+
PrintUsage();
84+
return null;
85+
}
86+
87+
return new Config(
88+
openApiUrl,
89+
outputFile,
90+
namespaceName,
91+
serverName,
92+
extensionsNamespace,
93+
extensionsClass
94+
);
95+
}
96+
97+
static string? GetNextArg(string[] args, int currentIndex, string optionName)
98+
{
99+
if (currentIndex + 1 >= args.Length)
100+
{
101+
Console.WriteLine($"Error: --{optionName} requires a value");
102+
return null;
103+
}
104+
105+
return args[currentIndex + 1];
106+
}
107+
108+
static async Task GenerateCode(Config config)
109+
{
110+
Console.WriteLine("RestClient.Net MCP Server Generator");
111+
Console.WriteLine("====================================\n");
112+
113+
string openApiSpec;
114+
115+
var isUrl =
116+
config.OpenApiUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
117+
|| config.OpenApiUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
118+
119+
if (!isUrl)
120+
{
121+
var filePath = config.OpenApiUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase)
122+
? config.OpenApiUrl[7..]
123+
: config.OpenApiUrl;
124+
125+
Console.WriteLine($"Reading OpenAPI spec from file: {filePath}");
126+
127+
if (!File.Exists(filePath))
128+
{
129+
Console.WriteLine($"Error: File not found: {filePath}");
130+
return;
131+
}
132+
133+
openApiSpec = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);
134+
}
135+
else
136+
{
137+
Console.WriteLine($"Downloading OpenAPI spec from: {config.OpenApiUrl}");
138+
using var httpClient = new HttpClient();
139+
openApiSpec = await httpClient.GetStringAsync(config.OpenApiUrl).ConfigureAwait(false);
140+
}
141+
142+
Console.WriteLine($"Read {openApiSpec.Length} characters\n");
143+
Console.WriteLine("Generating MCP tools code...");
144+
145+
var result = McpServerGenerator.Generate(
146+
openApiSpec,
147+
@namespace: config.Namespace,
148+
serverName: config.ServerName,
149+
extensionsNamespace: config.ExtensionsNamespace,
150+
extensionsClassName: config.ExtensionsClass
151+
);
152+
153+
#pragma warning disable IDE0010
154+
switch (result)
155+
#pragma warning restore IDE0010
156+
{
157+
case Outcome.Result<string, string>.Ok<string, string>(var code):
158+
File.WriteAllText(config.OutputFile, code);
159+
Console.WriteLine($"Generated {code.Length} characters of MCP tools code");
160+
Console.WriteLine($"\nSaved to: {config.OutputFile}");
161+
Console.WriteLine("\nGeneration completed successfully!");
162+
break;
163+
case Outcome.Result<string, string>.Error<string, string>(var error):
164+
Console.WriteLine("\nCode generation failed:");
165+
Console.WriteLine(error);
166+
break;
167+
}
168+
}
169+
170+
internal sealed record Config(
171+
string OpenApiUrl,
172+
string OutputFile,
173+
string Namespace,
174+
string ServerName,
175+
string ExtensionsNamespace,
176+
string ExtensionsClass
177+
);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
9+
</ItemGroup>
10+
<ItemGroup>
11+
<ProjectReference Include="..\RestClient.Net.McpGenerator\RestClient.Net.McpGenerator.csproj" />
12+
</ItemGroup>
13+
</Project>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
global using Microsoft.OpenApi;
2+
global using Microsoft.OpenApi.Reader;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#pragma warning disable CS8509
2+
3+
using Outcome;
4+
5+
namespace RestClient.Net.McpGenerator;
6+
7+
/// <summary>Generates MCP server code from OpenAPI specifications.</summary>
8+
public static class McpServerGenerator
9+
{
10+
/// <summary>Generates MCP server tools code from an OpenAPI document.</summary>
11+
/// <param name="openApiContent">The OpenAPI document content (JSON or YAML).</param>
12+
/// <param name="namespace">The namespace for generated MCP tools.</param>
13+
/// <param name="serverName">The MCP server name.</param>
14+
/// <param name="extensionsNamespace">The namespace of the pre-generated extensions.</param>
15+
/// <param name="extensionsClassName">The class name of the pre-generated extensions.</param>
16+
/// <returns>A Result containing the generated C# code or error message.</returns>
17+
#pragma warning disable CA1054
18+
public static Result<string, string> Generate(
19+
string openApiContent,
20+
string @namespace,
21+
string serverName,
22+
string extensionsNamespace,
23+
string extensionsClassName
24+
)
25+
#pragma warning restore CA1054
26+
{
27+
try
28+
{
29+
var settings = new OpenApiReaderSettings();
30+
settings.AddYamlReader();
31+
32+
var readResult = OpenApiDocument.Parse(openApiContent, settings: settings);
33+
34+
if (readResult.Document == null)
35+
{
36+
return new Result<string, string>.Error<string, string>("Error parsing OpenAPI: Document is null");
37+
}
38+
39+
var document = readResult.Document;
40+
41+
return new Result<string, string>.Ok<string, string>(
42+
McpToolGenerator.GenerateTools(
43+
document,
44+
@namespace,
45+
serverName,
46+
extensionsNamespace,
47+
extensionsClassName
48+
)
49+
);
50+
}
51+
catch (Exception ex)
52+
{
53+
return new Result<string, string>.Error<string, string>(
54+
$"Exception during code generation: {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}"
55+
);
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)