feat: Enhance server config with headers and schema normalization
This commit is contained in:
parent
778ad550b1
commit
f87b10ee66
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue