Skip to content

Commit 8f997cd

Browse files
Adds MCP generator CLI Tool and Integration (#141)
# TLDR; Adds the MCP generator, an example of integration with NucliaDB, and filtering options for specific tags. # Summary - Adds a CLI tool to generate MCP tool wrappers from OpenAPI specifications. - Adds tag filtering to reduce the number of generated tools. - Creates an example NucliaDB MCP server integrated with Claude. - Configures stdio server transport to support Claude Code. - Updates the NucliaDB MCP server sample to use the ModelContextProtocol library. - Updates the NucliaDB MCP server sample to use tag filtering.
1 parent a153367 commit 8f997cd

File tree

20 files changed

+600
-3143
lines changed

20 files changed

+600
-3143
lines changed

.github/pull_request_template.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# TLDR;
2+
3+
# Summary
4+
5+
# Details

README.md

Lines changed: 71 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
**The safest way to make REST calls in C#**
66

7+
**New!** Generate MCP servers from OpenAPI specs!!!
8+
79
Built from the ground up with functional programming, type safety, and modern .NET patterns. Successor to the [original RestClient.Net](https://www.nuget.org/packages/RestClient.Net.Abstractions).
810

911
## What Makes It Different
@@ -16,6 +18,7 @@ This library is uncompromising in its approach to type safety and functional des
1618

1719
## Features
1820

21+
- **Generate an MCP Server and client code** from an OpenAPI 3.x spec.
1922
- **Result Types** - Returns `Result<TSuccess, HttpError<TError>>` with closed hierarchy types for compile-time safety (Outcome package)
2023
- **Zero Exceptions** - No exception throwing for predictable error handling
2124
- **Progress Reporting** - Built-in download/upload progress tracking
@@ -108,6 +111,74 @@ global using ExceptionErrorPost = Outcome.HttpError<ErrorResponse>.ExceptionErro
108111

109112
If you use the OpenAPI generator, it will generate these type aliases for you automatically.
110113

114+
## OpenAPI Client and MCP Code Generation
115+
116+
Generate type-safe C# clients and MCP servers from OpenAPI specs.
117+
118+
### Client Generation
119+
120+
```bash
121+
dotnet add package RestClient.Net.OpenApiGenerator
122+
```
123+
124+
Generate extension methods from OpenAPI 3.x specs:
125+
126+
```csharp
127+
// Generated code usage
128+
using YourApi.Generated;
129+
130+
var httpClient = factory.CreateClient();
131+
132+
// All HTTP methods supported with Result types
133+
var user = await httpClient.GetUserById("123", ct);
134+
var created = await httpClient.CreateUser(newUser, ct);
135+
var updated = await httpClient.UpdateUser((Params: "123", Body: user), ct);
136+
var deleted = await httpClient.DeleteUser("123", ct);
137+
138+
// Pattern match on results
139+
switch (user)
140+
{
141+
case OkUser(var success):
142+
Console.WriteLine($"User: {success.Name}");
143+
break;
144+
case ErrorUser(var error):
145+
Console.WriteLine($"Error: {error.StatusCode}");
146+
break;
147+
}
148+
```
149+
150+
The generator creates extension methods on `HttpClient`, model classes from schemas, and result type aliases for pattern matching.
151+
152+
### MCP Server Generation
153+
154+
Generate Model Context Protocol servers for Claude Code from OpenAPI specs:
155+
156+
```bash
157+
# Generate API client first
158+
dotnet run --project RestClient.Net.OpenApiGenerator.Cli -- \
159+
-u api.yaml \
160+
-o Generated \
161+
-n YourApi.Generated
162+
163+
# Generate MCP tools from the same spec
164+
dotnet run --project RestClient.Net.McpGenerator.Cli -- \
165+
--openapi-url api.yaml \
166+
--output-file Generated/McpTools.g.cs \
167+
--namespace YourApi.Mcp \
168+
--server-name YourApi \
169+
--ext-namespace YourApi.Generated \
170+
--tags "Search,Resources"
171+
```
172+
173+
The MCP generator wraps the generated extension methods as MCP tools that Claude Code can invoke.
174+
175+
**Complete example:** See `Samples/NucliaDbClient.McpServer` for a working MCP server built from the NucliaDB OpenAPI spec. The example includes:
176+
- Generated client code (`Samples/NucliaDbClient/Generated`)
177+
- Generated MCP tools (`NucliaDbMcpTools.g.cs`)
178+
- MCP server host project (`NucliaDbClient.McpServer`)
179+
- Docker Compose setup for NucliaDB
180+
- Claude Code integration script (`run-for-claude.sh`)
181+
111182
## Exhaustiveness Checking with Exhaustion
112183

113184
**Exhaustion is integral to RestClient.Net's safety guarantees.** It's a Roslyn analyzer that ensures you handle every possible case when pattern matching on Result types.
@@ -155,110 +226,6 @@ Exhaustion works by analyzing sealed type hierarchies in switch expressions and
155226

156227
If you don't handle all three, your code won't compile.
157228

158-
### OpenAPI Code Generation
159-
160-
Generate type-safe extension methods from OpenAPI specs:
161-
162-
```csharp
163-
using JSONPlaceholder.Generated;
164-
165-
// Get HttpClient from factory
166-
var httpClient = factory.CreateClient();
167-
168-
// GET all todos
169-
var todos = await httpClient.GetTodos(ct);
170-
171-
// GET todo by ID
172-
var todo = await httpClient.GetTodoById(1, ct);
173-
switch (todo)
174-
{
175-
case OkTodo(var success):
176-
Console.WriteLine($"Todo: {success.Title}");
177-
break;
178-
case ErrorTodo(var error):
179-
Console.WriteLine($"Error: {error.StatusCode} - {error.Body}");
180-
break;
181-
}
182-
183-
// POST - create a new todo
184-
var newTodo = new TodoInput { Title = "New Task", UserId = 1, Completed = false };
185-
var created = await httpClient.CreateTodo(newTodo, ct);
186-
187-
// PUT - update with path param and body
188-
var updated = await httpClient.UpdateTodo((Params: 1, Body: newTodo), ct);
189-
190-
// DELETE - returns Unit
191-
var deleted = await httpClient.DeleteTodo(1, ct);
192-
```
193-
194-
```bash
195-
dotnet add package RestClient.Net.OpenApiGenerator
196-
```
197-
198-
Define your schema (OpenAPI 3.x):
199-
```yaml
200-
openapi: 3.0.0
201-
paths:
202-
/users/{id}:
203-
get:
204-
operationId: getUserById
205-
parameters:
206-
- name: id
207-
in: path
208-
required: true
209-
schema:
210-
type: string
211-
responses:
212-
'200':
213-
content:
214-
application/json:
215-
schema:
216-
$ref: '#/components/schemas/User'
217-
/users:
218-
post:
219-
operationId: createUser
220-
requestBody:
221-
content:
222-
application/json:
223-
schema:
224-
$ref: '#/components/schemas/User'
225-
responses:
226-
'201':
227-
content:
228-
application/json:
229-
schema:
230-
$ref: '#/components/schemas/User'
231-
```
232-
233-
The generator creates:
234-
1. **Extension methods** - Strongly-typed methods on `HttpClient`
235-
2. **Model classes** - DTOs from schema definitions
236-
3. **Result type aliases** - Convenient `OkUser` and `ErrorUser` types
237-
238-
Generated usage:
239-
```csharp
240-
// Get HttpClient from factory
241-
var httpClient = factory.CreateClient();
242-
243-
// GET with path parameter
244-
var user = await httpClient.GetUserById("123", ct);
245-
246-
// POST with body
247-
var created = await httpClient.CreateUser(newUser, ct);
248-
249-
// PUT with path param and body
250-
var updated = await httpClient.UpdateUser((Params: "123", Body: user), ct);
251-
252-
// DELETE returns Unit
253-
var deleted = await httpClient.DeleteUser("123", ct);
254-
```
255-
256-
All generated methods:
257-
- Create extension methods on `HttpClient` (use with `IHttpClientFactory.CreateClient()`)
258-
- Return `Result<TSuccess, HttpError<TError>>` for functional error handling
259-
- Bundle URL/body/headers into `HttpRequestParts` via `buildRequest`
260-
- Support progress reporting through `ProgressReportingHttpContent`
261-
262229
### Progress Reporting
263230

264231
You can track upload progress with `ProgressReportingHttpContent`. This example writes to the console when there is a progress report.

RestClient.Net.McpGenerator.Cli/Program.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ static void PrintUsage()
4141
Console.WriteLine(
4242
" --ext-class <class> Extensions class name (default: 'ApiExtensions')"
4343
);
44+
Console.WriteLine(
45+
" -t, --tags <tag1,tag2> Comma-separated list of OpenAPI tags to include (optional)"
46+
);
4447
Console.WriteLine(" -h, --help Show this help message");
4548
}
4649

@@ -52,6 +55,7 @@ static void PrintUsage()
5255
var serverName = "ApiMcp";
5356
var extensionsNamespace = "Generated";
5457
var extensionsClass = "ApiExtensions";
58+
string? tagsFilter = null;
5559

5660
for (var i = 0; i < args.Length; i++)
5761
{
@@ -79,6 +83,10 @@ static void PrintUsage()
7983
case "--ext-class":
8084
extensionsClass = GetNextArg(args, i++, "ext-class") ?? extensionsClass;
8185
break;
86+
case "-t"
87+
or "--tags":
88+
tagsFilter = GetNextArg(args, i++, "tags");
89+
break;
8290
default:
8391
break;
8492
}
@@ -104,7 +112,8 @@ static void PrintUsage()
104112
namespaceName,
105113
serverName,
106114
extensionsNamespace,
107-
extensionsClass
115+
extensionsClass,
116+
tagsFilter
108117
);
109118
}
110119

