@aikeymouse/chromelink-mcp 1.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 +282 -0
- package/bin/chromelink-mcp +11 -0
- package/index.js +658 -0
- package/package.json +57 -0
- package/scripts/prepare-publish.js +70 -0
- package/scripts/restore-publish.js +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# ChromeLink MCP Server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol (MCP) server that exposes Chrome browser automation capabilities to AI agents like Claude, GPT, and other MCP-compatible tools.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Option 1: NPM (Recommended)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @aikeymouse/chromelink-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then configure in your MCP client:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"chrome-link": {
|
|
19
|
+
"command": "chromelink-mcp"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Option 2: From Source
|
|
26
|
+
|
|
27
|
+
Clone the repository and use the server directly:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/aikeymouse/chrome-link.git
|
|
31
|
+
cd chrome-link/mcp-server
|
|
32
|
+
npm install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Configure with absolute path:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"chrome-link": {
|
|
41
|
+
"command": "node",
|
|
42
|
+
"args": ["/absolute/path/to/chrome-link/mcp-server/index.js"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
1. **Install ChromeLink extension** in Chrome
|
|
51
|
+
2. **Start browser-link-server** (automatically started by extension on first connection)
|
|
52
|
+
|
|
53
|
+
## Local Development
|
|
54
|
+
|
|
55
|
+
For local development with file dependencies:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cd chrome-link/mcp-server
|
|
59
|
+
npm install # Creates symlink to ../clients/node
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The MCP server uses a local file dependency (`"@aikeymouse/chromelink-client": "file:../clients/node"`) for development. This is automatically converted to an npm dependency (`^1.2.0`) during publishing via the `prepublishOnly` script.
|
|
63
|
+
|
|
64
|
+
After version changes, update the symlink:
|
|
65
|
+
```bash
|
|
66
|
+
cd mcp-server && npm install
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Running the MCP Server
|
|
70
|
+
|
|
71
|
+
### From NPM Installation
|
|
72
|
+
```bash
|
|
73
|
+
chromelink-mcp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### From Source
|
|
77
|
+
```bash
|
|
78
|
+
node mcp-server/index.js
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The MCP server will:
|
|
82
|
+
- Connect to `ws://localhost:9000` (browser-link-server)
|
|
83
|
+
- Expose 18 Chrome automation tools via MCP protocol
|
|
84
|
+
- Communicate with AI agents via stdio
|
|
85
|
+
|
|
86
|
+
## Available Tools
|
|
87
|
+
|
|
88
|
+
### Tab Management
|
|
89
|
+
- `chrome_list_tabs` - List all open tabs
|
|
90
|
+
- `chrome_open_tab` - Open new tab with URL
|
|
91
|
+
- `chrome_navigate_tab` - Navigate tab to URL
|
|
92
|
+
- `chrome_switch_tab` - Switch to specific tab
|
|
93
|
+
- `chrome_close_tab` - Close a tab
|
|
94
|
+
- `chrome_get_active_tab` - Get active tab info
|
|
95
|
+
|
|
96
|
+
### Navigation History
|
|
97
|
+
- `chrome_go_back` - Navigate back in history
|
|
98
|
+
- `chrome_go_forward` - Navigate forward in history
|
|
99
|
+
|
|
100
|
+
### DOM Interaction
|
|
101
|
+
- `chrome_wait_for_element` - Wait for element to appear
|
|
102
|
+
- `chrome_get_text` - Get element text content
|
|
103
|
+
- `chrome_click` - Click an element
|
|
104
|
+
- `chrome_type` - Type text into element
|
|
105
|
+
|
|
106
|
+
### JavaScript Execution
|
|
107
|
+
- `chrome_execute_js` - Execute arbitrary JavaScript
|
|
108
|
+
- `chrome_call_helper` - Call predefined helper functions:
|
|
109
|
+
- **Element Interaction**: clickElement, typeText, appendChar, clearContentEditable
|
|
110
|
+
- **Element Query**: getText, getHTML, getLastHTML, elementExists, isVisible, waitForElement
|
|
111
|
+
- **Element Highlighting**: highlightElement, removeHighlights
|
|
112
|
+
- **Element Positioning**: getElementBounds, scrollElementIntoView
|
|
113
|
+
- **Element Inspection**: inspectElement, getContainerElements
|
|
114
|
+
|
|
115
|
+
### Screenshots
|
|
116
|
+
- `chrome_capture_screenshot` - Capture tab screenshot (PNG/JPEG)
|
|
117
|
+
|
|
118
|
+
### Script Injection
|
|
119
|
+
- `chrome_register_injection` - Register content script
|
|
120
|
+
- `chrome_unregister_injection` - Unregister content script
|
|
121
|
+
|
|
122
|
+
## Configuration for AI Agents
|
|
123
|
+
|
|
124
|
+
### Claude Desktop
|
|
125
|
+
|
|
126
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"mcpServers": {
|
|
131
|
+
"chrome-link": {
|
|
132
|
+
"command": "node",
|
|
133
|
+
"args": ["/absolute/path/to/chrome-driver-extension/mcp-server/index.js"]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Important**: Use absolute path to `index.js`
|
|
140
|
+
|
|
141
|
+
After configuration:
|
|
142
|
+
1. Restart Claude Desktop
|
|
143
|
+
2. Start a conversation
|
|
144
|
+
3. Ask Claude: "What Chrome tools do you have?"
|
|
145
|
+
|
|
146
|
+
### Example Prompts
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
Open https://github.com in a new Chrome tab
|
|
150
|
+
|
|
151
|
+
List all my open Chrome tabs
|
|
152
|
+
|
|
153
|
+
Go to https://example.com and get the text of the h1 element
|
|
154
|
+
|
|
155
|
+
Take a screenshot of the current tab in PNG format
|
|
156
|
+
|
|
157
|
+
Fill out the login form:
|
|
158
|
+
1. Open https://the-internet.herokuapp.com/login
|
|
159
|
+
2. Type "tomsmith" in #username
|
|
160
|
+
3. Type "SuperSecretPassword!" in #password
|
|
161
|
+
4. Click the login button
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Architecture
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
AI Agent (Claude/GPT)
|
|
168
|
+
↓ stdio (MCP protocol)
|
|
169
|
+
MCP Server (this file)
|
|
170
|
+
↓ WebSocket (ws://localhost:9000)
|
|
171
|
+
browser-link-server
|
|
172
|
+
↓ native messaging (stdin/stdout)
|
|
173
|
+
Chrome Extension
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Key Design Points:**
|
|
177
|
+
- MCP server runs as **separate process** (avoids stdio conflict)
|
|
178
|
+
- Uses existing WebSocket client (thin wrapper, no code duplication)
|
|
179
|
+
- Zero changes needed to browser-link-server or extension
|
|
180
|
+
- Multiple MCP servers can connect simultaneously
|
|
181
|
+
|
|
182
|
+
## Testing
|
|
183
|
+
|
|
184
|
+
### Manual Test
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Start browser-link-server (if not auto-started)
|
|
188
|
+
node native-host/browser-link-server.js
|
|
189
|
+
|
|
190
|
+
# In another terminal, test MCP server
|
|
191
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | node mcp-server/index.js
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Expected output:
|
|
195
|
+
```json
|
|
196
|
+
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"chrome-link","version":"1.0.0"}}}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Test Tool Invocation
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Create test file
|
|
203
|
+
cat > /tmp/test-mcp.json << 'EOF'
|
|
204
|
+
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}
|
|
205
|
+
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"chrome_list_tabs","arguments":{}}}
|
|
206
|
+
EOF
|
|
207
|
+
|
|
208
|
+
# Run test
|
|
209
|
+
cat /tmp/test-mcp.json | node native-host/mcp-server.js 2>/dev/null
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Using MCP Inspector
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npm install -g @modelcontextprotocol/inspector
|
|
216
|
+
npx @modelcontextprotocol/inspector node mcp-server/index.js
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Opens web UI for testing tools interactively.
|
|
220
|
+
|
|
221
|
+
## Troubleshooting
|
|
222
|
+
|
|
223
|
+
### MCP Server Won't Connect
|
|
224
|
+
|
|
225
|
+
**Error**: `Failed to connect to browser-link-server`
|
|
226
|
+
|
|
227
|
+
**Solution**:
|
|
228
|
+
1. Check Chrome extension is installed: `chrome://extensions/`
|
|
229
|
+
2. Check browser-link-server is running: `curl http://localhost:9000/session` (expect `{"status":"ready","message":"Upgrade to WebSocket"}`)
|
|
230
|
+
3. Test WebSocket manually: `wscat -c ws://localhost:9000`
|
|
231
|
+
|
|
232
|
+
### Tools Not Showing in Claude
|
|
233
|
+
|
|
234
|
+
**Error**: Claude doesn't list Chrome tools
|
|
235
|
+
|
|
236
|
+
**Solution**:
|
|
237
|
+
1. Verify config file path is absolute (not relative)
|
|
238
|
+
2. Check JSON syntax: `python3 -m json.tool < ~/Library/Application\ Support/Claude/claude_desktop_config.json`
|
|
239
|
+
3. Restart Claude Desktop
|
|
240
|
+
4. Check Claude logs: `~/Library/Logs/Claude/`
|
|
241
|
+
|
|
242
|
+
### Tool Execution Fails
|
|
243
|
+
|
|
244
|
+
**Error**: `TypeError: tabId is not a number`
|
|
245
|
+
|
|
246
|
+
**Solution**:
|
|
247
|
+
1. Use `chrome_list_tabs` first to get valid tab IDs
|
|
248
|
+
2. Ensure tab still exists (not closed)
|
|
249
|
+
3. Check MCP server logs (stderr output)
|
|
250
|
+
|
|
251
|
+
## Logging
|
|
252
|
+
|
|
253
|
+
MCP server logs to **stderr** (stdout is reserved for MCP protocol):
|
|
254
|
+
- Connection status
|
|
255
|
+
- Tool invocations
|
|
256
|
+
- Errors and warnings
|
|
257
|
+
|
|
258
|
+
To see logs:
|
|
259
|
+
```bash
|
|
260
|
+
node native-host/mcp-server.js 2>&1 | grep "\[MCP Server\]"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Security
|
|
264
|
+
|
|
265
|
+
⚠️ **MCP server has full browser control** - use with trusted AI agents only
|
|
266
|
+
|
|
267
|
+
Recommendations:
|
|
268
|
+
- Run in separate browser profile for automation
|
|
269
|
+
- Monitor AI agent actions via logs
|
|
270
|
+
- Disable MCP server when not needed
|
|
271
|
+
- Consider adding authentication for production use
|
|
272
|
+
|
|
273
|
+
## Documentation
|
|
274
|
+
|
|
275
|
+
- [MCP Server Architecture](../docs/dev/MCP_SERVER_ARCHITECTURE.md) - Detailed design docs
|
|
276
|
+
- [MCP Server Configuration](../docs/dev/MCP_SERVER_CONFIGURATION.md) - Setup guide for various clients
|
|
277
|
+
- [ChromeLink Protocol](../docs/PROTOCOL.md) - WebSocket protocol specification
|
|
278
|
+
- [Node.js Client](../clients/node/README.md) - Client library used by MCP server
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT - See LICENSE file in repository root
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ChromeLink MCP Server - Executable
|
|
5
|
+
*
|
|
6
|
+
* This script launches the MCP server for Chrome browser automation.
|
|
7
|
+
* It connects to the browser-link-server via WebSocket and exposes
|
|
8
|
+
* Chrome automation capabilities via the Model Context Protocol.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
require('../index.js');
|
package/index.js
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ChromeLink MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Model Context Protocol (MCP) server that exposes Chrome browser automation
|
|
7
|
+
* capabilities to AI agents like Claude, GPT, etc.
|
|
8
|
+
*
|
|
9
|
+
* This server acts as a thin wrapper around the ChromeLink WebSocket client,
|
|
10
|
+
* exposing browser automation as MCP tools that AI agents can discover and use.
|
|
11
|
+
*
|
|
12
|
+
* Architecture:
|
|
13
|
+
* AI Agent (Claude/GPT) <-> MCP Server (this file) <-> browser-link-server <-> Chrome Extension
|
|
14
|
+
*
|
|
15
|
+
* Communication:
|
|
16
|
+
* - AI Agent ↔ MCP Server: stdio (MCP protocol)
|
|
17
|
+
* - MCP Server ↔ browser-link-server: WebSocket (ws://localhost:9000)
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node mcp-server.js
|
|
21
|
+
*
|
|
22
|
+
* Configuration (Claude Desktop):
|
|
23
|
+
* Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
|
|
24
|
+
* {
|
|
25
|
+
* "mcpServers": {
|
|
26
|
+
* "chrome-link": {
|
|
27
|
+
* "command": "node",
|
|
28
|
+
* "args": ["/path/to/chrome-driver-extension/native-host/mcp-server.js"]
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* }
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const ChromeLinkClient = require('@aikeymouse/chromelink-client');
|
|
35
|
+
|
|
36
|
+
// MCP Protocol Implementation
|
|
37
|
+
class MCPServer {
|
|
38
|
+
constructor() {
|
|
39
|
+
this.client = null;
|
|
40
|
+
this.requestId = 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Log to stderr (stdout is reserved for MCP protocol)
|
|
45
|
+
*/
|
|
46
|
+
log(message, ...args) {
|
|
47
|
+
console.error(`[MCP Server] ${message}`, ...args);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send MCP response to stdout
|
|
52
|
+
*/
|
|
53
|
+
sendResponse(response) {
|
|
54
|
+
const message = JSON.stringify(response);
|
|
55
|
+
process.stdout.write(message + '\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Send error response
|
|
60
|
+
*/
|
|
61
|
+
sendError(id, code, message) {
|
|
62
|
+
this.sendResponse({
|
|
63
|
+
jsonrpc: '2.0',
|
|
64
|
+
id,
|
|
65
|
+
error: {
|
|
66
|
+
code,
|
|
67
|
+
message
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initialize connection to browser-link-server
|
|
74
|
+
*/
|
|
75
|
+
async initialize() {
|
|
76
|
+
try {
|
|
77
|
+
this.client = new ChromeLinkClient('ws://localhost:9000');
|
|
78
|
+
await this.client.connect();
|
|
79
|
+
this.log('Connected to browser-link-server');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
this.log('Failed to connect to browser-link-server:', error.message);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get list of available MCP tools
|
|
88
|
+
*/
|
|
89
|
+
getTools() {
|
|
90
|
+
return {
|
|
91
|
+
tools: [
|
|
92
|
+
// Tab Management
|
|
93
|
+
{
|
|
94
|
+
name: 'chrome_list_tabs',
|
|
95
|
+
description: 'List all open tabs in the browser',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {},
|
|
99
|
+
required: []
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'chrome_open_tab',
|
|
104
|
+
description: 'Open a new tab with the specified URL',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
url: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: 'URL to open in the new tab'
|
|
111
|
+
},
|
|
112
|
+
focus: {
|
|
113
|
+
type: 'boolean',
|
|
114
|
+
description: 'Whether to focus the new tab (default: true)',
|
|
115
|
+
default: true
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
required: ['url']
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'chrome_navigate_tab',
|
|
123
|
+
description: 'Navigate an existing tab to a new URL',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
tabId: {
|
|
128
|
+
type: 'number',
|
|
129
|
+
description: 'ID of the tab to navigate'
|
|
130
|
+
},
|
|
131
|
+
url: {
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: 'URL to navigate to'
|
|
134
|
+
},
|
|
135
|
+
focus: {
|
|
136
|
+
type: 'boolean',
|
|
137
|
+
description: 'Whether to focus the tab (default: true)',
|
|
138
|
+
default: true
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
required: ['tabId', 'url']
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'chrome_switch_tab',
|
|
146
|
+
description: 'Switch to (focus) a specific tab',
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: 'object',
|
|
149
|
+
properties: {
|
|
150
|
+
tabId: {
|
|
151
|
+
type: 'number',
|
|
152
|
+
description: 'ID of the tab to switch to'
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
required: ['tabId']
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'chrome_close_tab',
|
|
160
|
+
description: 'Close a specific tab',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
tabId: {
|
|
165
|
+
type: 'number',
|
|
166
|
+
description: 'ID of the tab to close'
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
required: ['tabId']
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'chrome_get_active_tab',
|
|
174
|
+
description: 'Get information about the currently active tab',
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {},
|
|
178
|
+
required: []
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Navigation History
|
|
183
|
+
{
|
|
184
|
+
name: 'chrome_go_back',
|
|
185
|
+
description: 'Navigate back in tab history (only works after user navigation, not programmatic)',
|
|
186
|
+
inputSchema: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
tabId: {
|
|
190
|
+
type: 'number',
|
|
191
|
+
description: 'ID of the tab to navigate back'
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
required: ['tabId']
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'chrome_go_forward',
|
|
199
|
+
description: 'Navigate forward in tab history (only works after user navigation, not programmatic)',
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
tabId: {
|
|
204
|
+
type: 'number',
|
|
205
|
+
description: 'ID of the tab to navigate forward'
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
required: ['tabId']
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// DOM Interaction
|
|
213
|
+
{
|
|
214
|
+
name: 'chrome_wait_for_element',
|
|
215
|
+
description: 'Wait for an element matching the CSS selector to appear on the page',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
selector: {
|
|
220
|
+
type: 'string',
|
|
221
|
+
description: 'CSS selector for the element'
|
|
222
|
+
},
|
|
223
|
+
tabId: {
|
|
224
|
+
type: 'number',
|
|
225
|
+
description: 'ID of the tab'
|
|
226
|
+
},
|
|
227
|
+
timeout: {
|
|
228
|
+
type: 'number',
|
|
229
|
+
description: 'Maximum time to wait in milliseconds (default: 5000)',
|
|
230
|
+
default: 5000
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
required: ['selector', 'tabId']
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'chrome_get_text',
|
|
238
|
+
description: 'Get the text content of an element matching the CSS selector',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
selector: {
|
|
243
|
+
type: 'string',
|
|
244
|
+
description: 'CSS selector for the element'
|
|
245
|
+
},
|
|
246
|
+
tabId: {
|
|
247
|
+
type: 'number',
|
|
248
|
+
description: 'ID of the tab'
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
required: ['selector', 'tabId']
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'chrome_click',
|
|
256
|
+
description: 'Click an element matching the CSS selector',
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties: {
|
|
260
|
+
selector: {
|
|
261
|
+
type: 'string',
|
|
262
|
+
description: 'CSS selector for the element to click'
|
|
263
|
+
},
|
|
264
|
+
tabId: {
|
|
265
|
+
type: 'number',
|
|
266
|
+
description: 'ID of the tab'
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
required: ['selector', 'tabId']
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: 'chrome_type',
|
|
274
|
+
description: 'Type text into an element matching the CSS selector',
|
|
275
|
+
inputSchema: {
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
selector: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
description: 'CSS selector for the input element'
|
|
281
|
+
},
|
|
282
|
+
text: {
|
|
283
|
+
type: 'string',
|
|
284
|
+
description: 'Text to type into the element'
|
|
285
|
+
},
|
|
286
|
+
tabId: {
|
|
287
|
+
type: 'number',
|
|
288
|
+
description: 'ID of the tab'
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
required: ['selector', 'text', 'tabId']
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// JavaScript Execution
|
|
296
|
+
{
|
|
297
|
+
name: 'chrome_execute_js',
|
|
298
|
+
description: 'Execute arbitrary JavaScript code in the page context',
|
|
299
|
+
inputSchema: {
|
|
300
|
+
type: 'object',
|
|
301
|
+
properties: {
|
|
302
|
+
code: {
|
|
303
|
+
type: 'string',
|
|
304
|
+
description: 'JavaScript code to execute'
|
|
305
|
+
},
|
|
306
|
+
tabId: {
|
|
307
|
+
type: 'number',
|
|
308
|
+
description: 'ID of the tab'
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
required: ['code', 'tabId']
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: 'chrome_call_helper',
|
|
316
|
+
description: 'Call a predefined DOM helper function for CSP-restricted pages. Available helpers: Element Interaction (clickElement, typeText, appendChar, clearContentEditable), Element Query (getText, getHTML, getLastHTML, elementExists, isVisible, waitForElement), Element Highlighting (highlightElement, removeHighlights), Element Positioning (getElementBounds, scrollElementIntoView), Element Inspection (inspectElement, getContainerElements)',
|
|
317
|
+
inputSchema: {
|
|
318
|
+
type: 'object',
|
|
319
|
+
properties: {
|
|
320
|
+
functionName: {
|
|
321
|
+
type: 'string',
|
|
322
|
+
description: 'Name of the helper function',
|
|
323
|
+
enum: [
|
|
324
|
+
'clickElement',
|
|
325
|
+
'typeText',
|
|
326
|
+
'appendChar',
|
|
327
|
+
'clearContentEditable',
|
|
328
|
+
'getText',
|
|
329
|
+
'getHTML',
|
|
330
|
+
'getLastHTML',
|
|
331
|
+
'elementExists',
|
|
332
|
+
'isVisible',
|
|
333
|
+
'waitForElement',
|
|
334
|
+
'highlightElement',
|
|
335
|
+
'removeHighlights',
|
|
336
|
+
'getElementBounds',
|
|
337
|
+
'scrollElementIntoView',
|
|
338
|
+
'inspectElement',
|
|
339
|
+
'getContainerElements'
|
|
340
|
+
]
|
|
341
|
+
},
|
|
342
|
+
args: {
|
|
343
|
+
type: 'array',
|
|
344
|
+
description: 'Arguments to pass to the helper function',
|
|
345
|
+
items: {}
|
|
346
|
+
},
|
|
347
|
+
tabId: {
|
|
348
|
+
type: 'number',
|
|
349
|
+
description: 'ID of the tab'
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
required: ['functionName', 'args', 'tabId']
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
// Screenshots
|
|
357
|
+
{
|
|
358
|
+
name: 'chrome_capture_screenshot',
|
|
359
|
+
description: 'Capture a screenshot of the current tab',
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: 'object',
|
|
362
|
+
properties: {
|
|
363
|
+
format: {
|
|
364
|
+
type: 'string',
|
|
365
|
+
description: 'Image format (png or jpeg)',
|
|
366
|
+
enum: ['png', 'jpeg'],
|
|
367
|
+
default: 'png'
|
|
368
|
+
},
|
|
369
|
+
quality: {
|
|
370
|
+
type: 'number',
|
|
371
|
+
description: 'JPEG quality (0-100, only for jpeg format)',
|
|
372
|
+
minimum: 0,
|
|
373
|
+
maximum: 100,
|
|
374
|
+
default: 90
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
required: []
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
// Script Injection
|
|
382
|
+
{
|
|
383
|
+
name: 'chrome_register_injection',
|
|
384
|
+
description: 'Register a content script to be injected into matching pages',
|
|
385
|
+
inputSchema: {
|
|
386
|
+
type: 'object',
|
|
387
|
+
properties: {
|
|
388
|
+
id: {
|
|
389
|
+
type: 'string',
|
|
390
|
+
description: 'Unique identifier for this injection'
|
|
391
|
+
},
|
|
392
|
+
code: {
|
|
393
|
+
type: 'string',
|
|
394
|
+
description: 'JavaScript code to inject'
|
|
395
|
+
},
|
|
396
|
+
matches: {
|
|
397
|
+
type: 'array',
|
|
398
|
+
description: 'URL patterns to match (e.g., ["https://*.example.com/*"])',
|
|
399
|
+
items: {
|
|
400
|
+
type: 'string'
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
runAt: {
|
|
404
|
+
type: 'string',
|
|
405
|
+
description: 'When to inject the script',
|
|
406
|
+
enum: ['document_start', 'document_end', 'document_idle'],
|
|
407
|
+
default: 'document_idle'
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
required: ['id', 'code', 'matches']
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: 'chrome_unregister_injection',
|
|
415
|
+
description: 'Unregister a previously registered content script',
|
|
416
|
+
inputSchema: {
|
|
417
|
+
type: 'object',
|
|
418
|
+
properties: {
|
|
419
|
+
id: {
|
|
420
|
+
type: 'string',
|
|
421
|
+
description: 'ID of the injection to unregister'
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
required: ['id']
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
]
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Handle tool invocation
|
|
433
|
+
*/
|
|
434
|
+
async handleToolCall(name, args) {
|
|
435
|
+
this.log(`Tool call: ${name}`, args);
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
let result;
|
|
439
|
+
|
|
440
|
+
switch (name) {
|
|
441
|
+
// Tab Management
|
|
442
|
+
case 'chrome_list_tabs':
|
|
443
|
+
result = await this.client.listTabs();
|
|
444
|
+
break;
|
|
445
|
+
|
|
446
|
+
case 'chrome_open_tab':
|
|
447
|
+
result = await this.client.openTab(args.url, args.focus);
|
|
448
|
+
break;
|
|
449
|
+
|
|
450
|
+
case 'chrome_navigate_tab':
|
|
451
|
+
result = await this.client.navigateTab(args.tabId, args.url, args.focus);
|
|
452
|
+
break;
|
|
453
|
+
|
|
454
|
+
case 'chrome_switch_tab':
|
|
455
|
+
result = await this.client.switchTab(args.tabId);
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
case 'chrome_close_tab':
|
|
459
|
+
result = await this.client.closeTab(args.tabId);
|
|
460
|
+
break;
|
|
461
|
+
|
|
462
|
+
case 'chrome_get_active_tab':
|
|
463
|
+
result = await this.client.getActiveTab();
|
|
464
|
+
break;
|
|
465
|
+
|
|
466
|
+
// Navigation History
|
|
467
|
+
case 'chrome_go_back':
|
|
468
|
+
result = await this.client.goBack(args.tabId);
|
|
469
|
+
break;
|
|
470
|
+
|
|
471
|
+
case 'chrome_go_forward':
|
|
472
|
+
result = await this.client.goForward(args.tabId);
|
|
473
|
+
break;
|
|
474
|
+
|
|
475
|
+
// DOM Interaction
|
|
476
|
+
case 'chrome_wait_for_element':
|
|
477
|
+
result = await this.client.waitForElement(args.selector, args.timeout || 5000, args.tabId);
|
|
478
|
+
break;
|
|
479
|
+
|
|
480
|
+
case 'chrome_get_text':
|
|
481
|
+
result = await this.client.getText(args.selector, args.tabId);
|
|
482
|
+
break;
|
|
483
|
+
|
|
484
|
+
case 'chrome_click':
|
|
485
|
+
result = await this.client.click(args.selector, args.tabId);
|
|
486
|
+
break;
|
|
487
|
+
|
|
488
|
+
case 'chrome_type':
|
|
489
|
+
result = await this.client.type(args.selector, args.text, args.tabId);
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
// JavaScript Execution
|
|
493
|
+
case 'chrome_execute_js':
|
|
494
|
+
result = await this.client.executeJS(args.code, args.tabId);
|
|
495
|
+
break;
|
|
496
|
+
|
|
497
|
+
case 'chrome_call_helper':
|
|
498
|
+
result = await this.client.callHelper(args.functionName, args.args, args.tabId);
|
|
499
|
+
break;
|
|
500
|
+
|
|
501
|
+
// Screenshots
|
|
502
|
+
case 'chrome_capture_screenshot':
|
|
503
|
+
result = await this.client.captureScreenshot({
|
|
504
|
+
format: args.format || 'png',
|
|
505
|
+
quality: args.quality || 90
|
|
506
|
+
});
|
|
507
|
+
break;
|
|
508
|
+
|
|
509
|
+
// Script Injection
|
|
510
|
+
case 'chrome_register_injection':
|
|
511
|
+
result = await this.client.registerInjection(
|
|
512
|
+
args.id,
|
|
513
|
+
args.code,
|
|
514
|
+
args.matches,
|
|
515
|
+
args.runAt || 'document_idle'
|
|
516
|
+
);
|
|
517
|
+
break;
|
|
518
|
+
|
|
519
|
+
case 'chrome_unregister_injection':
|
|
520
|
+
result = await this.client.unregisterInjection(args.id);
|
|
521
|
+
break;
|
|
522
|
+
|
|
523
|
+
default:
|
|
524
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
this.log(`Tool result for ${name}:`, result);
|
|
528
|
+
return result;
|
|
529
|
+
|
|
530
|
+
} catch (error) {
|
|
531
|
+
this.log(`Tool error for ${name}:`, error.message);
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Handle incoming MCP request
|
|
538
|
+
*/
|
|
539
|
+
async handleRequest(request) {
|
|
540
|
+
const { id, method, params } = request;
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
switch (method) {
|
|
544
|
+
case 'initialize':
|
|
545
|
+
this.sendResponse({
|
|
546
|
+
jsonrpc: '2.0',
|
|
547
|
+
id,
|
|
548
|
+
result: {
|
|
549
|
+
protocolVersion: '2024-11-05',
|
|
550
|
+
capabilities: {
|
|
551
|
+
tools: {}
|
|
552
|
+
},
|
|
553
|
+
serverInfo: {
|
|
554
|
+
name: 'chrome-link',
|
|
555
|
+
version: '1.2.0'
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
break;
|
|
560
|
+
|
|
561
|
+
case 'tools/list':
|
|
562
|
+
this.sendResponse({
|
|
563
|
+
jsonrpc: '2.0',
|
|
564
|
+
id,
|
|
565
|
+
result: this.getTools()
|
|
566
|
+
});
|
|
567
|
+
break;
|
|
568
|
+
|
|
569
|
+
case 'tools/call':
|
|
570
|
+
const result = await this.handleToolCall(params.name, params.arguments || {});
|
|
571
|
+
this.sendResponse({
|
|
572
|
+
jsonrpc: '2.0',
|
|
573
|
+
id,
|
|
574
|
+
result: {
|
|
575
|
+
content: [
|
|
576
|
+
{
|
|
577
|
+
type: 'text',
|
|
578
|
+
text: JSON.stringify(result, null, 2)
|
|
579
|
+
}
|
|
580
|
+
]
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
break;
|
|
584
|
+
|
|
585
|
+
default:
|
|
586
|
+
this.sendError(id, -32601, `Method not found: ${method}`);
|
|
587
|
+
}
|
|
588
|
+
} catch (error) {
|
|
589
|
+
this.sendError(id, -32603, error.message);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Start the MCP server
|
|
595
|
+
*/
|
|
596
|
+
async start() {
|
|
597
|
+
this.log('Starting MCP server...');
|
|
598
|
+
|
|
599
|
+
// Connect to browser-link-server
|
|
600
|
+
await this.initialize();
|
|
601
|
+
|
|
602
|
+
// Handle stdin for MCP protocol
|
|
603
|
+
let buffer = '';
|
|
604
|
+
process.stdin.on('data', (chunk) => {
|
|
605
|
+
buffer += chunk.toString();
|
|
606
|
+
|
|
607
|
+
// Process complete JSON-RPC messages
|
|
608
|
+
const lines = buffer.split('\n');
|
|
609
|
+
buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
610
|
+
|
|
611
|
+
for (const line of lines) {
|
|
612
|
+
if (line.trim()) {
|
|
613
|
+
try {
|
|
614
|
+
const request = JSON.parse(line);
|
|
615
|
+
this.handleRequest(request);
|
|
616
|
+
} catch (error) {
|
|
617
|
+
this.log('Failed to parse request:', error.message);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
process.stdin.on('end', () => {
|
|
624
|
+
this.log('Stdin closed, shutting down...');
|
|
625
|
+
this.shutdown();
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Handle process signals
|
|
629
|
+
process.on('SIGINT', () => {
|
|
630
|
+
this.log('Received SIGINT, shutting down...');
|
|
631
|
+
this.shutdown();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
process.on('SIGTERM', () => {
|
|
635
|
+
this.log('Received SIGTERM, shutting down...');
|
|
636
|
+
this.shutdown();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
this.log('MCP server started successfully');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Shutdown the server
|
|
644
|
+
*/
|
|
645
|
+
shutdown() {
|
|
646
|
+
if (this.client) {
|
|
647
|
+
this.client.close();
|
|
648
|
+
}
|
|
649
|
+
process.exit(0);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Start the server
|
|
654
|
+
const server = new MCPServer();
|
|
655
|
+
server.start().catch((error) => {
|
|
656
|
+
console.error('[MCP Server] Fatal error:', error);
|
|
657
|
+
process.exit(1);
|
|
658
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aikeymouse/chromelink-mcp",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Model Context Protocol (MCP) server for Chrome browser automation via AI agents",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"chromelink-mcp": "./bin/chromelink-mcp"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node index.js",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
+
"prepublishOnly": "node scripts/prepare-publish.js",
|
|
13
|
+
"postpublish": "node scripts/restore-publish.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"chrome",
|
|
19
|
+
"browser-automation",
|
|
20
|
+
"ai",
|
|
21
|
+
"claude",
|
|
22
|
+
"gpt",
|
|
23
|
+
"automation",
|
|
24
|
+
"testing",
|
|
25
|
+
"websocket"
|
|
26
|
+
],
|
|
27
|
+
"author": "aikeymouse",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/aikeymouse/chrome-link.git",
|
|
32
|
+
"directory": "mcp-server"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/aikeymouse/chrome-link#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/aikeymouse/chrome-link/issues"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=16.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@aikeymouse/chromelink-client": "^1.2.0",
|
|
43
|
+
"ws": "^8.16.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {},
|
|
46
|
+
"files": [
|
|
47
|
+
"index.js",
|
|
48
|
+
"bin/",
|
|
49
|
+
"scripts/",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public",
|
|
55
|
+
"provenance": true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prepare package.json for publishing to npm
|
|
5
|
+
*
|
|
6
|
+
* This script runs before `npm publish` to:
|
|
7
|
+
* 1. Backup the current package.json
|
|
8
|
+
* 2. Replace local file dependency with npm version dependency
|
|
9
|
+
*
|
|
10
|
+
* The local dependency "@aikeymouse/chromelink-client": "file:../clients/node"
|
|
11
|
+
* is replaced with "@aikeymouse/chromelink-client": "^{VERSION}"
|
|
12
|
+
* where VERSION comes from the VERSION file in the project root.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const PACKAGE_JSON = path.join(__dirname, '..', 'package.json');
|
|
19
|
+
const BACKUP_FILE = PACKAGE_JSON + '.bak';
|
|
20
|
+
const VERSION_FILE = path.join(__dirname, '..', '..', 'VERSION');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Read VERSION file
|
|
24
|
+
if (!fs.existsSync(VERSION_FILE)) {
|
|
25
|
+
console.error('ERROR: VERSION file not found at', VERSION_FILE);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const version = fs.readFileSync(VERSION_FILE, 'utf8').trim();
|
|
30
|
+
if (!version.match(/^\d+\.\d+\.\d+$/)) {
|
|
31
|
+
console.error('ERROR: Invalid version format in VERSION file:', version);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`Preparing package.json for npm publish (version: ${version})`);
|
|
36
|
+
|
|
37
|
+
// Read package.json
|
|
38
|
+
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf8'));
|
|
39
|
+
|
|
40
|
+
// Backup original package.json
|
|
41
|
+
fs.writeFileSync(BACKUP_FILE, JSON.stringify(packageJson, null, 2) + '\n');
|
|
42
|
+
console.log('✓ Backed up package.json');
|
|
43
|
+
|
|
44
|
+
// Replace file dependency with npm version
|
|
45
|
+
if (packageJson.dependencies && packageJson.dependencies['@aikeymouse/chromelink-client']) {
|
|
46
|
+
const oldDep = packageJson.dependencies['@aikeymouse/chromelink-client'];
|
|
47
|
+
packageJson.dependencies['@aikeymouse/chromelink-client'] = `^${version}`;
|
|
48
|
+
console.log(`✓ Updated dependency: ${oldDep} → ^${version}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Write updated package.json
|
|
52
|
+
fs.writeFileSync(PACKAGE_JSON, JSON.stringify(packageJson, null, 2) + '\n');
|
|
53
|
+
console.log('✓ Package.json ready for publishing');
|
|
54
|
+
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('ERROR preparing package.json:', error.message);
|
|
57
|
+
|
|
58
|
+
// Try to restore from backup if it exists
|
|
59
|
+
if (fs.existsSync(BACKUP_FILE)) {
|
|
60
|
+
try {
|
|
61
|
+
fs.copyFileSync(BACKUP_FILE, PACKAGE_JSON);
|
|
62
|
+
fs.unlinkSync(BACKUP_FILE);
|
|
63
|
+
console.log('✓ Restored package.json from backup');
|
|
64
|
+
} catch (restoreError) {
|
|
65
|
+
console.error('ERROR restoring backup:', restoreError.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Restore package.json after publishing to npm
|
|
5
|
+
*
|
|
6
|
+
* This script runs after `npm publish` to:
|
|
7
|
+
* 1. Restore package.json from backup
|
|
8
|
+
* 2. Clean up backup file
|
|
9
|
+
*
|
|
10
|
+
* This ensures the local file dependency is restored for development.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const PACKAGE_JSON = path.join(__dirname, '..', 'package.json');
|
|
17
|
+
const BACKUP_FILE = PACKAGE_JSON + '.bak';
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
if (!fs.existsSync(BACKUP_FILE)) {
|
|
21
|
+
console.log('No backup file found, skipping restore');
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log('Restoring package.json from backup');
|
|
26
|
+
|
|
27
|
+
// Restore from backup
|
|
28
|
+
const backup = fs.readFileSync(BACKUP_FILE, 'utf8');
|
|
29
|
+
fs.writeFileSync(PACKAGE_JSON, backup);
|
|
30
|
+
console.log('✓ Restored package.json');
|
|
31
|
+
|
|
32
|
+
// Clean up backup file
|
|
33
|
+
fs.unlinkSync(BACKUP_FILE);
|
|
34
|
+
console.log('✓ Removed backup file');
|
|
35
|
+
|
|
36
|
+
console.log('✓ Package.json restored to development state');
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('ERROR restoring package.json:', error.message);
|
|
40
|
+
console.error('You may need to manually restore from', BACKUP_FILE);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|