Release v1.23.0
Adds a trusted-gateway auth mode for deployments behind a reverse proxy, plus fixes for tools that were returning the wrong thing: trashed files in Drive search, empty structure for tabbed Docs, missing metadata on ranged calendar events, and Apps Script pushes that deleted files you didn't touch.
Per-user identity behind a reverse proxy
The big addition is TRUST_GATEWAY_IDENTITY (#981, #891 - huge thanks @123andy for the original implementation), a new auth mode for anyone running the server behind an MCP-aware proxy like Pomerium, Cloudflare Access, oauth2-proxy, or Traefik ForwardAuth.
Previously, running behind a gateway meant the proxy and the MCP server both wanted to own the OAuth handshake. Now the server can verify the proxy's signed identity assertion (JWT, checked against its JWKS) and use the asserted email as the per-request principal - real per-user isolation without terminating MCP OAuth itself.
The mode is deliberately fail-closed:
- Assertions are verified cryptographically, with the algorithm pinned to block
none/alg-confusion, andaudalways checked so a token minted for another route can't be replayed. - A missing or invalid assertion rejects the request. There's no fallback to bearer tokens or transport-session identity.
- Like OAuth 2.1 mode,
user_google_emailis hidden from tool schemas and auto-filled from the verified principal, so a caller can't act as another account by passing one. A configuredUSER_GOOGLE_EMAILdefault is ignored - the principal always wins. - The Google consent flow binds its OAuth state to that principal, and
/oauth2callbackrejects a callback where the account that actually consented doesn't match.
Setup and the full env var reference live in docs/trusted-gateway-identity.md.
Alongside it, external OAuth metadata now aligns with the MCP mount path (#972 by @sebastianrosch) - protected-resource metadata is derived from where FastMCP actually mounts the endpoint, so discovery and the WWW-Authenticate challenge agree instead of advertising a resource URL that doesn't match.
Tools that return what you asked for
Four fixes in the same spirit - each one a case where a tool's output didn't match a reasonable reading of the request:
- Drive search no longer surfaces trashed files (#978 by @martiant).
search_drive_filesnow appendstrashed=falseby default, matchinglist_drive_itemsand the Drive web UI. An explicittrashedclause in your own query still wins, quoted literals aren't mistaken for one, andinclude_trashed=Trueopts back in. inspect_doc_structureworks on tabbed Docs without atab_id(#976 by @Ofori01). It was reading headers and footers from the first tab but leavingbodypointed at the empty top-level document, so structure inspection came back blank. Body and named ranges now come from the same tab.- Both
get_eventspaths emit identical metadata (#980 by @wdrwilson). The single-event lookup returnedcolorId, recurrence, creator, and organizer while the ranged listing didn't. Shared formatting incalendar_helpersnow drives both branches, with tests asserting parity - andeventType/statusare omitted at their API defaults so ordinary meetings stay compact. read_sheet_valuesclamps oversized reads (#986 by @SlayerBirden). Open-ended or huge A1 ranges are trimmed to 1000 rows before the Sheets request, bounding memory instead of pulling an entire sheet into the response.
Apps Script updates stop being destructive by default
update_script_content used to replace the project's entire file set with whatever you passed, so pushing one file deleted the rest. It now merges by default (#982 by @syf2211, addressing #923): your files overlay the existing project, keyed by (name, type) since Script API names exclude the extension and one project can hold both Code.gs and Code.html.
Ambiguity is rejected rather than guessed - an update missing type only resolves when exactly one existing file carries that name - and file types and the appsscript manifest name are validated up front. Per-script locks serialize concurrent merges within a process. Pass merge=False for the old full-replacement behavior.
Telemetry and startup polish
- Opt-in
user.emailspan attribute (#974 by @DrFaust92). Tool-call traces can now be attributed to a person rather than only the opaqueenduser.idFastMCP derives from the token. It's PII, so it's strictly off unless the operator setsWORKSPACE_MCP_OTEL_USER_EMAIL, and it no-ops cleanly when OpenTelemetry isn't installed. - Reworked startup output. A new
core/startup_ui.pycentralizes the ANSI palette, section rules, and column alignment that were scattered throughmain.py, so the startup screen reads as one coherent block. Emphasis uses SGR attributes instead of hardcoded greys, keeping it legible on light, dark, and tinted terminals, and everything renders through a caller-suppliedemitso the stdout-stays-clean-for-JSON-RPC rule lives in one place.
New Contributors
- @martiant - #978
- @Ofori01 - #976
- @wdrwilson - #980
- @sebastianrosch - #972
- @SlayerBirden - #986
Full Changelog: v1.22.2...v1.23.0