@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 +21 -0
- package/README.md +67 -24
- package/dist/browser/browser.d.ts +20 -0
- package/dist/browser/browser.js +8 -0
- package/dist/browser/chrome.d.ts +2 -0
- package/dist/{chrome.js → browser/chrome.js} +25 -62
- package/dist/browser/osascript.d.ts +7 -0
- package/dist/browser/osascript.js +42 -0
- package/dist/browser/safari.d.ts +2 -0
- package/dist/browser/safari.js +119 -0
- package/dist/cli.js +48 -21
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +8 -4
- package/dist/util.d.ts +4 -0
- package/dist/util.js +14 -0
- package/dist/view.d.ts +1 -1
- package/package.json +13 -11
- package/dist/chrome.d.ts +0 -16
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
|
-
|
|
4
|
-
> **macOS only** - This MCP server uses AppleScript and only works on macOS.
|
|
3
|
+
[](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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
> [!IMPORTANT]
|
|
17
|
+
> **macOS only** - This MCP server uses AppleScript and only works on macOS.
|
|
14
18
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
95
|
-
if (
|
|
96
|
-
throw new Error(
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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,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>
|
|
14
|
-
|
|
15
|
-
|
|
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>
|
|
18
|
-
|
|
19
|
-
|
|
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>
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
--
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
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 "./
|
|
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.
|
|
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
|
-
"
|
|
24
|
-
"test": "vitest",
|
|
25
|
-
"test:
|
|
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
|
-
"
|
|
30
|
-
"jsdom": "^
|
|
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
|
-
"@
|
|
37
|
+
"@playwright/test": "^1.54.1",
|
|
37
38
|
"@types/node": "^20.11.17",
|
|
38
|
-
"
|
|
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>;
|