feat: Enhance server config with headers and schema normalization

This commit is contained in:
Aleksander Grygier 2026-01-02 19:37:40 +01:00
parent 778ad550b1
commit f87b10ee66
4 changed files with 178 additions and 18 deletions

View File

@ -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<string, string> | undefined;
if (entry.headers) {
try {
const parsed = JSON.parse(entry.headers);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
headers = parsed as Record<string, string>;
}
} 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
};
}

View File

@ -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<string, unknown>): Record<string, unknown> {
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<string, Record<string, unknown>>;
const normalizedProps: Record<string, Record<string, unknown>> = {};
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<string, unknown>)
);
}
// Normalize items in array schemas
if (normalizedProp.items && typeof normalizedProp.items === 'object') {
normalizedProp.items = this.normalizeSchemaProperties(
normalizedProp.items as Record<string, unknown>
);
}
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<string, unknown>) ?? {
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<string, unknown>) ?? {
type: 'object',
properties: {},
required: []
}
parameters: normalizedSchema
}
});
}

View File

@ -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<string, unknown>): Record<string, unknown> {
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<string, Record<string, unknown>>;
const normalizedProps: Record<string, Record<string, unknown>> = {};
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<string, unknown>)
);
}
// Normalize items in array schemas
if (normalizedProp.items && typeof normalizedProp.items === 'object') {
normalizedProp.items = this.normalizeSchemaProperties(
normalizedProp.items as Record<string, unknown>
);
}
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<string, unknown>) ?? {
type: 'object',
properties: {},
required: []
return this.getAllTools().map((tool) => {
const rawSchema = (tool.inputSchema as Record<string, unknown>) ?? {
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
}
}
}));
};
});
}
/**

View File

@ -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;
};