type ToolArgs = Record<string, unknown>;
type ToolHelper = {
callTool: (name: string, args: ToolArgs) => Promise<unknown> | unknown;
};
type MessageEventLike = {
data: unknown;
origin?: string;
source?: unknown;
};
type MessageListener = (event: MessageEventLike) => void;
type MessageTarget = {
postMessage: (message: unknown, targetOrigin?: string) => void;
};
type BridgeHost = {
openai?: ToolHelper;
mcp?: ToolHelper;
parent?: MessageTarget;
addEventListener?: (type: 'message', listener: MessageListener) => void;
removeEventListener?: (type: 'message', listener: MessageListener) => void;
};
export interface ToolBridgeOptions {
host?: BridgeHost;
requestTimeoutMs?: number;
targetOrigin?: string;
idPrefix?: string;
}
const DEFAULT_TIMEOUT_MS = 5000;
function getDefaultTargetOrigin(): string {
if (typeof document === 'undefined') {
return '*';
}
const referrer = document.referrer;
if (typeof referrer !== 'string' || referrer.trim().length === 0) {
return '*';
}
try {
return new URL(referrer).origin;
} catch {
return '*';
}
}
function normalizeTargetOrigin(value: string): string {
if (value === '*') {
return value;
}
try {
return new URL(value).origin;
} catch {
throw new Error(`Invalid targetOrigin: ${value}`);
}
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function normalizeToolArgs(args: ToolArgs | undefined): ToolArgs {
return args ?? {};
}
function extractErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}
return String(error);
}
function isHelperUnavailableError(error: unknown): boolean {
if (!isObject(error)) {
return false;
}
const code = typeof error.code === 'string' ? error.code.toLowerCase() : '';
if (code === 'not_implemented' || code === 'not_supported' || code === 'unavailable') {
return true;
}
const message = extractErrorMessage(error).toLowerCase();
return message.includes('not implemented')
|| message.includes('not supported')
|| message.includes('unavailable')
|| message.includes('not available');
}
export function createToolBridge(options: ToolBridgeOptions = {}) {
const host = options.host ?? (globalThis as BridgeHost);
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS;
const targetOrigin = normalizeTargetOrigin(options.targetOrigin ?? getDefaultTargetOrigin());
const idPrefix = options.idPrefix ?? 'tool-bridge';
let requestCounter = 0;
async function callViaFallback(name: string, args: ToolArgs): Promise<unknown> {
if (!host.parent || !host.addEventListener || !host.removeEventListener) {
throw new Error('JSON-RPC fallback is unavailable in this host environment.');
}
const parent = host.parent;
const addListener = (type: 'message', listener: MessageListener): void => {
host.addEventListener?.(type, listener);
};
const removeListener = (type: 'message', listener: MessageListener): void => {
host.removeEventListener?.(type, listener);
};
requestCounter += 1;
const requestId = `${idPrefix}:${requestCounter}`;
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
removeListener('message', onMessage);
reject(new Error(`Tool call fallback timed out after ${timeoutMs}ms (request: ${requestId})`));
}, timeoutMs);
const onMessage: MessageListener = (event) => {
const payload = event.data;
if (event.source !== parent) {
return;
}
if (targetOrigin !== '*' && event.origin !== targetOrigin) {
return;
}
if (!isObject(payload) || payload.id !== requestId) {
return;
}
clearTimeout(timeoutHandle);
removeListener('message', onMessage);
if (isObject(payload.error)) {
const rawMessage = payload.error.message;
const message = typeof rawMessage === 'string'
? rawMessage
: 'Unknown tools/call fallback error';
reject(new Error(`Tool call fallback failed: ${message}`));
return;
}
resolve(payload.result);
};
addListener('message', onMessage);
parent.postMessage(
{
jsonrpc: '2.0',
id: requestId,
method: 'tools/call',
params: {
name,
arguments: args,
},
},
targetOrigin
);
});
}
async function callTool(name: string, args?: ToolArgs): Promise<unknown> {
const normalizedArgs = normalizeToolArgs(args);
const helperCandidates = [host.openai, host.mcp].filter(
(candidate): candidate is ToolHelper => Boolean(candidate?.callTool)
);
for (const helper of helperCandidates) {
try {
return await helper.callTool(name, normalizedArgs);
} catch (error) {
if (isHelperUnavailableError(error)) {
continue;
}
throw new Error(`Tool helper call failed: ${extractErrorMessage(error)}`);
}
}
try {
return await callViaFallback(name, normalizedArgs);
} catch (fallbackError) {
throw fallbackError;
}
}
return {
callTool,
};
}