Skip to content
bitzorcas
中EN

Concept

MCP Server Architecture & Source Generators

In-depth analysis of the BitzOrcas.Mcp compile-time Roslyn Source Generator, [GenerateMcpTool] attribute contract, JSON Schema derivation, and /mcp endpoint registration.

Last updated

MCP Server Architecture & Source Generators

In traditional monolithic or microservice architectures, exposing operational endpoints to Large Language Models (LLMs) requires extensive glue code: maintaining custom JSON Schemas, writing JSON-RPC deserializers, manually extracting documentation, and wiring custom model validation.

BitzOrcas.Modern introduces BitzOrcas.Mcp.SourceGenerator, a Roslyn-based analyzer that inspects CQRS messages annotated with [GenerateMcpTool] at build time. It extracts XML documentation comments and primary constructor parameter types to emit standard IMcpToolPartition registration code. The entire workflow eliminates runtime reflection, supporting Native AOT compilation, sub-millisecond execution, and low-memory startups.


1. Compile-Time Declaration: [GenerateMcpTool]

Developers annotate application-level Mediator Command or Query records with [GenerateMcpTool] and write standard C# XML documentation comments:

using BitzOrcas.Application.Commands;
using BitzOrcas.Domain.Results;
using BitzOrcas.Mcp.Attributes;
namespace BitzOrcas.Modules.Litigation.Application.Commands.Cases;
/// <summary>
/// Creates a new litigation case file and initializes the matter tracking number.
/// </summary>
/// <param name="CaseTitle">The full legal title of the litigation case.</param>
/// <param name="CaseType">The case classification token, such as CivilLitigation or CommercialArbitration.</param>
/// <param name="ClaimAmount">The monetary claim amount in standard currency, which must be strictly greater than zero.</param>
/// <param name="DefendantName">The legal name of the primary defendant or responding party.</param>
[GenerateMcpTool("create_litigation_case", "Creates a new litigation case file, generates the matter number, and binds lead attorneys.")]
public sealed record CreateLitigationCaseCommand(
string CaseTitle,
string CaseType,
decimal ClaimAmount,
string DefendantName
): ICommand<Result<string>>;

Physical Rules for Parameter Derivation

  1. Primary Constructor Parameters as Inputs: The generator only exposes primary constructor parameters to the LLM. Get-only calculated properties, delegated services, and internal metadata inside the record body remain completely hidden;
  2. XML Doc Comments Mapped to Descriptions: Parameter tags (<param name="parameterName">) become property description fields in the generated JSON Schema, providing the prompt context guiding LLM parameter selection;
  3. Type Constraints and Nullability: Non-nullable types are automatically registered in the JSON Schema required array; numeric types receive strict number or integer formatting;
  4. Stable Tool Naming: Tool names follow lowercase snake_case conventions (e.g., create_litigation_case), ensuring compatibility across OpenAI, Anthropic, and open-weight model tooling routers.

2. Emitted Source Output (Roslyn Emit)

The Roslyn generator emits a strongly typed tool partition class and dependency injection extension for each module during compilation:

// <auto-generated/>
#nullable enable
namespace BitzOrcas.Modules.Litigation.Generated;
/// <summary>
/// Compile-time generated MCP tool partition for the Litigation module.
/// </summary>
public sealed class LitigationMcpToolPartition : global::BitzOrcas.Mcp.Abstractions.IMcpToolPartition
{
public string ModuleName => "Litigation";
public global::System.Collections.Generic.IReadOnlyList<global::BitzOrcas.Mcp.Abstractions.McpToolDefinition> GetTools()
{
return new global::BitzOrcas.Mcp.Abstractions.McpToolDefinition[]
{
new global::BitzOrcas.Mcp.Abstractions.McpToolDefinition(
Name: "create_litigation_case",
Description: "Creates a new litigation case file, generates the matter number, and binds lead attorneys.",
InputSchemaJson: """
{
"type": "object",
"properties": {
"CaseTitle": {
"type": "string",
"description": "The full legal title of the litigation case."
},
"CaseType": {
"type": "string",
"description": "The case classification token, such as CivilLitigation or CommercialArbitration."
},
"ClaimAmount": {
"type": "number",
"description": "The monetary claim amount in standard currency, which must be strictly greater than zero."
},
"DefendantName": {
"type": "string",
"description": "The legal name of the primary defendant or responding party."
}
},
"required": ["CaseTitle", "CaseType", "ClaimAmount", "DefendantName"]
}
""",
ExecuteAsync: async (jsonElement, serviceProvider, cancellationToken) =>
{
var mediator = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions
.GetRequiredService<global::BitzOrcas.Application.Mediator.IMediator>(serviceProvider);
var command = global::System.Text.Json.JsonSerializer.Deserialize<global::BitzOrcas.Modules.Litigation.Application.Commands.Cases.CreateLitigationCaseCommand>(
jsonElement.GetRawText(),
new global::System.Text.Json.JsonSerializerOptions(global::System.Text.Json.JsonSerializerDefaults.Web));
if (command is null)
{
return global::BitzOrcas.Mcp.Abstractions.McpToolResult.Fail("Payload JSON deserialization failed.");
}
var result = await mediator.SendAsync(command, cancellationToken);
if (result.IsSuccess)
{
return global::BitzOrcas.Mcp.Abstractions.McpToolResult.Success(result.Value);
}
return global::BitzOrcas.Mcp.Abstractions.McpToolResult.Fail(result.Error.Message);
}
)
};
}
}

3. Host Composition & Endpoint Exposure

Registering the MCP protocol layer within src/Hosts/BitzOrcas.Api requires standard minimal service configuration:

3.1 Service Registration (Program.cs)

using BitzOrcas.Infrastructure.Mcp;
var builder = WebApplication.CreateBuilder(args);
// 1. Register core MCP transport and partition aggregator
builder.Services.AddBitzOrcasMcpProtocol();
// 2. Register generated tool partitions from business modules
builder.Services.AddLitigationMcpTools();
var app = builder.Build();

3.2 Endpoint Mapping

// Map MCP standard endpoint (defaults to /mcp)
app.MapBitzOrcasMcp(McpProtocolDependencyInjection.DefaultRoutePattern);
app.Run();

BitzOrcas.Api listens on /mcp using the Streamable HTTP / Server-Sent Events (SSE) transport protocol. Clients querying tools/list immediately receive the live, strongly typed catalog.


100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%