Skip to main content
McpEssentials.PostgreSQL is the authenticated task store for hosts that already talk to Postgres through Npgsql or Dapper. It subclasses NotifyingMcpTaskStoreBase and persists StoredMcpTask as one McpTasks table. Prefer this over Entity Framework Core with UseNpgsql when there is no DbContext to hang AddMcpTasksTable on, or when several MCP nodes wait on ElicitAsync and you want Postgres itself to wake the originating process. Redis pub/sub is the other native channel.

No EF model

The host already uses Npgsql or Dapper. There is no DbContext to hang AddMcpTasksTable on.

LISTEN / NOTIFY

Postgres itself wakes ElicitAsync waiters. Echoes of this process are dropped.

RowVersion CAS

Integer RowVersion on public.McpTasks. INSERT is committed before create returns.
The host owns NpgsqlDataSource. This package ships the store, the table script, and registration. Register the data source as a singleton.
Entity Framework Core already talks to PostgreSQL (UseNpgsql). Stay there when the host has a DbContext and migrations. EF + Npgsql still needs an optional IMcpTaskInputSignalBus. Native Postgres is that bus.

Install

1

Add the package

2

Register the data source

Wire the store

Same identity factory as Authenticated tasks. PostgreSQL has no generic context type, so the MCP builder method is WithPostgreSqlTasks rather than WithTasksPlus<TContext>.
Schema, table, and listen-channel names must be 1–63 ASCII letters, digits, or underscores, starting with a letter or underscore. The store quotes them for DDL.

Table and TTL

On first use the store creates the table when missing (CREATE TABLE IF NOT EXISTS) with indexes on OwnerUserId and DiscardAfter. Hosts that own migrations can create the same shape themselves. CreateTaskAsync awaits INSERT commit so a later get on any node can see the row. Updates compare TaskId and RowVersion. A terminal owner get with ServeAndDrop (default) deletes the row after serving it. DiscardAfter is timestamptz, stamped at write as CreatedAt + 2×TimeToLive. AutoSweep defaults to true. Sweep is DELETE WHERE "DiscardAfter" <= now. Call SweepExpiredAsync from a hosted service or pg_cron when AutoSweep is off. Unlimited TTL (null) leaves the row until a terminal owner get or an explicit delete.
LISTEN holds one connection for the life of the store. Size the pool for ordinary CRUD plus that extra connection. Postgres does not deliver notifications on a connection that is sitting in the pool.

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 row is not found; get does not distinguish expiry from isolation.

Events

After a successful resolve, the publishing node raises InputResponseReceived locally, then pg_notify with taskId + newline + requestId on the listen channel. Other nodes LISTEN and raise a stub response so ElicitAsync waiters wake. The publisher drops its own notify echo.
Postgres is the bus. Redis pub/sub is the other native channel on NotifyingMcpTaskStoreBase.

Sample

Runnable host at http://localhost:5320/. Send X-User-Id: alice. Default connection string is Host=localhost;Database=mcpessentials.

Stores

Pick a backing store by the engine you already run.

Entity Framework Core

Stay on EF + UseNpgsql when the host already has a DbContext.

Redis

The other native channel: pub/sub plus PEXPIREAT.