@hao-gd/page-agent-mcp 1.10.1

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 ADDED
@@ -0,0 +1,93 @@
1
+ # @hao-gd/page-agent-mcp
2
+
3
+ MCP server that lets AI agent clients (Claude Desktop, Copilot, etc.) control your browser through the [Page Agent](https://github.com/alibaba/page-agent) extension.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js >= 20
8
+ - [Page Agent Extension](https://chromewebstore.google.com/detail/page-agent-ext/akldabonmimlicnjlflnapfeklbfemhj) installed in Chrome
9
+ - An LLM API key (OpenAI-compatible)
10
+
11
+ ## Installation
12
+
13
+ ### Claude Desktop
14
+
15
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "page-agent": {
21
+ "command": "npx",
22
+ "args": ["-y", "@hao-gd/page-agent-mcp"],
23
+ "env": {
24
+ "LLM_BASE_URL": "https://dashscope.aliyuncs.com/compatible-mode/v1",
25
+ "LLM_API_KEY": "sk-xxx",
26
+ "LLM_MODEL_NAME": "qwen3.5-plus"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ### Cursor / Copilot
34
+
35
+ Same format — add the config to the MCP settings of your client.
36
+
37
+ ## MCP Tools
38
+
39
+ | Tool | Input | Description |
40
+ | -------------- | ------------------ | ----------------------------------------------------- |
41
+ | `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |
42
+ | `get_status` | — | Returns `{ connected, busy }` |
43
+ | `stop_task` | — | Stop the currently running task. |
44
+
45
+ ## Environment Variables
46
+
47
+ | Variable | Default | Description |
48
+ | ---------------- | ------- | --------------------- |
49
+ | `LLM_BASE_URL` | — | LLM API base URL |
50
+ | `LLM_API_KEY` | — | LLM API key |
51
+ | `LLM_MODEL_NAME` | — | Model name |
52
+ | `PORT` | `38401` | HTTP + WebSocket port |
53
+
54
+ ## How It Works
55
+
56
+ ```
57
+ ┌──────────────┐ stdio ┌──────────────────┐ WebSocket ┌──────────────┐
58
+ │ Claude / │◄────────►│ @hao-gd/page-agent-mcp │◄────────────►│ Hub tab │
59
+ │ Copilot │ (MCP) │ (Node.js) │ (localhost) │ (extension) │
60
+ └──────────────┘ └──────────────────┘ └──────┬───────┘
61
+ │ │
62
+ │ HTTP │ useAgent
63
+ ▼ ▼
64
+ ┌──────────────────┐ ┌──────────────┐
65
+ │ Launcher page │ │ MultiPage │
66
+ │ (localhost:PORT) │ │ Agent │
67
+ └──────────────────┘ └──────────────┘
68
+ ```
69
+
70
+ 1. Agent client starts the MCP server via stdio (`npx @hao-gd/page-agent-mcp`).
71
+ 2. Server starts HTTP + WS on `localhost:PORT`, opens the launcher page in browser.
72
+ 3. Launcher page triggers the extension to open a **hub tab** (`hub.html?ws=PORT`).
73
+ 4. Hub connects to the WS server. MCP tools now proxy tasks to the hub.
74
+
75
+ The hub tab speaks a generic WebSocket protocol (defined in `hub-ws.ts` in the extension package) and has no knowledge of MCP. See the hub's protocol docs for message format details.
76
+
77
+ ## Architecture
78
+
79
+ Pure JS ESM, no build step. Source files are the published artifacts.
80
+
81
+ ```
82
+ src/
83
+ ├── index.js # CLI entry: MCP server (stdio) + opens launcher
84
+ ├── hub-bridge.js # HTTP server + WebSocket bridge to hub tab
85
+ └── launcher.html # Bootstrap page: detects extension, triggers hub open
86
+ ```
87
+
88
+ ## Dev
89
+
90
+ ```bash
91
+ npm run dev:ext
92
+ npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
93
+ ```
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@hao-gd/page-agent-mcp",
3
+ "private": false,
4
+ "version": "1.10.1",
5
+ "type": "module",
6
+ "bin": {
7
+ "page-agent-mcp": "src/index.js"
8
+ },
9
+ "files": [
10
+ "src/"
11
+ ],
12
+ "description": "MCP server for controlling the browser via Page Agent extension",
13
+ "keywords": [
14
+ "page-agent",
15
+ "mcp",
16
+ "browser-automation",
17
+ "chrome-extension"
18
+ ],
19
+ "author": "hao-gd<guodonghao520@gmail.com>",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://gitee.com/hao_guo_dong/page-agent.git",
24
+ "directory": "packages/mcp"
25
+ },
26
+ "homepage": "https://gitee.com/hao_guo_dong/page-agent",
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.29.0",
32
+ "ws": "^8.21.0",
33
+ "zod": "^4.4.3"
34
+ }
35
+ }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs'
3
+ import http from 'node:http'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { WebSocketServer } from 'ws'
6
+
7
+ const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'
8
+ const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}`
9
+ const LOOPBACK_HOST = 'localhost'
10
+
11
+ const launcherTemplate = readFileSync(
12
+ fileURLToPath(new URL('./launcher.html', import.meta.url)),
13
+ 'utf-8'
14
+ )
15
+
16
+ /**
17
+ * HTTP + WebSocket bridge to the hub.html extension tab.
18
+ * - HTTP serves the launcher page (triggers extension to open hub)
19
+ * - WS carries execute/stop commands and result/error responses
20
+ */
21
+ export class HubBridge {
22
+ /** @type {number} */
23
+ port
24
+
25
+ /** @type {http.Server} */
26
+ #httpServer
27
+
28
+ /** @type {WebSocketServer} */
29
+ #wss
30
+
31
+ /** @type {import('ws').WebSocket | null} */
32
+ #hub = null
33
+
34
+ /** @type {{ resolve: (r: {success: boolean, data: string}) => void, reject: (e: Error) => void } | null} */
35
+ #pendingTask = null
36
+
37
+ /** @param {number} port */
38
+ constructor(port) {
39
+ this.port = port
40
+ this.#httpServer = http.createServer((_req, res) => {
41
+ const html = launcherTemplate
42
+ .replaceAll('__EXT_ID__', EXT_ID)
43
+ .replaceAll('__STORE_URL__', STORE_URL)
44
+ .replaceAll('__WS_PORT__', String(port))
45
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
46
+ res.end(html)
47
+ })
48
+ this.#wss = new WebSocketServer({ server: this.#httpServer })
49
+ this.#wss.on('connection', (ws) => this.#onConnection(ws))
50
+ }
51
+
52
+ /** @returns {Promise<void>} */
53
+ async start() {
54
+ return new Promise((resolve, reject) => {
55
+ this.#httpServer.on('error', (/** @type {NodeJS.ErrnoException} */ err) => {
56
+ if (err.code === 'EADDRINUSE') {
57
+ reject(
58
+ new Error(`Port ${this.port} is in use. Another Page Agent MCP server may be running.`)
59
+ )
60
+ } else {
61
+ reject(err)
62
+ }
63
+ })
64
+ this.#httpServer.listen(this.port, LOOPBACK_HOST, () => {
65
+ console.error(`[page-agent-mcp] HTTP + WS on http://${LOOPBACK_HOST}:${this.port}`)
66
+ resolve()
67
+ })
68
+ })
69
+ }
70
+
71
+ get connected() {
72
+ return this.#hub?.readyState === 1
73
+ }
74
+
75
+ get busy() {
76
+ return this.#pendingTask !== null
77
+ }
78
+
79
+ /**
80
+ * @param {string} task
81
+ * @param {Record<string, unknown>} [config]
82
+ * @returns {Promise<{success: boolean, data: string}>}
83
+ */
84
+ async executeTask(task, config) {
85
+ if (!this.connected) throw new Error('Hub is not connected. Is the extension running?')
86
+ if (this.#pendingTask) throw new Error('Agent is already running a task.')
87
+
88
+ return new Promise((resolve, reject) => {
89
+ this.#pendingTask = { resolve, reject }
90
+ this.#hub.send(JSON.stringify({ type: 'execute', task, config }))
91
+ })
92
+ }
93
+
94
+ stopTask() {
95
+ if (this.connected) {
96
+ this.#hub.send(JSON.stringify({ type: 'stop' }))
97
+ }
98
+ }
99
+
100
+ // TODO: Add version checking
101
+
102
+ /** @param {import('ws').WebSocket} ws */
103
+ #onConnection(ws) {
104
+ if (this.#hub && this.#hub.readyState === 1) {
105
+ ws.close(4000, 'Another hub is already connected')
106
+ return
107
+ }
108
+
109
+ this.#hub = ws
110
+ console.error('[page-agent-mcp] Hub connected')
111
+
112
+ ws.on('message', (/** @type {Buffer} */ rawData) => {
113
+ /** @type {{ type: string, success?: boolean, data?: string, message?: string }} */
114
+ let msg
115
+ try {
116
+ msg = JSON.parse(rawData.toString('utf-8'))
117
+ } catch {
118
+ return
119
+ }
120
+
121
+ if (msg.type === 'result') {
122
+ this.#pendingTask?.resolve({ success: msg.success ?? false, data: msg.data ?? '' })
123
+ this.#pendingTask = null
124
+ } else if (msg.type === 'error') {
125
+ this.#pendingTask?.reject(new Error(msg.message ?? 'Unknown error from hub'))
126
+ this.#pendingTask = null
127
+ }
128
+ })
129
+
130
+ ws.on('close', () => {
131
+ console.error('[page-agent-mcp] Hub disconnected')
132
+ if (this.#hub === ws) this.#hub = null
133
+ if (this.#pendingTask) {
134
+ this.#pendingTask.reject(new Error('Hub disconnected while task was running'))
135
+ this.#pendingTask = null
136
+ }
137
+ })
138
+ }
139
+ }
package/src/index.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
+ import { spawn } from 'node:child_process'
5
+ import { readFileSync } from 'node:fs'
6
+ import { platform } from 'node:os'
7
+ import * as z from 'zod/v4'
8
+
9
+ import { HubBridge } from './hub-bridge.js'
10
+
11
+ const env = process.env
12
+ const port = parseInt(env.PORT || '38401')
13
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
14
+
15
+ /** @type {Record<string, string>} */
16
+ const llmConfig = {}
17
+ if (env.LLM_BASE_URL) llmConfig.baseURL = env.LLM_BASE_URL
18
+ if (env.LLM_MODEL_NAME) llmConfig.model = env.LLM_MODEL_NAME
19
+ if (env.LLM_API_KEY) llmConfig.apiKey = env.LLM_API_KEY
20
+
21
+ // --- Hub bridge (HTTP + WebSocket) ---
22
+
23
+ const hub = new HubBridge(port)
24
+ await hub.start()
25
+
26
+ // Open launcher in default browser (use spawn() with array args to avoid shell injection)
27
+ const url = `http://localhost:${port}`
28
+ const openCmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open'
29
+ const openArgs = platform() === 'win32' ? ['""', url] : [url]
30
+ const child = spawn(openCmd, openArgs, { stdio: 'ignore', detached: true })
31
+ child.on('error', (err) => console.error(`[page-agent-mcp] Could not open browser: ${err.message}`))
32
+ child.unref()
33
+
34
+ // --- MCP server (stdio) ---
35
+
36
+ const mcpServer = new McpServer({ name: '@hao-gd/page-agent', version })
37
+
38
+ mcpServer.registerTool(
39
+ 'execute_task',
40
+ {
41
+ description: "Execute a task in user's browser.",
42
+ inputSchema: {
43
+ task: z
44
+ .string()
45
+ .describe(
46
+ 'Task description. Give specific instructions for the task. Steps preferable. And the information you want to get after the task is done.'
47
+ ),
48
+ },
49
+ },
50
+ async ({ task }) => {
51
+ try {
52
+ const config = Object.keys(llmConfig).length > 0 ? llmConfig : undefined
53
+ const result = await hub.executeTask(task, config)
54
+ return {
55
+ content: [
56
+ {
57
+ type: 'text',
58
+ text: result.success
59
+ ? `Task completed.\n\n${result.data}`
60
+ : `Task failed.\n\n${result.data}`,
61
+ },
62
+ ],
63
+ }
64
+ } catch (err) {
65
+ return {
66
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
67
+ isError: true,
68
+ }
69
+ }
70
+ }
71
+ )
72
+
73
+ mcpServer.registerTool(
74
+ 'get_status',
75
+ {
76
+ description: 'Check the current status of the Page Agent hub.',
77
+ },
78
+ async () => ({
79
+ content: [
80
+ {
81
+ type: 'text',
82
+ text: JSON.stringify({ connected: hub.connected, busy: hub.busy }, null, 2),
83
+ },
84
+ ],
85
+ })
86
+ )
87
+
88
+ mcpServer.registerTool(
89
+ 'stop_task',
90
+ {
91
+ description: 'Stop the currently running browser automation task.',
92
+ },
93
+ async () => {
94
+ hub.stopTask()
95
+ return { content: [{ type: 'text', text: 'Stop signal sent.' }] }
96
+ }
97
+ )
98
+
99
+ const transport = new StdioServerTransport()
100
+ await mcpServer.connect(transport)
101
+ console.error('[page-agent-mcp] MCP server ready (stdio)')
@@ -0,0 +1,245 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link
7
+ rel="icon"
8
+ href="https://img.alicdn.com/imgextra/i1/O1CN01mRGret1QrKiu7CFJI_!!6000000002029-2-tps-64-64.png"
9
+ />
10
+ <title>Page Agent MCP Launcher</title>
11
+ <style>
12
+ * {
13
+ margin: 0;
14
+ padding: 0;
15
+ box-sizing: border-box;
16
+ }
17
+ body {
18
+ font-family:
19
+ system-ui,
20
+ -apple-system,
21
+ sans-serif;
22
+ background: #09090b;
23
+ color: #e5e5e5;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ min-height: 100vh;
28
+ }
29
+ .card {
30
+ text-align: center;
31
+ max-width: 480px;
32
+ padding: 3rem 2rem;
33
+ }
34
+ .logo {
35
+ width: 72px;
36
+ height: 72px;
37
+ border-radius: 18px;
38
+ margin-bottom: 1rem;
39
+ }
40
+ .badge {
41
+ font-size: 0.6875rem;
42
+ font-weight: 500;
43
+ color: #52525b;
44
+ letter-spacing: 0.05em;
45
+ text-transform: uppercase;
46
+ margin-bottom: 1.5rem;
47
+ }
48
+ h1 {
49
+ font-size: 1.35rem;
50
+ font-weight: 600;
51
+ margin-bottom: 0.5rem;
52
+ }
53
+ .sub {
54
+ font-size: 0.875rem;
55
+ color: #a1a1aa;
56
+ line-height: 1.7;
57
+ }
58
+ .spinner {
59
+ width: 32px;
60
+ height: 32px;
61
+ border: 3px solid #27272a;
62
+ border-top-color: #fff;
63
+ border-radius: 50%;
64
+ animation: spin 0.8s linear infinite;
65
+ margin: 0 auto 1.5rem;
66
+ }
67
+ @keyframes spin {
68
+ to {
69
+ transform: rotate(360deg);
70
+ }
71
+ }
72
+ a {
73
+ color: #60a5fa;
74
+ text-decoration: none;
75
+ }
76
+ a:hover {
77
+ text-decoration: underline;
78
+ }
79
+
80
+ .install {
81
+ display: none;
82
+ }
83
+ .install.show {
84
+ display: block;
85
+ }
86
+
87
+ .tips {
88
+ margin-top: 1.75rem;
89
+ text-align: left;
90
+ background: #18181b;
91
+ border: 1px solid #27272a;
92
+ border-radius: 12px;
93
+ padding: 1.25rem 1.5rem;
94
+ }
95
+ .tips li {
96
+ font-size: 0.8125rem;
97
+ color: #a1a1aa;
98
+ line-height: 1.7;
99
+ margin-left: 1rem;
100
+ }
101
+ .tips li + li {
102
+ margin-top: 0.35rem;
103
+ }
104
+
105
+ .store-btn {
106
+ display: inline-flex;
107
+ align-items: center;
108
+ gap: 0.625rem;
109
+ margin-top: 1.5rem;
110
+ padding: 0.625rem 1.5rem;
111
+ background: #2563eb;
112
+ color: #fff;
113
+ border-radius: 10px;
114
+ font-size: 0.875rem;
115
+ font-weight: 500;
116
+ transition: background 0.15s;
117
+ }
118
+ .store-btn:hover {
119
+ background: #1d4ed8;
120
+ text-decoration: none;
121
+ }
122
+ .store-btn img {
123
+ width: 20px;
124
+ height: 20px;
125
+ }
126
+
127
+ .links {
128
+ margin-top: 1.75rem;
129
+ display: flex;
130
+ justify-content: center;
131
+ gap: 1.5rem;
132
+ font-size: 0.8125rem;
133
+ }
134
+ .links a {
135
+ color: #71717a;
136
+ transition: color 0.15s;
137
+ }
138
+ .links a:hover {
139
+ color: #a1a1aa;
140
+ text-decoration: none;
141
+ }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <div class="card">
146
+ <img
147
+ class="logo"
148
+ src="https://img.alicdn.com/imgextra/i3/O1CN01JPT4Fj1FJTfmHfNxO_!!6000000000466-49-tps-512-512.webp"
149
+ alt="Page Agent"
150
+ />
151
+ <div class="badge">Page Agent MCP Launcher</div>
152
+
153
+ <div id="connecting">
154
+ <div class="spinner"></div>
155
+ <h1 data-i18n="connecting_title">Connecting to Page Agent</h1>
156
+ <p class="sub" data-i18n="connecting_sub">Opening the hub in your browser…</p>
157
+ </div>
158
+
159
+ <div id="install" class="install">
160
+ <h1 data-i18n="install_title">Extension Required</h1>
161
+ <p class="sub" data-i18n="install_sub">
162
+ Page Agent requires the latest browser extension to work.
163
+ </p>
164
+
165
+ <a class="store-btn" href="__STORE_URL__" target="_blank">
166
+ <img
167
+ src="https://img.alicdn.com/imgextra/i3/O1CN01JpW0Vo1sR3FpiZKFM_!!6000000005762-55-tps-192-192.svg"
168
+ alt=""
169
+ />
170
+ <span data-i18n="install_btn">Install from Chrome Web Store</span>
171
+ </a>
172
+
173
+ <ul class="tips">
174
+ <li data-i18n="tip_outdated">
175
+ If the extension is outdated, please update it to the latest version.
176
+ </li>
177
+ <li data-i18n="tip_other_browser">
178
+ If the extension is not installed in this browser, open this page from the browser that
179
+ has it installed.
180
+ </li>
181
+ <li data-i18n="tip_refresh">Refresh this page after installing or updating.</li>
182
+ </ul>
183
+ </div>
184
+
185
+ <div class="links">
186
+ <a
187
+ href="https://alibaba.github.io/page-agent/docs/introduction/overview"
188
+ target="_blank"
189
+ data-i18n="link_docs"
190
+ >Docs</a
191
+ >
192
+ <a
193
+ href="https://github.com/alibaba/page-agent/issues"
194
+ target="_blank"
195
+ data-i18n="link_issues"
196
+ >Report an Issue</a
197
+ >
198
+ </div>
199
+ </div>
200
+
201
+ <script>
202
+ {
203
+ const EXT_ID = '__EXT_ID__'
204
+ const wsPort = __WS_PORT__
205
+
206
+ const zh = {
207
+ connecting_title: '正在连接 Page Agent',
208
+ connecting_sub: '正在浏览器中打开 Hub…',
209
+ install_title: '需要安装浏览器插件',
210
+ install_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',
211
+ install_btn: '从 Chrome 应用商店安装',
212
+ tip_outdated: '如果插件版本过旧,请更新到最新版本。',
213
+ tip_other_browser: '如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
214
+ tip_refresh: '安装或更新后,请刷新此页面。',
215
+ link_docs: '文档',
216
+ link_issues: '问题反馈',
217
+ }
218
+
219
+ if (/^zh\b/i.test(navigator.language)) {
220
+ document.querySelectorAll('[data-i18n]').forEach((el) => {
221
+ const key = el.getAttribute('data-i18n')
222
+ if (zh[key]) el.textContent = zh[key]
223
+ })
224
+ }
225
+
226
+ const showInstall = () => {
227
+ document.getElementById('connecting').style.display = 'none'
228
+ document.getElementById('install').classList.add('show')
229
+ }
230
+
231
+ try {
232
+ if (!globalThis.chrome?.runtime?.sendMessage) {
233
+ showInstall()
234
+ } else {
235
+ chrome.runtime.sendMessage(EXT_ID, { type: 'OPEN_HUB', wsPort }, (response) => {
236
+ if (chrome.runtime.lastError || !response?.ok) showInstall()
237
+ })
238
+ }
239
+ } catch {
240
+ showInstall()
241
+ }
242
+ }
243
+ </script>
244
+ </body>
245
+ </html>