🪨 fix: Normalize Empty MCP Tool Descriptions to undefined for Bedrock Compat. (#13217)

* fix(agents): normalize empty MCP tool descriptions to undefined

MCP servers (e.g. Asana MCP) can return tools with an empty string
description. AWS Bedrock's converse API rejects toolSpec.description
with length < 1, so any empty-description MCP tool caused the entire
request to fail with a validation error.

Convert empty strings to undefined at the two sites in
loadToolDefinitions where MCP tool definitions are built. An undefined
description is omitted from JSON serialization, so Bedrock never sees
the empty value. OpenAI and Anthropic direct APIs are unaffected.

Fixes #13209

* test(agents): add coverage for empty MCP tool description normalization

Verify that loadToolDefinitions converts '' descriptions to undefined
for both the sys__all__sys pattern and directly named MCP tools.
This commit is contained in:
Serhii Zghama 2026-05-23 20:43:40 +07:00 committed by GitHub
parent 6d6ea08da4
commit 7f0fde2d98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 74 additions and 2 deletions

View file

@ -553,6 +553,78 @@ describe('definitions.ts', () => {
expect(toolDef.name).toBe('list_items_mcp_my-server');
expect((toolDef as { serverName?: string }).serverName).toBe('my-server');
});
it('should convert empty MCP tool descriptions to undefined', async () => {
const mockServerTools = {
no_desc_tool_mcp_asana: {
function: {
name: 'no_desc_tool_mcp_asana',
description: '',
parameters: { type: 'object', properties: {} },
},
},
has_desc_tool_mcp_asana: {
function: {
name: 'has_desc_tool_mcp_asana',
description: 'List tasks',
parameters: { type: 'object', properties: {} },
},
},
};
mockGetOrFetchMCPServerTools.mockResolvedValue(mockServerTools);
const params: LoadToolDefinitionsParams = {
userId: 'user-123',
agentId: 'agent-123',
tools: ['sys__all__sys_mcp_asana'],
};
const deps: LoadToolDefinitionsDeps = {
getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools,
isBuiltInTool: mockIsBuiltInTool,
};
const result = await loadToolDefinitions(params, deps);
const noDef = result.toolDefinitions.find((d) => d.name === 'no_desc_tool_mcp_asana');
expect(noDef).toBeDefined();
expect(noDef?.description).toBeUndefined();
const hasDef = result.toolDefinitions.find((d) => d.name === 'has_desc_tool_mcp_asana');
expect(hasDef).toBeDefined();
expect(hasDef?.description).toBe('List tasks');
});
it('should convert empty description to undefined for directly named MCP tool', async () => {
const toolName = 'no_desc_tool_mcp_asana';
mockGetOrFetchMCPServerTools.mockResolvedValue({
[toolName]: {
function: {
name: toolName,
description: '',
parameters: { type: 'object', properties: {} },
},
},
});
const params: LoadToolDefinitionsParams = {
userId: 'user-123',
agentId: 'agent-123',
tools: [toolName],
};
const deps: LoadToolDefinitionsDeps = {
getOrFetchMCPServerTools: mockGetOrFetchMCPServerTools,
isBuiltInTool: mockIsBuiltInTool,
};
const result = await loadToolDefinitions(params, deps);
const def = result.toolDefinitions.find((d) => d.name === toolName);
expect(def).toBeDefined();
expect(def?.description).toBeUndefined();
});
});
describe('toolkit expansion', () => {

View file

@ -158,7 +158,7 @@ export async function loadToolDefinitions(
if (toolDef?.function) {
mcpToolDefs.push({
name: actualToolName,
description: toolDef.function.description,
description: toolDef.function.description || undefined,
parameters: toolDef.function.parameters
? normalizeJsonSchema(resolveJsonSchemaRefs(toolDef.function.parameters))
: undefined,
@ -173,7 +173,7 @@ export async function loadToolDefinitions(
if (toolDef?.function) {
mcpToolDefs.push({
name: toolName,
description: toolDef.function.description,
description: toolDef.function.description || undefined,
parameters: toolDef.function.parameters
? normalizeJsonSchema(resolveJsonSchemaRefs(toolDef.function.parameters))
: undefined,