Repository navigation
Highlights: a stricter runner guard (docker/podman arguments, container runtimes, and git/URL/archive/local package sources are refused), revoked clients are immutable, unverified OAuth clients get a shorter grant cap, and suspending or deactivating a user cuts their live agent streams. Several behaviour changes need attention on upgrade (see Upgrade notes). protocol wire types are unchanged.
Security
- Docker/podman MCP commands go through a default-deny parser: only
run, with a short list of confinement-preserving options (--rm -i -q --init --read-only --pull --network none|bridge --memory --cpus --pids-limit --cap-drop=ALL --security-opt=no-new-privileges --name --workdir) and a numeric non-zero--user(names are refused because the image's/etc/passwddecides what they map to). Mounts,--privileged, host namespaces,--cap-add, devices,--entrypoint,-e/--env-file, tmpfs and global-H/--host/--contextare rejected. nerdctl, finch and podman-remote get the same check; ctr, crictl, kubectl, buildah, runc, crun, nsenter, unshare, systemd-nspawn, bwrap, lxc-execute and chroot are refused outright.dockerstays out of the default command allowlist, and enabling it needs the Docker socket (host-root equivalent), so it is unsupported for untrusted MCPs (TD-042). - The env-name denylist now also blocks
DOCKER_*,PODMAN_*,CONTAINER_*andCONTAINERS_*(e.g.CONTAINERS_CONF,CONTAINERS_REGISTRIES_CONF) plusREGISTRY_AUTH_FILE,STORAGE_DRIVERandSTORAGE_OPTS(TD-042). - Runner commands (npx, npm exec, pnpm dlx, bunx, uvx/uv, pip, deno, bun) refuse non-registry sources. Per runner: git,
github:/file:/ssh:, http(s) and other URL specs are rejected for all of these runners; archives (.tgz .tar .tar.gz .zip .whl) for the npm family (npx, npm, pnpm, yarn, bunx, pnpx), uv and pip only (not deno or bun); npm'sowner/reposhorthand for the npm family only; local paths for pip only. Rejected sources are caught including after--, after value-less flags such asdeno --allow-net, and for every positional of install-style subcommands (add,install,uv pip install). Error text is generic and never echoes the value (TD-042). - Agent OAuth: dynamically registered (unverified) clients are capped at
agent_endpoint.max_dcr_grant_ttl_seconds(default 7 days, 60 s to 1 year) instead ofmax_grant_ttl_seconds; the effective cap is the smaller of the two. It is enforced at consent and again at code exchange; the consent screen shows the cap. Admin-registered clients, one-shot grants and existing grants are unchanged (TD-080). - Credential placeholders embedded in an MCP's fixed env values (e.g. Notion
OPENAPI_MCP_HEADERS) are filled from the user's credentials under a strict allowlist. Only auth/header-style variables are eligible (key ends in HEADER(S), AUTH, AUTHORIZATION, TOKEN, SECRET, CREDENTIAL(S) or API_KEY), URL/host/endpoint/proxy-shaped templates are never filled, the value must match^[A-Za-z0-9._~+/=:-]{1,4096}$, expansion is single-pass, and anything that cannot be filled is removed with only its key logged (TD-050). - GitHub Actions are pinned to commit SHAs (TD-079).
Changed
- Revoked clients are terminal. Any mutation of a revoked client (status change, rename, config) now returns 409; only DELETE is allowed. The check is part of the UPDATE (
status != 'revoked'), so a concurrent revoke cannot be overwritten, and side effects run only when a row changed. Applies to the admin and user client routes. - Suspending or deactivating a user (PATCH status or DELETE) now aborts that user's in-flight
/mcpagent streams immediately and writes anagent_streams_closedaudit event (reason, stream count, grant count). Grants are kept, so reactivation restores access (TD-025). - Archiving an MCP and pausing its subscriptions is now one transaction (TD-001).
compatTransactionno longer lets a failing ROLLBACK mask the original error, and its internals are typed (TD-003). GET /v1/admin/users/:userId/groupsuses one joined query instead of one lookup per group; the response is unchanged (TD-009, TD-010).- Consent details:
max_ttl_secondsis now the client's effective cap, with an optionalunverified_ttl_limited. The SPA shows "Unverified clients are limited to N" and filters the picker (older servers omit the field, so the SPA stays compatible). POST/PATCH /v1/users/me/clients400VALIDATION_ERRORdetails are now[{path, message}]instead of raw zod issues.- Docker/podman images without a
@sha256digest log a warning at spawn, MCP create/update and registry install. - Dependencies and tooling: vitest 3 to 4.1.12 (TD-016),
drizzle-ormanddrizzle-kitpinned to exact versions (TD-004), 30 stale ESLint disable comments removed and unused disable directives are now a lint error (TD-002), server vitest timeouts raised to 30 s (TD-047), the legacyamb_sk_prefix dropped from the config test fixture (TD-006).
Added
server.body_limit_bytes(default 1048576), used by both listeners.session.heartbeat_min_interval_seconds(default 10) replaces the hard-coded heartbeat limiter and its 429 message. Keep it below the shortest clientheartbeat_interval_seconds(clients allow 15 to 300 s), or that client gets 429s.agent_endpoint.max_dcr_grant_ttl_seconds(default 7 days), see Security.GET /v1/users/me/agent-endpointreturns it as an optional field (the effective cap for unverified clients).- One shared
ARGON2_OPTIONSin core (argon2id, 19456 KiB, 2 passes, 1 lane) replaces 6 copies. The values are identical, so existing and new hashes do not change. - README: a "Session heartbeat" table and a note that lowering
max_grant_ttl_secondsormax_dcr_grant_ttl_secondsonly affects grants created afterwards.
Fixed
PATCHof a user client with an invalidstatusreturned 500 (uncaught ZodError); it is now 400 (TD-013).- Hashed SPA assets under
/app/assets/are cacheable again (public, max-age=31536000, immutable); a globalno-storehook was overriding it. API, auth and HTML responses stay uncached (TD-021). - A per-user credential that fills an env placeholder slot (
KEY: '${KEY}') now lands under the slot's key, not the credential's casing, and a second case-variant credential for a filled slot is dropped (TD-051). - Tests: the group-tool-policy audit-event test selects the event by its metadata instead of racing other events (TD-047).
Upgrade notes
- Composite env values: an MCP env value that contains an embedded
${X}that cannot be filled is now removed instead of being passed with the literal${X}. Check entries whose fixed env contains${...}text that is not a credential. Composite templates are limited to auth/header-style keys (see Security). - Runner package sources: MCPs whose command points at a git URL, http(s) URL, archive,
github:/file:spec, GitHub shorthand or (for pip) a local path are now rejected at create, update, install and spawn. Existing entries that use them fail to start until they are changed to a registry package (e.g.pkg@1.2.3,pkg==1.0). The 73 entries in the community registry are unaffected. - Docker/podman MCPs: commands outside the allowlist above are rejected, and
--usermust be a numeric non-zero uid. The community registry's vendored runner-guard needs a re-sync. - Revoked clients: integrations that edited a revoked client now get 409; delete it instead.
- Heartbeat and body limit:
session.heartbeat_min_interval_secondsabove 60, or at or aboveheartbeat_expected_interval_seconds, now fails config validation at startup, andserver.body_limit_bytesoutside 1 KiB to 64 MiB fails to load. Keep the heartbeat minimum below your shortest clientheartbeat_interval_seconds. - Dynamically registered OAuth clients: they previously got grants up to
max_grant_ttl_seconds(30 days by default); they are now capped at 7 days. Setagent_endpoint.max_dcr_grant_ttl_secondsto keep the old cap. Existing grants are unchanged.