From 73107e6e3b0d926e4cfdbfd6639446d822121804 Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Fri, 6 Mar 2026 11:39:01 +0100 Subject: [PATCH] refactor(mcp): auto-detect OpenAI-compatible custom endpoints in formatToolContent Replace explicit provider allowlist with automatic detection for MCP tool content formatting. Unknown providers are assumed OpenAI-compatible unless listed in NON_OPENAI_PROVIDERS, eliminating the need to update code for each new custom endpoint (scaleway, together, perplexity, etc.). --- .../api/src/mcp/__tests__/parsers.test.ts | 66 ++++++++++++++ packages/api/src/mcp/parsers.ts | 89 ++++++++++++++++--- 2 files changed, 141 insertions(+), 14 deletions(-) diff --git a/packages/api/src/mcp/__tests__/parsers.test.ts b/packages/api/src/mcp/__tests__/parsers.test.ts index dd9a09a0fb..d4fc6d4216 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 - content array providers', () => { diff --git a/packages/api/src/mcp/parsers.ts b/packages/api/src/mcp/parsers.ts index 76e59b2e9c..55ebf486f5 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,9 +21,64 @@ 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 ]); + +// Known providers that use content array format (structured content blocks) +// These are the standard OpenAI-compatible providers plus Google and Anthropic const CONTENT_ARRAY_PROVIDERS = new Set(['google', 'anthropic', 'azureopenai', 'openai']); +/** + * 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; +} + +/** + * Check if a provider uses content array format (structured content blocks). + * + * Uses array format: + * - Standard OpenAI-compatible providers (openai, azureopenai) + * - Google and Anthropic (native array format) + * - New unknown custom endpoints (assumed OpenAI-compatible, so use array format) + * + * Uses string format: + * - Known custom providers with special handling (openrouter, xai, deepseek) + * - Other non-OpenAI providers (ollama, bedrock) + */ +function usesContentArrayFormat(provider: t.Provider): boolean { + if (CONTENT_ARRAY_PROVIDERS.has(provider)) { + return true; + } + + if (['openrouter', 'xai', 'deepseek'].includes(provider)) { + return false; + } + + if (!NON_OPENAI_PROVIDERS.has(provider)) { + return true; + } + + return false; +} + const imageFormatters: Record = { // google: (item) => ({ // type: 'image', @@ -81,19 +140,23 @@ function parseAsString(result: t.MCPToolCallResponse): string { } /** - * Converts MCPToolCallResponse content into recognized content block types - * First element: string or formatted content (excluding image_url) - * Second element: Recognized types - "image", "image_url", "text", "json" + * Formats MCP tool call response content for different provider types. * - * @param result - The MCPToolCallResponse object - * @param provider - The provider name (google, anthropic, openai) - * @returns Tuple of content and image_urls + * Handles provider-specific formatting: + * - OpenAI-compatible providers: Uses content array format with artifacts + * - Non-OpenAI providers: Uses string format + * + * Automatically detects custom OpenAI-compatible endpoints (not in NON_OPENAI_PROVIDERS). + * + * @param result - MCP tool call response with content array + * @param provider - Provider identifier (e.g., 'openai', 'scaleway', 'anthropic') + * @returns Tuple of [formattedContent, artifacts] where artifacts contain image URLs */ export function formatToolContent( result: t.MCPToolCallResponse, provider: t.Provider, ): t.FormattedContentResult { - if (!RECOGNIZED_PROVIDERS.has(provider)) { + if (!isRecognizedProvider(provider)) { return [parseAsString(result), undefined]; } @@ -122,7 +185,7 @@ export function formatToolContent( if (!isImageContent(item)) { return; } - if (CONTENT_ARRAY_PROVIDERS.has(provider) && currentTextBlock) { + if (usesContentArrayFormat(provider) && currentTextBlock) { formattedContent.push({ type: 'text', text: currentTextBlock }); currentTextBlock = ''; } @@ -195,7 +258,7 @@ UI Resource Markers Available: currentTextBlock += uiInstructions; } - if (CONTENT_ARRAY_PROVIDERS.has(provider) && currentTextBlock) { + if (usesContentArrayFormat(provider) && currentTextBlock) { formattedContent.push({ type: 'text', text: currentTextBlock }); } @@ -211,9 +274,7 @@ UI Resource Markers Available: }; } - if (CONTENT_ARRAY_PROVIDERS.has(provider)) { - return [formattedContent, artifacts]; - } - - return [currentTextBlock, artifacts]; + return usesContentArrayFormat(provider) + ? [formattedContent, artifacts] + : [currentTextBlock, artifacts]; }