opera-devtools-mcp 0.6.1 → 0.8.0
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/README.md +1 -1
- package/build/src/McpContext.js +78 -41
- package/build/src/McpPage.js +5 -1
- package/build/src/ToolHandler.js +54 -76
- package/build/src/bin/chrome-devtools-mcp-main.js +4 -4
- package/build/src/bin/chrome-devtools.js +56 -124
- package/build/src/bin/opera-browser-cli.js +102 -0
- package/build/src/bin/opera-devtools-cli-options.js +1 -1
- package/build/src/bin/opera-devtools-mcp-cli-options.js +1 -1
- package/build/src/bin/opera-devtools-mcp.js +20 -1
- package/build/src/browser.js +23 -25
- package/build/src/config/browser-options.js +126 -0
- package/build/src/config/category-options.js +81 -0
- package/build/src/{bin/chrome-devtools-cli-options.js → config/cli-options.js} +368 -26
- package/build/src/{bin/chrome-devtools-mcp-cli-options.js → config/mcp-options.js} +143 -164
- package/build/src/daemon/client.js +55 -40
- package/build/src/daemon/daemon.js +62 -39
- package/build/src/daemon/utils.js +6 -0
- package/build/src/devtools/DevtoolsUtils.js +27 -21
- package/build/src/formatters/NetworkFormatter.js +5 -2
- package/build/src/index.js +166 -100
- package/build/src/opera/branding.js +4 -2
- package/build/src/opera/browserActivity.js +62 -0
- package/build/src/opera/browserCleanup.js +123 -0
- package/build/src/opera/browserErrors.js +66 -0
- package/build/src/opera/browserFlags.js +184 -38
- package/build/src/opera/browserTarget.js +513 -0
- package/build/src/opera/cdpErrors.js +391 -0
- package/build/src/opera/cliCommands.js +378 -0
- package/build/src/opera/cliOutput.js +284 -0
- package/build/src/opera/compactSnapshot.js +525 -0
- package/build/src/opera/config.js +166 -0
- package/build/src/opera/daemonLifecycle.js +257 -0
- package/build/src/opera/daemonLog.js +103 -0
- package/build/src/opera/daemonPidFile.js +83 -0
- package/build/src/opera/daemonShutdown.js +66 -0
- package/build/src/opera/daemonSocket.js +87 -0
- package/build/src/opera/daemonStreaming.js +130 -0
- package/build/src/opera/daemonToolCall.js +26 -0
- package/build/src/opera/detect.js +114 -0
- package/build/src/opera/doctor.js +317 -0
- package/build/src/opera/envConfig.js +229 -0
- package/build/src/opera/launcherNotice.js +116 -0
- package/build/src/opera/legacyBridgeCleanup.js +297 -0
- package/build/src/opera/logs.js +133 -0
- package/build/src/opera/mcpServerSupervisor.js +128 -0
- package/build/src/opera/migrationShared.js +164 -0
- package/build/src/opera/operaPages.js +56 -0
- package/build/src/opera/pageIdRouting.js +35 -0
- package/build/src/opera/pageRecovery.js +53 -0
- package/build/src/opera/profile.js +270 -0
- package/build/src/opera/refArgs.js +36 -0
- package/build/src/opera/serviceWorkerRetry.js +46 -4
- package/build/src/opera/setup.js +290 -0
- package/build/src/opera/skills/SKILL.md +160 -0
- package/build/src/opera/streamingTools.js +73 -0
- package/build/src/opera/suggestions.js +67 -0
- package/build/src/opera/toolHandlerHooks.js +25 -1
- package/build/src/opera/tools/opera.js +107 -38
- package/build/src/opera/urlResolver.js +69 -0
- package/build/src/opera/webStorageWarning.js +92 -0
- package/build/src/processors/HeapSnapshotManager.js +12 -0
- package/build/src/telemetry/ClearcutLogger.js +19 -6
- package/build/src/telemetry/transformation.js +4 -0
- package/build/src/telemetry/types.js +4 -0
- package/build/src/third_party/THIRD_PARTY_NOTICES +5 -5
- package/build/src/third_party/bundled-packages.json +3 -3
- package/build/src/third_party/devtools-formatter-worker.js +23 -0
- package/build/src/third_party/devtools-heap-snapshot-worker.js +101 -20
- package/build/src/third_party/index.js +15460 -14256
- package/build/src/third_party/issue-descriptions/federatedAuthRequestAccountsBlockedByConnectionAllowlist.md +1 -0
- package/build/src/third_party/issue-descriptions/federatedAuthRequestConfigBlockedByConnectionAllowlist.md +1 -0
- package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenBlockedByConnectionAllowlist.md +1 -0
- package/build/src/third_party/issue-descriptions/federatedAuthRequestWellKnownBlockedByConnectionAllowlist.md +1 -0
- package/build/src/tools/ToolDefinition.js +15 -0
- package/build/src/tools/categories.js +0 -6
- package/build/src/tools/comments.js +90 -0
- package/build/src/tools/console.js +1 -1
- package/build/src/tools/emulation.js +1 -1
- package/build/src/tools/extensions.js +1 -1
- package/build/src/tools/memory.js +60 -6
- package/build/src/tools/network.js +2 -2
- package/build/src/tools/pages.js +26 -15
- package/build/src/tools/performance.js +4 -3
- package/build/src/tools/screencast.js +3 -2
- package/build/src/tools/screenshot.js +39 -8
- package/build/src/tools/script.js +17 -4
- package/build/src/tools/slim/tools.js +41 -33
- package/build/src/tools/snapshot.js +1 -1
- package/build/src/tools/tools.js +2 -0
- package/build/src/utils/WaitForHelper.js +12 -1
- package/build/src/utils/bytes.js +105 -0
- package/build/src/utils/url.js +79 -0
- package/build/src/version.js +1 -1
- package/package.json +29 -8
- package/build/src/bin/opera-devtools.js +0 -10
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ control and inspect a live browser. It acts as a Model-Context-Protocol
|
|
|
10
10
|
DevTools for reliable automation, in-depth debugging, and performance analysis.
|
|
11
11
|
When connected to Opera Neon, it also exposes Opera's built-in AI capabilities.
|
|
12
12
|
|
|
13
|
-
## [Tool reference](./docs/tool-reference.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) | [Troubleshooting](./docs/troubleshooting.md) | [Design Principles](./docs/design-principles.md)
|
|
13
|
+
## [Tool reference](./docs/tool-reference.md) | [Changelog](./CHANGELOG.md) | [Contributing](./CONTRIBUTING.md) | [Troubleshooting](./docs/troubleshooting.md) | [Design Principles](./docs/design-principles.md) | [Stress testing](./docs/stress-testing.md)
|
|
14
14
|
|
|
15
15
|
## Key features
|
|
16
16
|
|
package/build/src/McpContext.js
CHANGED
|
@@ -15,6 +15,7 @@ import { Locator, } from './third_party/index.js';
|
|
|
15
15
|
import { listPages } from './tools/pages.js';
|
|
16
16
|
import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
|
|
17
17
|
import { getTempFilePath, resolveCanonicalPath } from './utils/files.js';
|
|
18
|
+
import { isAllowedUrl } from './utils/url.js';
|
|
18
19
|
// Page ids are handed out from a process-wide counter so they stay unique
|
|
19
20
|
// across all contexts, in particular across browser reconnects. An id issued
|
|
20
21
|
// before a reconnect then fails to resolve instead of hitting an unrelated
|
|
@@ -81,6 +82,12 @@ export class McpContext {
|
|
|
81
82
|
}
|
|
82
83
|
#onTargetCreated = async (target) => {
|
|
83
84
|
try {
|
|
85
|
+
const url = target.url();
|
|
86
|
+
if (!isAllowedUrl(url, {
|
|
87
|
+
categoryExtensions: this.#options.categoryExtensions,
|
|
88
|
+
})) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
84
91
|
const page = await target.page();
|
|
85
92
|
if (!page) {
|
|
86
93
|
return;
|
|
@@ -137,28 +144,30 @@ export class McpContext {
|
|
|
137
144
|
}
|
|
138
145
|
async validatePath(filePath) {
|
|
139
146
|
if (filePath === undefined) {
|
|
140
|
-
return;
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
let canonicalPath;
|
|
150
|
+
try {
|
|
151
|
+
canonicalPath = await resolveCanonicalPath(filePath);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
155
|
+
console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
|
|
156
|
+
throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
|
|
141
157
|
}
|
|
142
158
|
// If the client never negotiated roots and the operator has explicitly
|
|
143
159
|
// opted into unrestricted access via --allow-unrestricted-paths, restore
|
|
144
160
|
// the previous permissive behavior and skip validation.
|
|
145
161
|
if (this.#roots === undefined && this.#allowUnrestrictedPaths) {
|
|
146
|
-
|
|
162
|
+
// Canonical path might not exist yet so we fallback to
|
|
163
|
+
// path.resolve(filePath). Consumers should not follow symlinks.
|
|
164
|
+
return canonicalPath || path.resolve(filePath);
|
|
147
165
|
}
|
|
148
166
|
// roots() always returns at least the temp directory, even if the
|
|
149
167
|
// connecting client never negotiated the optional `roots` capability.
|
|
150
168
|
// Path validation must not be skipped just because no workspace roots
|
|
151
169
|
// were configured.
|
|
152
170
|
const roots = this.roots();
|
|
153
|
-
let canonicalPath;
|
|
154
|
-
try {
|
|
155
|
-
canonicalPath = await resolveCanonicalPath(filePath);
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
159
|
-
console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
|
|
160
|
-
throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
|
|
161
|
-
}
|
|
162
171
|
let allowed = false;
|
|
163
172
|
const resolvedRoots = await Promise.allSettled(roots.map(async (root) => {
|
|
164
173
|
const rootPathUri = root.uri;
|
|
@@ -186,12 +195,12 @@ export class McpContext {
|
|
|
186
195
|
if (!allowed) {
|
|
187
196
|
throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
|
|
188
197
|
}
|
|
198
|
+
return canonicalPath || path.resolve(filePath);
|
|
189
199
|
}
|
|
190
200
|
async ensureExtension(filePath, extension) {
|
|
191
|
-
const
|
|
192
|
-
const currentExtension = path.extname(
|
|
193
|
-
const outputPath = `${
|
|
194
|
-
await this.validatePath(outputPath);
|
|
201
|
+
const resolved = await this.validatePath(filePath);
|
|
202
|
+
const currentExtension = path.extname(resolved);
|
|
203
|
+
const outputPath = `${resolved.slice(0, resolved.length - currentExtension.length)}${extension}`;
|
|
195
204
|
return outputPath;
|
|
196
205
|
}
|
|
197
206
|
async newPage(background, isolatedContextName) {
|
|
@@ -202,7 +211,7 @@ export class McpContext {
|
|
|
202
211
|
ctx = await this.browser.createBrowserContext();
|
|
203
212
|
this.#isolatedContexts.set(isolatedContextName, ctx);
|
|
204
213
|
}
|
|
205
|
-
page = await ctx.newPage();
|
|
214
|
+
page = await ctx.newPage({ background });
|
|
206
215
|
}
|
|
207
216
|
else {
|
|
208
217
|
page = await this.browser.newPage({ background });
|
|
@@ -266,6 +275,21 @@ export class McpContext {
|
|
|
266
275
|
}
|
|
267
276
|
return page;
|
|
268
277
|
}
|
|
278
|
+
getSelectedMcpPageUrl(page) {
|
|
279
|
+
let targetPage = page;
|
|
280
|
+
if (!targetPage) {
|
|
281
|
+
try {
|
|
282
|
+
targetPage = this.getSelectedMcpPage();
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (targetPage?.pptrPage?.isClosed() === false) {
|
|
289
|
+
return targetPage.pptrPage.url();
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
269
293
|
async getDevToolsData(page) {
|
|
270
294
|
const targetPage = page ?? this.#selectedPage;
|
|
271
295
|
if (!targetPage) {
|
|
@@ -372,6 +396,7 @@ export class McpContext {
|
|
|
372
396
|
hasNetworkBlockOrAllowlist: this.#hasNetworkBlockOrAllowlist,
|
|
373
397
|
isolatedContextName: this.#getBrowserContextToNameMap().get(page.browserContext()),
|
|
374
398
|
navigationTimeout: this.#options.navigationTimeout,
|
|
399
|
+
sourceMaps: this.#options.sourceMaps,
|
|
375
400
|
});
|
|
376
401
|
this.#mcpPages.set(page, mcpPage);
|
|
377
402
|
await mcpPage.init();
|
|
@@ -409,29 +434,38 @@ export class McpContext {
|
|
|
409
434
|
}
|
|
410
435
|
async #fetchBrowserPages() {
|
|
411
436
|
const allPages = (await this.browser.pages(this.#options.experimentalIncludeAllPages)).filter(page => {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
437
|
+
if (!this.#options.experimentalDevToolsDebugging &&
|
|
438
|
+
page.url().startsWith('devtools://')) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return isAllowedUrl(page.url(), {
|
|
442
|
+
categoryExtensions: this.#options.categoryExtensions,
|
|
443
|
+
});
|
|
419
444
|
});
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
445
|
+
if (this.#options.categoryExtensions) {
|
|
446
|
+
const allTargets = this.browser.targets();
|
|
447
|
+
const extensionTargets = allTargets.filter(target => {
|
|
448
|
+
return (target.url().startsWith('chrome-extension://') &&
|
|
449
|
+
target.type() === 'page');
|
|
450
|
+
});
|
|
451
|
+
await Promise.allSettled(extensionTargets.map(async (target) => {
|
|
452
|
+
try {
|
|
453
|
+
let page = await target.page();
|
|
454
|
+
if (!page) {
|
|
455
|
+
page = await target.asPage();
|
|
456
|
+
}
|
|
457
|
+
this.#extensionPages.set(target, page);
|
|
458
|
+
if (page &&
|
|
459
|
+
isAllowedUrl(page.url(), { categoryExtensions: true }) &&
|
|
460
|
+
!allPages.includes(page)) {
|
|
461
|
+
allPages.push(page);
|
|
462
|
+
}
|
|
425
463
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
allPages.push(page);
|
|
464
|
+
catch (e) {
|
|
465
|
+
this.logger?.('Failed to get page for extension target', e);
|
|
429
466
|
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
this.logger?.('Failed to get page for extension target', e);
|
|
433
|
-
}
|
|
434
|
-
}));
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
435
469
|
return allPages;
|
|
436
470
|
}
|
|
437
471
|
getExtensionServiceWorkers() {
|
|
@@ -441,16 +475,16 @@ export class McpContext {
|
|
|
441
475
|
return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target);
|
|
442
476
|
}
|
|
443
477
|
async #writeFile(filepath, data) {
|
|
444
|
-
await this.validatePath(filepath);
|
|
478
|
+
const resolved = await this.validatePath(filepath);
|
|
445
479
|
try {
|
|
446
|
-
await fs.mkdir(path.dirname(
|
|
480
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
447
481
|
// Open the file with flags to:
|
|
448
482
|
// - O_WRONLY: Write-only
|
|
449
483
|
// - O_CREAT: Create if it doesn't exist
|
|
450
484
|
// - O_TRUNC: Truncate to zero length if it exists
|
|
451
485
|
// - O_NOFOLLOW: DO NOT follow symlinks.
|
|
452
486
|
// - 0o600: Permissions: read/write for owner, no permissions for others.
|
|
453
|
-
await fs.writeFile(
|
|
487
|
+
await fs.writeFile(resolved, data, {
|
|
454
488
|
flag: fs.constants.O_WRONLY |
|
|
455
489
|
fs.constants.O_CREAT |
|
|
456
490
|
fs.constants.O_TRUNC |
|
|
@@ -515,6 +549,9 @@ export class McpContext {
|
|
|
515
549
|
async getHeapSnapshotDuplicateStrings(filePath) {
|
|
516
550
|
return await this.#heapSnapshotManager.getDuplicateStrings(filePath);
|
|
517
551
|
}
|
|
552
|
+
async queryHeapSnapshotObjects(filePath, options) {
|
|
553
|
+
return await this.#heapSnapshotManager.queryObjects(filePath, options);
|
|
554
|
+
}
|
|
518
555
|
async getHeapSnapshotStats(filePath) {
|
|
519
556
|
return await this.#heapSnapshotManager.getStats(filePath);
|
|
520
557
|
}
|
|
@@ -585,8 +622,8 @@ export class McpContext {
|
|
|
585
622
|
return response.text();
|
|
586
623
|
}
|
|
587
624
|
case 'file:': {
|
|
588
|
-
await this.validatePath(fileURLToPath(url));
|
|
589
|
-
return await fs.readFile(
|
|
625
|
+
const resolved = await this.validatePath(fileURLToPath(url));
|
|
626
|
+
return await fs.readFile(resolved, 'utf-8');
|
|
590
627
|
}
|
|
591
628
|
default:
|
|
592
629
|
throw new Error(`Unsupported protocol for: ${url}`);
|
package/build/src/McpPage.js
CHANGED
|
@@ -139,10 +139,12 @@ export class McpPage {
|
|
|
139
139
|
#hasNetworkBlockOrAllowlist;
|
|
140
140
|
#locatorClass;
|
|
141
141
|
#navigationTimeout;
|
|
142
|
+
#sourceMaps;
|
|
142
143
|
constructor(page, id, options) {
|
|
143
144
|
this.#hasNetworkBlockOrAllowlist = options.hasNetworkBlockOrAllowlist;
|
|
144
145
|
this.#locatorClass = options.locatorClass;
|
|
145
146
|
this.#navigationTimeout = options.navigationTimeout ?? NAVIGATION_TIMEOUT;
|
|
147
|
+
this.#sourceMaps = options.sourceMaps ?? true;
|
|
146
148
|
this.pptrPage = page;
|
|
147
149
|
this.id = id;
|
|
148
150
|
this.isolatedContextName = options.isolatedContextName;
|
|
@@ -183,7 +185,9 @@ export class McpPage {
|
|
|
183
185
|
}
|
|
184
186
|
try {
|
|
185
187
|
const session = await this.pptrPage.createCDPSession();
|
|
186
|
-
this.#devtoolsUniverse = await createTargetUniverse(session
|
|
188
|
+
this.#devtoolsUniverse = await createTargetUniverse(session, {
|
|
189
|
+
sourceMaps: this.#sourceMaps,
|
|
190
|
+
});
|
|
187
191
|
}
|
|
188
192
|
catch (e) {
|
|
189
193
|
logger?.('Failed to initialize DevTools universe', e);
|
package/build/src/ToolHandler.js
CHANGED
|
@@ -5,71 +5,44 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Modified by Opera Software AS: optional `hooks` seam (see
|
|
7
7
|
* ./opera/toolHandlerHooks.ts) for mutex bypass, browser relaunch and log
|
|
8
|
-
* streaming
|
|
8
|
+
* streaming, plus one page-resolution call that tolerates a selection the user
|
|
9
|
+
* closed (see ./opera/pageRecovery.ts). Keep the diff to the three
|
|
10
|
+
* `this.hooks?.` call sites and that line.
|
|
9
11
|
*/
|
|
10
12
|
import { McpResponse } from './McpResponse.js';
|
|
11
13
|
import { CLI_BIN_NAME } from './opera/branding.js';
|
|
14
|
+
import { resolveSelectedPage } from './opera/pageRecovery.js';
|
|
12
15
|
import { SlimMcpResponse } from './SlimMcpResponse.js';
|
|
13
16
|
import { ClearcutLogger } from './telemetry/ClearcutLogger.js';
|
|
14
|
-
import { bucketizeLatency, buildContext } from './telemetry/transformation.js';
|
|
15
17
|
import { zod } from './third_party/index.js';
|
|
16
|
-
import { labels
|
|
18
|
+
import { labels } from './tools/categories.js';
|
|
19
|
+
import { categoryToFlagName } from './config/category-options.js';
|
|
17
20
|
import { pageIdSchema } from './tools/ToolDefinition.js';
|
|
18
21
|
import { logger } from './utils/logger.js';
|
|
19
|
-
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
23
|
import { isLocalhost } from './utils/url.js';
|
|
21
|
-
export function buildFlag(category) {
|
|
22
|
-
return `category${category.charAt(0).toUpperCase() + category.slice(1)}`;
|
|
23
|
-
}
|
|
24
24
|
function buildDisabledMessage(toolName, flag, categoryLabel) {
|
|
25
25
|
const reason = categoryLabel
|
|
26
26
|
? `is in category ${categoryLabel} which`
|
|
27
|
-
: `requires experimental feature ${flag} and`;
|
|
27
|
+
: `requires ${flag.startsWith('--experimental') ? 'experimental feature' : 'flag'} ${flag} and`;
|
|
28
28
|
return `Tool ${toolName} ${reason} is currently disabled. Enable it by running ${CLI_BIN_NAME} start ${flag}=true. For more information check the README.`;
|
|
29
29
|
}
|
|
30
|
-
function getCategoryStatus(category, serverArgs) {
|
|
31
|
-
const categoryFlag = buildFlag(category);
|
|
32
|
-
const flagValue = serverArgs[categoryFlag];
|
|
33
|
-
const isDisabled = OFF_BY_DEFAULT_CATEGORIES.includes(category)
|
|
34
|
-
? !flagValue
|
|
35
|
-
: flagValue === false;
|
|
36
|
-
if (isDisabled) {
|
|
37
|
-
return {
|
|
38
|
-
categoryFlag,
|
|
39
|
-
disabled: true,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
disabled: false,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function getConditionStatus(condition, serverArgs) {
|
|
47
|
-
if (condition && !serverArgs[condition]) {
|
|
48
|
-
return { conditionFlag: condition, disabled: true };
|
|
49
|
-
}
|
|
50
|
-
return { disabled: false };
|
|
51
|
-
}
|
|
52
30
|
function getToolStatusInfo(tool, serverArgs) {
|
|
53
31
|
const category = tool.annotations.category;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!
|
|
57
|
-
|
|
32
|
+
if (category) {
|
|
33
|
+
const flag = categoryToFlagName(category);
|
|
34
|
+
if (!serverArgs[flag]) {
|
|
35
|
+
return {
|
|
36
|
+
disabled: true,
|
|
37
|
+
reason: buildDisabledMessage(tool.name, `--${flag}`, labels[category]),
|
|
38
|
+
};
|
|
58
39
|
}
|
|
59
|
-
return {
|
|
60
|
-
disabled: true,
|
|
61
|
-
reason: buildDisabledMessage(tool.name, `--${categoryCheck.categoryFlag}`, labels[category]),
|
|
62
|
-
};
|
|
63
40
|
}
|
|
64
41
|
for (const condition of tool.annotations.conditions || []) {
|
|
65
|
-
|
|
66
|
-
if (conditionCheck.disabled) {
|
|
67
|
-
if (!conditionCheck.conditionFlag) {
|
|
68
|
-
throw new Error('when the condition is disabled there should always be a flag set');
|
|
69
|
-
}
|
|
42
|
+
if (!serverArgs[condition]) {
|
|
70
43
|
return {
|
|
71
44
|
disabled: true,
|
|
72
|
-
reason: buildDisabledMessage(tool.name, `--${
|
|
45
|
+
reason: buildDisabledMessage(tool.name, `--${condition}`),
|
|
73
46
|
};
|
|
74
47
|
}
|
|
75
48
|
}
|
|
@@ -89,14 +62,20 @@ function buildUnknownArgumentsMessage(toolName, unknownArgumentNames, expectedAr
|
|
|
89
62
|
const correction = unknownArgumentNames.length === 1 ? 'Remove it' : 'Remove them';
|
|
90
63
|
return `Unknown ${unknownLabel} for tool "${toolName}": ${formatArgumentNames(unknownArgumentNames)}. ${expectedArguments} ${correction} and retry.`;
|
|
91
64
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
65
|
+
async function validateAndResolvePathOrUrl(filePathOrUrl, context) {
|
|
66
|
+
try {
|
|
67
|
+
const url = new URL(filePathOrUrl);
|
|
68
|
+
if (url.protocol === 'file:') {
|
|
69
|
+
return pathToFileURL(await context.validatePath(fileURLToPath(url))).href;
|
|
70
|
+
}
|
|
71
|
+
else if (['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) {
|
|
72
|
+
return filePathOrUrl;
|
|
73
|
+
}
|
|
95
74
|
}
|
|
96
|
-
|
|
97
|
-
|
|
75
|
+
catch {
|
|
76
|
+
// Suppress parsing errors for regular file paths.
|
|
98
77
|
}
|
|
99
|
-
return
|
|
78
|
+
return await context.validatePath(filePathOrUrl);
|
|
100
79
|
}
|
|
101
80
|
function isLocalBrowser(context) {
|
|
102
81
|
if (context.browser.process()) {
|
|
@@ -122,27 +101,25 @@ function shouldValidateFile(option, isLocal) {
|
|
|
122
101
|
}
|
|
123
102
|
async function validateToolFiles(tool, params, context) {
|
|
124
103
|
const isLocal = isLocalBrowser(context);
|
|
125
|
-
const pathsOrUrlsToValidate = [];
|
|
126
104
|
for (const [key, option] of Object.entries(tool.verifyFilesSchema)) {
|
|
127
105
|
if (shouldValidateFile(option, isLocal)) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
for (const filePathOrUrl of pathsOrUrlsToValidate) {
|
|
132
|
-
let filePath = filePathOrUrl;
|
|
133
|
-
try {
|
|
134
|
-
const url = new URL(filePathOrUrl);
|
|
135
|
-
if (url.protocol === 'file:') {
|
|
136
|
-
filePath = fileURLToPath(url);
|
|
106
|
+
const val = params[key];
|
|
107
|
+
if (typeof val === 'string') {
|
|
108
|
+
params[key] = await validateAndResolvePathOrUrl(val, context);
|
|
137
109
|
}
|
|
138
|
-
else if (
|
|
139
|
-
|
|
110
|
+
else if (Array.isArray(val)) {
|
|
111
|
+
const updated = [];
|
|
112
|
+
for (const item of val) {
|
|
113
|
+
if (typeof item === 'string') {
|
|
114
|
+
updated.push(await validateAndResolvePathOrUrl(item, context));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
throw new Error('Unexpected non-string value as a file path or URL');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
params[key] = updated;
|
|
140
121
|
}
|
|
141
122
|
}
|
|
142
|
-
catch {
|
|
143
|
-
// Suppress parsing errors for regular file paths.
|
|
144
|
-
}
|
|
145
|
-
await context.validatePath(filePath);
|
|
146
123
|
}
|
|
147
124
|
}
|
|
148
125
|
export class ToolHandler {
|
|
@@ -167,7 +144,7 @@ export class ToolHandler {
|
|
|
167
144
|
this.inputSchema =
|
|
168
145
|
'pageScoped' in tool &&
|
|
169
146
|
tool.pageScoped &&
|
|
170
|
-
serverArgs.
|
|
147
|
+
serverArgs.pageIdRouting &&
|
|
171
148
|
!serverArgs.slim
|
|
172
149
|
? { ...pageIdSchema, ...tool.schema }
|
|
173
150
|
: tool.schema;
|
|
@@ -226,11 +203,11 @@ export class ToolHandler {
|
|
|
226
203
|
if (isPageScopedTool(this.tool)) {
|
|
227
204
|
const pageId = typeof params.pageId === 'number' ? params.pageId : undefined;
|
|
228
205
|
page =
|
|
229
|
-
this.serverArgs.
|
|
206
|
+
this.serverArgs.pageIdRouting &&
|
|
230
207
|
pageId !== undefined &&
|
|
231
208
|
!this.serverArgs.slim
|
|
232
209
|
? context.getPageById(pageId)
|
|
233
|
-
: context
|
|
210
|
+
: await resolveSelectedPage(context, response);
|
|
234
211
|
response.setPage(page);
|
|
235
212
|
if (this.tool.blockedByDialog) {
|
|
236
213
|
page.throwIfDialogOpen();
|
|
@@ -252,10 +229,7 @@ export class ToolHandler {
|
|
|
252
229
|
response.setError(err);
|
|
253
230
|
}
|
|
254
231
|
devToolsData = await context.getDevToolsData(page);
|
|
255
|
-
|
|
256
|
-
if (targetPage?.pptrPage?.isClosed() === false) {
|
|
257
|
-
pageUrl = targetPage.pptrPage.url();
|
|
258
|
-
}
|
|
232
|
+
pageUrl = context.getSelectedMcpPageUrl(page);
|
|
259
233
|
// Resolve data format: --experimentalDataFormat takes precedence, fall back to legacy --experimentalToonFormat
|
|
260
234
|
let dataFormat = 'default';
|
|
261
235
|
if (this.serverArgs.experimentalDataFormat) {
|
|
@@ -294,14 +268,18 @@ export class ToolHandler {
|
|
|
294
268
|
};
|
|
295
269
|
}
|
|
296
270
|
finally {
|
|
297
|
-
|
|
271
|
+
// Before telemetry and the mutex release: the claim `beforeInvoke` took
|
|
272
|
+
// has to be released even when the invocation failed, or the next Opera
|
|
273
|
+
// AI tool waits on a browser this one only looks like it is using.
|
|
274
|
+
this.hooks?.afterInvoke(this.tool);
|
|
298
275
|
void ClearcutLogger.get()?.logToolInvocation({
|
|
299
276
|
toolName: this.tool.name,
|
|
300
277
|
params,
|
|
301
278
|
schema: this.inputSchema,
|
|
302
279
|
success,
|
|
303
|
-
latencyMs:
|
|
304
|
-
|
|
280
|
+
latencyMs: Date.now() - startTime,
|
|
281
|
+
devToolsData,
|
|
282
|
+
pageUrl,
|
|
305
283
|
});
|
|
306
284
|
guard?.[Symbol.dispose]();
|
|
307
285
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import '../utils/polyfill.js';
|
|
9
9
|
import process from 'node:process';
|
|
10
10
|
import { closeBrowser } from '../browser.js';
|
|
11
|
-
import {
|
|
11
|
+
import { McpServer, logDisclaimers } from '../index.js';
|
|
12
12
|
import { ENV_CRASH_ON_UNCAUGHT, PACKAGE_NAME, PRODUCT_NAME, } from '../opera/branding.js';
|
|
13
13
|
import { enforceTelemetryPolicy } from '../opera/policy.js';
|
|
14
14
|
import { ClearcutLogger } from '../telemetry/ClearcutLogger.js';
|
|
@@ -17,7 +17,7 @@ import { StdioServerTransport } from '../third_party/index.js';
|
|
|
17
17
|
import { checkForUpdates } from '../utils/check-for-updates.js';
|
|
18
18
|
import { logger, saveLogsToFile } from '../utils/logger.js';
|
|
19
19
|
import { VERSION } from '../version.js';
|
|
20
|
-
import {
|
|
20
|
+
import { mcpOptions, parseArguments } from '../config/mcp-options.js';
|
|
21
21
|
await checkForUpdates(`Run \`npm install ${PACKAGE_NAME}@latest\` to update.`);
|
|
22
22
|
export const args = parseArguments(VERSION);
|
|
23
23
|
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
@@ -65,7 +65,7 @@ process.on('SIGINT', () => {
|
|
|
65
65
|
process.on('SIGHUP', () => {
|
|
66
66
|
void shutdown('SIGHUP');
|
|
67
67
|
});
|
|
68
|
-
const
|
|
68
|
+
const server = await McpServer.from(args, {
|
|
69
69
|
logFile,
|
|
70
70
|
});
|
|
71
71
|
const transport = new StdioServerTransport();
|
|
@@ -73,5 +73,5 @@ await server.connect(transport);
|
|
|
73
73
|
logger?.(`${PRODUCT_NAME} connected`);
|
|
74
74
|
logDisclaimers(args);
|
|
75
75
|
void ClearcutLogger.get()?.logDailyActiveIfNeeded();
|
|
76
|
-
void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args,
|
|
76
|
+
void ClearcutLogger.get()?.logServerStart(computeFlagUsage(args, mcpOptions));
|
|
77
77
|
//# sourceMappingURL=chrome-devtools-mcp-main.js.map
|