@democratize-quality/mcp-server 1.0.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/LICENSE +15 -0
- package/README.md +423 -0
- package/browserControl.js +113 -0
- package/cli.js +187 -0
- package/docs/api/tool-reference.md +317 -0
- package/docs/api_tools_usage.md +477 -0
- package/docs/development/adding-tools.md +274 -0
- package/docs/development/configuration.md +332 -0
- package/docs/examples/authentication.md +124 -0
- package/docs/examples/basic-automation.md +105 -0
- package/docs/getting-started.md +214 -0
- package/docs/index.md +61 -0
- package/mcpServer.js +280 -0
- package/package.json +83 -0
- package/run-server.js +140 -0
- package/src/config/environments/api-only.js +53 -0
- package/src/config/environments/development.js +54 -0
- package/src/config/environments/production.js +69 -0
- package/src/config/index.js +341 -0
- package/src/config/server.js +41 -0
- package/src/config/tools/api.js +67 -0
- package/src/config/tools/browser.js +90 -0
- package/src/config/tools/default.js +32 -0
- package/src/services/browserService.js +325 -0
- package/src/tools/api/api-request.js +641 -0
- package/src/tools/api/api-session-report.js +1262 -0
- package/src/tools/api/api-session-status.js +395 -0
- package/src/tools/base/ToolBase.js +230 -0
- package/src/tools/base/ToolRegistry.js +269 -0
- package/src/tools/browser/advanced/browser-console.js +384 -0
- package/src/tools/browser/advanced/browser-dialog.js +319 -0
- package/src/tools/browser/advanced/browser-evaluate.js +337 -0
- package/src/tools/browser/advanced/browser-file.js +480 -0
- package/src/tools/browser/advanced/browser-keyboard.js +343 -0
- package/src/tools/browser/advanced/browser-mouse.js +332 -0
- package/src/tools/browser/advanced/browser-network.js +421 -0
- package/src/tools/browser/advanced/browser-pdf.js +407 -0
- package/src/tools/browser/advanced/browser-tabs.js +497 -0
- package/src/tools/browser/advanced/browser-wait.js +378 -0
- package/src/tools/browser/click.js +168 -0
- package/src/tools/browser/close.js +60 -0
- package/src/tools/browser/dom.js +70 -0
- package/src/tools/browser/launch.js +67 -0
- package/src/tools/browser/navigate.js +270 -0
- package/src/tools/browser/screenshot.js +351 -0
- package/src/tools/browser/type.js +174 -0
- package/src/tools/index.js +95 -0
- package/src/utils/browserHelpers.js +83 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
const ToolBase = require('../../base/ToolBase');
|
|
2
|
+
const browserService = require('../../../services/browserService');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Enhanced Tabs Tool - Comprehensive tab management
|
|
6
|
+
* Inspired by Playwright MCP tabs capabilities
|
|
7
|
+
*/
|
|
8
|
+
class BrowserTabsTool extends ToolBase {
|
|
9
|
+
static definition = {
|
|
10
|
+
name: "browser_tabs",
|
|
11
|
+
description: "Manage browser tabs - create, close, switch, list, and organize tabs with advanced features.",
|
|
12
|
+
input_schema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
browserId: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The ID of the browser instance"
|
|
18
|
+
},
|
|
19
|
+
action: {
|
|
20
|
+
type: "string",
|
|
21
|
+
enum: ["list", "create", "close", "switch", "duplicate", "pin", "unpin", "mute", "unmute", "refresh", "goBack", "goForward"],
|
|
22
|
+
description: "The tab action to perform"
|
|
23
|
+
},
|
|
24
|
+
tabId: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description: "The ID of the tab (for close, switch, duplicate, pin, mute, refresh, etc.)"
|
|
27
|
+
},
|
|
28
|
+
url: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "URL to open in new tab (for create action)"
|
|
31
|
+
},
|
|
32
|
+
title: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Title filter for finding tabs (supports partial matching)"
|
|
35
|
+
},
|
|
36
|
+
active: {
|
|
37
|
+
type: "boolean",
|
|
38
|
+
description: "Whether to make the tab active (for create and switch actions)"
|
|
39
|
+
},
|
|
40
|
+
windowId: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Window ID for tab operations"
|
|
43
|
+
},
|
|
44
|
+
pattern: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "URL pattern to match tabs (regex supported)"
|
|
47
|
+
},
|
|
48
|
+
includeDetails: {
|
|
49
|
+
type: "boolean",
|
|
50
|
+
default: false,
|
|
51
|
+
description: "Include detailed tab information (for list action)"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
required: ["browserId", "action"]
|
|
55
|
+
},
|
|
56
|
+
output_schema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {
|
|
59
|
+
success: { type: "boolean", description: "Whether the operation was successful" },
|
|
60
|
+
action: { type: "string", description: "The action that was performed" },
|
|
61
|
+
tabs: {
|
|
62
|
+
type: "array",
|
|
63
|
+
items: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
id: { type: "string" },
|
|
67
|
+
url: { type: "string" },
|
|
68
|
+
title: { type: "string" },
|
|
69
|
+
active: { type: "boolean" },
|
|
70
|
+
pinned: { type: "boolean" },
|
|
71
|
+
muted: { type: "boolean" },
|
|
72
|
+
windowId: { type: "string" },
|
|
73
|
+
status: { type: "string" }
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
description: "List of tabs"
|
|
77
|
+
},
|
|
78
|
+
tabId: { type: "string", description: "ID of the affected tab" },
|
|
79
|
+
message: { type: "string", description: "Operation result message" },
|
|
80
|
+
browserId: { type: "string", description: "Browser instance ID" }
|
|
81
|
+
},
|
|
82
|
+
required: ["success", "action", "browserId"]
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
async execute(parameters) {
|
|
87
|
+
const {
|
|
88
|
+
browserId,
|
|
89
|
+
action,
|
|
90
|
+
tabId,
|
|
91
|
+
url,
|
|
92
|
+
title,
|
|
93
|
+
active = true,
|
|
94
|
+
windowId,
|
|
95
|
+
pattern,
|
|
96
|
+
includeDetails = false
|
|
97
|
+
} = parameters;
|
|
98
|
+
|
|
99
|
+
const browser = browserService.getBrowserInstance(browserId);
|
|
100
|
+
if (!browser) {
|
|
101
|
+
throw new Error(`Browser instance '${browserId}' not found`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const client = browser.client;
|
|
105
|
+
|
|
106
|
+
let result = {
|
|
107
|
+
success: false,
|
|
108
|
+
action: action,
|
|
109
|
+
browserId: browserId
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
switch (action) {
|
|
113
|
+
case 'list':
|
|
114
|
+
const tabs = await this.listTabs(client, title, pattern, includeDetails);
|
|
115
|
+
result.success = true;
|
|
116
|
+
result.tabs = tabs;
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
case 'create':
|
|
120
|
+
if (!url) {
|
|
121
|
+
throw new Error('URL is required for create action');
|
|
122
|
+
}
|
|
123
|
+
const newTab = await this.createTab(client, url, active);
|
|
124
|
+
result.success = true;
|
|
125
|
+
result.tabId = newTab.targetId;
|
|
126
|
+
result.message = `Tab created with ID: ${newTab.targetId}`;
|
|
127
|
+
result.tabs = [newTab];
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
case 'close':
|
|
131
|
+
if (!tabId) {
|
|
132
|
+
throw new Error('Tab ID is required for close action');
|
|
133
|
+
}
|
|
134
|
+
await this.closeTab(client, tabId);
|
|
135
|
+
result.success = true;
|
|
136
|
+
result.tabId = tabId;
|
|
137
|
+
result.message = `Tab ${tabId} closed`;
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case 'switch':
|
|
141
|
+
if (!tabId) {
|
|
142
|
+
throw new Error('Tab ID is required for switch action');
|
|
143
|
+
}
|
|
144
|
+
await this.switchToTab(client, tabId);
|
|
145
|
+
result.success = true;
|
|
146
|
+
result.tabId = tabId;
|
|
147
|
+
result.message = `Switched to tab ${tabId}`;
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case 'duplicate':
|
|
151
|
+
if (!tabId) {
|
|
152
|
+
throw new Error('Tab ID is required for duplicate action');
|
|
153
|
+
}
|
|
154
|
+
const duplicatedTab = await this.duplicateTab(client, tabId);
|
|
155
|
+
result.success = true;
|
|
156
|
+
result.tabId = duplicatedTab.targetId;
|
|
157
|
+
result.message = `Tab duplicated with ID: ${duplicatedTab.targetId}`;
|
|
158
|
+
result.tabs = [duplicatedTab];
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case 'refresh':
|
|
162
|
+
if (!tabId) {
|
|
163
|
+
throw new Error('Tab ID is required for refresh action');
|
|
164
|
+
}
|
|
165
|
+
await this.refreshTab(client, tabId);
|
|
166
|
+
result.success = true;
|
|
167
|
+
result.tabId = tabId;
|
|
168
|
+
result.message = `Tab ${tabId} refreshed`;
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case 'goBack':
|
|
172
|
+
if (!tabId) {
|
|
173
|
+
throw new Error('Tab ID is required for goBack action');
|
|
174
|
+
}
|
|
175
|
+
await this.goBack(client, tabId);
|
|
176
|
+
result.success = true;
|
|
177
|
+
result.tabId = tabId;
|
|
178
|
+
result.message = `Navigated back in tab ${tabId}`;
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case 'goForward':
|
|
182
|
+
if (!tabId) {
|
|
183
|
+
throw new Error('Tab ID is required for goForward action');
|
|
184
|
+
}
|
|
185
|
+
await this.goForward(client, tabId);
|
|
186
|
+
result.success = true;
|
|
187
|
+
result.tabId = tabId;
|
|
188
|
+
result.message = `Navigated forward in tab ${tabId}`;
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case 'pin':
|
|
192
|
+
case 'unpin':
|
|
193
|
+
case 'mute':
|
|
194
|
+
case 'unmute':
|
|
195
|
+
// These actions would require additional Chrome extension APIs
|
|
196
|
+
// For now, we'll return a message indicating the limitation
|
|
197
|
+
result.success = false;
|
|
198
|
+
result.message = `${action} action requires browser extension capabilities not available via DevTools Protocol`;
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
default:
|
|
202
|
+
throw new Error(`Unsupported tabs action: ${action}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* List all tabs with optional filtering
|
|
210
|
+
*/
|
|
211
|
+
async listTabs(client, titleFilter, urlPattern, includeDetails) {
|
|
212
|
+
const targets = await client.Target.getTargets();
|
|
213
|
+
const tabs = targets.targetInfos
|
|
214
|
+
.filter(target => target.type === 'page')
|
|
215
|
+
.map(target => this.formatTabInfo(target, includeDetails));
|
|
216
|
+
|
|
217
|
+
let filteredTabs = tabs;
|
|
218
|
+
|
|
219
|
+
// Apply title filter
|
|
220
|
+
if (titleFilter) {
|
|
221
|
+
filteredTabs = filteredTabs.filter(tab =>
|
|
222
|
+
tab.title && tab.title.toLowerCase().includes(titleFilter.toLowerCase())
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Apply URL pattern filter
|
|
227
|
+
if (urlPattern) {
|
|
228
|
+
const regex = new RegExp(urlPattern, 'i');
|
|
229
|
+
filteredTabs = filteredTabs.filter(tab =>
|
|
230
|
+
tab.url && regex.test(tab.url)
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Get additional details if requested
|
|
235
|
+
if (includeDetails) {
|
|
236
|
+
for (const tab of filteredTabs) {
|
|
237
|
+
try {
|
|
238
|
+
const runtime = await client.Runtime.evaluate({
|
|
239
|
+
expression: `({
|
|
240
|
+
readyState: document.readyState,
|
|
241
|
+
visibilityState: document.visibilityState,
|
|
242
|
+
hasFocus: document.hasFocus(),
|
|
243
|
+
scrollPosition: { x: window.scrollX, y: window.scrollY },
|
|
244
|
+
windowSize: { width: window.innerWidth, height: window.innerHeight }
|
|
245
|
+
})`,
|
|
246
|
+
contextId: undefined,
|
|
247
|
+
targetId: tab.id
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (runtime.result && runtime.result.value) {
|
|
251
|
+
tab.details = runtime.result.value;
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// Ignore errors for inactive tabs
|
|
255
|
+
tab.details = { error: 'Could not retrieve details' };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return filteredTabs;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Create a new tab
|
|
265
|
+
*/
|
|
266
|
+
async createTab(client, url, makeActive = true) {
|
|
267
|
+
const target = await client.Target.createTarget({
|
|
268
|
+
url: url,
|
|
269
|
+
newWindow: false
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (makeActive) {
|
|
273
|
+
await this.switchToTab(client, target.targetId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Get updated tab info
|
|
277
|
+
const targets = await client.Target.getTargets();
|
|
278
|
+
const tabInfo = targets.targetInfos.find(t => t.targetId === target.targetId);
|
|
279
|
+
|
|
280
|
+
return this.formatTabInfo(tabInfo);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Close a tab
|
|
285
|
+
*/
|
|
286
|
+
async closeTab(client, tabId) {
|
|
287
|
+
await client.Target.closeTarget({
|
|
288
|
+
targetId: tabId
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Switch to a specific tab
|
|
294
|
+
*/
|
|
295
|
+
async switchToTab(client, tabId) {
|
|
296
|
+
await client.Target.activateTarget({
|
|
297
|
+
targetId: tabId
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Duplicate a tab
|
|
303
|
+
*/
|
|
304
|
+
async duplicateTab(client, tabId) {
|
|
305
|
+
// Get the URL of the tab to duplicate
|
|
306
|
+
const targets = await client.Target.getTargets();
|
|
307
|
+
const sourceTab = targets.targetInfos.find(t => t.targetId === tabId);
|
|
308
|
+
|
|
309
|
+
if (!sourceTab) {
|
|
310
|
+
throw new Error(`Tab with ID ${tabId} not found`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Create new tab with same URL
|
|
314
|
+
return this.createTab(client, sourceTab.url, false);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Refresh a tab
|
|
319
|
+
*/
|
|
320
|
+
async refreshTab(client, tabId) {
|
|
321
|
+
// First, attach to the target
|
|
322
|
+
const session = await client.Target.attachToTarget({
|
|
323
|
+
targetId: tabId,
|
|
324
|
+
flatten: true
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Use the session to reload the page
|
|
328
|
+
await client.Page.reload({
|
|
329
|
+
sessionId: session.sessionId
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Detach from the target
|
|
333
|
+
await client.Target.detachFromTarget({
|
|
334
|
+
sessionId: session.sessionId
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Navigate back in tab history
|
|
340
|
+
*/
|
|
341
|
+
async goBack(client, tabId) {
|
|
342
|
+
const session = await client.Target.attachToTarget({
|
|
343
|
+
targetId: tabId,
|
|
344
|
+
flatten: true
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const history = await client.Page.getNavigationHistory({
|
|
348
|
+
sessionId: session.sessionId
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (history.currentIndex > 0) {
|
|
352
|
+
await client.Page.navigateToHistoryEntry({
|
|
353
|
+
entryId: history.entries[history.currentIndex - 1].id,
|
|
354
|
+
sessionId: session.sessionId
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await client.Target.detachFromTarget({
|
|
359
|
+
sessionId: session.sessionId
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Navigate forward in tab history
|
|
365
|
+
*/
|
|
366
|
+
async goForward(client, tabId) {
|
|
367
|
+
const session = await client.Target.attachToTarget({
|
|
368
|
+
targetId: tabId,
|
|
369
|
+
flatten: true
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const history = await client.Page.getNavigationHistory({
|
|
373
|
+
sessionId: session.sessionId
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (history.currentIndex < history.entries.length - 1) {
|
|
377
|
+
await client.Page.navigateToHistoryEntry({
|
|
378
|
+
entryId: history.entries[history.currentIndex + 1].id,
|
|
379
|
+
sessionId: session.sessionId
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
await client.Target.detachFromTarget({
|
|
384
|
+
sessionId: session.sessionId
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Format tab information for output
|
|
390
|
+
*/
|
|
391
|
+
formatTabInfo(target, includeDetails = false) {
|
|
392
|
+
const formatted = {
|
|
393
|
+
id: target.targetId,
|
|
394
|
+
url: target.url,
|
|
395
|
+
title: target.title,
|
|
396
|
+
type: target.type,
|
|
397
|
+
attached: target.attached || false
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// Add browser context info if available
|
|
401
|
+
if (target.browserContextId) {
|
|
402
|
+
formatted.browserContextId = target.browserContextId;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Add opener info if available
|
|
406
|
+
if (target.openerId) {
|
|
407
|
+
formatted.openerId = target.openerId;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return formatted;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Find tabs by various criteria
|
|
415
|
+
*/
|
|
416
|
+
async findTabs(client, criteria = {}) {
|
|
417
|
+
const tabs = await this.listTabs(client, criteria.title, criteria.url, false);
|
|
418
|
+
|
|
419
|
+
return tabs.filter(tab => {
|
|
420
|
+
let matches = true;
|
|
421
|
+
|
|
422
|
+
if (criteria.active !== undefined) {
|
|
423
|
+
// Note: We'd need additional logic to determine which tab is currently active
|
|
424
|
+
// This would require tracking focus or using additional CDP calls
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (criteria.url && !tab.url.includes(criteria.url)) {
|
|
428
|
+
matches = false;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (criteria.title && !tab.title.toLowerCase().includes(criteria.title.toLowerCase())) {
|
|
432
|
+
matches = false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return matches;
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get detailed information about a specific tab
|
|
441
|
+
*/
|
|
442
|
+
async getTabDetails(client, tabId) {
|
|
443
|
+
const targets = await client.Target.getTargets();
|
|
444
|
+
const tab = targets.targetInfos.find(t => t.targetId === tabId);
|
|
445
|
+
|
|
446
|
+
if (!tab) {
|
|
447
|
+
throw new Error(`Tab with ID ${tabId} not found`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const formatted = this.formatTabInfo(tab, true);
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
// Try to get runtime information
|
|
454
|
+
const session = await client.Target.attachToTarget({
|
|
455
|
+
targetId: tabId,
|
|
456
|
+
flatten: true
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const runtime = await client.Runtime.evaluate({
|
|
460
|
+
expression: `({
|
|
461
|
+
readyState: document.readyState,
|
|
462
|
+
visibilityState: document.visibilityState,
|
|
463
|
+
hasFocus: document.hasFocus(),
|
|
464
|
+
location: {
|
|
465
|
+
href: location.href,
|
|
466
|
+
protocol: location.protocol,
|
|
467
|
+
hostname: location.hostname,
|
|
468
|
+
pathname: location.pathname,
|
|
469
|
+
search: location.search,
|
|
470
|
+
hash: location.hash
|
|
471
|
+
},
|
|
472
|
+
viewport: {
|
|
473
|
+
width: window.innerWidth,
|
|
474
|
+
height: window.innerHeight,
|
|
475
|
+
scrollX: window.scrollX,
|
|
476
|
+
scrollY: window.scrollY
|
|
477
|
+
}
|
|
478
|
+
})`,
|
|
479
|
+
sessionId: session.sessionId
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
if (runtime.result && runtime.result.value) {
|
|
483
|
+
formatted.runtime = runtime.result.value;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
await client.Target.detachFromTarget({
|
|
487
|
+
sessionId: session.sessionId
|
|
488
|
+
});
|
|
489
|
+
} catch (error) {
|
|
490
|
+
formatted.error = 'Could not retrieve runtime details: ' + error.message;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return formatted;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = BrowserTabsTool;
|