> ## Documentation Index
> Fetch the complete documentation index at: https://si.easyaf.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Redis

> Persist per-user MCP tasks as Redis strings. Native PEXPIREAT from DiscardAfter and pub/sub for multi-node elicitation.

`McpEssentials.Redis` is the authenticated task store for hosts that already run Redis. It subclasses [`NotifyingMcpTaskStoreBase`](https://si.easyaf.dev/mcpessentials/api-reference/CloudNimble/McpEssentials/Core/Tasks/NotifyingMcpTaskStoreBase) and persists [`StoredMcpTask`](https://si.easyaf.dev/mcpessentials/api-reference/CloudNimble/McpEssentials/Core/Tasks/StoredMcpTask) snapshots as JSON strings.

Prefer this over [`IDistributedCache`](https://si.easyaf.dev/mcpessentials/guides/stores/distributed-cache) when the cache is Redis and several nodes wait on `ElicitAsync`. Redis expires keys; the request-path sweep is off by default.

> [card] JSON strings
>
> One key per task. `OwnerUserId` lives on the snapshot.

> [card] Native expiry
>
> `PEXPIREAT` from `DiscardAfter`. `AutoSweep` stays off.

> [card] Pub/sub fan-out
>
> Resolved input publishes on `{prefix}:task:input`. Echoes of this process are dropped.

The host owns [`IConnectionMultiplexer`](https://stackexchange.github.io/StackExchange.Redis/Configuration). This package ships the store and registration. Register the multiplexer as a singleton.

## Install

**1. Add the package**

```bash theme={"dark"}
dotnet add package McpEssentials.Redis
```

**2. Register the multiplexer**

```csharp theme={"dark"}
builder.Services.AddSingleton<IConnectionMultiplexer>(
    _ => ConnectionMultiplexer.Connect(connectionString));
```

## Wire the store

Same identity factory as [Authenticated tasks](https://si.easyaf.dev/mcpessentials/guides/authenticated-tasks.md). Redis has no generic context type, so the MCP builder method is [`WithRedisTasks`](https://si.easyaf.dev/mcpessentials/api-reference/Microsoft/Extensions/DependencyInjection/IMcpServerBuilder) rather than `WithTasksPlus<TContext>`.

> [tab] HTTP
>
> ```csharp theme={"dark"}
> builder.Services.AddHttpContextAccessor();
> builder.Services.AddSingleton<IConnectionMultiplexer>(
>     _ => ConnectionMultiplexer.Connect(connectionString));
>
> builder.Services.AddMcpServer()
>     .WithHttpTransport(o => o.Stateless = true)
>     .WithTools<MyTools>()
>     .WithRedisTasks(
>         sp => sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.User,
>         o =>
>         {
>             o.DefaultTimeToLive = TimeSpan.FromMinutes(5);
>             o.KeyPrefix = "mcp";
>             o.WaitReplicas = 1;
>         });
> ```

> [tab] Stdio
>
> ```csharp theme={"dark"}
> builder.Services.AddSingleton<IConnectionMultiplexer>(
>     _ => ConnectionMultiplexer.Connect(connectionString));
>
> builder.Services.AddMcpServer()
>     .WithRedisTasks("local-user");
> ```

> [tab] Store only
>
> Same factory, without `IMcpServerBuilder`:
>
> ```csharp theme={"dark"}
> builder.Services.AddHttpContextAccessor();
> builder.Services.AddSingleton<IConnectionMultiplexer>(
>     _ => ConnectionMultiplexer.Connect(connectionString));
>
> builder.Services.AddMcpEssentialsRedisTasks(
>     sp => sp.GetRequiredService<IHttpContextAccessor>().HttpContext?.User,
>     o => o.DefaultTimeToLive = TimeSpan.FromMinutes(5));
> ```

## Keys and TTL

| Key / channel | Role |
| - | - |
| `{prefix}:task:{taskId}` | JSON snapshot, including `OwnerUserId` |
| `{prefix}:task:input` | Pub/sub for resolved input (`taskId` + `requestId`) |

`DiscardAfter` is stamped at write as `CreatedAt + 2×TimeToLive`. Redis `PEXPIREAT` uses that instant. Unlimited TTL (`null`) leaves the key until a terminal owner get (`ServeAndDrop`) or an explicit delete.

`AutoSweep` defaults to `false`. [`SweepExpiredAsync`](https://si.easyaf.dev/mcpessentials/api-reference/CloudNimble/McpEssentials/Core/Tasks/IMcpTaskStorePlus) is a no-op; Redis owns expiry. Per-row get still honors `ShouldDiscard` so a custom clock can drop a key before Redis does. A custom `ShouldDiscard` that disagrees with `DiscardAfter` should also override `SweepExpiredCoreAsync` and skip native expiry.

> [!NOTE]
>
> Replica sets: set `WaitReplicas` so `CreateTaskAsync` does not return until a later get on another node can see the key. Standalone Redis leaves this at 0.

## Isolation

`CreateTaskAsync` stamps `OwnerUserId`. Client-facing get/cancel/resolve require that user. Server-side complete/fail/input-request skip the live-user check. Another user's task is not found. A missing key is not found; get does not distinguish expiry from isolation.

Creates use a Lua `EXISTS` then `SET`. Updates compare the expected JSON then `SET` + `PEXPIREAT`. A terminal owner get with `ServeAndDrop` (default) deletes the key after serving it.

## Events

After a successful resolve, the publishing node raises `InputResponseReceived` locally, then publishes `taskId` + newline + `requestId` on `{prefix}:task:input`. Other nodes subscribe and raise a stub response so `ElicitAsync` waiters wake. The publisher drops its own pub/sub echo.

> [!TIP]
>
> Redis **is** the bus. Elicitation fan-out is pub/sub on `{prefix}:task:input`.

## Related

> [card] [Sample](https://github.com/CloudNimble/McpEssentials/tree/main/samples/CloudNimble.McpEssentials.Samples.Redis)
>
> Runnable host. Expects Redis at `localhost:6379`.

> [card] [Stores](https://si.easyaf.dev/mcpessentials/guides/stores.md)
>
> Pick a backing store by the engine you already run.

> [card] [PostgreSQL](https://si.easyaf.dev/mcpessentials/guides/stores/postgresql.md)
>
> The other native channel: `LISTEN` / `NOTIFY` on Npgsql.

> [card] [IDistributedCache](https://si.easyaf.dev/mcpessentials/guides/stores/distributed-cache.md)
>
> Specified abstraction over whatever cache you already registered.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.