mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-05 13:55:13 +00:00
🪦 feat: Allow Scope Overrides To Tombstone Inherited MCP Server Entries
Add tombstone semantics to the config merge layer for record-type sections that can grow scope-specific deletions. A scope override with a null value at \`mcpServers.<key>\` now removes that key from the effective config for the scope instead of either storing null or no-oping. The admin panel side will follow up by writing this null value where it previously sent a DELETE that could only remove an existing override. TOMBSTONE_SUPPORTED_PARENTS gates the behavior to mcpConfig (the remapped name for the mcpServers section). Nested null values beneath an entry key keep their literal meaning, a null at the section root keeps acting as a normal replacement, and a higher priority layer can re-assert an entry that a lower priority layer tombstoned. Tombstoning an entry the base does not define is a silent no-op. Six new resolution.spec tests cover those paths.
This commit is contained in:
parent
fb282a2afa
commit
e8287f1d56
2 changed files with 87 additions and 0 deletions
|
|
@ -428,6 +428,81 @@ describe('mergeConfigOverrides', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('mergeConfigOverrides — mcpServers tombstones', () => {
|
||||
const baseWithMcp = {
|
||||
interfaceConfig: { modelSelect: true },
|
||||
mcpConfig: {
|
||||
inherited: { type: 'sse', url: 'https://base.example.com' },
|
||||
other: { type: 'sse', url: 'https://other.example.com', title: 'Other' },
|
||||
},
|
||||
} as unknown as AppConfig;
|
||||
|
||||
it('drops an inherited mcpServers entry when the override sets it to null', () => {
|
||||
const configs = [fakeConfig({ mcpServers: { inherited: null } }, 10)];
|
||||
const result = mergeConfigOverrides(baseWithMcp, configs) as unknown as Record<string, unknown>;
|
||||
const mcpConfig = result.mcpConfig as Record<string, unknown>;
|
||||
expect(mcpConfig.inherited).toBeUndefined();
|
||||
expect(mcpConfig.other).toEqual({
|
||||
type: 'sse',
|
||||
url: 'https://other.example.com',
|
||||
title: 'Other',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves other inherited entries and applies partial overrides alongside a tombstone', () => {
|
||||
const configs = [
|
||||
fakeConfig({ mcpServers: { inherited: null, other: { title: 'Edited' } } }, 10),
|
||||
];
|
||||
const result = mergeConfigOverrides(baseWithMcp, configs) as unknown as Record<string, unknown>;
|
||||
const mcpConfig = result.mcpConfig as Record<string, unknown>;
|
||||
expect(mcpConfig.inherited).toBeUndefined();
|
||||
expect(mcpConfig.other).toEqual({
|
||||
type: 'sse',
|
||||
url: 'https://other.example.com',
|
||||
title: 'Edited',
|
||||
});
|
||||
});
|
||||
|
||||
it('lets a higher-priority override re-assert an entry tombstoned at lower priority', () => {
|
||||
const configs = [
|
||||
fakeConfig({ mcpServers: { inherited: null } }, 5),
|
||||
fakeConfig(
|
||||
{ mcpServers: { inherited: { type: 'sse', url: 'https://reassert.example.com' } } },
|
||||
10,
|
||||
),
|
||||
];
|
||||
const result = mergeConfigOverrides(baseWithMcp, configs) as unknown as Record<string, unknown>;
|
||||
const mcpConfig = result.mcpConfig as Record<string, unknown>;
|
||||
expect(mcpConfig.inherited).toEqual({ type: 'sse', url: 'https://reassert.example.com' });
|
||||
});
|
||||
|
||||
it('treats null at the section root as a normal merge replacement, not a tombstone', () => {
|
||||
const configs = [fakeConfig({ mcpServers: null }, 10)];
|
||||
const result = mergeConfigOverrides(baseWithMcp, configs) as unknown as Record<string, unknown>;
|
||||
expect(result.mcpConfig).toBeNull();
|
||||
});
|
||||
|
||||
it('does not tombstone null values nested beneath an entry key', () => {
|
||||
const configs = [fakeConfig({ mcpServers: { other: { title: null } } }, 10)];
|
||||
const result = mergeConfigOverrides(baseWithMcp, configs) as unknown as Record<string, unknown>;
|
||||
const mcpConfig = result.mcpConfig as Record<string, unknown>;
|
||||
expect(mcpConfig.other).toEqual({
|
||||
type: 'sse',
|
||||
url: 'https://other.example.com',
|
||||
title: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('is a no-op tombstone for an entry that does not exist in base', () => {
|
||||
const configs = [fakeConfig({ mcpServers: { neverExisted: null } }, 10)];
|
||||
const result = mergeConfigOverrides(baseWithMcp, configs) as unknown as Record<string, unknown>;
|
||||
const mcpConfig = result.mcpConfig as Record<string, unknown>;
|
||||
expect('neverExisted' in mcpConfig).toBe(false);
|
||||
expect(mcpConfig.inherited).toBeDefined();
|
||||
expect(mcpConfig.other).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('INTERFACE_PERMISSION_FIELDS', () => {
|
||||
it('contains all expected permission fields', () => {
|
||||
const expected = [
|
||||
|
|
|
|||
|
|
@ -20,6 +20,16 @@ const ARRAY_MERGE_KEYS: Record<string, string> = {
|
|||
'endpoints.custom': 'name',
|
||||
};
|
||||
|
||||
/**
|
||||
* Record-type sections whose override layer interprets a `null` value at a child
|
||||
* key as a tombstone: the entry inherited from base (or a lower-priority layer)
|
||||
* is removed from the effective config for this scope. Without this, a scope
|
||||
* can only add or amend entries, never subtract inherited ones, because the
|
||||
* per-field PATCH path has no way to express "for this scope, pretend the
|
||||
* inherited entry does not exist."
|
||||
*/
|
||||
const TOMBSTONE_SUPPORTED_PARENTS = new Set<string>(['mcpConfig']);
|
||||
|
||||
/**
|
||||
* Maps YAML-level override keys (TCustomConfig) to their AppConfig equivalents.
|
||||
* Overrides are stored with YAML keys but merged into the already-processed AppConfig
|
||||
|
|
@ -122,6 +132,8 @@ function deepMerge<T extends AnyObject>(target: T, source: AnyObject, depth = 0,
|
|||
depth,
|
||||
currentPath,
|
||||
);
|
||||
} else if (sourceVal === null && TOMBSTONE_SUPPORTED_PARENTS.has(path)) {
|
||||
delete result[key];
|
||||
} else {
|
||||
result[key] = sourceVal;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue