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.NpgsqlDataSource. This package ships the store, the table script, and registration. Register the data source as a singleton.
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 isWithPostgreSqlTasks rather than WithTasksPlus<TContext>.
- HTTP
- Stdio
- Store only
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 raisesInputResponseReceived 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.
Related
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.