@mcp-b/smart-dom-reader-server 0.2.0 → 5.1.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/lib/smart-dom-reader.bundle.js +1394 -1777
- package/package.json +22 -10
- package/src/index.ts +167 -178
package/src/index.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
3
|
import { constants as fsConstants } from 'node:fs';
|
|
4
|
-
import { access,
|
|
4
|
+
import { access, open } from 'node:fs/promises';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import { dirname, join, resolve } from 'node:path';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
type CallToolResult,
|
|
10
|
+
McpServer,
|
|
11
|
+
ProtocolError,
|
|
12
|
+
ProtocolErrorCode,
|
|
13
|
+
} from '@modelcontextprotocol/server';
|
|
14
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
10
15
|
import { type Browser, chromium, type LaunchOptions, type Page } from 'playwright';
|
|
11
16
|
import { z } from 'zod';
|
|
12
17
|
|
|
@@ -14,12 +19,14 @@ const { F_OK } = fsConstants;
|
|
|
14
19
|
|
|
15
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
21
|
const __dirname = dirname(__filename);
|
|
22
|
+
const packageVersion = (createRequire(import.meta.url)('../package.json') as { version: string })
|
|
23
|
+
.version;
|
|
17
24
|
|
|
18
25
|
const EMBEDDED_LIBRARY_RELATIVE_PATH = join('..', 'lib', 'smart-dom-reader.bundle.js');
|
|
19
26
|
const DEFAULT_LAUNCH_ARGS = ['--no-sandbox', '--disable-setuid-sandbox'] as const;
|
|
20
27
|
|
|
21
28
|
type ConnectBrowserArgs = {
|
|
22
|
-
executablePath?: string;
|
|
29
|
+
executablePath?: string | undefined;
|
|
23
30
|
headless: boolean;
|
|
24
31
|
};
|
|
25
32
|
|
|
@@ -28,29 +35,29 @@ type NavigateArgs = {
|
|
|
28
35
|
};
|
|
29
36
|
|
|
30
37
|
type OptionalSelectorArgs = {
|
|
31
|
-
selector?: string;
|
|
32
|
-
detail?: 'summary' | 'region' | 'deep';
|
|
33
|
-
maxTextLength?: number;
|
|
34
|
-
maxElements?: number;
|
|
38
|
+
selector?: string | undefined;
|
|
39
|
+
detail?: 'summary' | 'region' | 'deep' | undefined;
|
|
40
|
+
maxTextLength?: number | undefined;
|
|
41
|
+
maxElements?: number | undefined;
|
|
35
42
|
};
|
|
36
43
|
|
|
37
44
|
type RegionArgs = {
|
|
38
45
|
selector: string;
|
|
39
|
-
options?: ProgressiveExtractorRegionConfig & FormatOptions;
|
|
46
|
+
options?: (ProgressiveExtractorRegionConfig & FormatOptions) | undefined;
|
|
40
47
|
};
|
|
41
48
|
|
|
42
49
|
type ContentArgs = {
|
|
43
50
|
selector: string;
|
|
44
|
-
options?: ProgressiveExtractorContentConfig & FormatOptions;
|
|
51
|
+
options?: (ProgressiveExtractorContentConfig & FormatOptions) | undefined;
|
|
45
52
|
};
|
|
46
53
|
|
|
47
54
|
type InteractiveArgs = {
|
|
48
|
-
selector?: string;
|
|
49
|
-
options?: SmartDomReaderOptions & FormatOptions;
|
|
55
|
+
selector?: string | undefined;
|
|
56
|
+
options?: (SmartDomReaderOptions & FormatOptions) | undefined;
|
|
50
57
|
};
|
|
51
58
|
|
|
52
59
|
type ScreenshotArgs = {
|
|
53
|
-
path?: string;
|
|
60
|
+
path?: string | undefined;
|
|
54
61
|
fullPage: boolean;
|
|
55
62
|
};
|
|
56
63
|
|
|
@@ -96,7 +103,7 @@ const DEFAULT_CHROME_LOCATIONS: Partial<Record<NodeJS.Platform, readonly string[
|
|
|
96
103
|
? [join(process.env.PROGRAMFILES, 'Google/Chrome/Application/chrome.exe')]
|
|
97
104
|
: []),
|
|
98
105
|
...(process.env['ProgramFiles(x86)']
|
|
99
|
-
? [join(process.env['ProgramFiles(x86)']
|
|
106
|
+
? [join(process.env['ProgramFiles(x86)'], 'Google/Chrome/Application/chrome.exe')]
|
|
100
107
|
: []),
|
|
101
108
|
...(process.env.LOCALAPPDATA
|
|
102
109
|
? [join(process.env.LOCALAPPDATA, 'Google/Chrome/Application/chrome.exe')]
|
|
@@ -105,29 +112,55 @@ const DEFAULT_CHROME_LOCATIONS: Partial<Record<NodeJS.Platform, readonly string[
|
|
|
105
112
|
};
|
|
106
113
|
|
|
107
114
|
interface ProgressiveExtractorRegionConfig {
|
|
108
|
-
mode?: 'interactive' | 'full';
|
|
109
|
-
includeHidden?: boolean;
|
|
110
|
-
maxDepth?: number;
|
|
115
|
+
mode?: 'interactive' | 'full' | undefined;
|
|
116
|
+
includeHidden?: boolean | undefined;
|
|
117
|
+
maxDepth?: number | undefined;
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
interface ProgressiveExtractorContentConfig {
|
|
114
|
-
includeHeadings?: boolean;
|
|
115
|
-
includeLists?: boolean;
|
|
116
|
-
includeMedia?: boolean;
|
|
117
|
-
maxTextLength?: number;
|
|
121
|
+
includeHeadings?: boolean | undefined;
|
|
122
|
+
includeLists?: boolean | undefined;
|
|
123
|
+
includeMedia?: boolean | undefined;
|
|
124
|
+
maxTextLength?: number | undefined;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
interface SmartDomReaderOptions {
|
|
121
|
-
viewportOnly?: boolean;
|
|
122
|
-
maxDepth?: number;
|
|
128
|
+
viewportOnly?: boolean | undefined;
|
|
129
|
+
maxDepth?: number | undefined;
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
interface FormatOptions {
|
|
126
|
-
detail?: 'summary' | 'region' | 'deep';
|
|
127
|
-
maxTextLength?: number;
|
|
128
|
-
maxElements?: number;
|
|
133
|
+
detail?: 'summary' | 'region' | 'deep' | undefined;
|
|
134
|
+
maxTextLength?: number | undefined;
|
|
135
|
+
maxElements?: number | undefined;
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
const COMMON_TOOL_OPTIONS_SHAPE = {
|
|
139
|
+
maxDepth: z
|
|
140
|
+
.number()
|
|
141
|
+
.int()
|
|
142
|
+
.min(0)
|
|
143
|
+
.describe('Traversal depth for nested elements. Larger values inspect deeper trees.')
|
|
144
|
+
.optional(),
|
|
145
|
+
detail: z
|
|
146
|
+
.enum(['summary', 'region', 'deep'])
|
|
147
|
+
.default('region')
|
|
148
|
+
.describe("Output depth. 'region' is actionable; 'deep' expands more details.")
|
|
149
|
+
.optional(),
|
|
150
|
+
maxTextLength: z
|
|
151
|
+
.number()
|
|
152
|
+
.int()
|
|
153
|
+
.min(0)
|
|
154
|
+
.describe('Truncate text snippets to this many characters to save tokens.')
|
|
155
|
+
.optional(),
|
|
156
|
+
maxElements: z
|
|
157
|
+
.number()
|
|
158
|
+
.int()
|
|
159
|
+
.min(0)
|
|
160
|
+
.describe('Limit number of listed items per group (buttons, links, inputs).')
|
|
161
|
+
.optional(),
|
|
162
|
+
};
|
|
163
|
+
|
|
131
164
|
type LibraryCache = {
|
|
132
165
|
path: string;
|
|
133
166
|
code: string;
|
|
@@ -162,12 +195,13 @@ class SmartDomReaderServer {
|
|
|
162
195
|
private readonly server: McpServer;
|
|
163
196
|
private browser: Browser | null = null;
|
|
164
197
|
private page: Page | null = null;
|
|
198
|
+
private browserOperation: Promise<unknown> = Promise.resolve();
|
|
165
199
|
private cachedLibrary: LibraryCache | null = null;
|
|
166
200
|
|
|
167
201
|
constructor() {
|
|
168
202
|
this.server = new McpServer({
|
|
169
203
|
name: 'smart-dom-reader-server',
|
|
170
|
-
version:
|
|
204
|
+
version: packageVersion,
|
|
171
205
|
});
|
|
172
206
|
|
|
173
207
|
this.registerTools();
|
|
@@ -175,6 +209,16 @@ class SmartDomReaderServer {
|
|
|
175
209
|
|
|
176
210
|
async run(): Promise<void> {
|
|
177
211
|
const transport = new StdioServerTransport();
|
|
212
|
+
this.server.server.onclose = () => {
|
|
213
|
+
void this.queueBrowserOperation(() => this.closeBrowser()).catch((error) => {
|
|
214
|
+
console.error('Failed to close browser during shutdown', error);
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
process.stdin.once('end', () => {
|
|
218
|
+
void this.server.close().catch((error) => {
|
|
219
|
+
console.error('Failed to close MCP server after stdin ended', error);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
178
222
|
await this.server.connect(transport);
|
|
179
223
|
console.error('Smart DOM Reader MCP server running on stdio');
|
|
180
224
|
}
|
|
@@ -186,7 +230,7 @@ class SmartDomReaderServer {
|
|
|
186
230
|
title: 'Connect with Playwright',
|
|
187
231
|
description:
|
|
188
232
|
'Launch Chromium for subsequent tools. Use this first. Returns a brief status message. Run headless=false for visual debugging.',
|
|
189
|
-
inputSchema: {
|
|
233
|
+
inputSchema: z.object({
|
|
190
234
|
executablePath: z
|
|
191
235
|
.string()
|
|
192
236
|
.trim()
|
|
@@ -201,10 +245,9 @@ class SmartDomReaderServer {
|
|
|
201
245
|
.describe(
|
|
202
246
|
'Run without a visible window. Set false to watch interactions. Default: false.'
|
|
203
247
|
),
|
|
204
|
-
},
|
|
248
|
+
}),
|
|
205
249
|
},
|
|
206
|
-
|
|
207
|
-
async (args: ConnectBrowserArgs) => this.connectBrowser(args)
|
|
250
|
+
async (args) => this.queueBrowserOperation(() => this.connectBrowser(args))
|
|
208
251
|
);
|
|
209
252
|
|
|
210
253
|
this.server.registerTool(
|
|
@@ -213,15 +256,14 @@ class SmartDomReaderServer {
|
|
|
213
256
|
title: 'Navigate to URL',
|
|
214
257
|
description:
|
|
215
258
|
'Load an absolute URL in the active tab and wait for network idle. Use after browser_connect.',
|
|
216
|
-
inputSchema: {
|
|
259
|
+
inputSchema: z.object({
|
|
217
260
|
url: z
|
|
218
261
|
.string()
|
|
219
262
|
.url('url must be a valid absolute URL')
|
|
220
263
|
.describe('Absolute URL to navigate to (e.g., https://example.com).'),
|
|
221
|
-
},
|
|
264
|
+
}),
|
|
222
265
|
},
|
|
223
|
-
|
|
224
|
-
async (args: NavigateArgs) => this.navigate(args)
|
|
266
|
+
async (args) => this.navigate(args)
|
|
225
267
|
);
|
|
226
268
|
|
|
227
269
|
this.server.registerTool(
|
|
@@ -230,7 +272,7 @@ class SmartDomReaderServer {
|
|
|
230
272
|
title: 'Extract DOM Structure',
|
|
231
273
|
description:
|
|
232
274
|
'Start here. Returns an XML-wrapped Markdown outline (<outline>) describing page regions. Next: pick a selector/section and call dom_extract_region.',
|
|
233
|
-
inputSchema: {
|
|
275
|
+
inputSchema: z.object({
|
|
234
276
|
selector: z
|
|
235
277
|
.union([z.string().trim().min(1), z.literal('')])
|
|
236
278
|
.describe(
|
|
@@ -254,10 +296,9 @@ class SmartDomReaderServer {
|
|
|
254
296
|
.min(0)
|
|
255
297
|
.describe('Limit number of listed items per group (buttons, links, etc.).')
|
|
256
298
|
.optional(),
|
|
257
|
-
},
|
|
299
|
+
}),
|
|
258
300
|
},
|
|
259
|
-
|
|
260
|
-
async (args: OptionalSelectorArgs) => this.extractStructure(args)
|
|
301
|
+
async (args) => this.extractStructure(args)
|
|
261
302
|
);
|
|
262
303
|
|
|
263
304
|
this.server.registerTool(
|
|
@@ -266,7 +307,7 @@ class SmartDomReaderServer {
|
|
|
266
307
|
title: 'Extract DOM Region',
|
|
267
308
|
description:
|
|
268
309
|
'Use after the outline. Returns XML-wrapped Markdown (<section>) with actionable selectors for a specific region. Next: write a script or rerun with higher detail.',
|
|
269
|
-
inputSchema: {
|
|
310
|
+
inputSchema: z.object({
|
|
270
311
|
selector: z
|
|
271
312
|
.string()
|
|
272
313
|
.trim()
|
|
@@ -284,37 +325,12 @@ class SmartDomReaderServer {
|
|
|
284
325
|
.boolean()
|
|
285
326
|
.describe('Include elements that are hidden/offscreen. Default: false.')
|
|
286
327
|
.optional(),
|
|
287
|
-
|
|
288
|
-
.number()
|
|
289
|
-
.int()
|
|
290
|
-
.min(0)
|
|
291
|
-
.describe(
|
|
292
|
-
'Traversal depth for nested elements. Larger values inspect deeper trees.'
|
|
293
|
-
)
|
|
294
|
-
.optional(),
|
|
295
|
-
detail: z
|
|
296
|
-
.enum(['summary', 'region', 'deep'])
|
|
297
|
-
.default('region')
|
|
298
|
-
.describe("Output depth. 'region' is actionable; 'deep' expands more details.")
|
|
299
|
-
.optional(),
|
|
300
|
-
maxTextLength: z
|
|
301
|
-
.number()
|
|
302
|
-
.int()
|
|
303
|
-
.min(0)
|
|
304
|
-
.describe('Truncate text snippets to this many characters to save tokens.')
|
|
305
|
-
.optional(),
|
|
306
|
-
maxElements: z
|
|
307
|
-
.number()
|
|
308
|
-
.int()
|
|
309
|
-
.min(0)
|
|
310
|
-
.describe('Limit number of listed items per group (buttons, links, inputs).')
|
|
311
|
-
.optional(),
|
|
328
|
+
...COMMON_TOOL_OPTIONS_SHAPE,
|
|
312
329
|
})
|
|
313
330
|
.optional(),
|
|
314
|
-
},
|
|
331
|
+
}),
|
|
315
332
|
},
|
|
316
|
-
|
|
317
|
-
async (args: RegionArgs) => this.extractRegion(args)
|
|
333
|
+
async (args) => this.extractRegion(args)
|
|
318
334
|
);
|
|
319
335
|
|
|
320
336
|
this.server.registerTool(
|
|
@@ -323,7 +339,7 @@ class SmartDomReaderServer {
|
|
|
323
339
|
title: 'Extract DOM Content',
|
|
324
340
|
description:
|
|
325
341
|
'Readable text for a region as XML-wrapped Markdown (<content>). Use for comprehension; for selectors use dom_extract_region.',
|
|
326
|
-
inputSchema: {
|
|
342
|
+
inputSchema: z.object({
|
|
327
343
|
selector: z
|
|
328
344
|
.string()
|
|
329
345
|
.trim()
|
|
@@ -362,10 +378,9 @@ class SmartDomReaderServer {
|
|
|
362
378
|
.optional(),
|
|
363
379
|
})
|
|
364
380
|
.optional(),
|
|
365
|
-
},
|
|
381
|
+
}),
|
|
366
382
|
},
|
|
367
|
-
|
|
368
|
-
async (args: ContentArgs) => this.extractContent(args)
|
|
383
|
+
async (args) => this.extractContent(args)
|
|
369
384
|
);
|
|
370
385
|
|
|
371
386
|
this.server.registerTool(
|
|
@@ -374,7 +389,7 @@ class SmartDomReaderServer {
|
|
|
374
389
|
title: 'Extract Interactive Elements',
|
|
375
390
|
description:
|
|
376
391
|
'Quick list of controls within a scope as XML-wrapped Markdown (<section>). Alternative to region when you only need controls.',
|
|
377
|
-
inputSchema: {
|
|
392
|
+
inputSchema: z.object({
|
|
378
393
|
selector: z
|
|
379
394
|
.union([z.string().trim().min(1), z.literal('')])
|
|
380
395
|
.describe(
|
|
@@ -387,37 +402,12 @@ class SmartDomReaderServer {
|
|
|
387
402
|
.boolean()
|
|
388
403
|
.default(false)
|
|
389
404
|
.describe('Only include elements currently within the viewport. Default: false.'),
|
|
390
|
-
|
|
391
|
-
.number()
|
|
392
|
-
.int()
|
|
393
|
-
.min(0)
|
|
394
|
-
.describe(
|
|
395
|
-
'Traversal depth for nested elements. Larger values inspect deeper trees.'
|
|
396
|
-
)
|
|
397
|
-
.optional(),
|
|
398
|
-
detail: z
|
|
399
|
-
.enum(['summary', 'region', 'deep'])
|
|
400
|
-
.default('region')
|
|
401
|
-
.describe("Output depth. 'region' is actionable; 'deep' expands more details.")
|
|
402
|
-
.optional(),
|
|
403
|
-
maxTextLength: z
|
|
404
|
-
.number()
|
|
405
|
-
.int()
|
|
406
|
-
.min(0)
|
|
407
|
-
.describe('Truncate text snippets to this many characters to save tokens.')
|
|
408
|
-
.optional(),
|
|
409
|
-
maxElements: z
|
|
410
|
-
.number()
|
|
411
|
-
.int()
|
|
412
|
-
.min(0)
|
|
413
|
-
.describe('Limit number of listed items per group (buttons, links, inputs).')
|
|
414
|
-
.optional(),
|
|
405
|
+
...COMMON_TOOL_OPTIONS_SHAPE,
|
|
415
406
|
})
|
|
416
407
|
.optional(),
|
|
417
|
-
},
|
|
408
|
+
}),
|
|
418
409
|
},
|
|
419
|
-
|
|
420
|
-
async (args: InteractiveArgs) => this.extractInteractive(args)
|
|
410
|
+
async (args) => this.extractInteractive(args)
|
|
421
411
|
);
|
|
422
412
|
|
|
423
413
|
this.server.registerTool(
|
|
@@ -426,7 +416,7 @@ class SmartDomReaderServer {
|
|
|
426
416
|
title: 'Capture Screenshot',
|
|
427
417
|
description:
|
|
428
418
|
'Capture a PNG screenshot of the current page. Returns a short Markdown summary (path and fullPage flag).',
|
|
429
|
-
inputSchema: {
|
|
419
|
+
inputSchema: z.object({
|
|
430
420
|
path: z
|
|
431
421
|
.string()
|
|
432
422
|
.trim()
|
|
@@ -439,9 +429,9 @@ class SmartDomReaderServer {
|
|
|
439
429
|
.boolean()
|
|
440
430
|
.default(false)
|
|
441
431
|
.describe('Capture the full scrollable page. Default: false.'),
|
|
442
|
-
},
|
|
432
|
+
}),
|
|
443
433
|
},
|
|
444
|
-
async (args
|
|
434
|
+
async (args) => this.captureScreenshot(args)
|
|
445
435
|
);
|
|
446
436
|
|
|
447
437
|
this.server.registerTool(
|
|
@@ -450,12 +440,18 @@ class SmartDomReaderServer {
|
|
|
450
440
|
title: 'Close Browser',
|
|
451
441
|
description:
|
|
452
442
|
'Shut down the launched browser instance and release resources. Safe to call multiple times.',
|
|
453
|
-
inputSchema: {},
|
|
443
|
+
inputSchema: z.object({}),
|
|
454
444
|
},
|
|
455
|
-
async () => this.closeBrowser()
|
|
445
|
+
async () => this.queueBrowserOperation(() => this.closeBrowser())
|
|
456
446
|
);
|
|
457
447
|
}
|
|
458
448
|
|
|
449
|
+
private queueBrowserOperation(operation: () => Promise<CallToolResult>): Promise<CallToolResult> {
|
|
450
|
+
const result = this.browserOperation.then(operation);
|
|
451
|
+
this.browserOperation = result.catch(() => undefined);
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
|
|
459
455
|
private async connectBrowser(args: ConnectBrowserArgs): Promise<CallToolResult> {
|
|
460
456
|
if (this.browser?.isConnected()) {
|
|
461
457
|
return createTextResult('Browser already connected');
|
|
@@ -540,8 +536,8 @@ class SmartDomReaderServer {
|
|
|
540
536
|
selector: args.selector && args.selector.trim().length > 0 ? args.selector : null,
|
|
541
537
|
format: {
|
|
542
538
|
detail: args.detail ?? 'summary',
|
|
543
|
-
maxTextLength: args.maxTextLength,
|
|
544
|
-
maxElements: args.maxElements,
|
|
539
|
+
...(args.maxTextLength !== undefined && { maxTextLength: args.maxTextLength }),
|
|
540
|
+
...(args.maxElements !== undefined && { maxElements: args.maxElements }),
|
|
545
541
|
},
|
|
546
542
|
});
|
|
547
543
|
const duration = Date.now() - started;
|
|
@@ -562,8 +558,10 @@ class SmartDomReaderServer {
|
|
|
562
558
|
options: args.options ?? {},
|
|
563
559
|
format: {
|
|
564
560
|
detail: args.options?.detail ?? 'region',
|
|
565
|
-
|
|
566
|
-
|
|
561
|
+
...(args.options?.maxTextLength !== undefined && {
|
|
562
|
+
maxTextLength: args.options.maxTextLength,
|
|
563
|
+
}),
|
|
564
|
+
...(args.options?.maxElements !== undefined && { maxElements: args.options.maxElements }),
|
|
567
565
|
},
|
|
568
566
|
});
|
|
569
567
|
const duration = Date.now() - started;
|
|
@@ -584,8 +582,10 @@ class SmartDomReaderServer {
|
|
|
584
582
|
options: args.options ?? {},
|
|
585
583
|
format: {
|
|
586
584
|
detail: args.options?.detail ?? 'region',
|
|
587
|
-
|
|
588
|
-
|
|
585
|
+
...(args.options?.maxTextLength !== undefined && {
|
|
586
|
+
maxTextLength: args.options.maxTextLength,
|
|
587
|
+
}),
|
|
588
|
+
...(args.options?.maxElements !== undefined && { maxElements: args.options.maxElements }),
|
|
589
589
|
},
|
|
590
590
|
});
|
|
591
591
|
const duration = Date.now() - started;
|
|
@@ -606,8 +606,10 @@ class SmartDomReaderServer {
|
|
|
606
606
|
options: args.options ?? {},
|
|
607
607
|
format: {
|
|
608
608
|
detail: args.options?.detail ?? 'region',
|
|
609
|
-
|
|
610
|
-
|
|
609
|
+
...(args.options?.maxTextLength !== undefined && {
|
|
610
|
+
maxTextLength: args.options.maxTextLength,
|
|
611
|
+
}),
|
|
612
|
+
...(args.options?.maxElements !== undefined && { maxElements: args.options.maxElements }),
|
|
611
613
|
},
|
|
612
614
|
});
|
|
613
615
|
const duration = Date.now() - started;
|
|
@@ -655,12 +657,16 @@ class SmartDomReaderServer {
|
|
|
655
657
|
return createTextResult('Browser closed');
|
|
656
658
|
}
|
|
657
659
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
660
|
+
/**
|
|
661
|
+
* All formatting happens in-page via the bundled library; the server never
|
|
662
|
+
* imports it, so there are no server-side dynamic imports.
|
|
663
|
+
*/
|
|
664
|
+
private async runLibraryOperation<
|
|
665
|
+
TResult,
|
|
666
|
+
TArgs extends Record<string, unknown> & { format: FormatOptions },
|
|
667
|
+
>(operation: LibraryOperation, args: TArgs): Promise<TResult> {
|
|
662
668
|
const page = this.getActivePage();
|
|
663
|
-
const code = await this.
|
|
669
|
+
const code = await this.readLibraryFile(resolve(__dirname, EMBEDDED_LIBRARY_RELATIVE_PATH));
|
|
664
670
|
|
|
665
671
|
return page.evaluate<TResult, { code: string; operation: LibraryOperation; args: TArgs }>(
|
|
666
672
|
async ({ code, operation, args }) => {
|
|
@@ -672,6 +678,9 @@ class SmartDomReaderServer {
|
|
|
672
678
|
const { ProgressiveExtractor, MarkdownFormatter } = moduleExports;
|
|
673
679
|
const SmartDOMReader = moduleExports.SmartDOMReader ?? moduleExports.default;
|
|
674
680
|
|
|
681
|
+
const fmt = args.format;
|
|
682
|
+
const meta = { title: document.title, url: location.href };
|
|
683
|
+
|
|
675
684
|
switch (operation) {
|
|
676
685
|
case 'structure': {
|
|
677
686
|
if (!ProgressiveExtractor) {
|
|
@@ -679,14 +688,10 @@ class SmartDomReaderServer {
|
|
|
679
688
|
}
|
|
680
689
|
|
|
681
690
|
const { selector } = args as StructureOperationArgs;
|
|
682
|
-
const target = selector ?
|
|
691
|
+
const target = selector ? document.querySelector(selector) : document;
|
|
692
|
+
if (!target) throw new Error(`No matching element for selector ${selector}`);
|
|
683
693
|
const overview = ProgressiveExtractor.extractStructure(target);
|
|
684
694
|
if (!MarkdownFormatter) throw new Error('MarkdownFormatter export is unavailable.');
|
|
685
|
-
const argsWithFormat = args as Record<string, unknown> & {
|
|
686
|
-
format?: Record<string, unknown>;
|
|
687
|
-
};
|
|
688
|
-
const fmt = argsWithFormat.format ?? {};
|
|
689
|
-
const meta = { title: document.title, url: location.href };
|
|
690
695
|
return MarkdownFormatter.structure(overview, fmt, meta) as TResult;
|
|
691
696
|
}
|
|
692
697
|
|
|
@@ -696,20 +701,10 @@ class SmartDomReaderServer {
|
|
|
696
701
|
}
|
|
697
702
|
|
|
698
703
|
const { selector, options } = args as RegionOperationArgs;
|
|
699
|
-
const result = ProgressiveExtractor.extractRegion(
|
|
700
|
-
selector,
|
|
701
|
-
document,
|
|
702
|
-
options ?? {},
|
|
703
|
-
SmartDOMReader
|
|
704
|
-
);
|
|
704
|
+
const result = ProgressiveExtractor.extractRegion(selector, document, options ?? {});
|
|
705
705
|
if (!result)
|
|
706
706
|
return `No matching region for selector ${selector}` as unknown as TResult;
|
|
707
707
|
if (!MarkdownFormatter) throw new Error('MarkdownFormatter export is unavailable.');
|
|
708
|
-
const argsWithFormat = args as Record<string, unknown> & {
|
|
709
|
-
format?: Record<string, unknown>;
|
|
710
|
-
};
|
|
711
|
-
const fmt = argsWithFormat.format ?? {};
|
|
712
|
-
const meta = { title: document.title, url: location.href };
|
|
713
708
|
return MarkdownFormatter.region(result, fmt, meta) as TResult;
|
|
714
709
|
}
|
|
715
710
|
|
|
@@ -726,11 +721,6 @@ class SmartDomReaderServer {
|
|
|
726
721
|
);
|
|
727
722
|
if (!content) return `No content for selector ${selector}` as unknown as TResult;
|
|
728
723
|
if (!MarkdownFormatter) throw new Error('MarkdownFormatter export is unavailable.');
|
|
729
|
-
const argsWithFormat = args as Record<string, unknown> & {
|
|
730
|
-
format?: Record<string, unknown>;
|
|
731
|
-
};
|
|
732
|
-
const fmt = argsWithFormat.format ?? {};
|
|
733
|
-
const meta = { title: document.title, url: location.href };
|
|
734
724
|
return MarkdownFormatter.content(content, fmt, meta) as TResult;
|
|
735
725
|
}
|
|
736
726
|
|
|
@@ -740,27 +730,18 @@ class SmartDomReaderServer {
|
|
|
740
730
|
}
|
|
741
731
|
|
|
742
732
|
const { selector, options } = args as InteractiveOperationArgs;
|
|
743
|
-
const target = selector ?
|
|
733
|
+
const target = selector ? document.querySelector(selector) : document;
|
|
734
|
+
if (!target) throw new Error(`No matching element for selector ${selector}`);
|
|
744
735
|
|
|
745
736
|
if (typeof SmartDOMReader.extractInteractive === 'function') {
|
|
746
737
|
const result = SmartDOMReader.extractInteractive(target, options ?? {});
|
|
747
738
|
if (!MarkdownFormatter) throw new Error('MarkdownFormatter export is unavailable.');
|
|
748
|
-
const argsWithFormat = args as Record<string, unknown> & {
|
|
749
|
-
format?: Record<string, unknown>;
|
|
750
|
-
};
|
|
751
|
-
const fmt = argsWithFormat.format ?? {};
|
|
752
|
-
const meta = { title: document.title, url: location.href };
|
|
753
739
|
return MarkdownFormatter.region(result, fmt, meta) as TResult;
|
|
754
740
|
}
|
|
755
741
|
|
|
756
742
|
const reader = new SmartDOMReader({ ...options, mode: 'interactive' });
|
|
757
743
|
const result = reader.extract(target, options ?? {});
|
|
758
744
|
if (!MarkdownFormatter) throw new Error('MarkdownFormatter export is unavailable.');
|
|
759
|
-
const argsWithFormat = args as Record<string, unknown> & {
|
|
760
|
-
format?: Record<string, unknown>;
|
|
761
|
-
};
|
|
762
|
-
const fmt = argsWithFormat.format ?? {};
|
|
763
|
-
const meta = { title: document.title, url: location.href };
|
|
764
745
|
return MarkdownFormatter.region(result, fmt, meta) as TResult;
|
|
765
746
|
}
|
|
766
747
|
|
|
@@ -778,8 +759,8 @@ class SmartDomReaderServer {
|
|
|
778
759
|
|
|
779
760
|
private getActivePage(): Page {
|
|
780
761
|
if (!this.browser || !this.browser.isConnected() || !this.page) {
|
|
781
|
-
throw new
|
|
782
|
-
|
|
762
|
+
throw new ProtocolError(
|
|
763
|
+
ProtocolErrorCode.InvalidRequest,
|
|
783
764
|
'Browser not connected. Call browser_connect first.'
|
|
784
765
|
);
|
|
785
766
|
}
|
|
@@ -787,43 +768,51 @@ class SmartDomReaderServer {
|
|
|
787
768
|
return this.page;
|
|
788
769
|
}
|
|
789
770
|
|
|
790
|
-
// All formatting happens in-page via the bundled library; no server-side dynamic imports.
|
|
791
|
-
|
|
792
|
-
private async loadLibraryCode(): Promise<string> {
|
|
793
|
-
const embeddedPath = resolve(__dirname, EMBEDDED_LIBRARY_RELATIVE_PATH);
|
|
794
|
-
return this.readLibraryFile(embeddedPath);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
771
|
private async readLibraryFile(resolvedPath: string): Promise<string> {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
772
|
+
let file;
|
|
773
|
+
try {
|
|
774
|
+
file = await open(resolvedPath, 'r');
|
|
775
|
+
} catch (error) {
|
|
776
|
+
if (
|
|
777
|
+
typeof error === 'object' &&
|
|
778
|
+
error !== null &&
|
|
779
|
+
'code' in error &&
|
|
780
|
+
error.code === 'ENOENT'
|
|
781
|
+
) {
|
|
782
|
+
throw new ProtocolError(
|
|
783
|
+
ProtocolErrorCode.InvalidParams,
|
|
784
|
+
`Embedded library file not found at ${resolvedPath}. Ensure the bundled file exists.`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
throw error;
|
|
803
788
|
}
|
|
804
789
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
790
|
+
try {
|
|
791
|
+
// If cached, validate mtime to support hot updates without restart
|
|
792
|
+
const { mtimeMs } = await file.stat();
|
|
793
|
+
if (this.cachedLibrary?.path === resolvedPath && this.cachedLibrary.mtimeMs === mtimeMs) {
|
|
794
|
+
return this.cachedLibrary.code;
|
|
795
|
+
}
|
|
810
796
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
797
|
+
const code = await file.readFile('utf8');
|
|
798
|
+
this.cachedLibrary = { path: resolvedPath, code, mtimeMs };
|
|
799
|
+
return code;
|
|
800
|
+
} finally {
|
|
801
|
+
await file.close();
|
|
802
|
+
}
|
|
814
803
|
}
|
|
815
804
|
|
|
816
805
|
private handleToolError(error: unknown, fallbackMessage: string): never {
|
|
817
|
-
if (error instanceof
|
|
806
|
+
if (error instanceof ProtocolError) {
|
|
818
807
|
throw error;
|
|
819
808
|
}
|
|
820
809
|
|
|
821
810
|
if (error instanceof Error) {
|
|
822
811
|
console.error(fallbackMessage, error);
|
|
823
|
-
throw new
|
|
812
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, error.message);
|
|
824
813
|
}
|
|
825
814
|
|
|
826
|
-
throw new
|
|
815
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, fallbackMessage);
|
|
827
816
|
}
|
|
828
817
|
}
|
|
829
818
|
|