@mcp-b/smart-dom-reader-server 0.2.0 → 5.0.3

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/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, readFile, stat } from 'node:fs/promises';
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 { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
- import { type CallToolResult, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
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)']!, 'Google/Chrome/Application/chrome.exe')]
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: '0.2.0',
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
- // @ts-expect-error - Complex Zod schema causes TypeScript recursion depth error (TS2589)
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
- // @ts-expect-error - Complex Zod schema causes TypeScript recursion depth error (TS2589)
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
- // @ts-expect-error - Complex Zod schema causes TypeScript recursion depth error (TS2589)
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
- maxDepth: z
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
- // @ts-expect-error - Complex Zod schema causes TypeScript recursion depth error (TS2589)
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
- // @ts-expect-error - Complex Zod schema causes TypeScript recursion depth error (TS2589)
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
- maxDepth: z
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
- // @ts-expect-error - Complex Zod schema causes TypeScript recursion depth error (TS2589)
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: ScreenshotArgs) => this.captureScreenshot(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
- maxTextLength: args.options?.maxTextLength,
566
- maxElements: args.options?.maxElements,
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
- maxTextLength: args.options?.maxTextLength,
588
- maxElements: args.options?.maxElements,
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
- maxTextLength: args.options?.maxTextLength,
610
- maxElements: args.options?.maxElements,
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
- private async runLibraryOperation<TResult, TArgs extends Record<string, unknown>>(
659
- operation: LibraryOperation,
660
- args: TArgs
661
- ): Promise<TResult> {
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.loadLibraryCode();
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 ? (document.querySelector(selector) ?? document) : document;
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 ? (document.querySelector(selector) ?? document) : document;
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 McpError(
782
- ErrorCode.InvalidRequest,
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
- if (!(await pathExists(resolvedPath))) {
799
- throw new McpError(
800
- ErrorCode.InvalidParams,
801
- `Embedded library file not found at ${resolvedPath}. Ensure the bundled file exists.`
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
- // If cached, validate mtime to support hot updates without restart
806
- const { mtimeMs } = await stat(resolvedPath);
807
- if (this.cachedLibrary?.path === resolvedPath && this.cachedLibrary.mtimeMs === mtimeMs) {
808
- return this.cachedLibrary.code;
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
- const code = await readFile(resolvedPath, 'utf8');
812
- this.cachedLibrary = { path: resolvedPath, code, mtimeMs };
813
- return code;
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 McpError) {
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 McpError(ErrorCode.InternalError, error.message);
812
+ throw new ProtocolError(ProtocolErrorCode.InternalError, error.message);
824
813
  }
825
814
 
826
- throw new McpError(ErrorCode.InternalError, fallbackMessage);
815
+ throw new ProtocolError(ProtocolErrorCode.InternalError, fallbackMessage);
827
816
  }
828
817
  }
829
818