// bridgeUtils.ts - Shared utilities for MCP bridge functions
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
/* ───────── JSON Schema to Zod conversion ───────── */
export function jsonSchemaToZodShape(jsonSchema: any): Record<string, any> {
if (!jsonSchema?.properties) {
return {};
}
const shape: Record<string, any> = {};
for (const [key, prop] of Object.entries(jsonSchema.properties as Record<string, any>)) {
// Convert JSON schema types to Zod types
switch (prop.type) {
case 'string':
shape[key] = z.string();
break;
case 'number':
shape[key] = z.number();
break;
case 'integer':
shape[key] = z.number().int();
break;
case 'boolean':
shape[key] = z.boolean();
break;
case 'array':
// Handle array types more carefully
if (prop.items) {
// If items are defined, use them
const itemType = prop.items.type || 'any';
switch (itemType) {
case 'string':
shape[key] = z.array(z.string());
break;
case 'number':
shape[key] = z.array(z.number());
break;
case 'boolean':
shape[key] = z.array(z.boolean());
break;
default:
shape[key] = z.array(z.any());
}
} else {
// Default to string array if no items defined
shape[key] = z.array(z.string());
}
break;
case 'object':
shape[key] = z.object({}).passthrough();
break;
default:
shape[key] = z.any();
}
// Make optional if not in required array
if (!jsonSchema.required?.includes(key)) {
shape[key] = shape[key].optional();
}
// Add description if available
if (prop.description) {
shape[key] = shape[key].describe(prop.description);
}
}
return shape;
}
/* ───────── Tool registration helper ───────── */
export function registerBridgedTools(
hub: McpServer,
client: Client,
tools: any[],
prefix: string,
omit?: string[]
): void {
for (const t of tools as any[]) {
// Check if this tool should be omitted
if (omit && omit.includes(t.name)) {
console.error(`[bridge] skipping omitted tool: ${t.name}`);
continue;
}
// 1) Convert JSON schema to Zod schema shape
const zodShape = jsonSchemaToZodShape(t.inputSchema);
// 2) Build the Zod schema
const schema = z.object(zodShape).passthrough();
const keys = Object.keys(zodShape);
console.error(`[bridge] tool ${t.name} schema keys:`, keys.slice(0, 10), keys.length > 10 ? `(+${keys.length - 10} more)` : "");
const inputKeys = Object.keys(t.inputSchema?.properties ?? {});
const maxKeysToShow = 10;
console.error(
`[bridge] tool ${t.name} input schema keys:`,
inputKeys.slice(0, maxKeysToShow),
inputKeys.length > maxKeysToShow ? `(+${inputKeys.length - maxKeysToShow} more)` : ""
);
// 3) Pull description into annotations
const annotations = {
...(t.annotations ?? {}),
description: t.description ?? t.name,
openWorldHint: true,
};
// 4) Register tool with the hub
hub.tool(
`${prefix}_${t.name}`,
schema.shape,
annotations,
async (args: any, _extra: any) => {
console.error("[bridge] args =", args ? "OK" : "(none)");
console.error(`[bridge] → ${t.name} with `, _extra ? "extra" : "(no extra)");
try {
/* call the upstream tool */
const result = await client.callTool(
{ name: t.name, arguments: args }
);
console.error(`[bridge] ← ${t.name} result `, result ? "OK" : "(none)");
// Return the result as-is, let the MCP server handle any schema validation
return result as any;
} catch (e) {
/* log & surface the error without crashing the hub */
console.error(`[bridge] ← ${t.name} ERROR`, e instanceof Error ? e.message : String(e));
return {
content: [
{
type: "text",
text:
e instanceof Error
? e.message
: String(e ?? "unknown error"),
},
],
isError: true,
};
}
},
);
}
}