Both MCP servers serve six ui:// views (a circuit, a result with its verdict, a receipt, a critique, an estimate's frontier, a device target's map) through the protocol's extension for interactive views, version 2026-01-26, to a host that says it draws them: each tool names its view in _meta.ui.resourceUri, the view's data (the engine's layout and the circuit in words, a target's map) rides in the result's _meta, and the service's poll_quantum_job, which a view calls while a job runs, is kept from the model. The service keeps no session, so the client's word travels in the session id it is given at initialize. Every host gets each such answer in plain words. The views are one self-contained page (736 KB, held under 1 MiB) built from the Studio's components, carrying the engine (wai-quantum-views) as WebAssembly and translated to JavaScript. Under the extension's default content-security policy the browser refuses WebAssembly, so the JavaScript form checks the receipt there; a scripted host in a headless browser draws every view from both servers under both policies and holds the check, a changed result, the size and no outside request. The page rebuilds to the same bytes in the CI image. Co-Authored-By: Claude Opus 5.5 <[email protected]>
69 lines
3.3 KiB
JavaScript
69 lines
3.3 KiB
JavaScript
// The view's side of the host protocol for interactive views (the Model
|
|
// Context Protocol's extension io.modelcontextprotocol/ui, version
|
|
// 2026-01-26): JSON-RPC 2.0 over postMessage with the window that framed the
|
|
// view.
|
|
//
|
|
// The view asks ui/initialize, applies the host's context, says
|
|
// ui/notifications/initialized, and then hears the tool's input
|
|
// (ui/notifications/tool-input, once, before the result), its result
|
|
// (ui/notifications/tool-result), a cancellation, a change of context, and
|
|
// teardown. It may call the server's tools (tools/call: only the ones whose
|
|
// visibility includes the view), tell the host its size, and set what the
|
|
// model reads of it (ui/update-model-context).
|
|
export const PROTOCOL = '2026-01-26';
|
|
|
|
export function connect({ name, version, onInput, onResult, onCancelled, onContext, onTeardown }) {
|
|
let next = 1;
|
|
const waiting = new Map();
|
|
const send = (msg) => window.parent.postMessage(msg, '*');
|
|
const respond = (id, result) => send({ jsonrpc: '2.0', id, result });
|
|
const refuse = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
const request = (method, params = {}) => new Promise((resolve, reject) => {
|
|
const id = next++;
|
|
waiting.set(id, { resolve, reject });
|
|
send({ jsonrpc: '2.0', id, method, params });
|
|
});
|
|
const notify = (method, params = {}) => send({ jsonrpc: '2.0', method, params });
|
|
|
|
window.addEventListener('message', (ev) => {
|
|
if (ev.source !== window.parent) return;
|
|
const m = ev.data;
|
|
if (!m || m.jsonrpc !== '2.0') return;
|
|
if (m.method == null && m.id != null) {
|
|
const w = waiting.get(m.id);
|
|
if (!w) return;
|
|
waiting.delete(m.id);
|
|
if (m.error) w.reject(Object.assign(new Error(m.error.message || 'the host refused'), { code: m.error.code }));
|
|
else w.resolve(m.result);
|
|
return;
|
|
}
|
|
switch (m.method) {
|
|
case 'ui/notifications/tool-input': onInput?.(m.params?.arguments ?? {}); break;
|
|
case 'ui/notifications/tool-input-partial': break;
|
|
case 'ui/notifications/tool-result': onResult?.(m.params ?? {}); break;
|
|
case 'ui/notifications/tool-cancelled': onCancelled?.(m.params?.reason); break;
|
|
case 'ui/notifications/host-context-changed': onContext?.(m.params ?? {}); break;
|
|
case 'ui/resource-teardown': onTeardown?.(m.params?.reason); if (m.id != null) respond(m.id, {}); break;
|
|
case 'ping': if (m.id != null) respond(m.id, {}); break;
|
|
default: if (m.id != null) refuse(m.id, -32601, `the view takes no ${m.method}`);
|
|
}
|
|
});
|
|
|
|
return {
|
|
// The handshake: the host's answer, its context applied, then initialized.
|
|
async start() {
|
|
const init = await request('ui/initialize', {
|
|
protocolVersion: PROTOCOL,
|
|
appInfo: { name, version },
|
|
appCapabilities: { availableDisplayModes: ['inline'] },
|
|
});
|
|
onContext?.(init?.hostContext ?? {});
|
|
notify('ui/notifications/initialized');
|
|
return init;
|
|
},
|
|
callTool: (tool, args) => request('tools/call', { name: tool, arguments: args }),
|
|
sizeChanged: (width, height) => notify('ui/notifications/size-changed', { width, height }),
|
|
// What the model reads of the view; each call replaces the last.
|
|
tellModel: (text, structuredContent) => request('ui/update-model-context', { content: [{ type: 'text', text }], ...(structuredContent ? { structuredContent } : {}) }),
|
|
};
|
|
}
|