Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to **BuildingBlocks** packages in this repository are docume
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## BuildingBlocks.Mcp [1.1.0] - 2026-09-19

### Added

- **Distributed write idempotency** (`UseDistributedIdempotency` + `UseRedisLock` / `RedisMcpIdempotencyLock`): wait-and-replay across instances. Completed payloads live on the host `IDistributedCache` (`mcp:idemp:{tool}\u001f{clientKey}`); in-flight work is serialized with a SET NX PX lease (lock key `{payloadKey}:lock`) using the host `IConnectionMultiplexer`. Waiters poll and replay; they do not get HTTP Processing/409 while the lease is valid. Resolving `IMcpInvoker` fails if the lock is missing (never execute unlocked). Custom `IMcpIdempotencyLock` remains supported. The package does not reference `BuildingBlocks.Idempotency`.
- **2026 MRTR confirmation:** when the client advertises MCP `2026-07-28` (`IsMrtrSupported`), an unconfirmed `RequireConfirmation` write becomes SDK `InputRequiredException` / `resultType: input_required` elicitation for `confirmed` (`requestState` `awaiting-confirmation`, opaque echo — not a server session). Accept sets `McpInvokeContext.Confirmed` and invokes; decline returns `ConfirmationRequired` without invoking. MCP `2025-11-25` clients still receive `ConfirmationRequired` JSON. `confirmed: true` skips elicitation on both revisions.

### Notes

- Distributed mode is **not** exactly-once. The default **2-minute lease** is an in-flight safety window with **no renewal**. Lease expiry can overlap executions. A crash before Set, or a Set failure after a successful invoke, can cause another execution (at-least-once). Wait-budget exhaustion (`AcquireWaitBudget`, default 30 seconds) returns MCP `Conflict`; the client should retry (replay if Set completed). Queries never use the store or lock.
- Memory `UseMemoryIdempotency` is unchanged (process `SemaphoreSlim` wait-and-replay).
- Drop-in for hosts that stay on memory idempotency and `confirmed: true`. Lab default host remains memory; distributed Redis is a `WithWebHostBuilder` overlay using `RedisMcpIdempotencyLock`.

## BuildingBlocks.Pagination.EntityFrameworkCore [1.1.0] - 2026-09-04

