@pokutuna/mcp-chrome-tabs 0.1.4 → 0.3.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 pokutuna
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,78 +1,121 @@
1
1
  # @pokutuna/mcp-chrome-tabs
2
2
 
3
- > [!NOTE]
4
- > **macOS only** - This MCP server uses AppleScript and only works on macOS.
3
+ [![npm version](https://badge.fury.io/js/@pokutuna%2Fmcp-chrome-tabs.svg)](https://badge.fury.io/js/@pokutuna%2Fmcp-chrome-tabs)
5
4
 
6
5
  Model Context Protocol (MCP) server that provides direct access to your browser's open tabs content. No additional fetching or authentication required - simply access what you're already viewing.
7
6
 
8
- ## Features
7
+ ## Key Features
8
+
9
+ - **Direct browser tab access** - No web scraping needed, reads content from already open tabs
10
+ - **Content optimized for AI** - Automatic content extraction and markdown conversion to reduce token usage
11
+ - **Active tab shortcut** - Instant access to currently focused tab without specifying IDs
12
+ - **MCP listChanged notifications** - Follows MCP protocol to notify tab changes (support is limited in most clients)
9
13
 
10
- - **Access browser tabs** - Direct access to content from tabs currently open in your browser
11
- - **Active tab shortcut** - Quick reference to currently active tab without specifying tab ID
12
- - **URL opening** - Open AI-provided URLs directly in new browser tabs
13
- - **Tools & Resources** - Dual interface supporting both MCP tools and resources with automatic refresh notifications when tab collection changes
14
+ ## Requirements
15
+
16
+ > [!IMPORTANT]
17
+ > **macOS only** - This MCP server uses AppleScript and only works on macOS.
14
18
 
15
- ## Installation
19
+ - **Node.js** 20 or newer
20
+ - **MCP Client** such as Claude Desktop, Claude Code, or any MCP-compatible client
21
+ - **macOS** only (uses AppleScript for browser automation)
16
22
 
17
- > [!IMPORTANT]
18
- > **Requirements**: Enable "Allow JavaScript from Apple Events" in Chrome
19
- > - (en) **View** > **Developer** > **Allow JavaScript from Apple Events**
20
- > - (ja) **表示** > **開発 / 管理** > **Apple Events からのJavaScript を許可**
23
+ ## Getting Started
21
24
 
22
- ### Manual Configuration
23
- Add to your MCP configuration file (e.g., `claude_desktop_config.json`):
25
+ First, enable "Allow JavaScript from Apple Events" in Chrome:
26
+
27
+ - (en) **View** > **Developer** > **Allow JavaScript from Apple Events**
28
+ - (ja) **表示** > **開発 / 管理** > **Apple Events からのJavaScript を許可**
29
+
30
+ Standard config works in most MCP clients (e.g., `.claude.json`, `.mcp.json`):
24
31
 
25
32
  ```json
26
33
  {
27
34
  "mcpServers": {
28
35
  "chrome-tabs": {
29
36
  "command": "npx",
30
- "args": ["-y", "@pokutuna/mcp-chrome-tabs"]
37
+ "args": ["-y", "@pokutuna/mcp-chrome-tabs@latest"]
31
38
  }
32
39
  }
33
40
  }
34
41
  ```
35
42
 
36
- ### For Claude Code
43
+ Or for Claude Code:
44
+
37
45
  ```bash
38
- claude mcp add -s user chrome-tabs -- npx -y @pokutuna/mcp-chrome-tabs
46
+ claude mcp add -s user chrome-tabs -- npx -y @pokutuna/mcp-chrome-tabs@latest
39
47
  ```
40
48
 
41
49
  ### Command Line Options
50
+
42
51
  The server accepts optional command line arguments for configuration:
43
52
 
44
53
  - `--application-name` - Application name to control (default: "Google Chrome")
45
54
  - `--exclude-hosts` - Comma-separated list of domains to exclude from tab listing and content access
46
55
  - `--check-interval` - Interval in milliseconds to check for tab changes and notify clients (default: 3000, set to 0 to disable)
47
56
 
57
+ #### Experimental Safari Support
58
+
59
+ Limited Safari support is available. Note that Safari lacks unique tab IDs, making it sensitive to tab order changes during execution:
60
+
61
+ ```bash
62
+ npx @pokutuna/mcp-chrome-tabs --application-name=Safari --experimental-browser=safari
63
+ ```
64
+
65
+ ## MCP Features
66
+
67
+ ### Tools
48
68
 
49
- ## Tools
69
+ <details>
70
+ <summary><code>list_tabs</code></summary>
50
71
 
51
- ### `list_tabs`
52
72
  List all open tabs in the user's browser with their titles, URLs, and tab references.
73
+
53
74
  - Returns: Markdown formatted list of tabs with tab IDs for reference
54
75
 
55
- ### `read_tab_content`
76
+ </details>
77
+
78
+ <details>
79
+ <summary><code>read_tab_content</code></summary>
80
+
56
81
  Get readable content from a tab in the user's browser.
82
+
57
83
  - `id` (optional): Tab reference from `list_tabs` output (e.g., `ID:12345:67890`)
58
84
  - If `id` is omitted, uses the currently active tab
59
85
  - Returns: Clean, readable content extracted using Mozilla Readability
60
86
 
61
- ### `open_in_new_tab`
87
+ </details>
88
+
89
+ <details>
90
+ <summary><code>open_in_new_tab</code></summary>
91
+
62
92
  Open a URL in a new tab to present content or enable user interaction with webpages.
93
+
63
94
  - `url` (required): URL to open in the browser
64
95
 
65
- ## Resources
96
+ </details>
97
+
98
+ ### Resources
99
+
100
+ <details>
101
+ <summary><code>tab://current</code></summary>
66
102
 
67
- ### `tab://current`
68
103
  Resource representing the content of the currently active tab.
104
+
69
105
  - **URI**: `tab://current`
70
106
  - **MIME type**: `text/markdown`
71
107
  - **Content**: Real-time content of the active browser tab
72
108
 
73
- ### `tab://{windowId}/{tabId}`
109
+ </details>
110
+
111
+ <details>
112
+ <summary><code>tab://{windowId}/{tabId}</code></summary>
113
+
74
114
  Resource template for accessing specific tabs.
115
+
75
116
  - **URI pattern**: `tab://{windowId}/{tabId}`
76
117
  - **MIME type**: `text/markdown`
77
118
  - **Content**: Content of the specified tab
78
119
  - Resources are dynamically generated based on currently open tabs
120
+
121
+ </details>
@@ -0,0 +1,20 @@
1
+ export type Browser = "chrome" | "safari";
2
+ export type TabRef = {
3
+ windowId: string;
4
+ tabId: string;
5
+ };
6
+ export type Tab = TabRef & {
7
+ title: string;
8
+ url: string;
9
+ };
10
+ export type TabContent = {
11
+ title: string;
12
+ url: string;
13
+ content: string;
14
+ };
15
+ export type BrowserInterface = {
16
+ getTabList(applicationName: string): Promise<Tab[]>;
17
+ getPageContent(applicationName: string, tab?: TabRef | null): Promise<TabContent>;
18
+ openURL(applicationName: string, url: string): Promise<void>;
19
+ };
20
+ export declare function getInterface(browser: Browser): BrowserInterface;
@@ -0,0 +1,8 @@
1
+ import { chromeBrowser } from "./chrome.js";
2
+ import { safariBrowser } from "./safari.js";
3
+ export function getInterface(browser) {
4
+ if (browser === "safari") {
5
+ return safariBrowser;
6
+ }
7
+ return chromeBrowser;
8
+ }
@@ -0,0 +1,2 @@
1
+ import type { BrowserInterface } from "./browser.js";
2
+ export declare const chromeBrowser: BrowserInterface;
@@ -1,18 +1,7 @@
1
- import { execFile } from "child_process";
2
- import { JSDOM } from "jsdom";
3
- import { Readability } from "@mozilla/readability";
4
- import { promisify } from "util";
5
- import TurndownService from "turndown";
6
- import turndownPluginGfm from "turndown-plugin-gfm";
7
- const execFileAsync = promisify(execFile);
8
- function escapeAppleScript(str) {
9
- return str
10
- .replace(/\\/g, "\\\\")
11
- .replace(/"/g, '\\"')
12
- .replace(/\n/g, "\\n")
13
- .replace(/\r/g, "\\r");
14
- }
15
- export async function getChromeTabList(applicationName) {
1
+ import { Defuddle } from "defuddle/node";
2
+ import { withMockConsole } from "../util.js";
3
+ import { escapeAppleScript, executeAppleScript, separator, } from "./osascript.js";
4
+ async function getChromeTabList(applicationName) {
16
5
  const sep = separator();
17
6
  const appleScript = `
18
7
  tell application "${applicationName}"
@@ -45,7 +34,7 @@ export async function getChromeTabList(applicationName) {
45
34
  }
46
35
  return tabs;
47
36
  }
48
- export async function getPageContent(applicationName, tab) {
37
+ async function getPageContent(applicationName, tab) {
49
38
  const sep = separator();
50
39
  const inner = `
51
40
  set tabTitle to title
@@ -91,30 +80,30 @@ export async function getPageContent(applicationName, tab) {
91
80
  return "ERROR" & "${sep}" & errMsg
92
81
  end try
93
82
  `;
94
- const result = await executeAppleScript(appleScript);
95
- if (result.startsWith(`ERROR${sep}`))
96
- throw new Error(result.split(sep)[1]);
97
- const parts = result.split(sep).map((part) => part.trim());
98
- if (parts.length < 3)
83
+ const scriptResult = await executeAppleScript(appleScript);
84
+ if (scriptResult.startsWith(`ERROR${sep}`)) {
85
+ throw new Error(scriptResult.split(sep)[1]);
86
+ }
87
+ const parts = scriptResult.split(sep).map((part) => part.trim());
88
+ if (parts.length < 3) {
99
89
  throw new Error("Failed to read the tab content");
90
+ }
100
91
  const [title, url, content] = parts;
101
- const dom = new JSDOM(content, { url });
102
- const reader = new Readability(dom.window.document, {
103
- charThreshold: 10,
104
- });
105
- const article = reader.parse();
106
- if (!article?.content)
92
+ // Suppress defuddle's console.log output during parsing
93
+ const { result: defuddleResult } = await withMockConsole(() => Defuddle(content, url, {
94
+ markdown: true,
95
+ }));
96
+ if (!defuddleResult?.content) {
107
97
  throw new Error("Failed to parse the page content");
108
- const turndownService = new TurndownService();
109
- turndownService.use(turndownPluginGfm.gfm);
110
- const md = turndownService.turndown(article.content);
98
+ }
99
+ const md = defuddleResult.content;
111
100
  return {
112
101
  title,
113
102
  url,
114
103
  content: md,
115
104
  };
116
105
  }
117
- export async function openURL(applicationName, url) {
106
+ async function openURL(applicationName, url) {
118
107
  const escapedUrl = escapeAppleScript(url);
119
108
  const appleScript = `
120
109
  tell application "${applicationName}"
@@ -123,34 +112,8 @@ export async function openURL(applicationName, url) {
123
112
  `;
124
113
  await executeAppleScript(appleScript);
125
114
  }
126
- async function retry(fn, options) {
127
- const { maxRetries = 2, retryDelay = 1000 } = options || {};
128
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
129
- try {
130
- return await fn();
131
- }
132
- catch (error) {
133
- if (attempt === maxRetries) {
134
- console.error("retry failed after maximum attempts:", error);
135
- throw error;
136
- }
137
- await new Promise((resolve) => setTimeout(resolve, retryDelay * Math.pow(2, attempt)));
138
- }
139
- }
140
- throw new Error("unreachable");
141
- }
142
- async function executeAppleScript(script) {
143
- return retry(async () => {
144
- const { stdout, stderr } = await execFileAsync("osascript", ["-e", script], {
145
- timeout: 5 * 1000,
146
- maxBuffer: 5 * 1024 * 1024, // 5MB
147
- });
148
- if (stderr)
149
- console.error("AppleScript stderr:", stderr);
150
- return stdout.trim();
151
- });
152
- }
153
- function separator() {
154
- const uniqueId = Math.random().toString(36).substring(2);
155
- return `<|SEP:${uniqueId}|>`;
156
- }
115
+ export const chromeBrowser = {
116
+ getTabList: getChromeTabList,
117
+ getPageContent,
118
+ openURL,
119
+ };
@@ -0,0 +1,7 @@
1
+ export declare function escapeAppleScript(str: string): string;
2
+ export declare function retry<T>(fn: () => Promise<T>, options?: {
3
+ maxRetries?: number;
4
+ retryDelay?: number;
5
+ }): Promise<T>;
6
+ export declare function executeAppleScript(script: string): Promise<string>;
7
+ export declare function separator(): string;
@@ -0,0 +1,42 @@
1
+ import { execFile } from "child_process";
2
+ import { promisify } from "util";
3
+ const execFileAsync = promisify(execFile);
4
+ export function escapeAppleScript(str) {
5
+ // https://discussions.apple.com/thread/4247426?sortBy=rank
6
+ return str
7
+ .replace(/\\/g, "\\\\")
8
+ .replace(/"/g, '\\"')
9
+ .replace(/\n/g, "\\n")
10
+ .replace(/\r/g, "\\r");
11
+ }
12
+ export async function retry(fn, options) {
13
+ const { maxRetries = 1, retryDelay = 1000 } = options || {};
14
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
15
+ try {
16
+ return await fn();
17
+ }
18
+ catch (error) {
19
+ if (attempt === maxRetries) {
20
+ console.error("retry failed after maximum attempts:", error);
21
+ throw error;
22
+ }
23
+ await new Promise((resolve) => setTimeout(resolve, retryDelay * Math.pow(2, attempt)));
24
+ }
25
+ }
26
+ throw new Error("unreachable");
27
+ }
28
+ export async function executeAppleScript(script) {
29
+ return retry(async () => {
30
+ const { stdout, stderr } = await execFileAsync("osascript", ["-e", script], {
31
+ timeout: 5 * 1000,
32
+ maxBuffer: 5 * 1024 * 1024, // 5MB
33
+ });
34
+ if (stderr)
35
+ console.error("AppleScript stderr:", stderr);
36
+ return stdout.trim();
37
+ });
38
+ }
39
+ export function separator() {
40
+ const uniqueId = Math.random().toString(36).substring(2);
41
+ return `<|SEP:${uniqueId}|>`;
42
+ }
@@ -0,0 +1,2 @@
1
+ import type { BrowserInterface } from "./browser.js";
2
+ export declare const safariBrowser: BrowserInterface;
@@ -0,0 +1,119 @@
1
+ import { Defuddle } from "defuddle/node";
2
+ import { withMockConsole } from "../util.js";
3
+ import { escapeAppleScript, executeAppleScript, separator, } from "./osascript.js";
4
+ async function getSafariTabList(applicationName) {
5
+ const sep = separator();
6
+ const appleScript = `
7
+ tell application "${applicationName}"
8
+ set output to ""
9
+ repeat with aWindow in (every window)
10
+ set windowId to id of aWindow
11
+ repeat with aTab in (every tab of aWindow)
12
+ set tabIndex to index of aTab
13
+ set tabTitle to name of aTab
14
+ set tabURL to URL of aTab
15
+ set output to output & windowId & "${sep}" & tabIndex & "${sep}" & tabTitle & "${sep}" & tabURL & "\\n"
16
+ end repeat
17
+ end repeat
18
+ return output
19
+ end tell
20
+ `;
21
+ const result = await executeAppleScript(appleScript);
22
+ const lines = result.trim().split("\n");
23
+ const tabs = [];
24
+ for (const line of lines) {
25
+ const [wId, tId, title, url] = line.split(sep);
26
+ if (!/^https?:\/\//.test(url))
27
+ continue;
28
+ // Note: Safari tab IDs are volatile indices that change when tabs are closed
29
+ // Unlike Chrome, Safari doesn't provide stable unique tab identifiers
30
+ tabs.push({
31
+ windowId: wId,
32
+ tabId: tId,
33
+ title: title.trim(),
34
+ url: url.trim(),
35
+ });
36
+ }
37
+ return tabs;
38
+ }
39
+ async function getPageContent(applicationName, tab) {
40
+ const sep = separator();
41
+ const inner = `
42
+ set tabTitle to name
43
+ set tabURL to URL
44
+ set tabContent to do JavaScript "document.documentElement.outerHTML"
45
+ return tabTitle & "${sep}" & tabURL & "${sep}" & tabContent
46
+ `;
47
+ const appleScript = tab
48
+ ? `
49
+ try
50
+ tell application "${applicationName}"
51
+ tell window id "${tab.windowId}"
52
+ tell tab ${tab.tabId}
53
+ with timeout of 3 seconds
54
+ ${inner}
55
+ end timeout
56
+ end tell
57
+ end tell
58
+ end tell
59
+ on error errMsg
60
+ return "ERROR" & "${sep}" & errMsg
61
+ end try
62
+ `
63
+ : `
64
+ try
65
+ tell application "${applicationName}"
66
+ tell front window
67
+ set t to current tab
68
+ if URL of t is not "about:blank" then
69
+ tell t
70
+ with timeout of 3 seconds
71
+ ${inner}
72
+ end timeout
73
+ end tell
74
+ else
75
+ error "No active tab found"
76
+ end if
77
+ end tell
78
+ end tell
79
+ on error errMsg
80
+ return "ERROR" & "${sep}" & errMsg
81
+ end try
82
+ `;
83
+ const scriptResult = await executeAppleScript(appleScript);
84
+ if (scriptResult.startsWith(`ERROR${sep}`)) {
85
+ throw new Error(scriptResult.split(sep)[1]);
86
+ }
87
+ const parts = scriptResult.split(sep).map((part) => part.trim());
88
+ if (parts.length < 3) {
89
+ throw new Error("Failed to read the tab content");
90
+ }
91
+ const [title, url, content] = parts;
92
+ // Suppress defuddle's console.log output during parsing
93
+ const { result: defuddleResult } = await withMockConsole(() => Defuddle(content, url, {
94
+ markdown: true,
95
+ }));
96
+ if (!defuddleResult?.content) {
97
+ throw new Error("Failed to parse the page content");
98
+ }
99
+ const md = defuddleResult.content;
100
+ return {
101
+ title,
102
+ url,
103
+ content: md,
104
+ };
105
+ }
106
+ async function openURL(applicationName, url) {
107
+ const escapedUrl = escapeAppleScript(url);
108
+ const appleScript = `
109
+ tell application "${applicationName}"
110
+ open location "${escapedUrl}"
111
+ end tell
112
+ `;
113
+ await executeAppleScript(appleScript);
114
+ }
115
+ export const safariBrowser = {
116
+ getTabList: getSafariTabList,
117
+ getPageContent,
118
+ openURL,
119
+ };
package/dist/cli.js CHANGED
@@ -10,26 +10,31 @@ USAGE:
10
10
  mcp-chrome-tabs [OPTIONS]
11
11
 
12
12
  OPTIONS:
13
- --application-name=<name> Application name to control via AppleScript
14
- (default: "Google Chrome")
15
- Example: "Google Chrome Canary"
13
+ --application-name=<name> Application name to control via AppleScript
14
+ (default: "Google Chrome")
15
+ Example: "Google Chrome Canary"
16
16
 
17
- --exclude-hosts=<hosts> Comma-separated list of hosts to exclude
18
- (default: "")
19
- Example: "github.com,example.com,test.com"
17
+ --exclude-hosts=<hosts> Comma-separated list of hosts to exclude
18
+ (default: "")
19
+ Example: "github.com,example.com,test.com"
20
20
 
21
- --check-interval=<ms> Interval for checking browser tabs in milliseconds
22
- (default: 3000, set to 0 to disable)
23
- Example: 1000
21
+ --check-interval=<ms> Interval for checking browser tabs in milliseconds
22
+ (default: 3000, set to 0 to disable)
23
+ Example: 1000
24
24
 
25
- --help Show this help message
25
+ --experimental-browser=<b> Browser implementation to use
26
+ (default: "chrome")
27
+ Options: "chrome", "safari"
28
+
29
+ --help Show this help message
26
30
 
27
31
 
28
32
  REQUIREMENTS:
29
- Chrome must allow JavaScript from Apple Events:
30
- 1. Open Chrome
31
- 2. Go to View > Developer > Allow JavaScript from Apple Events
32
- 3. Enable the option
33
+ Chrome:
34
+ Chrome must allow JavaScript from Apple Events:
35
+ 1. Open Chrome
36
+ 2. Go to View > Developer > Allow JavaScript from Apple Events
37
+ 3. Enable the option
33
38
 
34
39
  MCP CONFIGURATION EXAMPLE:
35
40
  {
@@ -58,6 +63,10 @@ function parseCliArgs(args) {
58
63
  type: "string",
59
64
  default: "3000",
60
65
  },
66
+ "experimental-browser": {
67
+ type: "string",
68
+ default: "",
69
+ },
61
70
  help: {
62
71
  type: "boolean",
63
72
  default: false,
@@ -66,8 +75,16 @@ function parseCliArgs(args) {
66
75
  allowPositionals: false,
67
76
  tokens: true,
68
77
  });
78
+ function parseBrowserOption(browser) {
79
+ if (browser === "" || browser === "chrome")
80
+ return "chrome";
81
+ if (browser === "safari")
82
+ return "safari";
83
+ throw new Error(`Invalid --experimental-browser option: "${browser}". Use "chrome" or "safari".`);
84
+ }
69
85
  const parsed = {
70
86
  applicationName: values["application-name"],
87
+ browser: parseBrowserOption(values["experimental-browser"]),
71
88
  excludeHosts: values["exclude-hosts"]
72
89
  .split(",")
73
90
  .map((d) => d.trim())
@@ -77,11 +94,21 @@ function parseCliArgs(args) {
77
94
  };
78
95
  return parsed;
79
96
  }
80
- const options = parseCliArgs(process.argv.slice(2));
81
- if (options.help) {
82
- showHelp();
83
- process.exit(0);
97
+ async function main() {
98
+ const options = parseCliArgs(process.argv.slice(2));
99
+ if (options.help) {
100
+ showHelp();
101
+ process.exit(0);
102
+ }
103
+ const server = await createMcpServer(options);
104
+ const transport = new StdioServerTransport();
105
+ await server.connect(transport);
106
+ const shutdown = async () => {
107
+ await transport.close();
108
+ await server.close();
109
+ process.exit(0);
110
+ };
111
+ process.on("SIGINT", shutdown);
112
+ process.on("SIGTERM", shutdown);
84
113
  }
85
- const server = await createMcpServer(options);
86
- const transport = new StdioServerTransport();
87
- await server.connect(transport);
114
+ await main().catch(console.error);
package/dist/mcp.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { Browser } from "./browser/browser.js";
2
3
  export type McpServerOptions = {
3
4
  applicationName: string;
4
5
  excludeHosts: string[];
5
6
  checkInterval: number;
7
+ browser: Browser;
6
8
  };
7
9
  export declare function createMcpServer(options: McpServerOptions): Promise<McpServer>;
package/dist/mcp.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import * as chrome from "./chrome.js";
3
+ import { getInterface, } from "./browser/browser.js";
4
4
  import { readFile } from "fs/promises";
5
5
  import { dirname, join } from "path";
6
6
  import { fileURLToPath } from "url";
@@ -11,11 +11,13 @@ function isExcludedHost(url, excludeHosts) {
11
11
  return excludeHosts.some((d) => u.hostname === d || u.hostname.endsWith("." + d));
12
12
  }
13
13
  async function listTabs(opts) {
14
- const tabs = await chrome.getChromeTabList(opts.applicationName);
14
+ const browser = getInterface(opts.browser);
15
+ const tabs = await browser.getTabList(opts.applicationName);
15
16
  return tabs.filter((t) => !isExcludedHost(t.url, opts.excludeHosts));
16
17
  }
17
18
  async function getTab(tabRef, opts) {
18
- const content = await chrome.getPageContent(opts.applicationName, tabRef);
19
+ const browser = getInterface(opts.browser);
20
+ const content = await browser.getPageContent(opts.applicationName, tabRef);
19
21
  if (isExcludedHost(content.url, opts.excludeHosts)) {
20
22
  throw new Error("Content not available for excluded host");
21
23
  }
@@ -44,6 +46,7 @@ export async function createMcpServer(options) {
44
46
  name: "chrome-tabs",
45
47
  version: await packageVersion(),
46
48
  }, {
49
+ instructions: "Use this server to access the user's open browser tabs.",
47
50
  capabilities: {
48
51
  resources: {
49
52
  listChanged: true,
@@ -92,7 +95,8 @@ export async function createMcpServer(options) {
92
95
  },
93
96
  }, async (args) => {
94
97
  const { url } = args;
95
- await chrome.openURL(options.applicationName, url);
98
+ const browser = getInterface(options.browser);
99
+ await browser.openURL(options.applicationName, url);
96
100
  return {
97
101
  content: [
98
102
  {
package/dist/util.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare function withMockConsole<T>(fn: () => Promise<T>): Promise<{
2
+ result: T;
3
+ logs: unknown[][];
4
+ }>;
package/dist/util.js ADDED
@@ -0,0 +1,14 @@
1
+ export async function withMockConsole(fn) {
2
+ const originalConsoleLog = console.log;
3
+ const logs = [];
4
+ console.log = (...args) => {
5
+ logs.push(args);
6
+ };
7
+ try {
8
+ const result = await fn();
9
+ return { result, logs };
10
+ }
11
+ finally {
12
+ console.log = originalConsoleLog;
13
+ }
14
+ }
package/dist/view.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Tab, TabRef, TabContent } from "./chrome.js";
1
+ import type { Tab, TabRef, TabContent } from "./browser/browser.js";
2
2
  export declare function formatTabRef(tab: Tab): string;
3
3
  export declare function parseTabRef(tabRef: string): TabRef | null;
4
4
  export declare function formatList(tabs: Tab[]): string;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@pokutuna/mcp-chrome-tabs",
3
- "version": "0.1.4",
4
- "license": "MIT",
3
+ "version": "0.3.0",
5
4
  "repository": {
6
5
  "type": "git",
7
6
  "url": "https://github.com/pokutuna/mcp-chrome-tabs"
8
7
  },
8
+ "license": "MIT",
9
9
  "type": "module",
10
10
  "bin": {
11
11
  "mcp-chrome-tabs": "./dist/cli.js"
@@ -18,24 +18,26 @@
18
18
  "scripts": {
19
19
  "build": "tsc",
20
20
  "dev": "node --import tsx src/cli.ts",
21
+ "inspector": "npx @modelcontextprotocol/inspector",
22
+ "lint": "prettier --check .",
23
+ "lint:fix": "prettier --write .",
21
24
  "prepublishOnly": "npm run build",
22
25
  "start": "node dist/index.js",
23
- "inspector": "npx @modelcontextprotocol/inspector",
24
- "test": "vitest",
25
- "test:run": "vitest run"
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "test:e2e": "playwright test"
26
29
  },
27
30
  "dependencies": {
28
31
  "@modelcontextprotocol/sdk": "^1.16.0",
29
- "@mozilla/readability": "^0.6.0",
30
- "jsdom": "^26.1.0",
31
- "turndown": "^7.2.0",
32
- "turndown-plugin-gfm": "^1.0.2",
32
+ "defuddle": "^0.6.4",
33
+ "jsdom": "^24.0.0",
33
34
  "zod": "^3.25.76"
34
35
  },
35
36
  "devDependencies": {
36
- "@types/jsdom": "^21.1.7",
37
+ "@playwright/test": "^1.54.1",
37
38
  "@types/node": "^20.11.17",
38
- "@types/turndown": "^5.0.5",
39
+ "playwright": "^1.54.1",
40
+ "prettier": "^3.6.2",
39
41
  "tsx": "^4.7.1",
40
42
  "typescript": "~5.8.3",
41
43
  "vitest": "^3.2.4"
package/dist/chrome.d.ts DELETED
@@ -1,16 +0,0 @@
1
- export type TabRef = {
2
- windowId: string;
3
- tabId: string;
4
- };
5
- export type Tab = TabRef & {
6
- title: string;
7
- url: string;
8
- };
9
- export type TabContent = {
10
- title: string;
11
- url: string;
12
- content: string;
13
- };
14
- export declare function getChromeTabList(applicationName: string): Promise<Tab[]>;
15
- export declare function getPageContent(applicationName: string, tab?: TabRef | null): Promise<TabContent>;
16
- export declare function openURL(applicationName: string, url: string): Promise<void>;