mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🪬 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
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:
parent
abda15f4eb
commit
1746153c17
6 changed files with 335 additions and 2 deletions
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue