Skip to content

fix(oauth): use getBaseUrl() in DCR so endpoints respect APP_URL behind a proxy - #317

Open
oliverhees wants to merge 136 commits into
metatool-ai:mainfrom
oliverhees:fix/oauth-dcr-base-url
Open

fix(oauth): use getBaseUrl() in DCR so endpoints respect APP_URL behind a proxy#317
oliverhees wants to merge 136 commits into
metatool-ai:mainfrom
oliverhees:fix/oauth-dcr-base-url

Conversation

@oliverhees

Copy link
Copy Markdown

Problem

The Dynamic Client Registration endpoint (POST /oauth/register) builds the
authorization_endpoint / token_endpoint / userinfo_endpoint /
revocation_endpoint it returns from req.protocol + "://" + req.get("host").

Behind a reverse proxy, the backend's Host is the internal address
(e.g. localhost:12009), so registered clients receive unreachable
http://localhost:12009/oauth/* endpoints.

oauth/metadata.ts already solves this with the getBaseUrl(req) helper from
./utils, which prioritizes APP_URL and then X-Forwarded-*. As a result
.well-known/oauth-authorization-server returns the correct public URLs while
DCR returns localhost — an inconsistency between the two handlers.

Spec-compliant clients (e.g. Claude) ignore the non-standard endpoint fields in
the DCR response and use the authorization-server metadata, so they work. Clients
that trust the DCR-returned endpoints get the localhost URLs and fail.

Repro

curl -s -X POST https://<public-host>/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name":"probe","redirect_uris":["http://localhost:3000/callback"],"grant_types":["authorization_code"],"response_types":["code"],"token_endpoint_auth_method":"none"}'
# => "authorization_endpoint":"http://localhost:12009/oauth/authorize"  (despite APP_URL=https://<public-host>)

Fix

Use the existing getBaseUrl(req) helper in both /oauth/register handlers, so
registration is consistent with metadata.ts and respects APP_URL.

elvus and others added 30 commits November 17, 2025 15:20
Added instructions for building the development environment using Dev Containers in Visual Studio Code.
- Added mounted state to prevent hydration mismatches
- Component now returns empty div during SSR
- Only renders actual content after client-side mount
- Fixes Next.js hydration error in sidebar layout
- Introduced `override_title` field in the namespace tool mappings schema to allow customization of tool titles.
- Updated relevant repositories, serializers, and middleware to handle the new title override.
- Enhanced frontend components to support displaying and editing tool titles, including search and filter functionalities.
- Updated documentation to reflect the new tool override capabilities for titles.
  Fixes spawned processes (npx, uvx, pip) failing with SSL errors
  in corporate proxy & firewall environments with custom CA signing certificates.

  Added to whitelist:
  - SSL/certificate vars: NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, etc.
  - Python-specific: REQUESTS_CA_BUNDLE, PIP_CERT, UV_CERT, etc.
  - Proxy vars: HTTP_PROXY, HTTPS_PROXY, NO_PROXY (both cases)

  Affects: process-managed-transport.ts, utils.ts
