From f7ab8dc45492b23d51ac35f4623d083d1377a0b9 Mon Sep 17 00:00:00 2001 From: Ali Gulzar Date: Tue, 22 Sep 2026 13:04:12 +0900 Subject: [PATCH 1/3] feat: defer oversized MCP tool schemas by size --- api/server/services/ToolService.js | 3 + librechat.example.yaml | 6 ++ packages/api/src/tools/classification.ts | 56 +++++++++- packages/api/src/tools/deferBySize.spec.ts | 115 +++++++++++++++++++++ packages/api/src/tools/definitions.ts | 4 + packages/data-provider/src/config.ts | 5 + 6 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 packages/api/src/tools/deferBySize.spec.ts diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 05126536df4..480d0bdb81b 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1321,6 +1321,7 @@ async function loadToolDefinitionsWrapper({ tools: defsFilteredTools, toolOptions: agent.tool_options, deferredToolsEnabled, + deferSchemaBytes: appConfig?.mcpSettings?.deferSchemaBytes, programmaticToolsEnabled, codeExecutionEnabled, codeExecutionContext: resolvedCodeExecutionContext, @@ -1420,6 +1421,7 @@ async function loadToolDefinitionsWrapper({ tools: defsFilteredTools, toolOptions: agent.tool_options, deferredToolsEnabled, + deferSchemaBytes: appConfig?.mcpSettings?.deferSchemaBytes, programmaticToolsEnabled, codeExecutionEnabled, codeExecutionContext: resolvedCodeExecutionContext, @@ -1817,6 +1819,7 @@ async function loadAgentTools({ agentToolOptions: agent.tool_options, gitIdentity: agent.git_identity, deferredToolsEnabled, + deferSchemaBytes: appConfig?.mcpSettings?.deferSchemaBytes, programmaticToolsEnabled, codeExecutionEnabled, codeEnvironments: appConfig?.endpoints?.agents?.statefulCodeSessions?.environments, diff --git a/librechat.example.yaml b/librechat.example.yaml index 260912aa780..9f5a3f4340c 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -515,6 +515,12 @@ actions: # - 'host.docker.internal:8080' # - '127.0.0.1:8080' +# # Argument-schema size, in bytes, above which an MCP tool starts deferred: +# # the model sees its name and description and loads the schema through +# # tool_search when it needs it. A per-tool defer_loading toggle still wins. +# # 0, the default, leaves every tool exactly as its own options configure it. +# deferSchemaBytes: 4096 + # Example MCP Servers Object Structure # mcpServers: # everything: diff --git a/packages/api/src/tools/classification.ts b/packages/api/src/tools/classification.ts index 2c84a193e56..67d8f0def1a 100644 --- a/packages/api/src/tools/classification.ts +++ b/packages/api/src/tools/classification.ts @@ -117,16 +117,45 @@ export function aliasMCPToolOptions( } } +export function schemaByteSize(parameters: JsonSchemaType | undefined): number { + if (parameters == null) { + return 0; + } + try { + return JSON.stringify(parameters).length; + } catch { + return 0; + } +} + +/** An explicit per-tool choice always wins, so an operator-wide rule can never + * override a tool someone deliberately pinned open or shut. */ +export function resolveDeferLoading( + explicit: boolean | undefined, + parameters: JsonSchemaType | undefined, + deferSchemaBytes: number, +): boolean { + if (explicit != null) { + return explicit === true; + } + if (deferSchemaBytes <= 0) { + return false; + } + return schemaByteSize(parameters) > deferSchemaBytes; +} + /** * Builds a tool registry from agent-level tool_options. * * @param tools - Array of tool definitions * @param agentToolOptions - Per-tool configuration from the agent + * @param deferSchemaBytes - Size above which a schema defers absent an explicit choice * @returns Map of tool name to tool definition with classification */ export function buildToolRegistryFromAgentOptions( tools: ToolDefinition[], agentToolOptions: AgentToolOptions, + deferSchemaBytes = 0, ): LCToolRegistry { const registry: LCToolRegistry = new Map(); @@ -139,7 +168,11 @@ export function buildToolRegistryFromAgentOptions( ? agentOptions.allowed_callers : ['direct']; - const defer_loading = agentOptions?.defer_loading === true; + const defer_loading = resolveDeferLoading( + agentOptions?.defer_loading, + parameters, + deferSchemaBytes, + ); const toolDef: LCTool = { name, @@ -239,21 +272,27 @@ export function cleanupMCPToolSchemas(tools: MCPToolInstance[]): void { function buildToolRegistry( mcpToolDefs: ToolDefinition[], agentToolOptions?: AgentToolOptions, + deferSchemaBytes = 0, ): LCToolRegistry { if (agentToolOptions && Object.keys(agentToolOptions).length > 0) { - return buildToolRegistryFromAgentOptions(mcpToolDefs, agentToolOptions); + return buildToolRegistryFromAgentOptions(mcpToolDefs, agentToolOptions, deferSchemaBytes); } /** No agent options - build basic definitions for event-driven mode */ const registry: LCToolRegistry = new Map(); for (const toolDef of mcpToolDefs) { - registry.set(toolDef.name, { + const entry: LCTool = { name: toolDef.name, description: toolDef.description, parameters: toolDef.parameters, serverName: toolDef.serverName, toolType: 'mcp', - }); + }; + /** The size rule is operator-wide, so it reaches agents with no per-tool options too. */ + if (resolveDeferLoading(undefined, toolDef.parameters, deferSchemaBytes)) { + entry.defer_loading = true; + } + registry.set(toolDef.name, entry); } return registry; } @@ -285,6 +324,8 @@ export interface BuildToolClassificationParams { codeExecutionContext?: CodeExecutionContext; codeEnvironments?: readonly CodeEnvironmentConfig[]; getAppConfig?: CodeCapabilityConfigLoader; + /** See `mcpSettings.deferSchemaBytes`. `0` leaves every tool as configured. */ + deferSchemaBytes?: number; } /** Result from building tool classification */ @@ -358,6 +399,7 @@ export async function buildToolClassification( codeExecutionContext, codeEnvironments, getAppConfig, + deferSchemaBytes = 0, } = params; const isGoogle = provider === Providers.GOOGLE || provider === Providers.VERTEXAI; const additionalTools: GenericTool[] = []; @@ -376,7 +418,11 @@ export async function buildToolClassification( const mcpToolDefs = mcpTools.map(extractMCPToolDefinition); const mcpToolAliases = collectMCPToolAliases(mcpToolDefs); aliasMCPToolOptions(mcpToolAliases, agentToolOptions); - const toolRegistry: LCToolRegistry = buildToolRegistry(mcpToolDefs, agentToolOptions); + const toolRegistry: LCToolRegistry = buildToolRegistry( + mcpToolDefs, + agentToolOptions, + deferSchemaBytes, + ); /** Clean up temporary mcpJsonSchema property from tools now that registry is populated */ cleanupMCPToolSchemas(mcpTools); diff --git a/packages/api/src/tools/deferBySize.spec.ts b/packages/api/src/tools/deferBySize.spec.ts new file mode 100644 index 00000000000..63bbe5fd1d4 --- /dev/null +++ b/packages/api/src/tools/deferBySize.spec.ts @@ -0,0 +1,115 @@ +import type { AgentToolOptions } from 'librechat-data-provider'; +import type { JsonSchemaType } from '@librechat/agents'; +import type { ToolDefinition } from './classification'; +import { + schemaByteSize, + resolveDeferLoading, + buildToolRegistryFromAgentOptions, +} from './classification'; + +/** An argument schema of roughly the requested serialized size. */ +function schemaOfSize(bytes: number): JsonSchemaType { + const filler = 'x'.repeat(Math.max(1, bytes)); + return { + type: 'object', + properties: { body: { type: 'string', description: filler } }, + } as unknown as JsonSchemaType; +} + +const SMALL = schemaOfSize(50); +const HUGE = schemaOfSize(20_000); + +function toolDef(name: string, parameters?: JsonSchemaType): ToolDefinition { + return { name, description: `${name} description`, parameters, serverName: 'Server' }; +} + +describe('schemaByteSize', () => { + it('measures the serialized schema', () => { + expect(schemaByteSize(SMALL)).toBeGreaterThan(50); + expect(schemaByteSize(HUGE)).toBeGreaterThan(20_000); + }); + + it('treats a missing schema as weightless', () => { + expect(schemaByteSize(undefined)).toBe(0); + }); + + it('does not throw on a schema that cannot be serialized', () => { + const circular: Record = { type: 'object' }; + circular.self = circular; + + expect(schemaByteSize(circular as unknown as JsonSchemaType)).toBe(0); + }); +}); + +describe('resolveDeferLoading', () => { + it('leaves every tool loaded when the rule is off', () => { + expect(resolveDeferLoading(undefined, HUGE, 0)).toBe(false); + }); + + it('defers a schema over the limit', () => { + expect(resolveDeferLoading(undefined, HUGE, 4_096)).toBe(true); + }); + + it('leaves a schema under the limit alone', () => { + expect(resolveDeferLoading(undefined, SMALL, 4_096)).toBe(false); + }); + + it('lets an explicit false pin a huge tool open', () => { + expect(resolveDeferLoading(false, HUGE, 4_096)).toBe(false); + }); + + it('lets an explicit true defer a small tool', () => { + expect(resolveDeferLoading(true, SMALL, 0)).toBe(true); + }); + + it('treats a tool with no schema as under any limit', () => { + expect(resolveDeferLoading(undefined, undefined, 1)).toBe(false); + }); +}); + +describe('buildToolRegistryFromAgentOptions with a size rule', () => { + const tools = [toolDef('small_mcp_Server', SMALL), toolDef('huge_mcp_Server', HUGE)]; + + it('reproduces today behavior when the rule is not configured', () => { + const registry = buildToolRegistryFromAgentOptions(tools, {}); + + expect(registry.get('small_mcp_Server')?.defer_loading).toBe(false); + expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(false); + }); + + it('defers only the oversized tool once the rule is set', () => { + const registry = buildToolRegistryFromAgentOptions(tools, {}, 4_096); + + expect(registry.get('small_mcp_Server')?.defer_loading).toBe(false); + expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(true); + }); + + it('keeps the tool description, which is what a shortlist reads', () => { + const registry = buildToolRegistryFromAgentOptions(tools, {}, 4_096); + + expect(registry.get('huge_mcp_Server')?.description).toBe('huge_mcp_Server description'); + expect(registry.get('huge_mcp_Server')?.parameters).toBe(HUGE); + }); + + it('lets a per-tool choice override the rule in both directions', () => { + const options: AgentToolOptions = { + huge_mcp_Server: { defer_loading: false }, + small_mcp_Server: { defer_loading: true }, + }; + + const registry = buildToolRegistryFromAgentOptions(tools, options, 4_096); + + expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(false); + expect(registry.get('small_mcp_Server')?.defer_loading).toBe(true); + }); + + it('applies the rule to a tool whose options set something unrelated', () => { + const options: AgentToolOptions = { + huge_mcp_Server: { allowed_callers: ['direct'] }, + }; + + const registry = buildToolRegistryFromAgentOptions(tools, options, 4_096); + + expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(true); + }); +}); diff --git a/packages/api/src/tools/definitions.ts b/packages/api/src/tools/definitions.ts index eb4b01064da..5b469560d78 100644 --- a/packages/api/src/tools/definitions.ts +++ b/packages/api/src/tools/definitions.ts @@ -47,6 +47,8 @@ export interface LoadToolDefinitionsParams { toolOptions?: AgentToolOptions; /** Whether deferred tools feature is enabled */ deferredToolsEnabled?: boolean; + /** See `mcpSettings.deferSchemaBytes`. `0` leaves every tool as configured. */ + deferSchemaBytes?: number; /** Whether programmatic tool calling is enabled */ programmaticToolsEnabled?: boolean; /** Whether code execution is enabled and requested by this agent */ @@ -130,6 +132,7 @@ export async function loadToolDefinitions( tools, toolOptions = {}, deferredToolsEnabled = false, + deferSchemaBytes = 0, programmaticToolsEnabled = false, codeExecutionEnabled = false, codeExecutionContext, @@ -376,6 +379,7 @@ export async function loadToolDefinitions( getAppConfig, definitionsOnly: true, agentToolOptions: toolOptions, + deferSchemaBytes, }); const { toolDefinitions, hasDeferredTools } = classificationResult; diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 969b3305134..0832cdf14c6 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -2927,6 +2927,11 @@ export const configSchema = z.object({ .object({ allowedDomains: z.array(z.string()).optional(), allowedAddresses: allowedAddressesSchema, + /** + * Argument-schema size above which an MCP tool starts deferred. A per-tool + * `defer_loading` toggle still wins. `0` disables the rule. + */ + deferSchemaBytes: z.number().int().nonnegative().max(10_000_000).default(0), catalogRecovery: z .object({ discoveryBackoffMs: z From 6616e6dd63c1f8ed49f6932fdfeb5646955ac540 Mon Sep 17 00:00:00 2001 From: Ali Gulzar Date: Thu, 24 Sep 2026 12:13:50 +0900 Subject: [PATCH 2/3] fix: measure deferred schemas in characters and cover the no-options path --- api/server/services/ToolService.js | 6 +- librechat.example.yaml | 11 +- packages/api/src/tools/classification.spec.ts | 148 +++++++++++++++++- packages/api/src/tools/classification.ts | 28 ++-- packages/api/src/tools/deferBySize.spec.ts | 115 -------------- packages/api/src/tools/definitions.ts | 8 +- packages/data-provider/src/config.ts | 7 +- 7 files changed, 177 insertions(+), 146 deletions(-) delete mode 100644 packages/api/src/tools/deferBySize.spec.ts diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 480d0bdb81b..f9ea10fc819 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -1321,7 +1321,7 @@ async function loadToolDefinitionsWrapper({ tools: defsFilteredTools, toolOptions: agent.tool_options, deferredToolsEnabled, - deferSchemaBytes: appConfig?.mcpSettings?.deferSchemaBytes, + deferSchemaChars: appConfig?.mcpSettings?.deferSchemaChars, programmaticToolsEnabled, codeExecutionEnabled, codeExecutionContext: resolvedCodeExecutionContext, @@ -1421,7 +1421,7 @@ async function loadToolDefinitionsWrapper({ tools: defsFilteredTools, toolOptions: agent.tool_options, deferredToolsEnabled, - deferSchemaBytes: appConfig?.mcpSettings?.deferSchemaBytes, + deferSchemaChars: appConfig?.mcpSettings?.deferSchemaChars, programmaticToolsEnabled, codeExecutionEnabled, codeExecutionContext: resolvedCodeExecutionContext, @@ -1819,7 +1819,7 @@ async function loadAgentTools({ agentToolOptions: agent.tool_options, gitIdentity: agent.git_identity, deferredToolsEnabled, - deferSchemaBytes: appConfig?.mcpSettings?.deferSchemaBytes, + deferSchemaChars: appConfig?.mcpSettings?.deferSchemaChars, programmaticToolsEnabled, codeExecutionEnabled, codeEnvironments: appConfig?.endpoints?.agents?.statefulCodeSessions?.environments, diff --git a/librechat.example.yaml b/librechat.example.yaml index 9f5a3f4340c..7b86733dedc 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -515,11 +515,12 @@ actions: # - 'host.docker.internal:8080' # - '127.0.0.1:8080' -# # Argument-schema size, in bytes, above which an MCP tool starts deferred: -# # the model sees its name and description and loads the schema through -# # tool_search when it needs it. A per-tool defer_loading toggle still wins. -# # 0, the default, leaves every tool exactly as its own options configure it. -# deferSchemaBytes: 4096 +# # Argument-schema size, in characters of its JSON, above which an MCP tool +# # starts deferred: the model sees only its name and loads the schema through +# # tool_search when it needs it. Needs the agents `deferred_tools` capability. +# # A per-tool defer_loading toggle still wins. 0, the default, leaves every +# # tool exactly as its own options configure it. +# deferSchemaChars: 4096 # Example MCP Servers Object Structure # mcpServers: diff --git a/packages/api/src/tools/classification.spec.ts b/packages/api/src/tools/classification.spec.ts index 517e7d360e4..2f59d03e26e 100644 --- a/packages/api/src/tools/classification.spec.ts +++ b/packages/api/src/tools/classification.spec.ts @@ -1,9 +1,11 @@ import { Providers } from '@librechat/agents'; +import type { GenericTool, JsonSchemaType } from '@librechat/agents'; import type { AgentToolOptions } from 'librechat-data-provider'; -import type { GenericTool } from '@librechat/agents'; +import type { LCToolRegistry, ToolDefinition } from './classification'; import type { CodeEnvironmentConfig } from '~/agents/execution'; -import type { LCToolRegistry } from './classification'; import { + schemaSize, + resolveDeferLoading, buildToolRegistryFromAgentOptions, aliasMCPToolOptions, agentHasProgrammaticTools, @@ -754,3 +756,145 @@ describe('classification.ts', () => { }); }); }); + +/** An argument schema of roughly the requested serialized size. */ +function schemaOfSize(bytes: number): JsonSchemaType { + const filler = 'x'.repeat(Math.max(1, bytes)); + return { type: 'object', properties: { body: { type: 'string', description: filler } } }; +} + +const SMALL = schemaOfSize(50); +const HUGE = schemaOfSize(20_000); + +function toolDef(name: string, parameters?: JsonSchemaType): ToolDefinition { + return { name, description: `${name} description`, parameters, serverName: 'Server' }; +} + +describe('schemaSize', () => { + it('measures the serialized schema', () => { + expect(schemaSize(SMALL)).toBeGreaterThan(50); + expect(schemaSize(HUGE)).toBeGreaterThan(20_000); + }); + + it('treats a missing schema as weightless', () => { + expect(schemaSize(undefined)).toBe(0); + }); + + it('does not throw on a schema that cannot be serialized', () => { + const circular: JsonSchemaType = { type: 'object', properties: {} }; + circular.properties = { self: circular }; + + expect(schemaSize(circular)).toBe(0); + }); +}); + +describe('resolveDeferLoading', () => { + it('leaves every tool loaded when the rule is off', () => { + expect(resolveDeferLoading(undefined, HUGE, 0)).toBe(false); + }); + + it('defers a schema over the limit', () => { + expect(resolveDeferLoading(undefined, HUGE, 4_096)).toBe(true); + }); + + it('leaves a schema under the limit alone', () => { + expect(resolveDeferLoading(undefined, SMALL, 4_096)).toBe(false); + }); + + it('lets an explicit false pin a huge tool open', () => { + expect(resolveDeferLoading(false, HUGE, 4_096)).toBe(false); + }); + + it('lets an explicit true defer a small tool', () => { + expect(resolveDeferLoading(true, SMALL, 0)).toBe(true); + }); + + it('treats a tool with no schema as under any limit', () => { + expect(resolveDeferLoading(undefined, undefined, 1)).toBe(false); + }); +}); + +describe('buildToolRegistryFromAgentOptions with a size rule', () => { + const tools = [toolDef('small_mcp_Server', SMALL), toolDef('huge_mcp_Server', HUGE)]; + + it('reproduces today behavior when the rule is not configured', () => { + const registry = buildToolRegistryFromAgentOptions(tools, {}); + + expect(registry.get('small_mcp_Server')?.defer_loading).toBe(false); + expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(false); + }); + + it('defers only the oversized tool once the rule is set', () => { + const registry = buildToolRegistryFromAgentOptions(tools, {}, 4_096); + + expect(registry.get('small_mcp_Server')?.defer_loading).toBe(false); + expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(true); + }); + + it('keeps the tool description, which is what a shortlist reads', () => { + const registry = buildToolRegistryFromAgentOptions(tools, {}, 4_096); + + expect(registry.get('huge_mcp_Server')?.description).toBe('huge_mcp_Server description'); + expect(registry.get('huge_mcp_Server')?.parameters).toBe(HUGE); + }); + + it('lets a per-tool choice override the rule in both directions', () => { + const options: AgentToolOptions = { + huge_mcp_Server: { defer_loading: false }, + small_mcp_Server: { defer_loading: true }, + }; + + const registry = buildToolRegistryFromAgentOptions(tools, options, 4_096); + + expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(false); + expect(registry.get('small_mcp_Server')?.defer_loading).toBe(true); + }); + + it('applies the rule to a tool whose options set something unrelated', () => { + const options: AgentToolOptions = { + huge_mcp_Server: { allowed_callers: ['direct'] }, + }; + + const registry = buildToolRegistryFromAgentOptions(tools, options, 4_096); + + expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(true); + }); +}); + +describe('buildToolClassification with a size rule and no per-tool options', () => { + const mcpTool = (name: string, schema: JsonSchemaType) => + ({ + name, + description: `${name} description`, + mcp: true, + mcpJsonSchema: schema, + }) as unknown as GenericTool; + + it('defers only the oversized tool and adds tool_search for it', async () => { + const result = await buildToolClassification({ + loadedTools: [mcpTool('small_mcp_Server', SMALL), mcpTool('huge_mcp_Server', HUGE)], + userId: 'user1', + agentId: 'agent1', + deferredToolsEnabled: true, + deferSchemaChars: 4_096, + }); + + expect(result.toolRegistry?.get('huge_mcp_Server')?.defer_loading).toBe(true); + expect(result.toolRegistry?.get('small_mcp_Server')?.defer_loading).toBeUndefined(); + expect(result.hasDeferredTools).toBe(true); + expect(result.additionalTools.some((t) => t.name === 'tool_search')).toBe(true); + }); + + it('keeps every tool callable when the deferred_tools capability is off', async () => { + const result = await buildToolClassification({ + loadedTools: [mcpTool('huge_mcp_Server', HUGE)], + userId: 'user1', + agentId: 'agent1', + deferredToolsEnabled: false, + deferSchemaChars: 4_096, + }); + + expect(result.toolRegistry?.get('huge_mcp_Server')?.defer_loading).toBe(false); + expect(result.hasDeferredTools).toBe(false); + }); +}); diff --git a/packages/api/src/tools/classification.ts b/packages/api/src/tools/classification.ts index 67d8f0def1a..dc7c97f19cd 100644 --- a/packages/api/src/tools/classification.ts +++ b/packages/api/src/tools/classification.ts @@ -117,7 +117,7 @@ export function aliasMCPToolOptions( } } -export function schemaByteSize(parameters: JsonSchemaType | undefined): number { +export function schemaSize(parameters: JsonSchemaType | undefined): number { if (parameters == null) { return 0; } @@ -133,15 +133,15 @@ export function schemaByteSize(parameters: JsonSchemaType | undefined): number { export function resolveDeferLoading( explicit: boolean | undefined, parameters: JsonSchemaType | undefined, - deferSchemaBytes: number, + deferSchemaChars: number, ): boolean { if (explicit != null) { return explicit === true; } - if (deferSchemaBytes <= 0) { + if (deferSchemaChars <= 0) { return false; } - return schemaByteSize(parameters) > deferSchemaBytes; + return schemaSize(parameters) > deferSchemaChars; } /** @@ -149,13 +149,13 @@ export function resolveDeferLoading( * * @param tools - Array of tool definitions * @param agentToolOptions - Per-tool configuration from the agent - * @param deferSchemaBytes - Size above which a schema defers absent an explicit choice + * @param deferSchemaChars - Size above which a schema defers absent an explicit choice * @returns Map of tool name to tool definition with classification */ export function buildToolRegistryFromAgentOptions( tools: ToolDefinition[], agentToolOptions: AgentToolOptions, - deferSchemaBytes = 0, + deferSchemaChars = 0, ): LCToolRegistry { const registry: LCToolRegistry = new Map(); @@ -171,7 +171,7 @@ export function buildToolRegistryFromAgentOptions( const defer_loading = resolveDeferLoading( agentOptions?.defer_loading, parameters, - deferSchemaBytes, + deferSchemaChars, ); const toolDef: LCTool = { @@ -272,10 +272,10 @@ export function cleanupMCPToolSchemas(tools: MCPToolInstance[]): void { function buildToolRegistry( mcpToolDefs: ToolDefinition[], agentToolOptions?: AgentToolOptions, - deferSchemaBytes = 0, + deferSchemaChars = 0, ): LCToolRegistry { if (agentToolOptions && Object.keys(agentToolOptions).length > 0) { - return buildToolRegistryFromAgentOptions(mcpToolDefs, agentToolOptions, deferSchemaBytes); + return buildToolRegistryFromAgentOptions(mcpToolDefs, agentToolOptions, deferSchemaChars); } /** No agent options - build basic definitions for event-driven mode */ @@ -289,7 +289,7 @@ function buildToolRegistry( toolType: 'mcp', }; /** The size rule is operator-wide, so it reaches agents with no per-tool options too. */ - if (resolveDeferLoading(undefined, toolDef.parameters, deferSchemaBytes)) { + if (resolveDeferLoading(undefined, toolDef.parameters, deferSchemaChars)) { entry.defer_loading = true; } registry.set(toolDef.name, entry); @@ -324,8 +324,8 @@ export interface BuildToolClassificationParams { codeExecutionContext?: CodeExecutionContext; codeEnvironments?: readonly CodeEnvironmentConfig[]; getAppConfig?: CodeCapabilityConfigLoader; - /** See `mcpSettings.deferSchemaBytes`. `0` leaves every tool as configured. */ - deferSchemaBytes?: number; + /** See `mcpSettings.deferSchemaChars`. `0` leaves every tool as configured. */ + deferSchemaChars?: number; } /** Result from building tool classification */ @@ -399,7 +399,7 @@ export async function buildToolClassification( codeExecutionContext, codeEnvironments, getAppConfig, - deferSchemaBytes = 0, + deferSchemaChars = 0, } = params; const isGoogle = provider === Providers.GOOGLE || provider === Providers.VERTEXAI; const additionalTools: GenericTool[] = []; @@ -421,7 +421,7 @@ export async function buildToolClassification( const toolRegistry: LCToolRegistry = buildToolRegistry( mcpToolDefs, agentToolOptions, - deferSchemaBytes, + deferSchemaChars, ); /** Clean up temporary mcpJsonSchema property from tools now that registry is populated */ diff --git a/packages/api/src/tools/deferBySize.spec.ts b/packages/api/src/tools/deferBySize.spec.ts deleted file mode 100644 index 63bbe5fd1d4..00000000000 --- a/packages/api/src/tools/deferBySize.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { AgentToolOptions } from 'librechat-data-provider'; -import type { JsonSchemaType } from '@librechat/agents'; -import type { ToolDefinition } from './classification'; -import { - schemaByteSize, - resolveDeferLoading, - buildToolRegistryFromAgentOptions, -} from './classification'; - -/** An argument schema of roughly the requested serialized size. */ -function schemaOfSize(bytes: number): JsonSchemaType { - const filler = 'x'.repeat(Math.max(1, bytes)); - return { - type: 'object', - properties: { body: { type: 'string', description: filler } }, - } as unknown as JsonSchemaType; -} - -const SMALL = schemaOfSize(50); -const HUGE = schemaOfSize(20_000); - -function toolDef(name: string, parameters?: JsonSchemaType): ToolDefinition { - return { name, description: `${name} description`, parameters, serverName: 'Server' }; -} - -describe('schemaByteSize', () => { - it('measures the serialized schema', () => { - expect(schemaByteSize(SMALL)).toBeGreaterThan(50); - expect(schemaByteSize(HUGE)).toBeGreaterThan(20_000); - }); - - it('treats a missing schema as weightless', () => { - expect(schemaByteSize(undefined)).toBe(0); - }); - - it('does not throw on a schema that cannot be serialized', () => { - const circular: Record = { type: 'object' }; - circular.self = circular; - - expect(schemaByteSize(circular as unknown as JsonSchemaType)).toBe(0); - }); -}); - -describe('resolveDeferLoading', () => { - it('leaves every tool loaded when the rule is off', () => { - expect(resolveDeferLoading(undefined, HUGE, 0)).toBe(false); - }); - - it('defers a schema over the limit', () => { - expect(resolveDeferLoading(undefined, HUGE, 4_096)).toBe(true); - }); - - it('leaves a schema under the limit alone', () => { - expect(resolveDeferLoading(undefined, SMALL, 4_096)).toBe(false); - }); - - it('lets an explicit false pin a huge tool open', () => { - expect(resolveDeferLoading(false, HUGE, 4_096)).toBe(false); - }); - - it('lets an explicit true defer a small tool', () => { - expect(resolveDeferLoading(true, SMALL, 0)).toBe(true); - }); - - it('treats a tool with no schema as under any limit', () => { - expect(resolveDeferLoading(undefined, undefined, 1)).toBe(false); - }); -}); - -describe('buildToolRegistryFromAgentOptions with a size rule', () => { - const tools = [toolDef('small_mcp_Server', SMALL), toolDef('huge_mcp_Server', HUGE)]; - - it('reproduces today behavior when the rule is not configured', () => { - const registry = buildToolRegistryFromAgentOptions(tools, {}); - - expect(registry.get('small_mcp_Server')?.defer_loading).toBe(false); - expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(false); - }); - - it('defers only the oversized tool once the rule is set', () => { - const registry = buildToolRegistryFromAgentOptions(tools, {}, 4_096); - - expect(registry.get('small_mcp_Server')?.defer_loading).toBe(false); - expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(true); - }); - - it('keeps the tool description, which is what a shortlist reads', () => { - const registry = buildToolRegistryFromAgentOptions(tools, {}, 4_096); - - expect(registry.get('huge_mcp_Server')?.description).toBe('huge_mcp_Server description'); - expect(registry.get('huge_mcp_Server')?.parameters).toBe(HUGE); - }); - - it('lets a per-tool choice override the rule in both directions', () => { - const options: AgentToolOptions = { - huge_mcp_Server: { defer_loading: false }, - small_mcp_Server: { defer_loading: true }, - }; - - const registry = buildToolRegistryFromAgentOptions(tools, options, 4_096); - - expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(false); - expect(registry.get('small_mcp_Server')?.defer_loading).toBe(true); - }); - - it('applies the rule to a tool whose options set something unrelated', () => { - const options: AgentToolOptions = { - huge_mcp_Server: { allowed_callers: ['direct'] }, - }; - - const registry = buildToolRegistryFromAgentOptions(tools, options, 4_096); - - expect(registry.get('huge_mcp_Server')?.defer_loading).toBe(true); - }); -}); diff --git a/packages/api/src/tools/definitions.ts b/packages/api/src/tools/definitions.ts index 5b469560d78..242515adc4e 100644 --- a/packages/api/src/tools/definitions.ts +++ b/packages/api/src/tools/definitions.ts @@ -47,8 +47,8 @@ export interface LoadToolDefinitionsParams { toolOptions?: AgentToolOptions; /** Whether deferred tools feature is enabled */ deferredToolsEnabled?: boolean; - /** See `mcpSettings.deferSchemaBytes`. `0` leaves every tool as configured. */ - deferSchemaBytes?: number; + /** See `mcpSettings.deferSchemaChars`. `0` leaves every tool as configured. */ + deferSchemaChars?: number; /** Whether programmatic tool calling is enabled */ programmaticToolsEnabled?: boolean; /** Whether code execution is enabled and requested by this agent */ @@ -132,7 +132,7 @@ export async function loadToolDefinitions( tools, toolOptions = {}, deferredToolsEnabled = false, - deferSchemaBytes = 0, + deferSchemaChars = 0, programmaticToolsEnabled = false, codeExecutionEnabled = false, codeExecutionContext, @@ -379,7 +379,7 @@ export async function loadToolDefinitions( getAppConfig, definitionsOnly: true, agentToolOptions: toolOptions, - deferSchemaBytes, + deferSchemaChars, }); const { toolDefinitions, hasDeferredTools } = classificationResult; diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 0832cdf14c6..c0be29113a1 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -2928,10 +2928,11 @@ export const configSchema = z.object({ allowedDomains: z.array(z.string()).optional(), allowedAddresses: allowedAddressesSchema, /** - * Argument-schema size above which an MCP tool starts deferred. A per-tool - * `defer_loading` toggle still wins. `0` disables the rule. + * Argument-schema size, in characters of its JSON, above which an MCP tool + * starts deferred. A per-tool `defer_loading` toggle still wins. `0` + * disables the rule. */ - deferSchemaBytes: z.number().int().nonnegative().max(10_000_000).default(0), + deferSchemaChars: z.number().int().nonnegative().max(10_000_000).default(0), catalogRecovery: z .object({ discoveryBackoffMs: z From 9d4b1b3beb42e3c8e8aef8c9d9bf21226d900c4d Mon Sep 17 00:00:00 2001 From: Ali Gulzar Date: Thu, 24 Sep 2026 13:30:09 +0900 Subject: [PATCH 3/3] feat: show size-deferred tools in the agent builder and let authors keep them loaded --- api/server/controllers/mcp.js | 9 ++ api/server/routes/__tests__/mcp.spec.js | 32 +++++++ .../SidePanel/Agents/MCPToolItem.tsx | 9 +- .../Tools/ItemDialog/sections/McpSection.tsx | 10 ++- .../__tests__/useMCPToolOptions.spec.ts | 79 +++++++++++++++++ client/src/hooks/Agents/useMCPToolOptions.ts | 88 +++++++++++++++++-- client/src/locales/en/translation.json | 1 + packages/data-provider/src/schemas.ts | 2 + packages/data-provider/src/types/queries.ts | 2 + 9 files changed, 222 insertions(+), 10 deletions(-) diff --git a/api/server/controllers/mcp.js b/api/server/controllers/mcp.js index 5287f81be64..228c5f7a5b5 100644 --- a/api/server/controllers/mcp.js +++ b/api/server/controllers/mcp.js @@ -23,6 +23,7 @@ const { getMCPErrorResponse, prepareMCPServerOAuthDeletion, cleanupDeletedMCPServerOAuthUsers, + resolveDeferLoading, } = require('@librechat/api'); const { Constants, @@ -157,6 +158,7 @@ const getMCPTools = async (req, res) => { } const mcpServers = {}; + const deferSchemaChars = req.config?.mcpSettings?.deferSchemaChars ?? 0; const oboIdentityContext = createAuthIdentityContext({ user: req.user, tenantId: getTenantId(), @@ -251,6 +253,13 @@ const getMCPTools = async (req, res) => { * server-name prefix — the agent editor migrates legacy * persisted ids only when this proves the same tool. */ ...(toolData.serverToolName != null && { serverToolName: toolData.serverToolName }), + ...(resolveDeferLoading( + undefined, + toolData.function.parameters, + deferSchemaChars, + ) && { + deferredBySize: true, + }), }); } } diff --git a/api/server/routes/__tests__/mcp.spec.js b/api/server/routes/__tests__/mcp.spec.js index 974560068db..284d84579ea 100644 --- a/api/server/routes/__tests__/mcp.spec.js +++ b/api/server/routes/__tests__/mcp.spec.js @@ -3591,6 +3591,38 @@ describe('MCP Routes', () => { }); }); + it('marks the tools the schema size rule defers', async () => { + const { Constants } = require('librechat-data-provider'); + const small = `small${Constants.mcp_delimiter}user-server`; + const large = `large${Constants.mcp_delimiter}user-server`; + const tool = (name, parameters) => ({ + type: 'function', + function: { name, description: name, parameters }, + }); + const serverTools = { + [small]: tool(small, { type: 'object' }), + [large]: tool(large, { + type: 'object', + properties: { body: { type: 'string', description: 'x'.repeat(500) } }, + }), + }; + mockRequestConfig = { mcpSettings: { deferSchemaChars: 200 } }; + mockResolveAllMcpConfigs.mockResolvedValueOnce({ + 'user-server': { type: 'sse', url: 'https://user.example.com/sse' }, + }); + mockLoadMCPServerCatalogs.mockResolvedValueOnce({ + serverTools: new Map([['user-server', serverTools]]), + serversWithoutTools: [], + }); + + const response = await request(app).get('/api/mcp/tools'); + + expect(response.status).toBe(200); + const tools = response.body.servers['user-server'].tools; + expect(tools.find((t) => t.pluginKey === large).deferredBySize).toBe(true); + expect(tools.find((t) => t.pluginKey === small)).not.toHaveProperty('deferredBySize'); + }); + it('renders an authoritative empty catalog as a configured server', async () => { const serverConfig = { type: 'sse', url: 'https://empty.example.com/sse' }; mockResolveAllMcpConfigs.mockResolvedValueOnce({ 'empty-server': serverConfig }); diff --git a/client/src/components/SidePanel/Agents/MCPToolItem.tsx b/client/src/components/SidePanel/Agents/MCPToolItem.tsx index 4ba7fb4c7c2..173b857a587 100644 --- a/client/src/components/SidePanel/Agents/MCPToolItem.tsx +++ b/client/src/components/SidePanel/Agents/MCPToolItem.tsx @@ -10,6 +10,8 @@ interface MCPToolItemProps { tool: AgentToolType; isSelected: boolean; isDeferred: boolean; + /** Deferred by `mcpSettings.deferSchemaChars` rather than by a stored option. */ + deferredBySize?: boolean; isProgrammatic: boolean; isBackground: boolean; isIntent: boolean; @@ -34,6 +36,7 @@ export default function MCPToolItem({ tool, isSelected, isDeferred, + deferredBySize = false, onToggleDefer, onToggleSelect, isProgrammatic, @@ -87,7 +90,11 @@ export default function MCPToolItem({ icon={Clock} pressed={isDeferred} label={localize('com_ui_mcp_defer_loading')} - tooltip={localize('com_ui_mcp_click_to_defer')} + tooltip={localize( + deferredBySize && isDeferred + ? 'com_ui_mcp_deferred_by_size' + : 'com_ui_mcp_click_to_defer', + )} activeBorderClass="border-series-4" onToggle={onToggleDefer} /> diff --git a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/McpSection.tsx b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/McpSection.tsx index e52b62a2864..61aaf7e349e 100644 --- a/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/McpSection.tsx +++ b/client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/McpSection.tsx @@ -569,7 +569,11 @@ export default function McpSection({ item }: Props) { key={tool.tool_id} tool={tool} isSelected={selectedTools.includes(tool.tool_id)} - isDeferred={deferredToolsEnabled && isToolDeferred(tool.tool_id)} + isDeferred={ + deferredToolsEnabled && + isToolDeferred(tool.tool_id, tool.metadata?.deferredBySize === true) + } + deferredBySize={tool.metadata?.deferredBySize === true} isProgrammatic={programmaticToolsEnabled && isToolProgrammatic(tool.tool_id)} isBackground={backgroundToolsEnabled && isToolBackground(tool.tool_id)} isIntent={ @@ -584,7 +588,9 @@ export default function McpSection({ item }: Props) { backgroundToolsEnabled={backgroundToolsEnabled} toolIntentsEnabled={toolIntentsEnabled} onToggleSelect={() => toggleToolSelect(tool.tool_id)} - onToggleDefer={() => toggleToolDefer(tool.tool_id)} + onToggleDefer={() => + toggleToolDefer(tool.tool_id, tool.metadata?.deferredBySize === true) + } onToggleProgrammatic={() => toggleToolProgrammatic(tool.tool_id)} onToggleBackground={() => toggleToolBackground(tool.tool_id)} onToggleIntent={() => toggleToolIntent(tool.tool_id)} diff --git a/client/src/hooks/Agents/__tests__/useMCPToolOptions.spec.ts b/client/src/hooks/Agents/__tests__/useMCPToolOptions.spec.ts index aa81114fe38..3742d4bace1 100644 --- a/client/src/hooks/Agents/__tests__/useMCPToolOptions.spec.ts +++ b/client/src/hooks/Agents/__tests__/useMCPToolOptions.spec.ts @@ -211,6 +211,85 @@ describe('useMCPToolOptions', () => { }); }); + describe('tools deferred by schema size', () => { + const sizeDeferredTool = (toolId: string): AgentToolType => ({ + ...createMockTool(toolId), + metadata: { ...createMockTool(toolId).metadata, deferredBySize: true }, + }); + + it('shows a tool deferred by size as deferred when nothing is stored', () => { + (useWatch as jest.Mock).mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolDeferred('tool1', true)).toBe(true); + expect(result.current.isToolDeferred('tool1')).toBe(false); + }); + + it('shows a tool pinned open by an explicit false as loaded', () => { + (useWatch as jest.Mock).mockReturnValue({ tool1: { defer_loading: false } }); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect(result.current.isToolDeferred('tool1', true)).toBe(false); + }); + + it('keeps a tool deferred by size loaded by storing an explicit false', () => { + mockGetValues.mockReturnValue({}); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolDefer('tool1', true); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { defer_loading: false } }, + { shouldDirty: true }, + ); + }); + + it('returns a pinned tool to the size rule by clearing the stored choice', () => { + mockGetValues.mockReturnValue({ tool1: { defer_loading: false } }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleToolDefer('tool1', true); + }); + + expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true }); + }); + + it('counts size-deferred tools when deciding whether all tools are deferred', () => { + (useWatch as jest.Mock).mockReturnValue({ tool2: { defer_loading: true } }); + + const { result } = renderHook(() => useMCPToolOptions()); + + expect( + result.current.areAllToolsDeferred([sizeDeferredTool('tool1'), createMockTool('tool2')]), + ).toBe(true); + }); + + it('undefers all by pinning size-deferred tools and clearing the rest', () => { + (useWatch as jest.Mock).mockReturnValue({ tool2: { defer_loading: true } }); + mockGetValues.mockReturnValue({ tool2: { defer_loading: true } }); + + const { result } = renderHook(() => useMCPToolOptions()); + + act(() => { + result.current.toggleDeferAll([sizeDeferredTool('tool1'), createMockTool('tool2')]); + }); + + expect(mockSetValue).toHaveBeenCalledWith( + 'tool_options', + { tool1: { defer_loading: false } }, + { shouldDirty: true }, + ); + }); + }); + describe('toggleToolProgrammatic', () => { it('should enable programmatic calling for a tool with no existing options', () => { mockGetValues.mockReturnValue({}); diff --git a/client/src/hooks/Agents/useMCPToolOptions.ts b/client/src/hooks/Agents/useMCPToolOptions.ts index ee6a496f231..a18662f5dd1 100644 --- a/client/src/hooks/Agents/useMCPToolOptions.ts +++ b/client/src/hooks/Agents/useMCPToolOptions.ts @@ -21,12 +21,12 @@ interface ToolOptionsFormContext { interface UseMCPToolOptionsReturn { formToolOptions: AgentToolOptions | undefined; - isToolDeferred: (toolId: string) => boolean; + isToolDeferred: (toolId: string, deferredBySize?: boolean) => boolean; isToolProgrammatic: (toolId: string) => boolean; isToolBackground: (toolId: string) => boolean; isToolIntent: (toolId: string) => boolean; isToolProgrammaticOnly: (toolId: string) => boolean; - toggleToolDefer: (toolId: string) => void; + toggleToolDefer: (toolId: string, deferredBySize?: boolean) => void; toggleToolProgrammatic: (toolId: string) => void; toggleToolBackground: (toolId: string) => void; toggleToolIntent: (toolId: string) => void; @@ -69,6 +69,36 @@ export function withBooleanOption( return updatedOptions; } +/** + * Defer is the one flag a server rule can turn on: a tool whose schema is over + * `mcpSettings.deferSchemaChars` defers with no stored option. Keeping such a + * tool loaded stores an explicit `false`; deferring it again clears that, so + * the tool follows the rule. Other tools store exactly what they did before. + */ +export function withDeferOption( + options: AgentToolOptions, + toolId: string, + defer: boolean, + deferredBySize: boolean, +): AgentToolOptions { + if (!deferredBySize) { + return withBooleanOption(options, toolId, 'defer_loading', defer); + } + if (defer) { + return withBooleanOption(options, toolId, 'defer_loading', false); + } + return { ...options, [toolId]: { ...options[toolId], defer_loading: false } }; +} + +export function isDeferred( + options: AgentToolOptions | undefined, + toolId: string, + deferredBySize = false, +): boolean { + const explicit = options?.[toolId]?.defer_loading; + return explicit === true || (explicit == null && deferredBySize); +} + /** * Counterpart of {@link withBooleanOption} for flags whose ABSENCE means * default-on (background-native code execution): enabling clears the entry so @@ -149,7 +179,51 @@ export default function useMCPToolOptions(): UseMCPToolOptionsReturn { const formToolOptions = useWatch({ control, name: 'tool_options' }); const formContext: ToolOptionsFormContext = { formToolOptions, getValues, setValue }; - const defer = useBooleanToolOption('defer_loading', formContext); + const isToolDeferred = useCallback( + (toolId: string, deferredBySize = false): boolean => + isDeferred(formToolOptions, toolId, deferredBySize), + [formToolOptions], + ); + + const toggleToolDefer = useCallback( + (toolId: string, deferredBySize = false) => { + const currentOptions = getValues('tool_options') || {}; + const defer = !isDeferred(currentOptions, toolId, deferredBySize); + setValue('tool_options', withDeferOption(currentOptions, toolId, defer, deferredBySize), { + shouldDirty: true, + }); + }, + [getValues, setValue], + ); + + const areAllToolsDeferred = useCallback( + (tools: AgentToolType[]): boolean => + tools.length > 0 && + tools.every((tool) => + isDeferred(formToolOptions, tool.tool_id, tool.metadata?.deferredBySize === true), + ), + [formToolOptions], + ); + + const toggleDeferAll = useCallback( + (tools: AgentToolType[]) => { + if (tools.length === 0) { + return; + } + const defer = !areAllToolsDeferred(tools); + let updatedOptions = getValues('tool_options') || {}; + for (const tool of tools) { + updatedOptions = withDeferOption( + updatedOptions, + tool.tool_id, + defer, + tool.metadata?.deferredBySize === true, + ); + } + setValue('tool_options', updatedOptions, { shouldDirty: true }); + }, + [areAllToolsDeferred, getValues, setValue], + ); const background = useBooleanToolOption('run_in_background', formContext); const intent = useBooleanToolOption('describe_intent', formContext); @@ -257,20 +331,20 @@ export default function useMCPToolOptions(): UseMCPToolOptionsReturn { return { formToolOptions, - isToolDeferred: defer.isSet, + isToolDeferred, isToolProgrammatic, isToolBackground: background.isSet, isToolIntent: intent.isSet, isToolProgrammaticOnly, - toggleToolDefer: defer.toggle, + toggleToolDefer, toggleToolProgrammatic, toggleToolBackground: background.toggle, toggleToolIntent: intent.toggle, - areAllToolsDeferred: defer.areAllSet, + areAllToolsDeferred, areAllToolsProgrammatic, areAllToolsBackground: background.areAllSet, areAllToolsIntent: intent.areAllSet, - toggleDeferAll: defer.toggleAll, + toggleDeferAll, toggleProgrammaticAll, toggleBackgroundAll: background.toggleAll, toggleIntentAll: intent.toggleAll, diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index db1d8a3aedc..5d94eb988b6 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -1699,6 +1699,7 @@ "com_ui_mcp_defer": "Defer", "com_ui_mcp_defer_all": "Defer all tools", "com_ui_mcp_defer_loading": "Defer loading", + "com_ui_mcp_deferred_by_size": "Deferred automatically because this tool's schema is larger than the server's size limit. Click to keep it loaded instead.", "com_ui_mcp_dialog_title": "Configure Variables for {{serverName}}. Server Status: {{status}}", "com_ui_mcp_domain_not_allowed": "The MCP server domain is not in the allowed domains list. Please contact your administrator.", "com_ui_mcp_enter_var": "Enter value for {{0}}", diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index a43dfab2b11..6d85384cf9e 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -856,6 +856,8 @@ export const tPluginSchema = z.object({ /** Raw upstream tool name when the model-facing key stripped a redundant * server-name prefix — proves upstream identity for legacy id migration. */ serverToolName: z.string().optional(), + /** Deferred by `mcpSettings.deferSchemaChars` when the agent stores no choice for it. */ + deferredBySize: z.boolean().optional(), }); export type TPlugin = z.infer; diff --git a/packages/data-provider/src/types/queries.ts b/packages/data-provider/src/types/queries.ts index 73bbf447d6d..b163006504b 100644 --- a/packages/data-provider/src/types/queries.ts +++ b/packages/data-provider/src/types/queries.ts @@ -135,6 +135,8 @@ export type MCPTool = { /** Raw upstream tool name when the model-facing key stripped a redundant * server-name prefix — gates the agent editor's legacy id migration. */ serverToolName?: string; + /** Deferred by `mcpSettings.deferSchemaChars` when the agent stores no choice for it. */ + deferredBySize?: boolean; }; export type MCPServer = {