diff --git a/packages/api/src/mcp/__tests__/parsers.test.ts b/packages/api/src/mcp/__tests__/parsers.test.ts index afc3fd9de3..0e9bc8ff0a 100644 --- a/packages/api/src/mcp/__tests__/parsers.test.ts +++ b/packages/api/src/mcp/__tests__/parsers.test.ts @@ -29,6 +29,72 @@ describe('formatToolContent', () => { expect(content).toBe('(No response)'); expect(artifacts).toBeUndefined(); }); + + it('should return string for known non-OpenAI providers', () => { + const result: t.MCPToolCallResponse = { + content: [{ type: 'text', text: 'Test content' }], + }; + const [content] = formatToolContent(result, 'google' as t.Provider); + // Google is recognized but uses array format, so this should be an array + expect(Array.isArray(content)).toBe(true); + }); + }); + + describe('automatic detection of OpenAI-compatible custom endpoints', () => { + it('should automatically recognize new OpenAI-compatible custom endpoints', () => { + const result: t.MCPToolCallResponse = { + content: [ + { type: 'text', text: 'First text' }, + { type: 'text', text: 'Second text' }, + ], + }; + + // Test with a custom endpoint that's not explicitly listed + const [content, artifacts] = formatToolContent(result, 'scaleway' as t.Provider); + // Should be recognized and use array format (OpenAI-compatible) + expect(Array.isArray(content)).toBe(true); + expect(content).toEqual([{ type: 'text', text: 'First text\n\nSecond text' }]); + expect(artifacts).toBeUndefined(); + }); + + it('should use array format for unknown OpenAI-compatible endpoints', () => { + const result: t.MCPToolCallResponse = { + content: [ + { type: 'text', text: 'Before image' }, + { type: 'image', data: 'base64data', mimeType: 'image/png' }, + { type: 'text', text: 'After image' }, + ], + }; + + // Test with another custom endpoint (e.g., together, perplexity, anyscale) + const [content, artifacts] = formatToolContent(result, 'together' as t.Provider); + // Should use array format like OpenAI + expect(Array.isArray(content)).toBe(true); + expect(content).toEqual([ + { type: 'text', text: 'Before image' }, + { type: 'text', text: 'After image' }, + ]); + expect(artifacts).toEqual({ + content: [ + { + type: 'image_url', + image_url: { url: 'data:image/png;base64,base64data' }, + }, + ], + }); + }); + + it('should NOT recognize known non-OpenAI providers', () => { + const result: t.MCPToolCallResponse = { + content: [{ type: 'text', text: 'Test content' }], + }; + + // Non-OpenAI providers should return string format + const [content, artifacts] = formatToolContent(result, 'bedrock' as t.Provider); + expect(typeof content).toBe('string'); + expect(content).toBe('Test content'); + expect(artifacts).toBeUndefined(); + }); }); describe('recognized providers', () => { diff --git a/packages/api/src/mcp/parsers.ts b/packages/api/src/mcp/parsers.ts index c9b824e782..4b81d5b5b0 100644 --- a/packages/api/src/mcp/parsers.ts +++ b/packages/api/src/mcp/parsers.ts @@ -7,6 +7,10 @@ function generateResourceId(text: string): string { return crypto.createHash('sha256').update(text).digest('hex').substring(0, 10); } +// Known providers that are NOT OpenAI-compatible +// This is a small, stable list that rarely changes +const NON_OPENAI_PROVIDERS = new Set(['google', 'anthropic', 'bedrock', 'ollama']); + const RECOGNIZED_PROVIDERS = new Set([ 'google', 'anthropic', @@ -17,8 +21,32 @@ const RECOGNIZED_PROVIDERS = new Set([ 'deepseek', 'ollama', 'bedrock', + // Note: Custom OpenAI-compatible endpoints (like scaleway, together, perplexity, etc.) + // are automatically recognized if they're not in NON_OPENAI_PROVIDERS ]); +/** + * Check if a provider should receive structured content formatting for MCP tool responses. + * + * Recognizes: + * 1. Explicitly listed providers in RECOGNIZED_PROVIDERS + * 2. Custom OpenAI-compatible endpoints (any provider not in NON_OPENAI_PROVIDERS) + * + * Custom endpoints are passed with their endpoint name (not "openai"), so we automatically + * detect them rather than requiring explicit additions for each new provider. + */ +function isRecognizedProvider(provider: t.Provider): boolean { + if (RECOGNIZED_PROVIDERS.has(provider)) { + return true; + } + + if (!NON_OPENAI_PROVIDERS.has(provider)) { + return true; + } + + return false; +} + const imageFormatters: Record = { // google: (item) => ({ // type: 'image', @@ -92,7 +120,7 @@ export function formatToolContent( result: t.MCPToolCallResponse, provider: t.Provider, ): t.FormattedContentResult { - if (!RECOGNIZED_PROVIDERS.has(provider)) { + if (!isRecognizedProvider(provider)) { return [parseAsString(result), undefined]; }