diff --git a/tools/server/webui/src/lib/config/mcp.ts b/tools/server/webui/src/lib/config/mcp.ts index 83e08ae557..b85851d06f 100644 --- a/tools/server/webui/src/lib/config/mcp.ts +++ b/tools/server/webui/src/lib/config/mcp.ts @@ -34,12 +34,14 @@ export function parseMcpServerSettings( ); const url = typeof entry?.url === 'string' ? entry.url.trim() : ''; + const headers = typeof entry?.headers === 'string' ? entry.headers.trim() : undefined; return { id: generateMcpServerId((entry as { id?: unknown })?.id, index), enabled: Boolean((entry as { enabled?: unknown })?.enabled), url, - requestTimeoutSeconds + requestTimeoutSeconds, + headers: headers || undefined } satisfies MCPServerSettingsEntry; }); } @@ -52,11 +54,25 @@ function buildServerConfig( return undefined; } + // Parse custom headers if provided + let headers: Record | undefined; + if (entry.headers) { + try { + const parsed = JSON.parse(entry.headers); + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + headers = parsed as Record; + } + } catch { + console.warn('[MCP] Failed to parse custom headers JSON, ignoring:', entry.headers); + } + } + return { url: entry.url, transport: detectMcpTransportFromUrl(entry.url), handshakeTimeoutMs: connectionTimeoutMs, - requestTimeoutMs: Math.round(entry.requestTimeoutSeconds * 1000) + requestTimeoutMs: Math.round(entry.requestTimeoutSeconds * 1000), + headers }; } diff --git a/tools/server/webui/src/lib/mcp/client.ts b/tools/server/webui/src/lib/mcp/client.ts index 5386483524..3f2b4ce411 100644 --- a/tools/server/webui/src/lib/mcp/client.ts +++ b/tools/server/webui/src/lib/mcp/client.ts @@ -78,6 +78,69 @@ export class MCPClient { return Array.from(this.toolsToServer.keys()); } + /** + * Normalize JSON Schema properties to ensure all have explicit types. + * Infers type from default value if missing - fixes compatibility with + * llama.cpp which requires explicit types in tool schemas. + */ + private normalizeSchemaProperties(schema: Record): Record { + if (!schema || typeof schema !== 'object') return schema; + + const normalized = { ...schema }; + + // Process properties object + if (normalized.properties && typeof normalized.properties === 'object') { + const props = normalized.properties as Record>; + const normalizedProps: Record> = {}; + + for (const [key, prop] of Object.entries(props)) { + if (!prop || typeof prop !== 'object') { + normalizedProps[key] = prop; + continue; + } + + const normalizedProp = { ...prop }; + + // Infer type from default if missing + if (!normalizedProp.type && normalizedProp.default !== undefined) { + const defaultVal = normalizedProp.default; + if (typeof defaultVal === 'string') { + normalizedProp.type = 'string'; + } else if (typeof defaultVal === 'number') { + normalizedProp.type = Number.isInteger(defaultVal) ? 'integer' : 'number'; + } else if (typeof defaultVal === 'boolean') { + normalizedProp.type = 'boolean'; + } else if (Array.isArray(defaultVal)) { + normalizedProp.type = 'array'; + } else if (typeof defaultVal === 'object' && defaultVal !== null) { + normalizedProp.type = 'object'; + } + } + + // Recursively normalize nested schemas + if (normalizedProp.properties) { + Object.assign( + normalizedProp, + this.normalizeSchemaProperties(normalizedProp as Record) + ); + } + + // Normalize items in array schemas + if (normalizedProp.items && typeof normalizedProp.items === 'object') { + normalizedProp.items = this.normalizeSchemaProperties( + normalizedProp.items as Record + ); + } + + normalizedProps[key] = normalizedProp; + } + + normalized.properties = normalizedProps; + } + + return normalized; + } + async getToolsDefinition(): Promise< { type: 'function'; @@ -91,16 +154,21 @@ export class MCPClient { for (const [, server] of this.servers) { for (const tool of server.tools) { + const rawSchema = (tool.inputSchema as Record) ?? { + type: 'object', + properties: {}, + required: [] + }; + + // Normalize schema to fix missing types + const normalizedSchema = this.normalizeSchemaProperties(rawSchema); + tools.push({ type: 'function', function: { name: tool.name, description: tool.description, - parameters: (tool.inputSchema as Record) ?? { - type: 'object', - properties: {}, - required: [] - } + parameters: normalizedSchema } }); } diff --git a/tools/server/webui/src/lib/mcp/host-manager.ts b/tools/server/webui/src/lib/mcp/host-manager.ts index 73181a6de3..84df0afd32 100644 --- a/tools/server/webui/src/lib/mcp/host-manager.ts +++ b/tools/server/webui/src/lib/mcp/host-manager.ts @@ -178,23 +178,93 @@ export class MCPHostManager { return allTools; } + /** + * Normalize JSON Schema properties to ensure all have explicit types. + * Infers type from default value if missing - fixes compatibility with + * llama.cpp which requires explicit types in tool schemas. + */ + private normalizeSchemaProperties(schema: Record): Record { + if (!schema || typeof schema !== 'object') return schema; + + const normalized = { ...schema }; + + // Process properties object + if (normalized.properties && typeof normalized.properties === 'object') { + const props = normalized.properties as Record>; + const normalizedProps: Record> = {}; + + for (const [key, prop] of Object.entries(props)) { + if (!prop || typeof prop !== 'object') { + normalizedProps[key] = prop; + continue; + } + + const normalizedProp = { ...prop }; + + // Infer type from default if missing + if (!normalizedProp.type && normalizedProp.default !== undefined) { + const defaultVal = normalizedProp.default; + if (typeof defaultVal === 'string') { + normalizedProp.type = 'string'; + } else if (typeof defaultVal === 'number') { + normalizedProp.type = Number.isInteger(defaultVal) ? 'integer' : 'number'; + } else if (typeof defaultVal === 'boolean') { + normalizedProp.type = 'boolean'; + } else if (Array.isArray(defaultVal)) { + normalizedProp.type = 'array'; + } else if (typeof defaultVal === 'object' && defaultVal !== null) { + normalizedProp.type = 'object'; + } + } + + // Recursively normalize nested schemas + if (normalizedProp.properties) { + Object.assign( + normalizedProp, + this.normalizeSchemaProperties(normalizedProp as Record) + ); + } + + // Normalize items in array schemas + if (normalizedProp.items && typeof normalizedProp.items === 'object') { + normalizedProp.items = this.normalizeSchemaProperties( + normalizedProp.items as Record + ); + } + + normalizedProps[key] = normalizedProp; + } + + normalized.properties = normalizedProps; + } + + return normalized; + } + /** * Returns tools in OpenAI function calling format. * Ready to be sent to /v1/chat/completions API. */ getToolDefinitionsForLLM(): OpenAIToolDefinition[] { - return this.getAllTools().map((tool) => ({ - type: 'function' as const, - function: { - name: tool.name, - description: tool.description, - parameters: (tool.inputSchema as Record) ?? { - type: 'object', - properties: {}, - required: [] + return this.getAllTools().map((tool) => { + const rawSchema = (tool.inputSchema as Record) ?? { + type: 'object', + properties: {}, + required: [] + }; + + // Normalize schema to fix missing types + const normalizedSchema = this.normalizeSchemaProperties(rawSchema); + + return { + type: 'function' as const, + function: { + name: tool.name, + description: tool.description, + parameters: normalizedSchema } - } - })); + }; + }); } /** diff --git a/tools/server/webui/src/lib/types/mcp.ts b/tools/server/webui/src/lib/types/mcp.ts index dbbff703e5..82e168f254 100644 --- a/tools/server/webui/src/lib/types/mcp.ts +++ b/tools/server/webui/src/lib/types/mcp.ts @@ -75,4 +75,10 @@ export type MCPServerSettingsEntry = { enabled: boolean; url: string; requestTimeoutSeconds: number; + /** Optional custom HTTP headers (JSON string of key-value pairs). */ + headers?: string; + /** Server name from metadata (fetched during health check). */ + name?: string; + /** Server icon URL from metadata (fetched during health check). */ + iconUrl?: string; };