@@ -154,13 +163,29 @@ static async Task GenerateCode(Config config)
154163
}
155164

156165
Console.WriteLine($"Read {openApiSpec.Length} characters\n");
166+
167+
// Parse tags filter if provided
168+
ISet<string>? includeTags = null;
169+
if (!string.IsNullOrWhiteSpace(config.TagsFilter))
170+
{
171+
includeTags = new HashSet<string>(
172+
config.TagsFilter.Split(
173+
',',
174+
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
175+
),
176+
StringComparer.OrdinalIgnoreCase
177+
);
178+
Console.WriteLine($"Filtering to tags: {string.Join(", ", includeTags)}");
179+
}
180+
157181
Console.WriteLine("Generating MCP tools code...");
158182

159183
var result = McpServerGenerator.Generate(
160184
openApiSpec,
161185
@namespace: config.Namespace,
162186
serverName: config.ServerName,
163-
extensionsNamespace: config.ExtensionsNamespace
187+
extensionsNamespace: config.ExtensionsNamespace,
188+
includeTags: includeTags
164189
);
165190

166191
#pragma warning disable IDE0010
@@ -186,5 +211,6 @@ internal sealed record Config(
186211
string Namespace,
187212
string ServerName,
188213
string ExtensionsNamespace,
189-
string ExtensionsClass
214+
string ExtensionsClass,
215+
string? TagsFilter
190216
);