### Added
Expand Down
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Formerly [FeatureManagement](https://github.com/Maxofpower/FeatureManagement) (G
## Table of contents

- [BuildingBlocks](#buildingblocks)
- [In-repo (not on NuGet)](#in-repo-not-on-nuget)
- [How they work together](#how-they-work-together)
- [BuildingBlocks.Mediator](#buildingblocksmediator)
- [BuildingBlocks.Mcp](#buildingblocksmcp)
Expand Down Expand Up @@ -59,14 +60,26 @@ NuGet packages you can install in **your** hosts. The FeatureFusion API is a sho
| Package | Version | Role | TFMs |
|---------|---------|------|------|
| **[BuildingBlocks.Mediator](https://www.nuget.org/packages/BuildingBlocks.Mediator)** | **1.1.0** | CQRS **Send** + ordered pipeline (`ICommand` / `IQuery`, typed behaviors, opt-in traces + metrics) | net8 / net9 / net10 |
| **[BuildingBlocks.Mcp](https://www.nuget.org/packages/BuildingBlocks.Mcp)** | **1.0.0** | Message types → MCP tools on the official SDK (deny-by-default, `McpResult`, HTTP + opt-in stdio) | net8 / net9 / net10 |
| **[BuildingBlocks.Mcp](https://www.nuget.org/packages/BuildingBlocks.Mcp)** | **1.1.0** | Message types → MCP tools on the official SDK (deny-by-default, `McpResult`, HTTP + opt-in stdio; distributed wait-and-replay idempotency, 2026 MRTR confirmation) | net8 / net9 / net10 |
| **[BuildingBlocks.Idempotency](https://www.nuget.org/packages/BuildingBlocks.Idempotency)** | **1.0.1** | HTTP **Idempotency-Key** — MVC + Minimal API, 2xx envelope replay, ProblemDetails, optional Redis lock, fingerprint, ActivitySource | net8 / net9 / net10 |
| **[BuildingBlocks.Pagination.EntityFrameworkCore](https://www.nuget.org/packages/BuildingBlocks.Pagination.EntityFrameworkCore)** | **1.1.0** | Typed keyset (cursor) pagination for EF Core (IR bundled): any-width Npgsql row comparison, `NULLS FIRST/LAST`, `HasKeysetIndex` + `NullOrder` | net8 / net9 / net10 |
| **[BuildingBlocks.Telemetry](https://www.nuget.org/packages/BuildingBlocks.Telemetry)** | **1.0.2** | Config-driven OpenTelemetry (traces, metrics, logs) + `IntegrateMediator` / opt-in `IntegrateMcp` | net8 / net9 / net10 |
| **[BuildingBlocks.Aspire.Hosting.SigNoz](https://www.nuget.org/packages/BuildingBlocks.Aspire.Hosting.SigNoz)** | **1.0.0** | Local-dev Aspire `AddSigNoz()` + `WithSigNozOtlpExporter` | net10 (AppHost) |

Production apps use **Mediator + Telemetry** and export OTLP to any backend. SigNoz hosting is **local AppHost only**.

### In-repo (not on NuGet)

These BuildingBlocks live in the same solution and are used by the lab. They are **project-reference only** — not published to nuget.org. Do not treat the NuGet table above as the full catalog.

| Project | Role | TFMs |
|---------|------|------|
| **[BuildingBlocks.Domain](src/BuildingBlocks/Domain/AGENTS.md)** | Focused DDD primitives: `Entity`, `AggregateRoot`, `ValueObject`, typed `Identity` / `AggregateId` / `EntityId`, `IBusinessRule` | net8 / net9 / net10 |
| **[BuildingBlocks.Domain.EntityFrameworkCore](src/BuildingBlocks/Domain.EntityFrameworkCore/AGENTS.md)** | EF Core converters for Domain identities and value objects (`HasIdentityConversion` / `HasValueObjectConversion`) | net8 / net9 / net10 |
| **[BuildingBlocks.Pagination.Dapper](src/BuildingBlocks/Pagination.Dapper/AGENTS.md)** | Same keyset IR as the EF package, over `IDbConnection` (`QueryCursorPageAsync`). Lab/dev adapter — not a nupkg | net8 / net9 / net10 |

Pagination IR (`BuildingBlocks.Pagination`) is **bundled into** `BuildingBlocks.Pagination.EntityFrameworkCore`. Do not pack IR or Dapper separately. Domain is ready as a sibling; it has not earned a nuget.org boundary yet.

### How they work together

```mermaid
Expand Down Expand Up @@ -235,6 +248,10 @@ await sender.Send((object)new CreateOrder("SKU-1", 2), ct); // MCP / dynamic

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.

**What's new in 1.1.0:**
- **Distributed write idempotency** — `UseDistributedIdempotency` + `UseRedisLock` (wait-and-replay across instances; not HTTP Processing/409). Default 2-minute lease is a safety window with **no renewal**, not exactly-once. Wait-budget exhaustion is MCP `Conflict` (client retries). MCP keys `mcp:idemp:…` are distinct from HTTP `Idempotency_*`. Custom `IMcpIdempotencyLock` still works. This package does not reference `BuildingBlocks.Idempotency`.
- **2026 MRTR confirmation** — unconfirmed `RequireConfirmation` writes elicit `confirmed` via SDK `InputRequiredException` when the client is MCP `2026-07-28`. Accept invokes; decline does not. `2025-11-25` still returns `ConfirmationRequired` JSON.

```bash
dotnet add package BuildingBlocks.Mcp
```
Expand Down Expand Up @@ -336,7 +353,7 @@ MCP has no HTTP verb on Mediator messages. **Command ≈ POST/PUT**; **Query ≈
| 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`.
Register a store with `o.UseMemoryIdempotency(ttl)` (single instance, process wait-and-replay). Multi-instance: `o.UseDistributedIdempotency().UseRedisLock()` (host `IDistributedCache` + `IConnectionMultiplexer`; wait-and-replay; 2-minute lease is a safety window, not exactly-once; **no renewal** in 1.1.0). Custom `IMcpIdempotencyLock` instead of `UseRedisLock` is still allowed. Cache Get/Set without a lock is not enough. Wait-budget exhaustion (`AcquireWaitBudget`, default 30 seconds) is MCP `Conflict` — not HTTP 409 Processing; the client should retry. MCP payload keys (`mcp:idemp:…`) are distinct from HTTP `Idempotency_*`. Queries never use this mechanism. 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` still requires `confirmed: true`; MCP `2026-07-28` clients get elicitation, `2025-11-25` stays `ConfirmationRequired` JSON.

Cursor HTTP:

Expand All @@ -362,7 +379,7 @@ Cursor HTTP:
[![NuGet](https://img.shields.io/nuget/v/BuildingBlocks.Idempotency.svg?logo=nuget)](https://www.nuget.org/packages/BuildingBlocks.Idempotency)
[![GitHub Release](https://img.shields.io/github/v/release/Maxofpower/FeatureFusion?filter=idempotency-v*&logo=github&label=GitHub%20Release)](https://github.com/Maxofpower/FeatureFusion/releases?q=idempotency-v)

ASP.NET Core HTTP **Idempotency-Key** for MVC and Minimal API. Host-owned `IDistributedCache`, **2xx** envelope replay, ProblemDetails on conflicts, optional Redis SET NX lock, opt-in method/path/body fingerprint, per-endpoint TTL, optional ActivitySource. Distinct from MCP write idempotency (`UseMemoryIdempotency` / `IMcpIdempotencyStore` above).
ASP.NET Core HTTP **Idempotency-Key** for MVC and Minimal API. Host-owned `IDistributedCache`, **2xx** envelope replay, ProblemDetails on conflicts, optional Redis SET NX lock, opt-in method/path/body fingerprint, per-endpoint TTL, optional ActivitySource. Distinct from MCP write idempotency (`UseMemoryIdempotency` / `UseDistributedIdempotency` + `IMcpIdempotencyLock` above).

**What's new in 1.0.1:** NuGet package icon; **System.Text.Json** for cache envelope and MVC `ObjectResult` capture (dropped Newtonsoft.Json). No API surface change from 1.0.0.

Expand Down Expand Up @@ -716,6 +733,7 @@ Install the packages above in your own hosts, **or** clone this repo and run **F
| Area | What you get |
|------|----------------|
| Mediator (CQRS) | **`BuildingBlocks.Mediator`** — used by FeatureFusion handlers |
| Domain | **`BuildingBlocks.Domain`** + **`Domain.EntityFrameworkCore`** — aggregates / identities (in-repo, not packed) |
| MCP | **`BuildingBlocks.Mcp`** — opt-in tools (`[McpTool]` on types/methods or `MapTool`) at `/mcp` |
| Telemetry | **`BuildingBlocks.Telemetry`** in ServiceDefaults; **`BuildingBlocks.Aspire.Hosting.SigNoz`** on AppHost |
| Event bus | RabbitMQ + transactional outbox/inbox, DLQ, dedup hooks |
Expand Down Expand Up @@ -983,6 +1001,7 @@ See [Pagination showcase](#pagination-showcase) for the FeatureFusion catalog (`
|---------|-------------------|
| **Mediator / CQRS** | `BuildingBlocks.Mediator` — `ICommand`/`IQuery` Send + pipeline; host handlers in FeatureFusion |
| **CQRS** | `Features/.../Commands` + `Queries` with dedicated handlers |
| **DDD primitives** | `BuildingBlocks.Domain` — `Entity` / `AggregateRoot` / `ValueObject` / typed ids (in-repo, not packed) |
| **Void command** | `ICommand : ICommand<Unit>` — concrete type in pipeline (no Adapter / `IRequest`) |
| **Decorator** | Pipeline behaviors; EventBus handler decorators in tests |
| **Singleton** | Cached mediator wrappers / long-lived Redis multiplexer |
Expand All @@ -991,7 +1010,7 @@ See [Pagination showcase](#pagination-showcase) for the FeatureFusion catalog (`
| **Unit of work** | `ResilientTransaction` spanning business write + outbox |
| **Strategy** | Feature filters & validation styles (endpoint filter vs ValidationBehavior) |
| **Template method** | `BaseValidator.PostInitialize` |
| **Keyset pagination** | `BuildingBlocks.Pagination.EntityFrameworkCore` — typed bidirectional cursors |
| **Keyset pagination** | `BuildingBlocks.Pagination.EntityFrameworkCore` — typed bidirectional cursors; Dapper adapter is in-repo only |
| **Chain of Responsibility** | Feature toggle rule evaluation; mediator pipeline chain |
| **Observer / messaging** | RabbitMQ integration events (outbox → bus → handlers) |
| **Outbox / Inbox** | `TransactionalOutbox` + `OutBoxWorker` |
Expand Down Expand Up @@ -1039,9 +1058,10 @@ dotnet test FeatureFusion.sln -c Release

| Project | Notes |
|---------|--------|
| `BuildingBlocks.Domain.Tests` | DDD primitives (in-repo; not a nupkg) |
| `BuildingBlocks.Mediator.Tests` | Package suite on **net8 / net9 / net10** |
| `BuildingBlocks.Mediator.Analyzers.Tests` | BBM001 / BBM002 |
| `BuildingBlocks.Mcp.Tests` | Catalog, invoker, endpoint methods, MapTool scoped SP, idempotency, filters |
| `BuildingBlocks.Mcp.Tests` | Catalog, invoker, endpoint methods, MapTool scoped SP, memory + distributed idempotency, 2026 MRTR protocol HTTP, filters |
| `BuildingBlocks.Mcp.Analyzers.Tests` | BBMCP001–005 |
| `BuildingBlocks.Pagination.Tests` | Codec, registry, identifiers (net8 / net9 / net10) |
| `BuildingBlocks.Pagination.EntityFrameworkCore.Tests` | Sqlite keyset + shadow + projection; Postgres Testcontainers when Docker is available |
Expand Down
3 changes: 3 additions & 0 deletions docs/building-blocks/MCP_TEST_MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ xUnit on **net8.0 / net9.0 / net10.0**. No coverlet gate. CI: `.github/workflows
| Aspire live HTTP | `FeatureFusionMcpTests` — tools/list (`demo.echo`, `products.list`, `orders.create`, `lab.ping`), echo, orders.create, products.list schema, `structuredContent`, catalog://tools, `lab.ping` |
| Cursor HTTP | `src/.cursor/mcp.json` → `http://localhost:5141/mcp`; API must be running (see [`mcp.md`](mcp.md)) |
| Idempotency | Commands only; `UseMemoryIdempotency`; missing key; store prevents double dispatch; namespaced keys; TTL; in-flight lock; `JsonElement` replay; queries ignore store |
| Distributed idempotency | `UseDistributedIdempotency` + `UseRedisLock` / `RedisMcpIdempotencyLock` (or custom `IMcpIdempotencyLock`); wait-and-replay (not HTTP 409); shared store+lock one handler; concurrent same key; completed replay; handler throw no Set; abandoned/expired lease; cancel releases; lease-expiry overlap characterized; wait-budget MCP Conflict; wrong-owner Release; acquire throw fail-closed; cache Get/Set failures; different keys; query/unconfirmed skip store+lock; confirmed then replay; negative cache-only two handlers; memory wait-and-replay preserved |
| Redis lock | `RedisMcpIdempotencyLock`: acquire, contention, lease expiry, wrong-owner release, canceled token, Redis error, `UseRedisLock` DI |
| 2026 MRTR / confirmation | Protocol HTTP: `2026-07-28` unconfirmed → `input_required` elicitation; accept invokes; decline `ConfirmationRequired` without invoke; `requestState` echo; `2025-11-25` stays `ConfirmationRequired` JSON; `confirmed: true` skips elicitation |
| Rate limit | Deny → `RateLimited` |
| Confirm / timeout | ConfirmationRequired; Timeout |
| Filter | Hidden from list and invoke |
Expand Down
2 changes: 1 addition & 1 deletion docs/building-blocks/cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ api.MapPost("/items", CreateItem)

### Idempotency

Commands ≈ POST/PUT; queries ≈ GET. Store: `UseMemoryIdempotency(ttl)`. Schema `format: uuid`; runtime any non-empty string. `Idempotent = false` to opt a command out. Queries never use the store. Multi-instance: `IMcpIdempotencyStore`. Lab: `orders.create` (key + `confirmed`), `demo.echo` (opt-out), `lab.ping` (query).
Commands ≈ POST/PUT; queries ≈ GET. Store: `UseMemoryIdempotency(ttl)` (single process, wait-and-replay) or `UseDistributedIdempotency` + `UseRedisLock` (farms; host Redis multiplexer; wait-and-replay; 2-minute lease is not exactly-once; **no renewal**; wait-budget exhaustion is MCP `Conflict`, not HTTP 409). Custom `IMcpIdempotencyLock` instead of `UseRedisLock` is still allowed. Schema `format: uuid`; runtime any non-empty string. `Idempotent = false` to opt a command out. Queries never use the store or lock. `RequireConfirmation`: MCP `2026-07-28` elicits `confirmed`; `2025-11-25` returns `ConfirmationRequired` JSON. Lab: `orders.create` (key + `confirmed`), `demo.echo` (opt-out), `lab.ping` (query). Default Lab host stays on memory idempotency.

Reload Cursor MCP after tool changes.

Expand Down
2 changes: 1 addition & 1 deletion docs/building-blocks/idempotency.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@ Do not reintroduce Lab-local idempotency filter copies; use the package.

## Not this package

MCP write idempotency (`UseMemoryIdempotency`) lives in **BuildingBlocks.Mcp**.
MCP write idempotency (`UseMemoryIdempotency` / `UseDistributedIdempotency` + `UseRedisLock`) lives in **BuildingBlocks.Mcp**. It is wait-and-replay, not HTTP Processing/409, and does not reference this package.
Loading
Loading