BuildingBlocks.Mcp
1.0.0
dotnet add package BuildingBlocks.Mcp --version 1.0.0
NuGet\Install-Package BuildingBlocks.Mcp -Version 1.0.0
<PackageReference Include="BuildingBlocks.Mcp" Version="1.0.0" />
<PackageVersion Include="BuildingBlocks.Mcp" Version="1.0.0" />
<PackageReference Include="BuildingBlocks.Mcp" />
paket add BuildingBlocks.Mcp --version 1.0.0
#r "nuget: BuildingBlocks.Mcp, 1.0.0"
#:package BuildingBlocks.Mcp@1.0.0
#addin nuget:?package=BuildingBlocks.Mcp&version=1.0.0
#tool nuget:?package=BuildingBlocks.Mcp&version=1.0.0
BuildingBlocks.Mcp
Map application message types (commands, queries, DTOs) and public static Minimal API methods to MCP tools on the official C# SDK. Deny-by-default catalog, typed McpResult, HTTP by default (stdio opt-in). Does not require BuildingBlocks.Mediator.
When to use: Cursor or Claude should call the same logic as HTTP. Opt-in with [McpTool] on a message type or a public static Minimal API method (WithMcp optional), or use MapTool. Unmarked types/methods are never tools. MVC controllers are unsupported for now. HTTP-only inputs (FromHeader) cannot be the MCP body.
What's in 1.0.0
- Message types or public static endpoint methods as MCP tools (
[McpTool], deny-by-default scanner), orMapToolhandlers - Scoped
MapTooloverload: handler receivesIServiceProviderfrom a new DI scope - Idempotency:
UseMemoryIdempotency(ttl)on the builder; commands (POST/PUT) requireidempotencyKey(format: uuidin the schema; any non-empty string accepted); queries never use the store. Namespaced keys, lock,JsonElementreplay. Redis viaIMcpIdempotencyStore. inputSchemafrom CLR: defaults/nullable = optional, enum members,[Description]/ Swagger parameter text- Successful calls return JSON text and
structuredContent - Safe writes:
idempotencyKey, confirmation, timeout,IMcpToolFilter,IMcpRateLimiter,catalog://tools - Analyzers BBMCP001–005 packed in the NuGet
- Opt-in ActivitySource
BuildingBlocks.Mcp
Install
dotnet add package BuildingBlocks.Mcp
Requires .NET 8, .NET 9, or .NET 10. Host OpenTelemetry: BuildingBlocks.Telemetry with IntegrateMcp = true.
After you add or rename tools, restart the API and reload the MCP server in Cursor. Aspire restart alone does not refresh Cursor’s cached tools/list.
Quick start
[McpTool("orders.create", Description = "Create an order")]
public sealed record CreateOrder(int ProductId, int Quantity);
builder.Services.AddBuildingBlocksMcp(o =>
{
o.ScanAssemblyContaining<CreateOrder>();
o.UseMemoryIdempotency(TimeSpan.FromHours(1));
}).UseDispatcher(async (sp, msg, ct) =>
{
await using var scope = sp.CreateAsyncScope();
return await scope.ServiceProvider.GetRequiredService<ISender>().Send(msg, ct);
});
app.MapBuildingBlocksMcp(); // Cursor: { "url": "http://localhost:5141/mcp" }
API must be running. Reload the MCP server in Cursor after tool changes. Minimal API / MapTool / stdio: sections below.
Quick start — Mediator / ISender
[McpTool("orders.create", Description = "Create an order", Kind = McpToolKind.Command, Idempotent = true)]
public sealed record CreateOrder(int ProductId, int Quantity);
builder.Services.AddBuildingBlocksMcp(o =>
{
o.ScanAssemblyContaining<CreateOrder>();
o.UseTelemetry();
o.UseMemoryIdempotency(TimeSpan.FromHours(1));
}).UseDispatcher(async (sp, msg, ct) =>
{
await using var scope = sp.CreateAsyncScope();
return await scope.ServiceProvider.GetRequiredService<ISender>().Send(msg, ct);
});
app.MapBuildingBlocksMcp(); // HTTP /mcp — Cursor url (API must already be running)
UseDispatcher is a singleton; create a scope before resolving scoped ISender. Kind can be omitted when the type implements Mediator ICommand / IQuery. Tool-level Description is required.
You can scan and MapTool in the same AddBuildingBlocksMcp call. Scanned types use UseDispatcher; scanned methods invoke via DI; MapTool uses its handler.
Minimal API — registration approaches
Use a public static method-group (the same method you pass to MapGet / MapPost). JSON binds to one request parameter. CancellationToken, McpInvokeContext, interfaces, and ILogger<T> come from DI. HttpContext is not the MCP body (null outside HTTP). GET query strings: bind with [AsParameters] on a request type. Property [Description] is optional (schema text only).
MVC controllers are unsupported for now — do not put [McpTool] on a controller class or action.
A. [McpTool] + assembly scan
The scanner finds public static methods with [McpTool]. You do not need WithMcp for the catalog.
[McpTool("lab.ping", Description = "Minimal API ping", Kind = McpToolKind.Query)]
public static string LabPing([AsParameters] LabPingRequest request)
=> string.IsNullOrWhiteSpace(request.Name) ? "pong" : $"pong:{request.Name}";
api.MapGet("/lab-ping", LabPing);
builder.Services.AddBuildingBlocksMcp(o => o.ScanAssembly(Assembly.GetExecutingAssembly()));
app.MapBuildingBlocksMcp();
B. [McpTool] + .WithMcp(app)
Same tool as A. WithMcp registers from the endpoint convention. If you also scan, the catalog dedupes by tool name. Pass the IEndpointRouteBuilder used for MapGet (the app or group root, not a nested group that is not the MCP host).
api.MapGet("/lab-ping", LabPing).WithMcp(app);
C. .WithMcp(app, name, description) — no attribute
Kind is inferred from the HTTP verb: GET → Query (no idempotency key), POST/PUT → Command (idempotent write when a store is registered). Other verbs: set Kind in configure.
api.MapPost("/items", CreateItem)
.WithMcp(app, "items.create", "Create an item");
api.MapGet("/search", SearchItems)
.WithMcp(app, "items.search", "Search items");
D. MapTool — HTTP DTO cannot be the MCP input
Use when the HTTP signature uses [FromHeader], multiple body shapes, or other bindings that are not a single JSON object. Keep the HTTP method HTTP-only; map a dedicated DTO.
o.MapTool<GreetingMcpRequest, string>(
"greetings.custom",
"Dedicated MCP DTO — not the HTTP FromHeader model",
async (sp, msg, ctx, ct) => McpResult.Ok("…"),
a => a.Kind = McpToolKind.Query);
The IServiceProvider overload creates a scope per call (validators, feature flags, scoped services).
Idempotency (writes only)
MCP has no HTTP verb on Mediator messages. Treat Command ≈ POST/PUT and Query ≈ GET. The library never retries writes.
| Command | Query | |
|---|---|---|
| Default | Idempotent = true |
never uses the store |
| Client | must send idempotencyKey when a store is registered |
no key |
| Schema | string + format: uuid (hint) |
no key property |
| Runtime | any non-empty string (UUID preferred; ULID is fine) | — |
| Opt out | Idempotent = false |
— |
Register the in-memory store (single process) on the builder — do not add IMcpIdempotencyStore by hand unless you have a custom implementation:
o.UseMemoryIdempotency(TimeSpan.FromHours(1));
Multi-instance hosts: implement IMcpIdempotencyStore (Redis, etc.) and register it as a singleton. Keys are namespaced per tool. Concurrent calls with the same key share a lock. Successful results are stored as JSON and replayed as JsonElement.
Cursor/Claude fill idempotencyKey because the schema marks it required. They do not invent a key unless the field exists. Generate a new UUID for a new write; reuse the same key only when retrying that same write (timeouts, disconnects). RequireConfirmation = true adds required confirmed: true (lab orders.create). Lab demo.echo uses Idempotent = false so smoke calls need no key.
Transport and Cursor
HTTP is the default. Stdio is opt-in for console hosts only: o.UseStdioTransport(). Log to stderr. Do not enable stdio on a web API.
Cursor HTTP (API must already be running):
{
"mcpServers": {
"myapi": {
"url": "http://localhost:5141/mcp"
}
}
}
Features
- Deny-by-default: only
[McpTool]types/methods andMapTool/WithMcpregistrations - Host styles:
Scan+UseDispatcher,[McpTool]on static endpoint methods,WithMcp, orMapTool - Typed
McpResult/McpError - HTTP default:
MapBuildingBlocksMcp()→/mcp - Schema agents can use: optional vs required, JSON Schema
enum, property descriptions structuredContenton success- Catalog resource:
catalog://tools - Roslyn analyzers BBMCP001–005
UseTelemetry(): ActivitySourceBuildingBlocks.Mcp
What it is not (v1)
- MVC controllers are unsupported for now (actions,
[FromHeader],ActionResult). Use public static Minimal API methods, message types, orMapTool. - Not OpenAPI → MCP, not a SOLID linter.
[FromHeader]DTOs stay HTTP-only. - No prompts, elicitation, or OAuth
- Do not call
UseStdioTransport()on a web API - Production hosts should leave MCP unmapped (FeatureFusion registers it only in Development)
Docs
Demo host: FeatureFusion (orders.create, products.list, demo.echo, lab.ping at http://localhost:5141/mcp in Development).
License
MIT — Copyright (c) 2026 Mohammad Hasan Hosseini
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- ModelContextProtocol (>= 2.2.0)
- ModelContextProtocol.AspNetCore (>= 2.2.0)
-
net8.0
- ModelContextProtocol (>= 2.2.0)
- ModelContextProtocol.AspNetCore (>= 2.2.0)
-
net9.0
- ModelContextProtocol (>= 2.2.0)
- ModelContextProtocol.AspNetCore (>= 2.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 42 | 8/29/2026 |
1.0.0: [McpTool] on types or static endpoint methods, or MapTool; inputSchema; structuredContent; namespaced idempotency (schema format uuid); analyzers BBMCP001–005. https://github.com/Maxofpower/FeatureFusion/blob/main/CHANGELOG.md