RestClient.Net.McpGenerator/McpServerGenerator.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ 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="includeTags">Optional set of tags to include. If specified, only operations with these tags are generated.</param>
1516
/// <returns>A Result containing the generated C# code or error message.</returns>
1617
#pragma warning disable CA1054
1718
public static Result<string, string> Generate(
1819
string openApiContent,
1920
string @namespace,
2021
string serverName,
21-
string extensionsNamespace
22+
string extensionsNamespace,
23+
ISet<string>? includeTags = null
2224
)
2325
#pragma warning restore CA1054
2426
{
@@ -43,7 +45,8 @@ string extensionsNamespace
4345
document,
4446
@namespace,
4547
serverName,
46-
extensionsNamespace
48+
extensionsNamespace,
49+
includeTags
4750
)
4851
);
4952
}

RestClient.Net.McpGenerator/McpToolGenerator.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ internal static class McpToolGenerator
1818
/// <param name="namespace">The namespace for the MCP server.</param>
1919
/// <param name="serverName">The MCP server name.</param>
2020
/// <param name="extensionsNamespace">The namespace of the extensions.</param>
21+
/// <param name="includeTags">Optional set of tags to filter operations. If specified, only operations with these tags are generated.</param>
2122
/// <returns>The generated MCP tools code.</returns>
2223
public static string GenerateTools(
2324
OpenApiDocument document,
2425
string @namespace,
2526
string serverName,
26-
string extensionsNamespace
27+
string extensionsNamespace,
28+
ISet<string>? includeTags = null
2729
)
2830
{
2931
var tools = new List<string>();
@@ -38,6 +40,21 @@ string extensionsNamespace
3840

3941
foreach (var operation in path.Value.Operations)
4042
{
43+
// Skip if tags filter is specified and operation doesn't match
44+
if (includeTags != null && includeTags.Count > 0)
45+
{
46+
var operationTags = operation.Value.Tags;
47+
if (
48+
operationTags == null
49+
|| !operationTags.Any(tag =>
50+
includeTags.Contains(tag.Name, StringComparer.OrdinalIgnoreCase)
51+
)
52+
)
53+
{
54+
continue;
55+
}
56+
}
57+
4158
var toolMethod = GenerateTool(
4259
path.Key,
4360
operation.Key,
@@ -61,11 +78,13 @@ string extensionsNamespace
6178
using System.Text.Json;
6279
using Outcome;
6380
using {{extensionsNamespace}};
81+
using ModelContextProtocol.Server;
6482
6583
namespace {{@namespace}};
6684
6785
/// <summary>MCP server tools for {{serverName}} API.</summary>
68-
public class {{serverName}}Tools(IHttpClientFactory httpClientFactory)
86+
[McpServerToolType]
87+
public static class {{serverName}}Tools
6988
{
7089
private static readonly JsonSerializerOptions JsonOptions = new()
7190
{
@@ -208,13 +227,17 @@ string errorType
208227
var okAlias = $"Ok{responseType}";
209228
var errorAlias = $"Error{responseType}";
210229

230+
var httpClientParam =
231+
methodParamsStr.Length > 0 ? "HttpClient httpClient, " : "HttpClient httpClient";
232+
var allParams = httpClientParam + methodParamsStr;
233+
211234
return $$"""
212235
/// <summary>{{SanitizeDescription(summary)}}</summary>
213236
{{paramDescriptions}}
214-
[Description("{{SanitizeDescription(summary)}}")]
215-
public async Task<string> {{toolName}}({{methodParamsStr}})
237+
/// <param name="httpClient">HttpClient instance</param>
238+
[McpServerTool, Description("{{SanitizeDescription(summary)}}")]
239+
public static async Task<string> {{toolName}}({{allParams}})
216240
{
217-
var httpClient = httpClientFactory.CreateClient();
218241
var result = await httpClient.{{extensionMethodName}}({{extensionCallArgsStr}});
219242
220243
return result switch

0 commit comments

Comments
 (0)