@cyanheads/mcp-ts-core 0.13.10 → 0.13.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -6
- package/CLAUDE.md +9 -6
- package/README.md +29 -3
- package/biome.json +1 -1
- package/changelog/0.13.x/0.13.11.md +113 -0
- package/changelog/0.13.x/0.13.12.md +50 -0
- package/dist/cli/app-render.d.ts +11 -0
- package/dist/cli/app-render.d.ts.map +1 -0
- package/dist/cli/app-render.js +150 -0
- package/dist/cli/app-render.js.map +1 -0
- package/dist/cli/init.d.ts +2 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +10 -2
- package/dist/cli/init.js.map +1 -1
- package/dist/core/context.d.ts +3 -2
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +25 -16
- package/dist/core/context.js.map +1 -1
- package/dist/mcp-server/handlerContext.d.ts +14 -8
- package/dist/mcp-server/handlerContext.d.ts.map +1 -1
- package/dist/mcp-server/handlerContext.js +16 -9
- package/dist/mcp-server/handlerContext.js.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +15 -7
- package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts +8 -2
- package/dist/mcp-server/tools/utils/inputPrevalidation.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/inputPrevalidation.js +18 -7
- package/dist/mcp-server/tools/utils/inputPrevalidation.js.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js +79 -8
- package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.d.ts +20 -0
- package/dist/mcp-server/transports/auth/lib/authUtils.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/lib/authUtils.js +28 -2
- package/dist/mcp-server/transports/auth/lib/authUtils.js.map +1 -1
- package/dist/mcp-server/transports/auth/lib/checkScopes.d.ts.map +1 -1
- package/dist/mcp-server/transports/auth/lib/checkScopes.js +2 -1
- package/dist/mcp-server/transports/auth/lib/checkScopes.js.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpErrorHandler.js +4 -2
- package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
- package/dist/services/mirror/sqlite/handle.d.ts +22 -5
- package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
- package/dist/services/mirror/sqlite/handle.js +61 -12
- package/dist/services/mirror/sqlite/handle.js.map +1 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts +6 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.d.ts.map +1 -1
- package/dist/services/mirror/sqlite/sqliteMirrorStore.js +3 -2
- package/dist/services/mirror/sqlite/sqliteMirrorStore.js.map +1 -1
- package/dist/testing/apps/browser.d.ts +27 -0
- package/dist/testing/apps/browser.d.ts.map +1 -0
- package/dist/testing/apps/browser.js +162 -0
- package/dist/testing/apps/browser.js.map +1 -0
- package/dist/testing/apps/cdp-pipe.d.ts +79 -0
- package/dist/testing/apps/cdp-pipe.d.ts.map +1 -0
- package/dist/testing/apps/cdp-pipe.js +200 -0
- package/dist/testing/apps/cdp-pipe.js.map +1 -0
- package/dist/testing/apps/csp.d.ts +17 -0
- package/dist/testing/apps/csp.d.ts.map +1 -0
- package/dist/testing/apps/csp.js +50 -0
- package/dist/testing/apps/csp.js.map +1 -0
- package/dist/testing/apps/host-pages.d.ts +47 -0
- package/dist/testing/apps/host-pages.d.ts.map +1 -0
- package/dist/testing/apps/host-pages.js +138 -0
- package/dist/testing/apps/host-pages.js.map +1 -0
- package/dist/testing/apps/index.d.ts +167 -0
- package/dist/testing/apps/index.d.ts.map +1 -0
- package/dist/testing/apps/index.js +36 -0
- package/dist/testing/apps/index.js.map +1 -0
- package/dist/testing/apps/partial-json.d.ts +14 -0
- package/dist/testing/apps/partial-json.d.ts.map +1 -0
- package/dist/testing/apps/partial-json.js +71 -0
- package/dist/testing/apps/partial-json.js.map +1 -0
- package/dist/testing/apps/run.d.ts +13 -0
- package/dist/testing/apps/run.d.ts.map +1 -0
- package/dist/testing/apps/run.js +659 -0
- package/dist/testing/apps/run.js.map +1 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.d.ts +1 -0
- package/dist/utils/internal/error-handler/errorHandler.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/errorHandler.js +22 -11
- package/dist/utils/internal/error-handler/errorHandler.js.map +1 -1
- package/dist/utils/internal/error-handler/helpers.d.ts +16 -5
- package/dist/utils/internal/error-handler/helpers.d.ts.map +1 -1
- package/dist/utils/internal/error-handler/helpers.js +12 -7
- package/dist/utils/internal/error-handler/helpers.js.map +1 -1
- package/dist/utils/internal/error-handler/types.d.ts +11 -4
- package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
- package/dist/utils/internal/logger.d.ts +21 -1
- package/dist/utils/internal/logger.d.ts.map +1 -1
- package/dist/utils/internal/logger.js +31 -14
- package/dist/utils/internal/logger.js.map +1 -1
- package/dist/utils/internal/observabilityCap.d.ts +35 -0
- package/dist/utils/internal/observabilityCap.d.ts.map +1 -0
- package/dist/utils/internal/observabilityCap.js +43 -0
- package/dist/utils/internal/observabilityCap.js.map +1 -0
- package/dist/utils/internal/performance.d.ts +3 -1
- package/dist/utils/internal/performance.d.ts.map +1 -1
- package/dist/utils/internal/performance.js +8 -3
- package/dist/utils/internal/performance.js.map +1 -1
- package/dist/utils/network/fetchWithTimeout.d.ts +3 -1
- package/dist/utils/network/fetchWithTimeout.d.ts.map +1 -1
- package/dist/utils/network/fetchWithTimeout.js +70 -16
- package/dist/utils/network/fetchWithTimeout.js.map +1 -1
- package/dist/utils/network/retry.d.ts +22 -2
- package/dist/utils/network/retry.d.ts.map +1 -1
- package/dist/utils/network/retry.js +29 -1
- package/dist/utils/network/retry.js.map +1 -1
- package/dist/utils/security/sanitization.d.ts +16 -13
- package/dist/utils/security/sanitization.d.ts.map +1 -1
- package/dist/utils/security/sanitization.js +74 -33
- package/dist/utils/security/sanitization.js.map +1 -1
- package/dist/utils/telemetry/attributes.d.ts +7 -1
- package/dist/utils/telemetry/attributes.d.ts.map +1 -1
- package/dist/utils/telemetry/attributes.js +7 -1
- package/dist/utils/telemetry/attributes.js.map +1 -1
- package/framework-skills/add-app-tool/SKILL.md +2 -1
- package/framework-skills/add-test/SKILL.md +15 -15
- package/framework-skills/add-tool/SKILL.md +2 -2
- package/framework-skills/api-auth/SKILL.md +3 -1
- package/framework-skills/api-canvas/SKILL.md +2 -2
- package/framework-skills/api-config/SKILL.md +11 -2
- package/framework-skills/api-context/SKILL.md +5 -5
- package/framework-skills/api-errors/SKILL.md +6 -6
- package/framework-skills/api-mirror/SKILL.md +6 -3
- package/framework-skills/api-telemetry/SKILL.md +15 -9
- package/framework-skills/api-testing/SKILL.md +5 -1
- package/framework-skills/api-utils/SKILL.md +5 -5
- package/framework-skills/code-simplifier/SKILL.md +2 -2
- package/framework-skills/field-test/SKILL.md +53 -8
- package/framework-skills/git-wrapup/SKILL.md +4 -4
- package/framework-skills/orchestrations/SKILL.md +1 -1
- package/framework-skills/orchestrations/workflows/greenfield-build.md +2 -2
- package/framework-skills/polish-docs-meta/SKILL.md +4 -3
- package/framework-skills/polish-docs-meta/references/server-json.md +24 -26
- package/framework-skills/release-and-publish/SKILL.md +2 -2
- package/framework-skills/release-pr-review/SKILL.md +5 -5
- package/framework-skills/setup/SKILL.md +2 -2
- package/framework-skills/tool-defs-analysis/SKILL.md +2 -2
- package/package.json +38 -23
- package/scripts/devcheck.ts +43 -21
- package/scripts/lint-packaging.ts +91 -1
- package/scripts/prune-musl-packages.ts +146 -0
- package/templates/.github/workflows/codeql.yml +10 -1
- package/templates/Dockerfile +13 -4
- package/templates/_.dockerignore +3 -5
- package/templates/_.gitignore +4 -4
- package/templates/package.json +4 -4
- package/templates/server.json +7 -21
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview One headless MCP Apps host run, loaded on demand by `renderAppTool`.
|
|
3
|
+
* Node holds the MCP client and the official `AppBridge`; the bridge's transport relays
|
|
4
|
+
* JSON-RPC through the host page's isolated-world relay into the sandbox proxy and the
|
|
5
|
+
* view. CDP supplies the instrumentation: exceptions and console errors from
|
|
6
|
+
* `Runtime`, CSP violations from `Audits` issues, frames from `Page`. The optional peers
|
|
7
|
+
* are imported here, inside the run, never at module load.
|
|
8
|
+
* @module src/testing/apps/run
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
14
|
+
import { FRAMEWORK_VERSION } from '../../config/index.js';
|
|
15
|
+
import { configurationError, notFound, serviceUnavailable, validationError, } from '../../types-global/errors.js';
|
|
16
|
+
import { discoverBrowser, launchBrowser } from './browser.js';
|
|
17
|
+
import { buildCsp } from './csp.js';
|
|
18
|
+
import { HOST_WORLD, RECEIVE_BINDING, RESIZE_FUNCTION, SANDBOX_FRAME_ID, SEND_FUNCTION, startHostPages, } from './host-pages.js';
|
|
19
|
+
import { partialArguments } from './partial-json.js';
|
|
20
|
+
const UI_EXTENSION = 'io.modelcontextprotocol/ui';
|
|
21
|
+
const APP_MIME_TYPE = 'text/html;profile=mcp-app';
|
|
22
|
+
const HOST_INFO = { name: 'mcp-ts-core-app-host', version: FRAMEWORK_VERSION };
|
|
23
|
+
const DEFAULT_WIDTH = 720;
|
|
24
|
+
/** Starting container height when the host follows the view's size reports. */
|
|
25
|
+
const INITIAL_HEIGHT = 480;
|
|
26
|
+
const MAX_HEIGHT = 4000;
|
|
27
|
+
const MIN_VIEWPORT_HEIGHT = 900;
|
|
28
|
+
const SERVER_STDERR_TAIL = 4 * 1024;
|
|
29
|
+
/** Script evaluations the CSP reports with no URL, named as `securitypolicyviolation` names them. */
|
|
30
|
+
const BLOCKED_EVALUATIONS = {
|
|
31
|
+
kEvalViolation: 'eval',
|
|
32
|
+
kWasmEvalViolation: 'wasm-eval',
|
|
33
|
+
kInlineViolation: 'inline',
|
|
34
|
+
};
|
|
35
|
+
/** Run one render. See `renderAppTool`. */
|
|
36
|
+
export async function runAppRender(options) {
|
|
37
|
+
const started = performance.now();
|
|
38
|
+
const peers = await loadPeers();
|
|
39
|
+
const executable = await discoverBrowser(options.browserPath);
|
|
40
|
+
const client = new peers.Client(HOST_INFO, {
|
|
41
|
+
capabilities: { extensions: { [UI_EXTENSION]: { mimeTypes: [APP_MIME_TYPE] } } },
|
|
42
|
+
});
|
|
43
|
+
let pages;
|
|
44
|
+
let browser;
|
|
45
|
+
try {
|
|
46
|
+
await connect(peers, client, options.server);
|
|
47
|
+
const tools = await listTools(client);
|
|
48
|
+
const tool = tools.find((candidate) => candidate.name === options.tool);
|
|
49
|
+
if (!tool) {
|
|
50
|
+
throw notFound(`The server has no tool named ${options.tool}. Its tools: ${tools.map((t) => t.name).join(', ') || '(none)'}.`, { reason: 'tool_not_found' });
|
|
51
|
+
}
|
|
52
|
+
const resourceUri = peers.getToolUiResourceUri(tool);
|
|
53
|
+
if (!resourceUri) {
|
|
54
|
+
throw validationError(`Tool ${options.tool} declares no UI resource (_meta.ui.resourceUri), so there is no view to render.`, { reason: 'no_ui_resource' });
|
|
55
|
+
}
|
|
56
|
+
const resource = await readView(client, resourceUri);
|
|
57
|
+
const csp = buildCsp(resource.csp);
|
|
58
|
+
const args = options.arguments ?? {};
|
|
59
|
+
let toolResult;
|
|
60
|
+
let toolError;
|
|
61
|
+
try {
|
|
62
|
+
toolResult = (await client.callTool({ name: tool.name, arguments: args }));
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
toolError = errorMessage(err);
|
|
66
|
+
}
|
|
67
|
+
const host = options.host ?? {};
|
|
68
|
+
pages = await startHostPages(csp, {
|
|
69
|
+
allow: peers.buildAllowAttribute(resource.permissions),
|
|
70
|
+
width: host.width ?? DEFAULT_WIDTH,
|
|
71
|
+
height: host.height ?? INITIAL_HEIGHT,
|
|
72
|
+
background: host.theme === 'dark' ? '#1f1f1f' : '#ffffff',
|
|
73
|
+
});
|
|
74
|
+
browser = await launchBrowser(executable);
|
|
75
|
+
const report = {
|
|
76
|
+
tool: tool.name,
|
|
77
|
+
resourceUri,
|
|
78
|
+
arguments: args,
|
|
79
|
+
csp,
|
|
80
|
+
initialized: false,
|
|
81
|
+
errors: [],
|
|
82
|
+
cspViolations: [],
|
|
83
|
+
messages: [],
|
|
84
|
+
steps: [],
|
|
85
|
+
text: '',
|
|
86
|
+
screenshots: [],
|
|
87
|
+
browser: { executable, pid: browser.process.pid, profileDir: browser.profileDir },
|
|
88
|
+
durationMs: 0,
|
|
89
|
+
...(toolResult && { toolResult: toolResult }),
|
|
90
|
+
...(toolError !== undefined && { toolError }),
|
|
91
|
+
};
|
|
92
|
+
const run = new BrowserRun({
|
|
93
|
+
peers,
|
|
94
|
+
client,
|
|
95
|
+
tools,
|
|
96
|
+
tool,
|
|
97
|
+
resource,
|
|
98
|
+
options,
|
|
99
|
+
pages,
|
|
100
|
+
cdp: browser.process.cdp,
|
|
101
|
+
report,
|
|
102
|
+
started,
|
|
103
|
+
...(toolResult && { toolResult }),
|
|
104
|
+
...(toolError !== undefined && { toolError }),
|
|
105
|
+
});
|
|
106
|
+
try {
|
|
107
|
+
await run.execute();
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
report.failure ??= errorMessage(err);
|
|
111
|
+
}
|
|
112
|
+
report.durationMs = Math.round(performance.now() - started);
|
|
113
|
+
return report;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await closeAll([
|
|
117
|
+
() => browser?.close(),
|
|
118
|
+
() => pages?.close(),
|
|
119
|
+
() => client.close().catch(() => { }),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Run every closer in order, each one even after an earlier one fails; then rethrow the first failure. */
|
|
124
|
+
async function closeAll(closers) {
|
|
125
|
+
const failures = [];
|
|
126
|
+
for (const close of closers) {
|
|
127
|
+
try {
|
|
128
|
+
await close();
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
failures.push(err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (failures.length > 0)
|
|
135
|
+
throw failures[0];
|
|
136
|
+
}
|
|
137
|
+
// ── Setup ──────────────────────────────────────────────────────────────
|
|
138
|
+
/** The specifier a resolution error reports as missing, in Node's and Bun's wording. */
|
|
139
|
+
const MISSING_SPECIFIER = /Cannot find (?:package|module) ['"]([^'"]+)['"]/i;
|
|
140
|
+
async function loadPeer(pkg, load) {
|
|
141
|
+
try {
|
|
142
|
+
return await load();
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
const missing = MISSING_SPECIFIER.exec(errorMessage(err))?.[1];
|
|
146
|
+
if (missing === pkg || missing?.startsWith(`${pkg}/`)) {
|
|
147
|
+
throw configurationError(`renderAppTool needs the optional peer dependency ${pkg}, which is not installed. Install it with \`bun add -d ${pkg}\`.`, { reason: 'missing_peer', package: pkg }, { cause: err });
|
|
148
|
+
}
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function loadPeers() {
|
|
153
|
+
const client = await loadPeer('@modelcontextprotocol/client', () => import('@modelcontextprotocol/client'));
|
|
154
|
+
const stdio = await loadPeer('@modelcontextprotocol/client', () => import('@modelcontextprotocol/client/stdio'));
|
|
155
|
+
const apps = await loadPeer('@modelcontextprotocol/ext-apps', () => import('@modelcontextprotocol/ext-apps/app-bridge'));
|
|
156
|
+
return {
|
|
157
|
+
Client: client.Client,
|
|
158
|
+
StreamableHTTPClientTransport: client.StreamableHTTPClientTransport,
|
|
159
|
+
StdioClientTransport: stdio.StdioClientTransport,
|
|
160
|
+
AppBridge: apps.AppBridge,
|
|
161
|
+
buildAllowAttribute: apps.buildAllowAttribute,
|
|
162
|
+
getToolUiResourceUri: apps.getToolUiResourceUri,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async function connect(peers, client, server) {
|
|
166
|
+
let stderrTail = '';
|
|
167
|
+
let target;
|
|
168
|
+
let transport;
|
|
169
|
+
if ('url' in server) {
|
|
170
|
+
target = server.url;
|
|
171
|
+
transport = new peers.StreamableHTTPClientTransport(new URL(server.url), server.headers ? { requestInit: { headers: server.headers } } : undefined);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
target = [server.command, ...(server.args ?? [])].join(' ');
|
|
175
|
+
const stdio = new peers.StdioClientTransport({
|
|
176
|
+
command: server.command,
|
|
177
|
+
stderr: 'pipe',
|
|
178
|
+
...(server.args && { args: server.args }),
|
|
179
|
+
...(server.cwd && { cwd: server.cwd }),
|
|
180
|
+
...(server.env && { env: server.env }),
|
|
181
|
+
});
|
|
182
|
+
stdio.stderr?.setEncoding('utf8').on('data', (text) => {
|
|
183
|
+
stderrTail = (stderrTail + text).slice(-SERVER_STDERR_TAIL);
|
|
184
|
+
});
|
|
185
|
+
transport = stdio;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
await client.connect(transport);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const stderr = stderrTail.trim().split('\n').at(-1);
|
|
192
|
+
throw serviceUnavailable(`Could not connect to the MCP server (${target}): ${errorMessage(err)}${stderr ? ` Server stderr: ${stderr}` : ''}`, { reason: 'server_unreachable' }, { cause: err });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function listTools(client) {
|
|
196
|
+
const tools = [];
|
|
197
|
+
let cursor;
|
|
198
|
+
do {
|
|
199
|
+
const page = await client.listTools(cursor ? { cursor } : {});
|
|
200
|
+
tools.push(...page.tools);
|
|
201
|
+
cursor = page.nextCursor;
|
|
202
|
+
} while (cursor);
|
|
203
|
+
return tools;
|
|
204
|
+
}
|
|
205
|
+
async function readView(client, uri) {
|
|
206
|
+
const read = await client.readResource({ uri }).catch((err) => {
|
|
207
|
+
throw notFound(`Could not read the tool's UI resource ${uri}: ${errorMessage(err)}`, { reason: 'ui_resource_unreadable' }, { cause: err });
|
|
208
|
+
});
|
|
209
|
+
const content = read.contents[0];
|
|
210
|
+
if (!content) {
|
|
211
|
+
throw notFound(`The UI resource ${uri} returned no contents.`, {
|
|
212
|
+
reason: 'ui_resource_unreadable',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const html = 'text' in content ? content.text : Buffer.from(content.blob, 'base64').toString('utf8');
|
|
216
|
+
const ui = content._meta?.ui;
|
|
217
|
+
return { html, csp: ui?.csp, permissions: ui?.permissions };
|
|
218
|
+
}
|
|
219
|
+
class BrowserRun {
|
|
220
|
+
#init;
|
|
221
|
+
#report;
|
|
222
|
+
#timeoutMs;
|
|
223
|
+
#contexts = new Map();
|
|
224
|
+
/** View requests still waiting for the host's response. */
|
|
225
|
+
#pending = new Set();
|
|
226
|
+
#initialized = Promise.withResolvers();
|
|
227
|
+
#sessionId = '';
|
|
228
|
+
#mainFrame = '';
|
|
229
|
+
#sandboxFrame;
|
|
230
|
+
#viewFrame;
|
|
231
|
+
#viewportHeight = MIN_VIEWPORT_HEIGHT;
|
|
232
|
+
#outDir;
|
|
233
|
+
constructor(init) {
|
|
234
|
+
this.#init = init;
|
|
235
|
+
this.#report = init.report;
|
|
236
|
+
this.#timeoutMs = init.options.timeoutMs ?? 15_000;
|
|
237
|
+
}
|
|
238
|
+
async execute() {
|
|
239
|
+
const { cdp, options, pages } = this.#init;
|
|
240
|
+
const host = options.host ?? {};
|
|
241
|
+
const { targetId } = await cdp.send('Target.createTarget', {
|
|
242
|
+
url: 'about:blank',
|
|
243
|
+
});
|
|
244
|
+
this.#mainFrame = targetId;
|
|
245
|
+
const { sessionId } = await cdp.send('Target.attachToTarget', {
|
|
246
|
+
targetId,
|
|
247
|
+
flatten: true,
|
|
248
|
+
});
|
|
249
|
+
this.#sessionId = sessionId;
|
|
250
|
+
this.#instrument();
|
|
251
|
+
await this.#send('Page.enable');
|
|
252
|
+
await this.#send('Runtime.enable');
|
|
253
|
+
await this.#send('Audits.enable');
|
|
254
|
+
await this.#send('Emulation.setFocusEmulationEnabled', { enabled: true });
|
|
255
|
+
this.#viewportHeight = Math.max(MIN_VIEWPORT_HEIGHT, host.height ?? 0);
|
|
256
|
+
await this.#setViewport();
|
|
257
|
+
await this.#send('Emulation.setEmulatedMedia', {
|
|
258
|
+
features: [{ name: 'prefers-color-scheme', value: host.theme ?? 'light' }],
|
|
259
|
+
});
|
|
260
|
+
await this.#send('Runtime.addBinding', {
|
|
261
|
+
name: RECEIVE_BINDING,
|
|
262
|
+
executionContextName: HOST_WORLD,
|
|
263
|
+
});
|
|
264
|
+
await this.#send('Page.addScriptToEvaluateOnNewDocument', {
|
|
265
|
+
source: pages.relayScript,
|
|
266
|
+
worldName: HOST_WORLD,
|
|
267
|
+
runImmediately: true,
|
|
268
|
+
});
|
|
269
|
+
const transport = new RelayTransport((json) => this.#evaluate(this.#mainFrame, `${SEND_FUNCTION}(${JSON.stringify(json)})`, HOST_WORLD), (direction, message) => this.#record(direction, message));
|
|
270
|
+
this.#on('Runtime.bindingCalled', ({ name, payload }) => {
|
|
271
|
+
if (name === RECEIVE_BINDING)
|
|
272
|
+
transport.receive(JSON.parse(payload));
|
|
273
|
+
});
|
|
274
|
+
const bridge = await this.#connectBridge(transport);
|
|
275
|
+
await this.#send('Page.navigate', { url: pages.hostUrl });
|
|
276
|
+
const initialized = await this.#within(this.#initialized.promise, this.#timeoutMs);
|
|
277
|
+
if (!initialized) {
|
|
278
|
+
this.#report.failure ??= `The view did not complete ui/initialize within ${this.#timeoutMs} ms.`;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
await this.#deliverToolCall(bridge);
|
|
282
|
+
await this.#settle();
|
|
283
|
+
for (const step of options.steps ?? [])
|
|
284
|
+
this.#report.steps.push(await this.#runStep(step));
|
|
285
|
+
}
|
|
286
|
+
if (this.#viewFrame) {
|
|
287
|
+
this.#report.text = await this.#evaluate(this.#viewFrame, 'document.body ? document.body.innerText : ""').catch(() => '');
|
|
288
|
+
await this.#screenshot('final').catch((err) => {
|
|
289
|
+
this.#report.failure ??= `Final screenshot failed: ${errorMessage(err)}`;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (initialized)
|
|
293
|
+
await bridge.teardownResource({}, { timeout: 3_000 }).catch(() => { });
|
|
294
|
+
await bridge.close().catch(() => { });
|
|
295
|
+
}
|
|
296
|
+
async #connectBridge(transport) {
|
|
297
|
+
const { peers, client, options, tool, tools, resource } = this.#init;
|
|
298
|
+
const host = options.host ?? {};
|
|
299
|
+
const width = host.width ?? DEFAULT_WIDTH;
|
|
300
|
+
const displayModes = host.availableDisplayModes ?? ['inline'];
|
|
301
|
+
let displayMode = host.displayMode ?? 'inline';
|
|
302
|
+
const hostContext = {
|
|
303
|
+
theme: host.theme ?? 'light',
|
|
304
|
+
platform: 'web',
|
|
305
|
+
displayMode,
|
|
306
|
+
availableDisplayModes: displayModes,
|
|
307
|
+
containerDimensions: host.height !== undefined
|
|
308
|
+
? { width, height: host.height }
|
|
309
|
+
: { width, maxHeight: MAX_HEIGHT },
|
|
310
|
+
toolInfo: { tool },
|
|
311
|
+
// McpUiStyles requires every variable; a host passes a partial set.
|
|
312
|
+
...(host.styles && { styles: { variables: host.styles } }),
|
|
313
|
+
...(host.locale && { locale: host.locale }),
|
|
314
|
+
...(host.timeZone && { timeZone: host.timeZone }),
|
|
315
|
+
};
|
|
316
|
+
const sandbox = {
|
|
317
|
+
...(resource.csp && { csp: resource.csp }),
|
|
318
|
+
...(resource.permissions && { permissions: resource.permissions }),
|
|
319
|
+
};
|
|
320
|
+
const capabilities = {
|
|
321
|
+
openLinks: {},
|
|
322
|
+
downloadFile: {},
|
|
323
|
+
serverTools: {},
|
|
324
|
+
serverResources: {},
|
|
325
|
+
logging: {},
|
|
326
|
+
message: { text: {} },
|
|
327
|
+
updateModelContext: { text: {}, structuredContent: {} },
|
|
328
|
+
sandbox,
|
|
329
|
+
};
|
|
330
|
+
/**
|
|
331
|
+
* The bridge gets no client: given one, it forwards every view tools/call unchecked.
|
|
332
|
+
* The handlers below forward to the server and enforce the spec's "Visibility" rule.
|
|
333
|
+
*/
|
|
334
|
+
const bridge = new peers.AppBridge(null, HOST_INFO, capabilities, { hostContext });
|
|
335
|
+
bridge.onsandboxready = () => {
|
|
336
|
+
bridge.sendSandboxResourceReady({ html: resource.html, ...sandbox }).catch((err) => {
|
|
337
|
+
this.#report.failure ??= `Could not deliver the view HTML: ${errorMessage(err)}`;
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
bridge.oninitialized = () => {
|
|
341
|
+
this.#report.initialized = true;
|
|
342
|
+
const appInfo = bridge.getAppVersion();
|
|
343
|
+
if (appInfo)
|
|
344
|
+
this.#report.appInfo = { name: appInfo.name, version: appInfo.version };
|
|
345
|
+
this.#initialized.resolve();
|
|
346
|
+
};
|
|
347
|
+
bridge.onsizechange = (size) => {
|
|
348
|
+
this.#report.size = {
|
|
349
|
+
...(size.width !== undefined && { width: size.width }),
|
|
350
|
+
...(size.height !== undefined && { height: size.height }),
|
|
351
|
+
};
|
|
352
|
+
if (host.height === undefined && size.height !== undefined) {
|
|
353
|
+
void this.#resize(Math.min(MAX_HEIGHT, Math.ceil(size.height))).catch(() => { });
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
bridge.onmessage = async () => ({});
|
|
357
|
+
bridge.onopenlink = async () => ({});
|
|
358
|
+
bridge.ondownloadfile = async () => ({});
|
|
359
|
+
bridge.onupdatemodelcontext = async () => ({});
|
|
360
|
+
bridge.onrequestdisplaymode = ({ mode }) => {
|
|
361
|
+
if (displayModes.includes(mode))
|
|
362
|
+
displayMode = mode;
|
|
363
|
+
return Promise.resolve({ mode: displayMode });
|
|
364
|
+
};
|
|
365
|
+
bridge.oncalltool = (params) => {
|
|
366
|
+
const visibility = tools.find((t) => t.name === params.name)?._meta?.ui?.visibility;
|
|
367
|
+
if (Array.isArray(visibility) && !visibility.includes('app')) {
|
|
368
|
+
return Promise.reject(new Error(`Tool ${params.name} is not callable from an app: its _meta.ui.visibility is ${JSON.stringify(visibility)}.`));
|
|
369
|
+
}
|
|
370
|
+
return client.callTool(params);
|
|
371
|
+
};
|
|
372
|
+
bridge.onreadresource = (params) => client.readResource(params);
|
|
373
|
+
bridge.onlistresources = (params) => client.listResources(params);
|
|
374
|
+
bridge.onlistresourcetemplates = (params) => client.listResourceTemplates(params);
|
|
375
|
+
await bridge.connect(transport);
|
|
376
|
+
return bridge;
|
|
377
|
+
}
|
|
378
|
+
async #deliverToolCall(bridge) {
|
|
379
|
+
const { options, toolResult, toolError } = this.#init;
|
|
380
|
+
const args = this.#report.arguments;
|
|
381
|
+
if (options.host?.streamInput) {
|
|
382
|
+
for (const partial of partialArguments(args)) {
|
|
383
|
+
await bridge.sendToolInputPartial({ arguments: partial });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
await bridge.sendToolInput({ arguments: args });
|
|
387
|
+
if (toolResult)
|
|
388
|
+
await bridge.sendToolResult(toolResult);
|
|
389
|
+
else
|
|
390
|
+
await bridge.sendToolCancelled({ reason: toolError ?? 'The tool call failed.' });
|
|
391
|
+
}
|
|
392
|
+
async #runStep(step) {
|
|
393
|
+
try {
|
|
394
|
+
if ('click' in step) {
|
|
395
|
+
await this.#click(step.click);
|
|
396
|
+
await this.#settle();
|
|
397
|
+
return { step, ok: true };
|
|
398
|
+
}
|
|
399
|
+
if ('fill' in step) {
|
|
400
|
+
await this.#evaluate(this.#requireView(), `(() => { const el = document.querySelector(${JSON.stringify(step.fill)}); if (!el) throw new Error('No element matches ' + ${JSON.stringify(step.fill)}); el.focus(); if (typeof el.select === 'function') el.select(); return true; })()`);
|
|
401
|
+
await this.#send('Input.insertText', { text: step.value });
|
|
402
|
+
await this.#settle();
|
|
403
|
+
return { step, ok: true };
|
|
404
|
+
}
|
|
405
|
+
if ('waitFor' in step) {
|
|
406
|
+
const deadline = Date.now() + (step.timeoutMs ?? this.#timeoutMs);
|
|
407
|
+
const probe = `!!document.querySelector(${JSON.stringify(step.waitFor)})`;
|
|
408
|
+
while (!(await this.#evaluate(this.#requireView(), probe))) {
|
|
409
|
+
if (Date.now() > deadline)
|
|
410
|
+
throw new Error(`Timed out waiting for ${step.waitFor}`);
|
|
411
|
+
await delay(50);
|
|
412
|
+
}
|
|
413
|
+
return { step, ok: true };
|
|
414
|
+
}
|
|
415
|
+
if ('evaluate' in step) {
|
|
416
|
+
const value = await this.#evaluate(this.#requireView(), step.evaluate, undefined, AbortSignal.timeout(this.#timeoutMs)).catch((err) => {
|
|
417
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
418
|
+
throw new Error(`The expression did not settle within ${this.#timeoutMs} ms.`);
|
|
419
|
+
}
|
|
420
|
+
throw err;
|
|
421
|
+
});
|
|
422
|
+
await this.#settle();
|
|
423
|
+
return { step, ok: true, value };
|
|
424
|
+
}
|
|
425
|
+
return { step, ok: true, screenshot: await this.#screenshot(step.screenshot) };
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
return { step, ok: false, error: errorMessage(err) };
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async #click(selector) {
|
|
432
|
+
const view = this.#requireView();
|
|
433
|
+
const point = await this.#evaluate(view, `(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return null; el.scrollIntoView({ block: 'center', inline: 'center' }); const r = el.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; })()`);
|
|
434
|
+
if (!point)
|
|
435
|
+
throw new Error(`No element matches ${selector}`);
|
|
436
|
+
const outer = await this.#evaluate(this.#mainFrame, frameOrigin(`document.getElementById(${JSON.stringify(SANDBOX_FRAME_ID)})`));
|
|
437
|
+
if (!this.#sandboxFrame)
|
|
438
|
+
throw new Error('The sandbox frame was never created.');
|
|
439
|
+
const inner = await this.#evaluate(this.#sandboxFrame, frameOrigin(`document.querySelector('iframe')`));
|
|
440
|
+
const x = outer.x + inner.x + point.x;
|
|
441
|
+
const y = outer.y + inner.y + point.y;
|
|
442
|
+
for (const type of ['mousePressed', 'mouseReleased']) {
|
|
443
|
+
await this.#send('Input.dispatchMouseEvent', { type, x, y, button: 'left', clickCount: 1 });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async #screenshot(name) {
|
|
447
|
+
const rect = await this.#evaluate(this.#mainFrame, `(() => { const r = document.getElementById(${JSON.stringify(SANDBOX_FRAME_ID)}).getBoundingClientRect(); return { x: r.left + scrollX, y: r.top + scrollY, width: r.width, height: r.height }; })()`);
|
|
448
|
+
const { data } = await this.#send('Page.captureScreenshot', {
|
|
449
|
+
format: 'png',
|
|
450
|
+
clip: { ...rect, scale: 1 },
|
|
451
|
+
captureBeyondViewport: true,
|
|
452
|
+
});
|
|
453
|
+
const dir = await this.#screenshotDir();
|
|
454
|
+
const file = path.join(dir, `${name.replace(/[^\w.-]+/g, '-') || 'screenshot'}.png`);
|
|
455
|
+
await writeFile(file, Buffer.from(data, 'base64'));
|
|
456
|
+
this.#report.screenshots.push(file);
|
|
457
|
+
return file;
|
|
458
|
+
}
|
|
459
|
+
#screenshotDir() {
|
|
460
|
+
const { outDir } = this.#init.options;
|
|
461
|
+
this.#outDir ??= outDir
|
|
462
|
+
? mkdir(outDir, { recursive: true }).then(() => outDir)
|
|
463
|
+
: mkdtemp(path.join(os.tmpdir(), 'mcp-app-render-'));
|
|
464
|
+
return this.#outDir;
|
|
465
|
+
}
|
|
466
|
+
async #resize(height) {
|
|
467
|
+
await this.#evaluate(this.#mainFrame, `${RESIZE_FUNCTION}(${height})`, HOST_WORLD);
|
|
468
|
+
if (height > this.#viewportHeight) {
|
|
469
|
+
this.#viewportHeight = height;
|
|
470
|
+
await this.#setViewport();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
#setViewport() {
|
|
474
|
+
return this.#send('Emulation.setDeviceMetricsOverride', {
|
|
475
|
+
width: this.#init.options.host?.width ?? DEFAULT_WIDTH,
|
|
476
|
+
height: this.#viewportHeight,
|
|
477
|
+
deviceScaleFactor: 1,
|
|
478
|
+
mobile: false,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
/** Wait for the view's outstanding requests to be answered, then for two frames to paint. */
|
|
482
|
+
async #settle() {
|
|
483
|
+
await delay(100);
|
|
484
|
+
const deadline = Date.now() + this.#timeoutMs;
|
|
485
|
+
while (this.#pending.size > 0 && Date.now() < deadline)
|
|
486
|
+
await delay(25);
|
|
487
|
+
if (this.#viewFrame) {
|
|
488
|
+
await this.#within(this.#evaluate(this.#viewFrame, 'new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))').catch(() => { }), 1_000);
|
|
489
|
+
}
|
|
490
|
+
await delay(100);
|
|
491
|
+
}
|
|
492
|
+
#record(direction, message) {
|
|
493
|
+
const fields = message;
|
|
494
|
+
if (fields.method?.startsWith('ui/notifications/sandbox-'))
|
|
495
|
+
return;
|
|
496
|
+
if (fields.id !== undefined) {
|
|
497
|
+
if (direction === 'view-to-host' && fields.method !== undefined)
|
|
498
|
+
this.#pending.add(fields.id);
|
|
499
|
+
if (direction === 'host-to-view' && fields.method === undefined)
|
|
500
|
+
this.#pending.delete(fields.id);
|
|
501
|
+
}
|
|
502
|
+
this.#report.messages.push({
|
|
503
|
+
at: Math.round(performance.now() - this.#init.started),
|
|
504
|
+
direction,
|
|
505
|
+
message: message,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
#instrument() {
|
|
509
|
+
this.#on('Page.frameAttached', (frame) => {
|
|
510
|
+
if (frame.parentFrameId === this.#mainFrame)
|
|
511
|
+
this.#sandboxFrame = frame.frameId;
|
|
512
|
+
else if (frame.parentFrameId === this.#sandboxFrame)
|
|
513
|
+
this.#viewFrame = frame.frameId;
|
|
514
|
+
});
|
|
515
|
+
this.#on('Runtime.executionContextCreated', ({ context }) => {
|
|
516
|
+
if (!context.auxData?.frameId)
|
|
517
|
+
return;
|
|
518
|
+
this.#contexts.set(context.id, {
|
|
519
|
+
frameId: context.auxData.frameId,
|
|
520
|
+
isDefault: context.auxData.isDefault === true,
|
|
521
|
+
name: context.name,
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
this.#on('Runtime.executionContextDestroyed', (event) => {
|
|
525
|
+
this.#contexts.delete(event.executionContextId);
|
|
526
|
+
});
|
|
527
|
+
this.#on('Runtime.executionContextsCleared', () => this.#contexts.clear());
|
|
528
|
+
this.#on('Runtime.exceptionThrown', ({ exceptionDetails: d }) => {
|
|
529
|
+
this.#report.errors.push({
|
|
530
|
+
source: 'exception',
|
|
531
|
+
frame: this.#frameOf(d.executionContextId),
|
|
532
|
+
message: d.exception?.description ?? d.text,
|
|
533
|
+
...location(d.url, d.lineNumber, d.columnNumber),
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
this.#on('Runtime.consoleAPICalled', (event) => {
|
|
537
|
+
if (event.type !== 'error' && event.type !== 'assert')
|
|
538
|
+
return;
|
|
539
|
+
const top = event.stackTrace?.callFrames[0];
|
|
540
|
+
this.#report.errors.push({
|
|
541
|
+
source: 'console',
|
|
542
|
+
frame: this.#frameOf(event.executionContextId),
|
|
543
|
+
message: event.args.map(describeRemote).join(' '),
|
|
544
|
+
...location(top?.url, top?.lineNumber, top?.columnNumber),
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
this.#on('Audits.issueAdded', ({ issue }) => {
|
|
548
|
+
const details = issue.details.contentSecurityPolicyIssueDetails;
|
|
549
|
+
if (issue.code !== 'ContentSecurityPolicyIssue' || !details)
|
|
550
|
+
return;
|
|
551
|
+
const type = details.contentSecurityPolicyViolationType;
|
|
552
|
+
const source = details.sourceCodeLocation;
|
|
553
|
+
this.#report.cspViolations.push({
|
|
554
|
+
directive: details.violatedDirective,
|
|
555
|
+
blockedURI: details.blockedURL ?? BLOCKED_EVALUATIONS[type] ?? type,
|
|
556
|
+
violationType: type,
|
|
557
|
+
...(source?.url && { sourceURL: source.url }),
|
|
558
|
+
...(source && source.lineNumber >= 0 && { line: source.lineNumber + 1 }),
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
#frameOf(contextId) {
|
|
563
|
+
const frameId = contextId === undefined ? undefined : this.#contexts.get(contextId)?.frameId;
|
|
564
|
+
if (frameId === undefined)
|
|
565
|
+
return 'unknown';
|
|
566
|
+
if (frameId === this.#viewFrame)
|
|
567
|
+
return 'view';
|
|
568
|
+
if (frameId === this.#sandboxFrame)
|
|
569
|
+
return 'sandbox';
|
|
570
|
+
return frameId === this.#mainFrame ? 'host' : 'unknown';
|
|
571
|
+
}
|
|
572
|
+
#requireView() {
|
|
573
|
+
if (!this.#viewFrame)
|
|
574
|
+
throw new Error('The view frame was never created.');
|
|
575
|
+
return this.#viewFrame;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Evaluate `expression` in a frame's main world, or in the named isolated world. An
|
|
579
|
+
* aborted `signal` rejects with its reason and drops the browser's eventual answer.
|
|
580
|
+
*/
|
|
581
|
+
async #evaluate(frameId, expression, world, signal) {
|
|
582
|
+
let contextId;
|
|
583
|
+
for (const [id, info] of this.#contexts) {
|
|
584
|
+
if (info.frameId !== frameId)
|
|
585
|
+
continue;
|
|
586
|
+
if (world === undefined ? info.isDefault : info.name === world)
|
|
587
|
+
contextId = id;
|
|
588
|
+
}
|
|
589
|
+
if (contextId === undefined)
|
|
590
|
+
throw new Error(`No JavaScript context for frame ${frameId}.`);
|
|
591
|
+
const result = await this.#send('Runtime.evaluate', { expression, contextId, awaitPromise: true, returnByValue: true }, signal);
|
|
592
|
+
if (result.exceptionDetails) {
|
|
593
|
+
const d = result.exceptionDetails;
|
|
594
|
+
throw new Error(d.exception?.description ?? d.text);
|
|
595
|
+
}
|
|
596
|
+
return result.result.value;
|
|
597
|
+
}
|
|
598
|
+
#send(method, params = {}, signal) {
|
|
599
|
+
return this.#init.cdp.send(method, params, { sessionId: this.#sessionId, signal });
|
|
600
|
+
}
|
|
601
|
+
#on(method, listener) {
|
|
602
|
+
this.#init.cdp.on(method, (params, sessionId) => {
|
|
603
|
+
if (sessionId === this.#sessionId)
|
|
604
|
+
listener(params);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
async #within(promise, ms) {
|
|
608
|
+
const timer = new AbortController();
|
|
609
|
+
const timedOut = delay(ms, false, { signal: timer.signal }).catch(() => false);
|
|
610
|
+
const settled = await Promise.race([promise.then(() => true), timedOut]);
|
|
611
|
+
timer.abort();
|
|
612
|
+
return settled;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/** The bridge's transport: messages for the view go out through the host relay. */
|
|
616
|
+
class RelayTransport {
|
|
617
|
+
onclose;
|
|
618
|
+
onerror;
|
|
619
|
+
onmessage;
|
|
620
|
+
#deliver;
|
|
621
|
+
#record;
|
|
622
|
+
constructor(deliver, record) {
|
|
623
|
+
this.#deliver = deliver;
|
|
624
|
+
this.#record = record;
|
|
625
|
+
}
|
|
626
|
+
async start() { }
|
|
627
|
+
async send(message) {
|
|
628
|
+
this.#record('host-to-view', message);
|
|
629
|
+
await this.#deliver(JSON.stringify(message));
|
|
630
|
+
}
|
|
631
|
+
close() {
|
|
632
|
+
this.onclose?.();
|
|
633
|
+
return Promise.resolve();
|
|
634
|
+
}
|
|
635
|
+
/** A message from the view (or the sandbox proxy), relayed by the host page. */
|
|
636
|
+
receive(message) {
|
|
637
|
+
this.#record('view-to-host', message);
|
|
638
|
+
this.onmessage?.(message);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function frameOrigin(element) {
|
|
642
|
+
return `(() => { const r = ${element}.getBoundingClientRect(); return { x: r.left, y: r.top }; })()`;
|
|
643
|
+
}
|
|
644
|
+
function describeRemote(arg) {
|
|
645
|
+
if (arg.value !== undefined)
|
|
646
|
+
return typeof arg.value === 'string' ? arg.value : JSON.stringify(arg.value);
|
|
647
|
+
return arg.description ?? arg.unserializableValue ?? arg.type;
|
|
648
|
+
}
|
|
649
|
+
function location(url, line, column) {
|
|
650
|
+
return {
|
|
651
|
+
...(url && { url }),
|
|
652
|
+
...(line !== undefined && line >= 0 && { line: line + 1 }),
|
|
653
|
+
...(column !== undefined && column >= 0 && { column: column + 1 }),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function errorMessage(err) {
|
|
657
|
+
return err instanceof Error ? err.message : String(err);
|
|
658
|
+
}
|
|
659
|
+
//# sourceMappingURL=run.js.map
|