From 58408aaa83646f10dfe02e51c0a5d555c0cc276e Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Fri, 23 Oct 2026 20:17:17 +0200 Subject: [PATCH] fix(19.7): RA-IN-03 document that the Redis session store needs a standalone Redis reserveScript reads the record keys of the binding set's members, which it cannot declare in KEYS, and its declared keys hash to different slots, so Redis Cluster refuses it and a key-pattern ACL must allow the whole mcp: range. The script's comment, the pier README's mcp.sessions.store row (which also no longer says the store opens on the first request) and docs/services/mcp.md say so. --- docs/services/mcp.md | 2 +- modules/pier/README.md | 2 +- modules/pier/store_redis.go | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/services/mcp.md b/docs/services/mcp.md index be1cba94..affd4e87 100644 --- a/docs/services/mcp.md +++ b/docs/services/mcp.md @@ -33,7 +33,7 @@ path: /mcp | `mcp.enabled` | `false` | Mounts POST, GET and DELETE at `mcp.path`. | | `mcp.path` | `/mcp` | The endpoint path. A route a plugin already registered there fails boot. | | `mcp.allow_personal_tokens` | `false` | Also accepts personal API tokens (see Identity). | -| `mcp.sessions.store` | `memory` | `memory` for one instance, `redis` to share sessions and events across instances through `redis.url`. | +| `mcp.sessions.store` | `memory` | `memory` for one instance, `redis` to share sessions and events across instances through `redis.url` (a standalone Redis; Redis Cluster is not supported). | | `mcp.sessions.idle_ttl` | `30m` | Sliding idle TTL of a session. | | `mcp.sessions.max_per_binding` | `20` | Open sessions one client-and-user pair (or one personal token) may hold. | | `mcp.resource` | `app.url` + `mcp.path` | The resource indicator; must equal the OAuth server's MCP resource. | diff --git a/modules/pier/README.md b/modules/pier/README.md index fb550687..4bf81297 100644 --- a/modules/pier/README.md +++ b/modules/pier/README.md @@ -121,7 +121,7 @@ Configuration keys are exported as constants: `pier.EnabledKey`, `pier.PathKey`, | `mcp.enabled` | `false` | Mounts the endpoint. `true` in a binary built without the `summercms_mcp` tag fails boot naming the tag. | | `mcp.path` | `/mcp` | The endpoint path: an absolute URL path without scheme, host, query, wildcard or dot segments. A route a plugin registered there fails boot naming the plugin. | | `mcp.allow_personal_tokens` | `false` | Lets personal API tokens open sessions: an admin `g15_` token yields backend tools only, a frontend token (through the published resolver) frontend tools only, each capped by the token's own scope. OAuth tokens are always accepted. | -| `mcp.sessions.store` | `memory` | `memory` (one instance; sessions end with the process and events reach only its own sessions) or `redis` (sessions and events shared by every instance through `redis.url`, opened on the first request). | +| `mcp.sessions.store` | `memory` | `memory` (one instance; sessions end with the process and events reach only its own sessions) or `redis` (sessions and events shared by every instance through `redis.url`, opened at mount by a background warm-up). The Redis store needs a standalone Redis, not Redis Cluster: its atomic reservation script reads keys of several hash slots, some not declared in `KEYS`, so an ACL restricted by key pattern must allow the whole `mcp:` range. | | `mcp.sessions.idle_ttl` | `30m` | Sliding idle TTL of a session record. | | `mcp.sessions.max_per_binding` | `20` | Open sessions one binding (client and user, or personal token) may hold; the next `initialize` answers 429. | | `mcp.resource` | `app.url` + `mcp.path` | The resource indicator the endpoint identifies itself by; must equal the OAuth server's MCP resource. Required (or `app.url`) when enabled. | diff --git a/modules/pier/store_redis.go b/modules/pier/store_redis.go index 3802e405..71435e29 100644 --- a/modules/pier/store_redis.go +++ b/modules/pier/store_redis.go @@ -141,6 +141,11 @@ func (s *redisStore) Count(ctx context.Context, binding string) (int, error) { // writes the record with its PX TTL, adds the id to the set and slides the // set's TTL (1). KEYS: the binding set, the record. ARGV: the record key // prefix, max, the record JSON, the TTL in milliseconds, the session id. +// The script also reads the record keys of the set's members, which it +// cannot declare in KEYS, and the binding set and the record do not share a +// hash slot: the store needs a standalone Redis (or a primary with +// replicas). Redis Cluster refuses the script as cross-slot, and an ACL +// that restricts keys by pattern must allow the whole mcp: range. var reserveScript = redis.NewScript(` local members = redis.call('SMEMBERS', KEYS[1]) local n = 0