@pokutuna/mcp-chrome-tabs 0.1.4 → 0.2.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/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} +9 -44
- package/dist/browser/osascript.d.ts +7 -0
- package/dist/browser/osascript.js +41 -0
- package/dist/browser/safari.d.ts +2 -0
- package/dist/browser/safari.js +121 -0
- package/dist/cli.js +31 -14
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +8 -4
- package/dist/view.d.ts +1 -1
- package/package.json +6 -3
- package/dist/chrome.d.ts +0 -16
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 readability processing 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,9 @@
|
|
|
1
|
-
import { execFile } from "child_process";
|
|
2
1
|
import { JSDOM } from "jsdom";
|
|
3
2
|
import { Readability } from "@mozilla/readability";
|
|
4
|
-
import { promisify } from "util";
|
|
5
3
|
import TurndownService from "turndown";
|
|
6
4
|
import turndownPluginGfm from "turndown-plugin-gfm";
|
|
7
|
-
|
|
8
|
-
function
|
|
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) {
|
|
5
|
+
import { escapeAppleScript, executeAppleScript, separator, } from "./osascript.js";
|
|
6
|
+
async function getChromeTabList(applicationName) {
|
|
16
7
|
const sep = separator();
|
|
17
8
|
const appleScript = `
|
|
18
9
|
tell application "${applicationName}"
|
|
@@ -45,7 +36,7 @@ export async function getChromeTabList(applicationName) {
|
|
|
45
36
|
}
|
|
46
37
|
return tabs;
|
|
47
38
|
}
|
|
48
|
-
|
|
39
|
+
async function getPageContent(applicationName, tab) {
|
|
49
40
|
const sep = separator();
|
|
50
41
|
const inner = `
|
|
51
42
|
set tabTitle to title
|
|
@@ -114,7 +105,7 @@ export async function getPageContent(applicationName, tab) {
|
|
|
114
105
|
content: md,
|
|
115
106
|
};
|
|
116
107
|
}
|
|
117
|
-
|
|
108
|
+
async function openURL(applicationName, url) {
|
|
118
109
|
const escapedUrl = escapeAppleScript(url);
|
|
119
110
|
const appleScript = `
|
|
120
111
|
tell application "${applicationName}"
|
|
@@ -123,34 +114,8 @@ export async function openURL(applicationName, url) {
|
|
|
123
114
|
`;
|
|
124
115
|
await executeAppleScript(appleScript);
|
|
125
116
|
}
|
|
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
|
-
}
|
|
117
|
+
export const chromeBrowser = {
|
|
118
|
+
getTabList: getChromeTabList,
|
|
119
|
+
getPageContent,
|
|
120
|
+
openURL,
|
|
121
|
+
};
|
|
@@ -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,41 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export function escapeAppleScript(str) {
|
|
5
|
+
return str
|
|
6
|
+
.replace(/\\/g, "\\\\")
|
|
7
|
+
.replace(/"/g, '\\"')
|
|
8
|
+
.replace(/\n/g, "\\n")
|
|
9
|
+
.replace(/\r/g, "\\r");
|
|
10
|
+
}
|
|
11
|
+
export async function retry(fn, options) {
|
|
12
|
+
const { maxRetries = 2, retryDelay = 1000 } = options || {};
|
|
13
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
14
|
+
try {
|
|
15
|
+
return await fn();
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (attempt === maxRetries) {
|
|
19
|
+
console.error("retry failed after maximum attempts:", error);
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay * Math.pow(2, attempt)));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
throw new Error("unreachable");
|
|
26
|
+
}
|
|
27
|
+
export async function executeAppleScript(script) {
|
|
28
|
+
return retry(async () => {
|
|
29
|
+
const { stdout, stderr } = await execFileAsync("osascript", ["-e", script], {
|
|
30
|
+
timeout: 5 * 1000,
|
|
31
|
+
maxBuffer: 5 * 1024 * 1024, // 5MB
|
|
32
|
+
});
|
|
33
|
+
if (stderr)
|
|
34
|
+
console.error("AppleScript stderr:", stderr);
|
|
35
|
+
return stdout.trim();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function separator() {
|
|
39
|
+
const uniqueId = Math.random().toString(36).substring(2);
|
|
40
|
+
return `<|SEP:${uniqueId}|>`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { JSDOM } from "jsdom";
|
|
2
|
+
import { Readability } from "@mozilla/readability";
|
|
3
|
+
import TurndownService from "turndown";
|
|
4
|
+
import turndownPluginGfm from "turndown-plugin-gfm";
|
|
5
|
+
import { escapeAppleScript, executeAppleScript, separator, } from "./osascript.js";
|
|
6
|
+
async function getSafariTabList(applicationName) {
|
|
7
|
+
const sep = separator();
|
|
8
|
+
const appleScript = `
|
|
9
|
+
tell application "${applicationName}"
|
|
10
|
+
set output to ""
|
|
11
|
+
repeat with aWindow in (every window)
|
|
12
|
+
set windowId to id of aWindow
|
|
13
|
+
repeat with aTab in (every tab of aWindow)
|
|
14
|
+
set tabIndex to index of aTab
|
|
15
|
+
set tabTitle to name of aTab
|
|
16
|
+
set tabURL to URL of aTab
|
|
17
|
+
set output to output & windowId & "${sep}" & tabIndex & "${sep}" & tabTitle & "${sep}" & tabURL & "\\n"
|
|
18
|
+
end repeat
|
|
19
|
+
end repeat
|
|
20
|
+
return output
|
|
21
|
+
end tell
|
|
22
|
+
`;
|
|
23
|
+
const result = await executeAppleScript(appleScript);
|
|
24
|
+
const lines = result.trim().split("\n");
|
|
25
|
+
const tabs = [];
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const [wId, tId, title, url] = line.split(sep);
|
|
28
|
+
if (!/^https?:\/\//.test(url))
|
|
29
|
+
continue;
|
|
30
|
+
// Note: Safari tab IDs are volatile indices that change when tabs are closed
|
|
31
|
+
// Unlike Chrome, Safari doesn't provide stable unique tab identifiers
|
|
32
|
+
tabs.push({
|
|
33
|
+
windowId: wId,
|
|
34
|
+
tabId: tId,
|
|
35
|
+
title: title.trim(),
|
|
36
|
+
url: url.trim(),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return tabs;
|
|
40
|
+
}
|
|
41
|
+
async function getPageContent(applicationName, tab) {
|
|
42
|
+
const sep = separator();
|
|
43
|
+
const inner = `
|
|
44
|
+
set tabTitle to name
|
|
45
|
+
set tabURL to URL
|
|
46
|
+
set tabContent to do JavaScript "document.documentElement.outerHTML"
|
|
47
|
+
return tabTitle & "${sep}" & tabURL & "${sep}" & tabContent
|
|
48
|
+
`;
|
|
49
|
+
const appleScript = tab
|
|
50
|
+
? `
|
|
51
|
+
try
|
|
52
|
+
tell application "${applicationName}"
|
|
53
|
+
tell window id "${tab.windowId}"
|
|
54
|
+
tell tab ${tab.tabId}
|
|
55
|
+
with timeout of 3 seconds
|
|
56
|
+
${inner}
|
|
57
|
+
end timeout
|
|
58
|
+
end tell
|
|
59
|
+
end tell
|
|
60
|
+
end tell
|
|
61
|
+
on error errMsg
|
|
62
|
+
return "ERROR" & "${sep}" & errMsg
|
|
63
|
+
end try
|
|
64
|
+
`
|
|
65
|
+
: `
|
|
66
|
+
try
|
|
67
|
+
tell application "${applicationName}"
|
|
68
|
+
tell front window
|
|
69
|
+
set t to current tab
|
|
70
|
+
if URL of t is not "about:blank" then
|
|
71
|
+
tell t
|
|
72
|
+
with timeout of 3 seconds
|
|
73
|
+
${inner}
|
|
74
|
+
end timeout
|
|
75
|
+
end tell
|
|
76
|
+
else
|
|
77
|
+
error "No active tab found"
|
|
78
|
+
end if
|
|
79
|
+
end tell
|
|
80
|
+
end tell
|
|
81
|
+
on error errMsg
|
|
82
|
+
return "ERROR" & "${sep}" & errMsg
|
|
83
|
+
end try
|
|
84
|
+
`;
|
|
85
|
+
const result = await executeAppleScript(appleScript);
|
|
86
|
+
if (result.startsWith(`ERROR${sep}`))
|
|
87
|
+
throw new Error(result.split(sep)[1]);
|
|
88
|
+
const parts = result.split(sep).map((part) => part.trim());
|
|
89
|
+
if (parts.length < 3)
|
|
90
|
+
throw new Error("Failed to read the tab content");
|
|
91
|
+
const [title, url, content] = parts;
|
|
92
|
+
const dom = new JSDOM(content, { url });
|
|
93
|
+
const reader = new Readability(dom.window.document, {
|
|
94
|
+
charThreshold: 10,
|
|
95
|
+
});
|
|
96
|
+
const article = reader.parse();
|
|
97
|
+
if (!article?.content)
|
|
98
|
+
throw new Error("Failed to parse the page content");
|
|
99
|
+
const turndownService = new TurndownService();
|
|
100
|
+
turndownService.use(turndownPluginGfm.gfm);
|
|
101
|
+
const md = turndownService.turndown(article.content);
|
|
102
|
+
return {
|
|
103
|
+
title,
|
|
104
|
+
url,
|
|
105
|
+
content: md,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function openURL(applicationName, url) {
|
|
109
|
+
const escapedUrl = escapeAppleScript(url);
|
|
110
|
+
const appleScript = `
|
|
111
|
+
tell application "${applicationName}"
|
|
112
|
+
open location "${escapedUrl}"
|
|
113
|
+
end tell
|
|
114
|
+
`;
|
|
115
|
+
await executeAppleScript(appleScript);
|
|
116
|
+
}
|
|
117
|
+
export const safariBrowser = {
|
|
118
|
+
getTabList: getSafariTabList,
|
|
119
|
+
getPageContent,
|
|
120
|
+
openURL,
|
|
121
|
+
};
|
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())
|
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/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.2.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,9 +18,11 @@
|
|
|
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
26
|
"test": "vitest",
|
|
25
27
|
"test:run": "vitest run"
|
|
26
28
|
},
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"@types/jsdom": "^21.1.7",
|
|
37
39
|
"@types/node": "^20.11.17",
|
|
38
40
|
"@types/turndown": "^5.0.5",
|
|
41
|
+
"prettier": "^3.6.2",
|
|
39
42
|
"tsx": "^4.7.1",
|
|
40
43
|
"typescript": "~5.8.3",
|
|
41
44
|
"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>;
|