Install
$ agentstack add mcp-maxofpower-featurefusion ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
FeatureFusion
BuildingBlocks for .NET — CQRS Send + pipeline, config-driven OpenTelemetry, MCP tools, keyset pagination, and a local Aspire SigNoz stack — plus a runnable lab that uses them.
Formerly FeatureManagement (GitHub redirects).
[](https://dotnet.microsoft.com/) [](https://learn.microsoft.com/dotnet/aspire/) [](https://www.nuget.org/packages/BuildingBlocks.Mediator) [](https://www.nuget.org/packages/BuildingBlocks.Mcp) [](https://www.nuget.org/packages/BuildingBlocks.Pagination.EntityFrameworkCore) [](https://www.nuget.org/packages/BuildingBlocks.Telemetry) [](https://www.nuget.org/packages/BuildingBlocks.Aspire.Hosting.SigNoz) [](LICENSE.txt) [](https://github.com/Maxofpower/FeatureFusion/stargazers) [](https://github.com/Maxofpower/FeatureFusion/commits) [](https://www.linkedin.com/in/mhhoseini/) [](https://github.com/Maxofpower/FeatureFusion)
Author · Mohammad Hasan Hosseini · Technical Team Lead & .NET enthusiast
Table of contents
- [BuildingBlocks](#buildingblocks)
- [How they work together](#how-they-work-together)
- [BuildingBlocks.Mediator](#buildingblocksmediator)
- [BuildingBlocks.Mcp](#buildingblocksmcp)
- [BuildingBlocks.Pagination.EntityFrameworkCore](#buildingblockspaginationentityframeworkcore)
- [BuildingBlocks.Telemetry](#buildingblockstelemetry)
- [BuildingBlocks.Aspire.Hosting.SigNoz](#buildingblocksaspirehostingsignoz)
- [Lab](#lab)
- [Pagination showcase](#pagination-showcase)
- [Architecture](#architecture)
- [Stack](#stack)
- [Repository layout](#repository-layout)
- [Prerequisites](#prerequisites)
- [Run the lab](#run-the-lab)
- [Lab features](#lab-features)
- [Design patterns](#design-patterns)
- [LinkedIn catalog](#linkedin-catalog)
- [What's next](#whats-next)
- [Testing](#testing)
- [Contributing](#contributing)
BuildingBlocks
NuGet packages you can install in your hosts. The FeatureFusion API is a showcase, not a required dependency.
| Package | Role | TFMs | |---------|------|------| | BuildingBlocks.Mediator | CQRS Send + ordered pipeline (ICommand / IQuery, typed behaviors, opt-in traces + metrics) | net8 / net9 / net10 | | BuildingBlocks.Mcp | Message types → MCP tools on the official SDK (deny-by-default, McpResult, HTTP + opt-in stdio) | net8 / net9 / net10 | | BuildingBlocks.Pagination.EntityFrameworkCore | Typed keyset (cursor) pagination for EF Core (IR bundled) | net8 / net9 / net10 | | BuildingBlocks.Telemetry | Config-driven OpenTelemetry (traces, metrics, logs) + IntegrateMediator / opt-in IntegrateMcp | net8 / net9 / net10 | | BuildingBlocks.Aspire.Hosting.SigNoz | Local-dev Aspire AddSigNoz() + WithSigNozOtlpExporter | net10 (AppHost) |
Production apps use Mediator + Telemetry and export OTLP to any backend. SigNoz hosting is local AppHost only.
How they work together
flowchart LR
host[Your host]
med[Mediator]
tel[Telemetry]
otlp[OTLP backend]
signoz[SigNoz AppHost]
host --> med
med -->|"UseTelemetry"| tel
tel -->|"AddTelemetry IntegrateMediator"| otlp
signoz -->|"local collector"| otlp
- Mediator dispatches commands/queries through an ordered pipeline.
UseTelemetry()wraps Send (not a pipeline behavior) with an ActivitySource and Meter namedBuildingBlocks.Mediator. - Telemetry
AddTelemetry+IntegrateMediator = trueregisters that source and meter so spans andmediator.sendmetrics export with the rest of the host. - SigNoz hosting (optional, local) provisions a collector + UI.
WithSigNozOtlpExportersetsOTEL_EXPORTER_OTLP_*on a project resource. In production, set the same env vars to your collector.
Compose (same shape as this lab):
// API / worker — BuildingBlocks.Telemetry + BuildingBlocks.Mediator
builder.AddTelemetry(o =>
{
o.IntegrateMediator = true;
o.Instrumentation.Npgsql = true;
});
builder.Services.AddMediator(cfg =>
{
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
cfg.AddOpenBehavior(typeof(ValidationBehavior), order: 0); // host-owned
cfg.UseTelemetry();
cfg.ValidateOnStartup = true;
});
// AppHost — BuildingBlocks.Aspire.Hosting.SigNoz (local)
var signoz = builder.AddSigNoz("signoz")
.WithUi()
.WithDashboards();
builder.AddProject("api")
.WithSigNozOtlpExporter(signoz);
BuildingBlocks.Mediator
[](https://www.nuget.org/packages/BuildingBlocks.Mediator) [](https://www.nuget.org/packages/BuildingBlocks.Mediator)
CQRS-first Send + ordered pipeline. Manual control over registration, pipeline order, validation, and telemetry — not a MediatR or messaging replacement (no Publish / INotification in v1).
What's new in 1.1.0: typed ICommandPipelineBehavior / IQueryPipelineBehavior (MS.DI does not construct the opposite kind), AddOpenCommandBehavior / AddOpenQueryBehavior, opt-in Send metrics. Drop-in from 1.0.1.
dotnet add package BuildingBlocks.Mediator
Quick start
public sealed record CreateOrder(string Product, int Qty) : ICommand;
public sealed class CreateOrderHandler : ICommandHandler
{
public Task Handle(CreateOrder command, CancellationToken ct)
=> Task.FromResult(Guid.NewGuid());
}
services.AddMediator(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining();
cfg.AddOpenBehavior(typeof(ValidationBehavior), order: 0);
cfg.UseTelemetry();
cfg.ValidateOnStartup = true;
});
await sender.Send(new CreateOrder("SKU-1", 2), ct);
Prefer ISender. Host OTel: AddSource + AddMeter "BuildingBlocks.Mediator" (or Telemetry IntegrateMediator).
All options
Markers: ICommand / ICommand / IQuery (no public IRequest, no non-generic IQuery). Void: ICommand : ICommand. IMediator is the same Send surface.
public sealed record CreateOrder(string Product, int Qty) : ICommand;
public sealed record CancelOrder(Guid Id) : ICommand;
public sealed record GetOrder(Guid Id) : IQuery;
public sealed class CreateOrderHandler : ICommandHandler
{
public Task Handle(CreateOrder command, CancellationToken ct)
=> Task.FromResult(Guid.NewGuid());
}
public sealed class ValidationBehavior : IPipelineBehavior
{
public Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken ct)
=> next(ct);
}
public sealed class AuditCommands : ICommandPipelineBehavior
where TCommand : ICommand
{
public Task Handle(TCommand command, RequestHandlerDelegate next, CancellationToken ct)
=> next(ct);
}
public sealed class CacheQueries : IQueryPipelineBehavior
where TQuery : IQuery
{
public Task Handle(TQuery query, RequestHandlerDelegate next, CancellationToken ct)
=> next(ct);
}
services.AddMediator(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(CreateOrderHandler).Assembly);
cfg.RegisterServicesFromAssemblyContaining(); // same assembly is deduped
cfg.Lifetime = ServiceLifetime.Scoped; // ISender / IMediator — default Scoped
cfg.HandlerLifetime = ServiceLifetime.Transient; // discovered handlers — default Transient
// Open-generic handlers always resolve Transient (ignore HandlerLifetime)
cfg.AddOpenBehavior(typeof(ValidationBehavior), order: 0); // lower = outermost
cfg.AddOpenCommandBehavior(typeof(AuditCommands), order: 10);
cfg.AddOpenQueryBehavior(typeof(CacheQueries), order: 20);
// cfg.AddBehavior(order: 5);
cfg.UseTelemetry(o =>
{
o.ActivitySourceName = "BuildingBlocks.Mediator";
o.MeterName = ""; // empty → copies ActivitySourceName
o.EnableMetrics = true; // mediator.send.duration, mediator.send
o.EnableLogging = true;
o.RecordException = true;
});
cfg.ValidateOnStartup = true;
});
await sender.Send(new CreateOrder("SKU-1", 2), ct);
await sender.Send(new GetOrder(id), ct);
await sender.Send(new CancelOrder(id), ct);
await sender.Send((object)new CreateOrder("SKU-1", 2), ct); // MCP / dynamic
1.0.1 bases CommandPipelineBehavior / QueryPipelineBehavior still work (runtime skip). Analyzers BBM001 / BBM002. No Publish / INotification.
- Package README: [
src/BuildingBlocks/Mediator/PACKAGE_README.md](src/BuildingBlocks/Mediator/PACKAGE_README.md) - Docs: [getting-started](docs/building-blocks/getting-started.md) · [pipeline](docs/building-blocks/pipeline-behaviors.md) · [cookbook](docs/building-blocks/cookbook.md) · [test matrix](docs/building-blocks/TEST_MATRIX.md)
- Freeze / ADR: [
docs/building-blocks/mediator.md](docs/building-blocks/mediator.md) · [docs/adr/0001-mediator-building-blocks-in-monorepo.md](docs/adr/0001-mediator-building-blocks-in-monorepo.md) - LinkedIn: BuildingBlocks.Mediator v1.0.1 · Mediator Pattern + Pipeline Behavior (prior)
BuildingBlocks.Mcp
[](https://www.nuget.org/packages/BuildingBlocks.Mcp)
Map application message types (commands, queries, DTOs) and public static Minimal API methods to MCP tools. The official C# SDK owns the protocol; this package owns the catalog, McpResult, filters, and safe defaults. Not OpenAPI, not MVC controllers (unsupported for now), not a SOLID linter.
dotnet add package BuildingBlocks.Mcp
Requires .NET 8 / 9 / 10. HTTP default: MapBuildingBlocksMcp() → /mcp. Cursor talks to a running API (url). Stdio (UseStdioTransport(), logs on stderr) is for console hosts only — do not enable it on a web API. Host OpenTelemetry: IntegrateMcp = true plus o.UseTelemetry() on the MCP builder.
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();
o.UseMemoryIdempotency(TimeSpan.FromHours(1));
}).UseDispatcher(async (sp, msg, ct) =>
{
await using var scope = sp.CreateAsyncScope();
return await scope.ServiceProvider.GetRequiredService().Send(msg, ct);
});
app.MapBuildingBlocksMcp();
All options — 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();
o.UseTelemetry();
o.UseMemoryIdempotency(TimeSpan.FromHours(1));
}).UseDispatcher(async (sp, msg, ct) =>
{
await using var scope = sp.CreateAsyncScope();
return await scope.ServiceProvider.GetRequiredService().Send(msg, ct);
});
app.MapBuildingBlocksMcp();
UseDispatcher is a singleton; create a scope per call (ISender is scoped). Kind can be omitted when the type implements Mediator ICommand / IQuery. Tool-level Description is required. Property [Description] is optional (JSON Schema text only).
All options — Minimal API — same method as MapGet / MapPost
JSON binds to one request parameter. CancellationToken, McpInvokeContext, interfaces, and ILogger come from DI. HttpContext is not the MCP body (null outside HTTP). Do not use [FromHeader] types as the MCP input.
A — [McpTool] + scan (attribute is enough; scan picks up public static methods):
[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()));
B — [McpTool] + .WithMcp(app) (same tool; scan and WithMcp dedupe by name). Pass the IEndpointRouteBuilder used for MapGet:
api.MapGet("/lab-ping", LabPing).WithMcp(app);
C — .WithMcp(app, "name", "description") without an attribute. GET → query (no idempotency key). POST/PUT → command (idempotent write). Other verbs need Kind in configure.
api.MapPost("/items", CreateItem).WithMcp(app, "items.create", "Create an item");
D — MapTool when the HTTP signature cannot be the MCP input (FromHeader, multiple bodies). Dedicated DTO + handler (scoped IServiceProvider overload for validators / feature flags).
o.MapTool(
"greetings.custom",
"Dedicated MCP DTO — not the HTTP FromHeader model",
async (sp, msg, ctx, ct) => McpResult.Ok("…"),
a => a.Kind = McpToolKind.Query);
MVC controller classes and actions are unsupported for now.
Idempotency (writes only)
MCP has no HTTP verb on Mediator messages. Command ≈ POST/PUT; Query ≈ GET.
| | Command | Query | |--|---------|--------| | Default | Idempotent = true | never uses the store | | Client | must send idempotencyKey when a store is registered | do not require a key | | Schema | string + format: uuid (hint; host accepts any non-empty string, including ULID) | no key property | | Opt out | Idempotent = false (lab demo.echo) | — |
Register a store with o.UseMemoryIdempotency(ttl) (single instance). Multi-instance: implement IMcpIdempotencyStore (Redis, etc.). Keys are namespaced per tool; in-flight calls share a lock; success is replayed as JsonElement. The library never retries writes. Cursor/Claude fill idempotencyKey from the tool schema (they do not inject a key unless it is required). Reuse the same UUID only when retrying the same write. RequireConfirmation adds required confirmed: true.
Cursor HTTP:
{
"mcpServers": {
"featurefusion": {
"url": "http://localhost:5141/mcp"
}
}
}
- Package README: [
src/BuildingBlocks/Mcp/PACKAGE_README.md](src/BuildingBlocks/Mcp/PACKAGE_README.md) - Docs: [
docs/building-blocks/mcp.md](docs/building-blocks/mcp.md) · ADR [0002](docs/adr/0002-mcp-message-tools.md) · [test matrix](docs/building-blocks/MCPTESTMATRIX.md) - Lab (Development):
orders.create,products.list,demo.echo,lab.pingathttp://localhost:5141/mcp - Catalog:
docs/linkedin-posts.md→mcp-message-tools(planned)
BuildingBlocks.Pagination.EntityFrameworkCore
[](https://www.nuget.org/packages/BuildingBlocks.Pagination.EntityFrameworkCore)
Typed keyset (cursor) pagination for EF Core. One package — SortKey / cursors ship inside it. Hosts map a sort enum to a prebuilt key — the library never reflects "Price" into a property. Unique last column required. Set SigningKey on public HTTP APIs. Dapper is an in-repo lab project, not a nupkg. There is no IEnumerable adapter.
dotnet add package BuildingBlocks.Pagination.EntityFrameworkCore
Requires .NET 8 / 9 / 10.
var key = SortKey.For()
.By(p => p.Price)
.ThenByUnique(p => p.Id);
var page = await db.Products
.AsNoTracking()
.ToCursorPageAsync(new CursorRequest(cursor, 20), key);
Optional PaginationOptions.Hint defaults to None. ReadUncommitted is SQL Server session isolation (not WITH (NOLOCK)): EF starts one transaction around COUNT+PAGE when there is no ambient transaction, then restores READ COMMITTED on the still-open connection; ambient is ignored; PostgreSQL and Sqlite ignore it. Host `AsNoTrackin
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Maxofpower
- Source: Maxofpower/FeatureFusion
- License: MIT
- Homepage: https://www.linkedin.com/in/mhhoseini/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.