* 🔐 fix: Honor Admin-Panel MCP Allowlist Overrides Without Restart
MCPServersRegistry was built once at boot from getAppConfig({ baseOnly:
true }), freezing allowedDomains/allowedAddresses to YAML. Admin-panel
mcpSettings overrides were ignored by both inspection (addServer/
reinspectServer/updateServer/lazyInitConfigServer) and runtime connection
enforcement (assertResolvedRuntimeConfigAllowed), so a domain allowed only
via the panel failed inspection and never connected.
Make the registry's effective allowlists mutable and refresh them from the
merged admin-panel config: seed at boot, and re-apply on every config
mutation via invalidateConfigCaches -> clearMcpConfigCache. Both inspection
and connection paths read the same getters, so both honor overrides without
a restart. Fail-safe: current allowlists are preserved when the merged read
fails.
* 🛡️ fix: Scope MCP allowlist refresh to global config, fail-safe on DB error
Address Codex P1 review findings on the allowlist-refresh path:
- Tenant-scoped config mutations no longer push one tenant's merged
mcpSettings into the process-wide registry singleton (read by all MCP
connection paths), which would leak allowlists across tenants. Only
global (non-tenant) mutations refresh the registry; tenant mutations
still evict the config-server cache.
- The refresh read now uses strictOverrides:true so a transient DB error
throws instead of silently returning YAML base config — preserving the
last-known allowlists rather than overwriting them with fallback values.
Adds the strictOverrides option to getAppConfig (default off, no behavior
change for existing callers).
* ♻️ refactor: Resolve MCP allowlists per-request (tenant-scoped) instead of a global singleton
Supersedes the prior global-mutation approach. MCP allowlists live in
mcpSettings, which is tenant/principal-scoped admin config, so a process-wide
singleton value is the wrong model — it caused cross-tenant bleed and stale
reads.
Instead, inject a resolver (from the app layer, where the merged config lives)
that the registry calls per inspection and per connection. It reads the ALS
tenant context via getAppConfig and accepts the acting user so user/role-scoped
overrides resolve; config-source inspection (no user) resolves at tenant scope.
Falls back to the YAML base allowlists when no resolver is set or the lookup
fails, so a transient error fails to the operator baseline rather than
disabling the allowlist.
Removes the now-unnecessary setAllowlists / boot-seed / invalidateConfigCaches
refresh / getAppConfig.strictOverrides machinery.
* 🔒 fix: Scope config-source cache by allowlist; resolve OAuth allowlists per-request
Address Codex review of the per-request resolver:
- Config-source cache key now folds in the resolved allowlists, not just the
raw-config hash. Inspection results became allowlist-dependent, so without
this a tenant whose allowlist rejects a URL could poison the shared key with
an inspectionFailed stub for a tenant that allows it (and vice versa). The
tenant-scoped allowlist is resolved once per ensureConfigServers pass and
threaded through the cache key + inspection.
- The two remaining request-time OAuth allowlist reads now use the merged
config instead of the YAML base getters: the fallback OAuth-initiate path
(routes/mcp.js) via resolveAllowlists, and OAuth revocation
(UserController.maybeUninstallOAuthMCP) via the request's already-merged
appConfig.mcpSettings. Without this, an OAuth endpoint allowed only by an
admin-panel override was rejected while inspection/connection allowed it.
* ✅ test: Update MCP OAuth registry/config mocks for per-request allowlists
CI fix for the Finding-12 change. The OAuth-initiate route now calls
registry.resolveAllowlists() and the revocation path reads the merged
appConfig.mcpSettings, so the affected specs' mocks were asserting the old
base-getter values:
- routes/__tests__/mcp.spec.js: add resolveAllowlists to the registry mock.
- UserController.mcpOAuth.spec.js: provide mcpSettings on the getAppConfig
mock so revokeOAuthToken still receives the expected allowlists.
* 🧪 test: e2e proof that admin-panel MCP allowlist override takes effect
Adds a Playwright mock-harness spec for #13809. A URL-based MCP fixture
(e2e-http, streamable-http SDK server) boots inspectionFailed because its
origin is omitted from the YAML mcpSettings.allowedDomains; the spec adds that
origin via an admin config override (PUT /api/admin/config/user/:id) and
asserts the server reinitializes — exercising the real resolver path through
the backend + DB. Before the fix, reinspection used the frozen YAML allowlist
and the server stayed unreachable.
- e2e/setup/fake-mcp-http-server.js: streamable-HTTP MCP fixture (health GET /).
- e2e/playwright.config.mock.ts: boot the fixture as a second webServer.
- e2e/config/librechat.e2e.yaml: mcpSettings.allowedDomains (excludes 127.0.0.1)
+ the e2e-http server.
- e2e/specs/mock/mcp-allowlist-override.spec.ts: login → baseline reinit fails →
apply override → reinit succeeds.
* 🎭 test: Run Mock E2E Suite Through createRun With In-Process Fake Model
Replace the standalone HTTP mock LLM server with an in-process fake model
injected into the real createRun -> Run.create pipeline via
run.Graph.overrideTestModel, so the mock suite exercises the agents
integration end-to-end without a live provider or a separate server.
- Bump @librechat/agents to 3.2.2 for the FakeChatModel/createFakeStreamingLLM exports
- Add an env-gated applyTestRunHook seam in packages/api createRun (no /api changes)
- Add e2e/setup/fake-model.js to drive default replies + the skill-authoring tool-call flow
- Drop the mock-llm webServer from playwright.config.mock.ts and set LIBRECHAT_TEST_RUN_HOOK
* 🧹 test: Retire Standalone Mock LLM Server From E2E Recorder
Migrate the `--profile=mock` recorder onto the same in-process fake model
as the Playwright mock suite, then delete the now-unused HTTP mock server
so the fake-LLM logic lives in a single place.
- Point record.js mock profile at the fake model via LIBRECHAT_TEST_RUN_HOOK
- Remove the mock-llm-server spawn/wait and MOCK_LLM_PORT plumbing from record.js
- Delete e2e/setup/mock-llm-server.js (e2e/setup/fake-model.js is now the only source)
- Update e2e/README.md to describe the in-process fake LLM
* 🏷️ ci: Rename Playwright Mock E2E Check to Playwright E2E Tests