- add override_annotations column/migrations and propagate through repos, middleware, TRPC, and Zod schemas
- expose tool namespace annotation editing in the enhanced tools table with validation, tooltips, reset behavior, and badge indicators
- adjust tool details layout, README docs, and localization strings to explain annotations editing
- Replace extra headers from TOC
- add `<!-- omit in toc -->`
Added instructions for building the development environment using Dev Containers in VSCode.
- Add ToolsSyncCache for efficient tracking of tool changes per MCP server.
- Implement sync method in tools repository to upsert tools and delete obsolete ones.
- Update tool management in frontend to utilize the new sync functionality.
- Introduce vitest configuration and tests for tools synchronization logic.
- Enhance tools repository with new methods for deleting obsolete tools and syncing tools.
Bumps [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth) from 1.2.9 to 1.4.2.
- [Release notes](https://github.com/better-auth/better-auth/releases)
- [Commits](https://github.com/better-auth/better-auth/commits/v1.4.2/packages/better-auth)

---
updated-dependencies:
- dependency-name: better-auth
  dependency-version: 1.4.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) from 1.16.0 to 1.24.0.
- [Release notes](https://github.com/modelcontextprotocol/typescript-sdk/releases)
- [Commits](modelcontextprotocol/typescript-sdk@1.16.0...1.24.0)

---
updated-dependencies:
- dependency-name: "@modelcontextprotocol/sdk"
  dependency-version: 1.24.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
bobbyhyam and others added 28 commits June 12, 2026 07:57
STDIO MCP servers launched via `uvx <pkg>@latest` cold-install their
dependencies on first spawn. With no volume at the uv cache directory,
every `docker compose up -d`/recreate wipes the writable layer and forces
a cold install again — slow enough to trip the crash detector and stick
the server in error_status=ERROR.

- Dockerfile: create /home/nextjs/.cache/uv at build time, owned by nextjs
  (uid 1001) via the existing chown, so a fresh named volume inherits the
  correct ownership on first mount instead of root:root.
- docker-compose.yml: mount a named volume (uv_cache) at the uv cache dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-leaks

Fix subprocess leaks from race conditions in STDIO transport lifecycle
…oken

feat: implement OAuth refresh_token grant type
…-timeout

fix: touch session timestamps on access for idle-based cleanup

Resolved conflicts in mcp-server-pool.ts by combining:
- metatool-ai#260's per-server connection cap (canCreateConnectionForServer guard)
- ai-dev's env-driven MAX_TOTAL_CONNECTIONS and generation-counter
  concurrency safety from metatool-ai#273

In createIdleSession(), the per-server cap check now runs before the
generation guard, mirroring the already-merged createIdleSessionAsync().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-env-var

fix: read SESSION_LIFETIME from env var for session cleanup
…mable-http-session-reinit-on-404

fix: re-initialize backend session on HTTP 404 Session not found

Resolved conflict in metamcp-proxy.ts tools/call catch block: adopted
metatool-ai#283's guarded re-initialization flow (only retry when
isBackendSessionLostError(error) is true, otherwise rethrow), while
keeping ai-dev's logger convention — converted metatool-ai#283's console.error/
console.warn calls in this block to logger.error/logger.warn to match.

mcp-server-pool.ts (invalidateServerConnection) and the new
session-error.ts / session-error.test.ts files merged cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(i18n): add Portuguese (pt) and Spanish (es) translations
docs(i18n): refine Chinese translation in README.md
Persist uv cache across container recreates
…tes-deployment

docs: add Kubernetes deployment manifests
…v-configurable-session-lifetime

feat(auth): make better-auth session lifetimes env-var configurable
…sert-race-and-ssr-safe

fix(oauth): atomic upsert + SSR-safe OAuth provider (closes metatool-ai#296, metatool-ai#297)
…overy-detectors

fix(session): hardened session-lost + transport-lost recovery detectors

Resolved add/add conflict in session-error.ts / session-error.test.ts by
adopting metatool-ai#293's hardened implementation wholesale. It is a superset of the
naive isBackendSessionLostError() that metatool-ai#283 added: same 404 / "Session not
found" / -32001 / -32600 matching, plus .cause-chain walking (depth 8),
object/string throwable handling, and numeric/string .code inspection.

Also adds isBackendTransportLostError() (-32603 "Not connected") and the
combined isRecoverableBackendError(). These new detectors are additive and
will be wired into the proxy retry paths by the stacked PR metatool-ai#310.

The existing tools/call callsite in metamcp-proxy.ts keeps using
isBackendSessionLostError() with identical (now hardened) behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r-recovery

fix(proxy): invalidate-and-retry recovery in the aggregate list handlers

Extends the session-recovery pattern (previously only on the tools/call
handler from metatool-ai#283) to the four aggregate list handlers (tools/list,
prompts/list, resources/list, resources/templates/list) via a new
requestWithSessionRecovery() helper, using metatool-ai#293's isRecoverableBackendError
(session-lost OR transport-lost).

Conflict resolution: git auto-merged without markers but that silently
produced a DUPLICATE invalidateServerConnection() (one from metatool-ai#283 already on
ai-dev, one from metatool-ai#310). Kept metatool-ai#310's version — it cascades invalidation
across every session slot for the serverUuid (not just the triggering
session) and uses logger instead of console — and deleted metatool-ai#283's narrower
duplicate. Signatures are identical so all callers are unaffected. The
tools/call handler keeps its existing inline metatool-ai#283 recovery.

session-error.ts/.test.ts were already current from the metatool-ai#293 merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-heal server error_status on successful reconnect

Removes the ERROR-state early-return in client.ts that made ERROR a one-way
trapdoor (a recovered server could never reconnect, so could never clear the
flag without a manual UI edit). Now it attempts the connection and, on
success, calls serverErrorTracker.resetServerErrorState() to clear the flag;
a genuinely-broken server gets re-flagged by the crash tracker.

Conflict resolution: config.service.ts MCP_MAX_ATTEMPTS default — both ai-dev
(via metatool-ai#260) and metatool-ai#311 already changed it from 1 to 3, so kept the agreed value
3 and retained metatool-ai#311's explanatory comment. server-error-tracker.ts
fallbackMaxAttempts was likewise already 3 on ai-dev (metatool-ai#260); auto-merged with
no change. Net new behavior is the client.ts self-heal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tool-ai#276

metatool-ai#276 hand-wrote drizzle/0014_oauth_refresh_token.sql but never ran
`drizzle-kit generate`, so there was no meta/0014_snapshot.json and no
_journal.json entry. Because docker-entrypoint.sh applies migrations via
`drizzle-kit migrate` (journal-driven), the un-journaled 0014 was silently
never applied — the refresh_token / refresh_token_expires_at columns would be
missing at runtime and the OAuth refresh-token grant (metatool-ai#276) would fail.

Regenerated 0014 from schema.ts so it now has a proper journal entry (idx 14)
and snapshot. SQL is identical to the hand-written version (drizzle emits
CREATE INDEX without IF NOT EXISTS). This also establishes a correct snapshot
baseline so subsequent migrations can be regenerated cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ver-header-forwarding

feat: per-server client header forwarding for SSE/StreamableHTTP

Adds a forward_headers jsonb column on mcp_servers plus a header-forwarding
module so configured client headers are forwarded to upstream MCP servers.

Conflict resolution:
- metamcp-proxy.ts import block: kept both metatool-ai#310's requestWithSessionRecovery
  import and metatool-ai#256's header-forwarding imports.
- mcp-server-pool.ts cleanupSession: kept ai-dev's version (delete the
  sessionToServers mapping without recreating idle sessions — the metatool-ai#273
  subprocess-leak fix). metatool-ai#256 had instead guarded the now-removed idle
  recreation with a forward_headers skip; ai-dev's removal already satisfies
  that intent, and getSession still skips idle reuse for forward_headers
  servers via serverRequiresForwardedHeaders().

Migrations: dropped metatool-ai#256's colliding 0014_dapper_jigsaw / 0015_small_wallow
and regenerated 0015_forward_headers from the merged schema via drizzle-kit
(sequential after the repaired 0014_oauth_refresh_token).

Note: metatool-ai#256 also adds the `headers` field to the server zod schema, which
fixes a batch of pre-existing `Property 'headers' does not exist` type errors
once @repo/zod-types is rebuilt. Backend: 0 type errors, 85 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ools-on-endpoints

feat: expose MetaMCP admin tools on endpoints

Adds an enable_metamcp_admin_tools toggle on endpoints plus an admin-mcp
module that surfaces MetaMCP admin operations as MCP tools (metamcp-admin__*),
threaded through the proxy and both transport routers.

Conflict resolution (combining metatool-ai#309's adminTools threading with metatool-ai#256's
clientRequestHeaders threading, both of which add params to the same paths):
- metamcp-server-pool.ts getServer(): kept both params (clientRequestHeaders,
  adminTools), kept metatool-ai#256's needsFreshServer header-forwarding gate, and added
  applyAdminToolsContext() on both the idle-reuse and new-server paths.
- metamcp-proxy.ts / sse.ts / streamable-http.ts: kept both sides' imports and
  threaded both clientRequestHeaders and adminTools through getServer() calls.
- Fixed a merge gap: the background idle-server creation in
  createIdleServerAsync() now sets internalSessionId (required by
  MetaMcpServerInstance, added by metatool-ai#309) — the metatool-ai#256-touched region had dropped it.
- tools-registry getAdminToolsForMcp(): cast inputSchema to Tool["inputSchema"]
  (the zod-to-json-schema output is a valid object schema at runtime).

Migrations: dropped metatool-ai#309's colliding 0014_metamcp_admin_tools and regenerated
0016_metamcp_admin_tools from the merged schema. zod-to-json-schema dependency
(added by metatool-ai#309) installed. Backend: 0 new type errors, 85 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ublic-mcp-tool-calls

feat: audit public MCP tool calls

Adds an mcp_request_audit_logs table + audit middleware that records public
endpoint tool calls (endpoint, auth, server, tool, status, duration), threading
a requestContext (endpointName/auth) through the proxy and transport routers.

Conflict resolution — this combined metatool-ai#288's requestContext threading with the
already-merged metatool-ai#256 (clientRequestHeaders) and metatool-ai#309 (adminTools) threads that
run through the same call chain. 15 conflicts across 7 files:
- functional-middleware.ts MetaMCPHandlerContext: kept all three (clientRequest-
  Headers, endpointName, auth).
- metamcp-server-pool.ts getServer()/createNewServer(): combined signatures to
  (clientRequestHeaders?, adminTools?, requestContext?). Rather than metatool-ai#288's
  separate "if (requestContext) build fresh + return" early branch, folded the
  audit requirement into metatool-ai#256's needsFreshServer gate (requestContext forces a
  fresh server) so an audited request still gets header forwarding AND adminTools.
- metamcp-proxy.ts createServer()/handlerContext: kept clientRequestHeaders +
  endpointName + auth.
- sse.ts / streamable-http.ts: threaded clientRequestHeaders, adminTools, and
  getRequestContext(authReq) through getServer() in signature order.
- client.ts: kept metatool-ai#311's self-heal (no ERROR early-return) over metatool-ai#288's
  early-return+log variant.
- mcp-server-pool.ts createIdleSession(): kept metatool-ai#273's generation-counter logic
  and grafted metatool-ai#288's metamcpLogStore.addLog on idle-session success.

Migrations: dropped metatool-ai#288's colliding 0014/0015 and regenerated
0017_audit_public_tool_calls. Rebuilt @repo/trpc / @repo/zod-types (audit
router + types). Backend: 0 new type errors, 89 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix rate limit duplicate counts, share postgres port in devcontainer,
pnpm 10 upgrade + security overrides, and adopt MCP SDK 1.26

Lands metatool-ai#258's rate-limit duplicate-count fix, devcontainer postgres port
sharing, the pnpm 9->10 upgrade, and the pnpm.overrides security pins
(esbuild/tar/qs/lodash/prismjs/etc.). The overrides also force
@modelcontextprotocol/sdk to >=1.26.0, which is a breaking bump (its schemas
use the zod 4 API), so this commit also carries the full SDK 1.26 migration.

SDK 1.26 / zod migration:
- Resolution: do NOT pin zod globally. pnpm naturally dedupes the SDK to the
  workspace's zod 3.25.76 (the SDK accepts ^3.25 || ^4 and exposes zod 4's API
  via zod 3.25's zod/v4 subpath) while better-auth keeps its own zod 4 (it
  needs z.ipv4). A global zod override broke better-auth's frontend build.
- Frontend: useConnection.ts exports a zod 3/4 compat contract
  (AnySchema/SchemaOutput/MakeRequestFn) mirroring Client.request; all inspector
  components + tool-management use MakeRequestFn instead of inline
  <T extends z.ZodType>; z.infer<typeof SdkSchema> -> SchemaOutput<...>;
  notificationTypes.ts composes via zod/v4 to match SDK schemas; Client is no
  longer over-parameterized (let it infer).
- Backend: metamcp-proxy uses the SDK's exported List*Result types instead of
  z.infer<typeof *Schema>; client.ts drops the invalid server-side client
  capabilities (1.26 tightened the client capability type); tool-execution
  narrows the content-block union before reading .text; zod-to-mcp-schema casts
  through a narrowed signature to avoid TS2589.
- Also fixed an metatool-ai#284<->metatool-ai#288 i18n merge gap: pt/es locales were missing the
  audit-logs namespace that metatool-ai#288 added to the Translations type (English
  fallback added).

Verified: frontend `next build` succeeds; frontend tsc 0 errors (was 143 at
branch start); backend builds, 89 tests pass, 0 new type errors. Runtime MCP
behavior under SDK 1.26 (capabilities/content-block changes) was not verified
against live MCP servers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctions-env

fix(mcp-server-pool): read MAX_TOTAL_CONNECTIONS from env in getInstance

getInstance() was passing a hardcoded 100 as maxTotalConnections, which
overrode the constructor's env-based default (added by metatool-ai#273). Since the
constructor is private and only getInstance constructs the pool, the
MAX_TOTAL_CONNECTIONS env var never actually took effect.

Conflict resolution: combined metatool-ai#303's env-reading logic (with NaN / non-positive
guarding) with ai-dev's getInstance signature — passing the resolved maxConn as
maxTotalConnections while keeping the maxConnectionsPerServer argument (metatool-ai#260).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rver-side-token-exchange

fix(oauth): server-side token exchange + pre-registered upstream OAuth clients

Adds backend-side upstream OAuth token exchange (oauth-upstream/ module:
token-exchange, refresh-on-401) so CORS-restricted enterprise providers
(Salesforce/Okta/Auth0/...) work, plus a pre-registered upstream OAuth client
UI (AdvancedOAuthSection) for providers without RFC 7591 dynamic registration.

Conflict resolution (13 conflicts):
- vitest.config.ts: kept ai-dev's @/ alias (already added by metatool-ai#310); metatool-ai#295's was a
  duplicate.
- oauth.zod.ts: took metatool-ai#295's side for the 3 regions — its new exchange schemas and
  the wider UpstreamTokenResponseSchema for oauth_sessions.tokens. metatool-ai#300's
  oauth-sessions.repo is token-type-agnostic (spreads input.tokens with an &&
  guard), so the widening is compatible.
- mcp-servers.zod.ts, mcp-servers.json: union — kept metatool-ai#256's forward-headers
  schemas/strings and metatool-ai#295's pre-registered-OAuth schemas/strings.
- edit-mcp-server.tsx, page.tsx: union — both the forward-headers field/parsing
  (metatool-ai#256) and the AdvancedOAuthSection + oauthClientInfo payload (metatool-ai#295) are wired
  into the form and apiPayload.

No DB migration in metatool-ai#295. Verified: backend builds, 165 tests pass (incl. metatool-ai#295's
new oauth-upstream + impl suites), 0 new type errors; frontend tsc 0, lint clean.
Runtime OAuth flows not verified against a live upstream provider.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st-auth-retry-csrf

fix(oauth): post-auth retry + state CSRF validation (closes metatool-ai#298, metatool-ai#299)

Stacked on metatool-ai#295. Adds: retry of the initial tools/list after token exchange to
ride out the upstream session-establishment race (recoverFromPostAuthRace +
attemptConnect refactor in client.ts), per-OAuth-session expected_state
persistence, and server-side state validation at exchangeToken (CSRF defence).

Conflict resolution:
- client.ts: kept metatool-ai#301's attemptConnect / post-auth-race refactor, and grafted
  ai-dev's metatool-ai#311 self-heal (wasInErrorState + resetServerErrorState on success,
  no ERROR-state early-return) and metatool-ai#288 crash logging (metamcpLogStore) into it.
  Updated attemptConnect's doc comment to reflect the self-heal behavior.
- oauth.zod.ts (UpsertOAuthSessionRequestSchema): kept ai-dev's non-nullable
  tokens/code_verifier (metatool-ai#300 atomic-upsert contract) and added metatool-ai#301's
  expected_state; merged both comment notes.

Migration: dropped metatool-ai#301's colliding 0014_same_the_hunter and regenerated
0018_oauth_expected_state from the merged schema.

Verified: backend builds, 186 tests pass (incl. metatool-ai#301's new suites), 0 new type
errors; frontend tsc 0. Runtime OAuth/CSRF flows not verified against a live
upstream provider.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.