🪬 fix: Skip MCP Tools When Required Custom User Vars Are Unset (#13152)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions

* fix: skip MCP tools when required customUserVars are unset (#10969)

* fix: whitespace-only values

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: guard MCP registry lookup and unknown server config in customUserVars gate

* fix: fail closed on MCP registry lookup errors

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Max 2026-05-23 15:24:29 +02:00 committed by GitHub
parent abda15f4eb
commit 1746153c17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 335 additions and 2 deletions

View file

@ -5,6 +5,8 @@ import {
redactServerSecrets,
isInvalidClientMessage,
isClientRejectionMessage,
getMissingCustomUserVars,
hasCustomUserVars,
isUserSourced,
} from '~/mcp/utils';
import type { ParsedServerConfig } from '~/mcp/types';
@ -340,3 +342,55 @@ describe('isUserSourced', () => {
expect(isUserSourced({})).toBe(false);
});
});
describe('getMissingCustomUserVars', () => {
const configWithVars = (keys: string[]): Pick<ParsedServerConfig, 'customUserVars'> => ({
customUserVars: Object.fromEntries(
keys.map((key) => [key, { title: key, description: `${key} description` }]),
),
});
it('returns an empty array when the server declares no customUserVars', () => {
expect(getMissingCustomUserVars({}, {})).toEqual([]);
expect(getMissingCustomUserVars({ customUserVars: undefined }, undefined)).toEqual([]);
});
it('returns an empty array when customUserVars is an empty object', () => {
const config: Pick<ParsedServerConfig, 'customUserVars'> = { customUserVars: {} };
expect(hasCustomUserVars(config)).toBe(false);
expect(getMissingCustomUserVars(config, undefined)).toEqual([]);
});
it('reports every declared variable when no values are provided', () => {
const config = configWithVars(['THINGY_TOKEN', 'THINGY_REGION']);
expect(getMissingCustomUserVars(config, undefined)).toEqual(['THINGY_TOKEN', 'THINGY_REGION']);
expect(getMissingCustomUserVars(config, null)).toEqual(['THINGY_TOKEN', 'THINGY_REGION']);
expect(getMissingCustomUserVars(config, {})).toEqual(['THINGY_TOKEN', 'THINGY_REGION']);
});
it('reports only the variables the user has not set', () => {
const config = configWithVars(['THINGY_TOKEN', 'THINGY_REGION']);
expect(getMissingCustomUserVars(config, { THINGY_TOKEN: 'abc123' })).toEqual(['THINGY_REGION']);
});
it('treats empty-string and whitespace-only values as missing', () => {
const config = configWithVars(['THINGY_TOKEN']);
expect(getMissingCustomUserVars(config, { THINGY_TOKEN: '' })).toEqual(['THINGY_TOKEN']);
expect(getMissingCustomUserVars(config, { THINGY_TOKEN: ' ' })).toEqual(['THINGY_TOKEN']);
expect(getMissingCustomUserVars(config, { THINGY_TOKEN: '\t\n ' })).toEqual(['THINGY_TOKEN']);
});
it('returns an empty array when every declared variable has a value', () => {
const config = configWithVars(['THINGY_TOKEN', 'THINGY_REGION']);
expect(
getMissingCustomUserVars(config, { THINGY_TOKEN: 'abc123', THINGY_REGION: 'eu-west-1' }),
).toEqual([]);
});
it('ignores provided values for variables the server does not declare', () => {
const config = configWithVars(['THINGY_TOKEN']);
expect(
getMissingCustomUserVars(config, { THINGY_TOKEN: 'abc123', UNRELATED: 'value' }),
).toEqual([]);
});
});

View file

@ -18,6 +18,29 @@ export function hasCustomUserVars(config: Pick<ParsedServerConfig, 'customUserVa
return !!config.customUserVars && Object.keys(config.customUserVars).length > 0;
}
/**
* Returns the names of `customUserVars` declared on the server config for which
* the user has not supplied a non-blank value (unset, empty, or whitespace-only
* values count as missing, since they still fail auth). An empty array means
* every declared variable is satisfied (or the server declares none).
*
* Used to gate tool exposure: a server that requires user-provided credentials
* should not surface its tools to the model until those values are set,
* otherwise every tool call fails authentication. See issue #10969.
*/
export function getMissingCustomUserVars(
config: Pick<ParsedServerConfig, 'customUserVars'>,
providedVars?: Record<string, string> | null,
): string[] {
if (!hasCustomUserVars(config)) {
return [];
}
return Object.keys(config.customUserVars ?? {}).filter((key) => {
const value = providedVars?.[key];
return value == null || (typeof value === 'string' && value.trim() === '');
});
}
/**
* Determines whether a server config is user-sourced (sandboxed placeholder resolution).
* When `source` is set, it is authoritative. When absent (pre-upgrade cached configs),