Source code
Revision control
Copy as Markdown
Other Tools
<!DOCTYPE html>
<html>
<head>
<title>WebMCP Iframe Tool Registration</title>
<script src="/resources/testharness.js"></script>
</head>
<body>
<script>
// This is a helper function to convert an array of `Tool` objects to plain
// objects that can be sent over `postMessage()`. This exists because `Tool`
// itself is not cloneable, and neither is its `window` attribute, so we must
// reduce the tool structure a bit before sending it.
//
// `Tool`'s attributes are accessors on `Tool.prototype`, not own properties
// of each instance, so we enumerate the prototype's getters to copy every
// attribute (except `window`).
const kToolKeys =
Object.entries(Object.getOwnPropertyDescriptors(Tool.prototype))
.filter(([key, desc]) => desc.get && key !== 'window')
.map(([key, _]) => key);
function convertToCloneableTools(tools) {
return tools.map(tool =>
Object.fromEntries(kToolKeys.map(key => [key, tool[key]])));
}
// Map from tool_name => `AbortController`, so that this iframe document can
// manage/unregister any tool that it registers.
const controllers = new Map();
window.onmessage = async e => {
const action = typeof e.data === 'string' ? e.data : e.data.action;
if (action === 'register') {
try {
const controller = new AbortController();
const tool = e.data.tool;
if (e.data.delayResolutionAndReportAbort) {
tool.execute = (input, opts) => new Promise((resolve, reject) => {
document.modelContext.addEventListener('toolcancel', ev => {
parent.postMessage({ action: 'toolcancel_fired', toolName: ev.toolName }, '*');
}, { once: true });
parent.postMessage({ action: 'tool_started' }, '*');
if (!opts || !opts.signal) {
parent.postMessage({ action: 'signal_aborted_result', error: 'no signal provided' }, '*');
return;
}
opts.signal.addEventListener('abort', () => {
parent.postMessage({ action: 'signal_aborted_result', reason: String(opts.signal.reason) }, '*');
});
const finishListener = e => {
if (typeof e.data === 'string' && e.data === 'finish_tool') {
window.removeEventListener('message', finishListener);
resolve('delayed_result');
}
};
window.addEventListener('message', finishListener);
});
} else if (e.data.hangsForeverAndReportsAbort) {
tool.execute = (input, opts) => new Promise((resolve, reject) => {
document.modelContext.addEventListener('toolcancel', ev => {
parent.postMessage({ action: 'toolcancel_fired', toolName: ev.toolName }, '*');
}, { once: true });
parent.postMessage({ action: 'tool_started' }, '*');
if (!opts || !opts.signal) {
parent.postMessage({ action: 'signal_aborted_result', error: 'no signal provided' }, '*');
return;
}
opts.signal.addEventListener('abort', () => {
parent.postMessage({ action: 'signal_aborted_result', reason: String(opts.signal.reason) }, '*');
});
});
} else if (e.data.hangsForever) {
tool.execute = () => new Promise(() => {});
} else {
tool.execute = async () => 'hello from iframe';
}
const givenOptions = e.data.options;
const options = { signal: controller.signal, ...givenOptions};
await document.modelContext.registerTool(tool, options);
controllers.set(tool.name, controller);
} catch (e) {
parent.postMessage(`tool registration failed: ${e}`, '*');
}
} else if (action === 'unregister') {
try {
const name = e.data.name || 'default_iframe_tool';
const controller = controllers.get(name);
if (controller) {
controller.abort();
controllers.delete(name);
}
} catch (e) {
parent.postMessage(`tool unregistration failed: ${e}`, '*');
}
} else if (action === 'getTools') {
const options = e.data.options || {};
document.modelContext.getTools(options).then(tools => {
parent.postMessage({action: 'getToolsResponse', tools: convertToCloneableTools(tools)}, '*');
}).catch(e => {
parent.postMessage(`getTools promise rejected: ${e}`, '*');
});
} else if (action === 'execute') {
const name = e.data.name;
const args = e.data.args || {};
const options = e.data.options || {};
document.modelContext.getTools(options).then(tools => {
const tool = tools.find(t => t.name === name);
document.modelContext.executeTool(tool, args).then(result => {
parent.postMessage({action: 'executeResponse', result: result, success: true}, '*');
}).catch(err => {
parent.postMessage({action: 'executeResponse', result: String(err), success: false}, '*');
});
});
} else if (action === 'listenForToolchange') {
let timerId;
const listener = () => {
clearTimeout(timerId);
parent.postMessage({ action: 'toolchange_result', result: 'fired' }, '*');
};
document.modelContext.addEventListener('toolchange', listener, { once: true });
// Send ACK so the caller knows the listener is active.
parent.postMessage({ action: 'toolchange_listening_ack' }, '*');
timerId = step_timeout(() => {
document.modelContext.removeEventListener('toolchange', listener);
parent.postMessage({ action: 'toolchange_result', result: 'timeout' }, '*');
}, 4000);
}
};
</script>
</body>
</html>