mirror of
https://github.com/AIPexStudio/AIPex.git
synced 2026-05-13 18:51:35 +00:00
feat: update MCP bridge to support multiple clients and enhance functionality
- Bumped version to 3.1.0 and updated the description to reflect new capabilities. - Introduced a shared daemon for handling multiple simultaneous clients, improving performance. - Added new CLI commands and updated the README for better user guidance. - Enhanced the package.json to include new dependencies and scripts for the daemon and CLI. - Refactored the bridge architecture to utilize StreamableHTTP for improved communication with AI agents. These changes significantly enhance the usability and functionality of the MCP bridge.
This commit is contained in:
1
mcp-bridge/.npmrc
Normal file
1
mcp-bridge/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
registry=https://registry.npmjs.org
|
||||
@@ -1,44 +1,40 @@
|
||||
# aipex-mcp-bridge
|
||||
|
||||
MCP bridge that connects AI agents to the [AIPex](https://aipex.ai) browser extension via WebSocket.
|
||||
|
||||
Works with **any** MCP client that supports stdio transport — Cursor, Claude Desktop, Claude Code, VS Code Copilot, Windsurf, Zed, and more.
|
||||
MCP server that connects AI agents to the [AIPex](https://aipex.ai) browser extension. Supports **multiple simultaneous clients** (Cursor, Claude Code, VS Code Copilot, etc.) via StreamableHTTP.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
AI Agent (MCP client) ──stdio──▶ aipex-mcp-bridge ──WebSocket──▶ AIPex Chrome Extension
|
||||
Cursor ──HTTP POST /mcp──┐
|
||||
Claude Code ──HTTP POST /mcp──┤── aipex-mcp-server ──WebSocket──▶ AIPex Chrome Extension
|
||||
VS Code ──HTTP POST /mcp──┘
|
||||
```
|
||||
|
||||
The bridge starts a WebSocket server on `localhost:9223` (configurable) and communicates with your AI agent over stdio using the MCP protocol. The AIPex extension connects to the WebSocket server to expose browser control tools.
|
||||
The server runs on `localhost:9223` and provides:
|
||||
|
||||
- **`/mcp`** — StreamableHTTP endpoint for MCP clients
|
||||
- **`/extension`** — WebSocket endpoint for the AIPex Chrome extension
|
||||
- **`/health`** — Health check endpoint
|
||||
|
||||
## Quick start
|
||||
|
||||
### 1. Configure your AI agent
|
||||
### 1. Start the server
|
||||
|
||||
Add the following to your agent's MCP configuration:
|
||||
|
||||
**Cursor** (`.cursor/mcp.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"aipex-browser": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "aipex-mcp-bridge"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```bash
|
||||
npx aipex-mcp-server
|
||||
```
|
||||
|
||||
**Claude Desktop** (`claude_desktop_config.json`):
|
||||
The server stays running and handles all AI agent connections.
|
||||
|
||||
### 2. Configure your AI agent
|
||||
|
||||
**Cursor** (`.cursor/mcp.json` or `~/.cursor/mcp.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"aipex-browser": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "aipex-mcp-bridge"]
|
||||
"url": "http://localhost:9223/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +43,7 @@ Add the following to your agent's MCP configuration:
|
||||
**Claude Code**:
|
||||
|
||||
```bash
|
||||
claude mcp add aipex-browser -- npx -y aipex-mcp-bridge
|
||||
claude mcp add --transport http aipex-browser http://localhost:9223/mcp
|
||||
```
|
||||
|
||||
**VS Code Copilot** (`.vscode/mcp.json`):
|
||||
@@ -56,63 +52,119 @@ claude mcp add aipex-browser -- npx -y aipex-mcp-bridge
|
||||
{
|
||||
"servers": {
|
||||
"aipex-browser": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "aipex-mcp-bridge"]
|
||||
"url": "http://localhost:9223/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Windsurf** (`mcp_config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"aipex-browser": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "aipex-mcp-bridge"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Connect AIPex extension
|
||||
### 3. Connect AIPex extension
|
||||
|
||||
1. Open Chrome → AIPex extension → Options page
|
||||
2. Set WebSocket URL to `ws://localhost:9223`
|
||||
2. Set WebSocket URL to `ws://localhost:9223/extension`
|
||||
3. Click **Connect**
|
||||
|
||||
Your AI agent can now control the browser through AIPex.
|
||||
Your AI agents can now control the browser through AIPex — all simultaneously.
|
||||
|
||||
## Options
|
||||
|
||||
```
|
||||
npx aipex-mcp-bridge [--port <port>]
|
||||
npx aipex-mcp-server [--port <port>] [--host <host>]
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
| ----------------- | ------- | ---------------------------------- |
|
||||
| `--port <port>` | `9223` | WebSocket port for AIPex extension |
|
||||
| `--help`, `-h` | | Show help message |
|
||||
| `--version`, `-v` | | Show version |
|
||||
| Option | Default | Description |
|
||||
| ----------------- | ----------- | ----------------------------------------------------------- |
|
||||
| `--port <port>` | `9223` | Server port |
|
||||
| `--host <host>` | `127.0.0.1` | Bind address (`0.0.0.0` to allow remote/Docker connections) |
|
||||
| `--help`, `-h` | | Show help message |
|
||||
| `--version`, `-v` | | Show version |
|
||||
|
||||
### Custom port example
|
||||
---
|
||||
|
||||
## Stdio Bridge (backward compatibility)
|
||||
|
||||
For MCP clients that only support stdio transport, a thin bridge is included:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"aipex-browser": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "aipex-mcp-bridge", "--port", "8080"]
|
||||
"args": ["-y", "aipex-mcp-bridge"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The stdio bridge forwards tool calls to the HTTP server at `http://localhost:9223/mcp`. The server must be running separately.
|
||||
|
||||
---
|
||||
|
||||
## AIPex CLI
|
||||
|
||||
Command-line tool for controlling the browser directly from the terminal.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
aipex-cli <tool_name> [--param value ...]
|
||||
aipex-cli --list # List all tools
|
||||
aipex-cli --help <tool_name> # Show tool parameters
|
||||
aipex-cli --json '{"name":"...","arguments":{...}}' # Raw JSON
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
aipex-cli get_all_tabs
|
||||
aipex-cli create_new_tab --url https://example.com
|
||||
aipex-cli search_elements --tabId 123 --query "button*"
|
||||
aipex-cli click --tabId 123 --uid btn-42
|
||||
aipex-cli capture_screenshot
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------------- | --------------------------- | ---------------------- |
|
||||
| `AIPEX_SERVER_URL` | `http://localhost:9223/mcp` | HTTP server URL |
|
||||
| `AIPEX_WS_URL` | `ws://localhost:9223/cli` | WebSocket fallback URL |
|
||||
| `AIPEX_CONNECT_TIMEOUT` | `60000` | Max ms to wait |
|
||||
|
||||
---
|
||||
|
||||
## Docker Image
|
||||
|
||||
```bash
|
||||
docker pull butterman2/aipex-browser:latest
|
||||
docker run -d --name aipex --shm-size=2g \
|
||||
-p 9223:9223 -p 5900:5900 -p 6080:6080 \
|
||||
butterman2/aipex-browser:latest
|
||||
```
|
||||
|
||||
| Port | Service |
|
||||
| ---- | ---------------------- |
|
||||
| 9223 | MCP Server (HTTP + WS) |
|
||||
| 5900 | VNC |
|
||||
| 6080 | noVNC (web-based) |
|
||||
|
||||
## Migration from v2.x
|
||||
|
||||
v3.0 replaces the daemon+proxy architecture with a single HTTP server:
|
||||
|
||||
| v2.x (daemon) | v3.0 (HTTP server) |
|
||||
| ------------------------------------------- | ----------------------------------------------------- |
|
||||
| `npx aipex-mcp-bridge` (stdio per IDE) | `npx aipex-mcp-server` (one server) |
|
||||
| Each IDE spawns its own bridge process | All IDEs connect to one HTTP endpoint |
|
||||
| Daemon with PID files, idle timeout | Standard HTTP server, no background process |
|
||||
| Extension connects to `ws://localhost:9223` | Extension connects to `ws://localhost:9223/extension` |
|
||||
|
||||
**Breaking change**: The AIPex extension WebSocket URL changed from `ws://localhost:9223` to `ws://localhost:9223/extension`. Update the URL in AIPex extension Options.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js >= 18
|
||||
- AIPex Chrome extension installed
|
||||
- AIPex Chrome extension installed (not needed for Docker image)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"name": "aipex-mcp-bridge",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP bridge that connects AI agents (Cursor, Claude, VS Code Copilot, etc.) to the AIPex browser extension via WebSocket",
|
||||
"version": "3.1.0",
|
||||
"description": "MCP bridge that connects AI agents (Cursor, Claude Code, VS Code Copilot, etc.) to the AIPex browser extension. Auto-spawns a shared daemon to support multiple simultaneous clients.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"aipex-mcp-bridge": "./dist/bridge.js"
|
||||
"aipex-mcp-bridge": "./dist/bridge.js",
|
||||
"aipex-mcp-daemon": "./dist/daemon.js",
|
||||
"aipex-cli": "./dist/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -12,9 +14,11 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsx src/bridge.ts"
|
||||
"dev": "tsx src/bridge.ts",
|
||||
"dev:daemon": "tsx src/daemon.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.28.0",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
736
mcp-bridge/pnpm-lock.yaml
generated
736
mcp-bridge/pnpm-lock.yaml
generated
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: ^1.28.0
|
||||
version: 1.28.0(zod@4.3.6)
|
||||
ws:
|
||||
specifier: ^8.18.0
|
||||
version: 8.19.0
|
||||
@@ -186,6 +189,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@hono/node-server@1.19.11':
|
||||
resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==}
|
||||
engines: {node: '>=18.14.1'}
|
||||
peerDependencies:
|
||||
hono: ^4
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
@@ -199,6 +208,16 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.28.0':
|
||||
resolution: {integrity: sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@cfworker/json-schema': ^4.1.1
|
||||
zod: ^3.25 || ^4.0
|
||||
peerDependenciesMeta:
|
||||
'@cfworker/json-schema':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.59.0':
|
||||
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
|
||||
cpu: [arm]
|
||||
@@ -333,24 +352,55 @@ packages:
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ajv-formats@3.0.1:
|
||||
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
||||
peerDependencies:
|
||||
ajv: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
ajv:
|
||||
optional: true
|
||||
|
||||
ajv@8.18.0:
|
||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
body-parser@2.2.2:
|
||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
bundle-require@5.1.0:
|
||||
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
peerDependencies:
|
||||
esbuild: '>=0.18'
|
||||
|
||||
bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-bound@1.0.4:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
@@ -366,6 +416,30 @@ packages:
|
||||
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
|
||||
engines: {node: ^14.18.0 || >=16.10.0}
|
||||
|
||||
content-disposition@1.0.1:
|
||||
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
content-type@1.0.5:
|
||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
cookie-signature@1.2.2:
|
||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||
engines: {node: '>=6.6.0'}
|
||||
|
||||
cookie@0.7.2:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
cors@2.8.6:
|
||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -375,11 +449,69 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
encodeurl@2.0.0:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-errors@1.3.0:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
escape-html@1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
|
||||
etag@1.8.1:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
eventsource-parser@3.0.6:
|
||||
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
eventsource@3.0.7:
|
||||
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
express-rate-limit@8.3.0:
|
||||
resolution: {integrity: sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
express: '>= 4.11'
|
||||
|
||||
express@5.2.1:
|
||||
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-uri@3.1.0:
|
||||
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -389,21 +521,94 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
finalhandler@2.1.1:
|
||||
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
|
||||
engines: {node: '>= 18.0.0'}
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
fresh@2.0.0:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-tsconfig@4.13.6:
|
||||
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hono@4.12.5:
|
||||
resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
iconv-lite@0.7.2:
|
||||
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ip-address@10.1.0:
|
||||
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
is-promise@4.0.0:
|
||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
jose@6.2.0:
|
||||
resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==}
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-schema-typed@8.0.2:
|
||||
resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
|
||||
|
||||
lilconfig@3.1.3:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -418,6 +623,26 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
media-typer@1.1.0:
|
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
merge-descriptors@2.0.0:
|
||||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mime-db@1.54.0:
|
||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@3.0.2:
|
||||
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mlly@1.8.0:
|
||||
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
||||
|
||||
@@ -427,10 +652,36 @@ packages:
|
||||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
object-inspect@1.13.4:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
on-finished@2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-to-regexp@8.3.0:
|
||||
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
@@ -445,6 +696,10 @@ packages:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
pkce-challenge@5.0.1:
|
||||
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
@@ -466,10 +721,30 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
qs@6.15.0:
|
||||
resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
raw-body@3.0.2:
|
||||
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-from@5.0.0:
|
||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -482,10 +757,56 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
router@2.2.0:
|
||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
send@1.2.1:
|
||||
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
serve-static@2.2.1:
|
||||
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shebang-regex@3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel-map@1.0.1:
|
||||
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel-weakmap@1.0.2:
|
||||
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel@1.1.0:
|
||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
source-map@0.7.6:
|
||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
statuses@2.0.2:
|
||||
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
sucrase@3.35.1:
|
||||
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -505,6 +826,10 @@ packages:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
toidentifier@1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
tree-kill@1.2.2:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
@@ -536,6 +861,10 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
type-is@2.0.1:
|
||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -547,6 +876,22 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
unpipe@1.0.0:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
ws@8.19.0:
|
||||
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -559,6 +904,14 @@ packages:
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
zod-to-json-schema@3.25.1:
|
||||
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
|
||||
peerDependencies:
|
||||
zod: ^3.25 || ^4
|
||||
|
||||
zod@4.3.6:
|
||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
@@ -639,6 +992,10 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@hono/node-server@1.19.11(hono@4.12.5)':
|
||||
dependencies:
|
||||
hono: 4.12.5
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
@@ -653,6 +1010,28 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@modelcontextprotocol/sdk@1.28.0(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.11(hono@4.12.5)
|
||||
ajv: 8.18.0
|
||||
ajv-formats: 3.0.1(ajv@8.18.0)
|
||||
content-type: 1.0.5
|
||||
cors: 2.8.6
|
||||
cross-spawn: 7.0.6
|
||||
eventsource: 3.0.7
|
||||
eventsource-parser: 3.0.6
|
||||
express: 5.2.1
|
||||
express-rate-limit: 8.3.0(express@5.2.1)
|
||||
hono: 4.12.5
|
||||
jose: 6.2.0
|
||||
json-schema-typed: 8.0.2
|
||||
pkce-challenge: 5.0.1
|
||||
raw-body: 3.0.2
|
||||
zod: 4.3.6
|
||||
zod-to-json-schema: 3.25.1(zod@4.3.6)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.59.0':
|
||||
optional: true
|
||||
|
||||
@@ -738,17 +1117,59 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.19.13
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.2
|
||||
negotiator: 1.0.0
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
ajv-formats@3.0.1(ajv@8.18.0):
|
||||
optionalDependencies:
|
||||
ajv: 8.18.0
|
||||
|
||||
ajv@8.18.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.0
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
body-parser@2.2.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.5
|
||||
debug: 4.4.3
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.7.2
|
||||
on-finished: 2.4.1
|
||||
qs: 6.15.0
|
||||
raw-body: 3.0.2
|
||||
type-is: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
bundle-require@5.1.0(esbuild@0.27.3):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
load-tsconfig: 0.2.5
|
||||
|
||||
bytes@3.1.2: {}
|
||||
|
||||
cac@6.7.14: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
call-bound@1.0.4:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
@@ -759,10 +1180,49 @@ snapshots:
|
||||
|
||||
consola@3.4.2: {}
|
||||
|
||||
content-disposition@1.0.1: {}
|
||||
|
||||
content-type@1.0.5: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
cors@2.8.6:
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
vary: 1.1.2
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
@@ -792,25 +1252,150 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.27.3
|
||||
'@esbuild/win32-x64': 0.27.3
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
eventsource-parser@3.0.6: {}
|
||||
|
||||
eventsource@3.0.7:
|
||||
dependencies:
|
||||
eventsource-parser: 3.0.6
|
||||
|
||||
express-rate-limit@8.3.0(express@5.2.1):
|
||||
dependencies:
|
||||
express: 5.2.1
|
||||
ip-address: 10.1.0
|
||||
|
||||
express@5.2.1:
|
||||
dependencies:
|
||||
accepts: 2.0.0
|
||||
body-parser: 2.2.2
|
||||
content-disposition: 1.0.1
|
||||
content-type: 1.0.5
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.2.2
|
||||
debug: 4.4.3
|
||||
depd: 2.0.0
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
finalhandler: 2.1.1
|
||||
fresh: 2.0.0
|
||||
http-errors: 2.0.1
|
||||
merge-descriptors: 2.0.0
|
||||
mime-types: 3.0.2
|
||||
on-finished: 2.4.1
|
||||
once: 1.4.0
|
||||
parseurl: 1.3.3
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.15.0
|
||||
range-parser: 1.2.1
|
||||
router: 2.2.0
|
||||
send: 1.2.1
|
||||
serve-static: 2.2.1
|
||||
statuses: 2.0.2
|
||||
type-is: 2.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-uri@3.1.0: {}
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
finalhandler@2.1.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
statuses: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
dependencies:
|
||||
magic-string: 0.30.21
|
||||
mlly: 1.8.0
|
||||
rollup: 4.59.0
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
get-tsconfig@4.13.6:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hono@4.12.5: {}
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
inherits: 2.0.4
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.2
|
||||
toidentifier: 1.0.1
|
||||
|
||||
iconv-lite@0.7.2:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ip-address@10.1.0: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
is-promise@4.0.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jose@6.2.0: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-schema-typed@8.0.2: {}
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
@@ -821,6 +1406,18 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
merge-descriptors@2.0.0: {}
|
||||
|
||||
mime-db@1.54.0: {}
|
||||
|
||||
mime-types@3.0.2:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
|
||||
mlly@1.8.0:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
@@ -836,8 +1433,26 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
thenify-all: 1.6.0
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-to-regexp@8.3.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@@ -846,6 +1461,8 @@ snapshots:
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pkce-challenge@5.0.1: {}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
@@ -858,8 +1475,28 @@ snapshots:
|
||||
optionalDependencies:
|
||||
tsx: 4.21.0
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
|
||||
qs@6.15.0:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
raw-body@3.0.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.1
|
||||
iconv-lite: 0.7.2
|
||||
unpipe: 1.0.0
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
@@ -895,8 +1532,83 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.59.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
router@2.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
depd: 2.0.0
|
||||
is-promise: 4.0.0
|
||||
parseurl: 1.3.3
|
||||
path-to-regexp: 8.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
send@1.2.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
fresh: 2.0.0
|
||||
http-errors: 2.0.1
|
||||
mime-types: 3.0.2
|
||||
ms: 2.1.3
|
||||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
statuses: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
serve-static@2.2.1:
|
||||
dependencies:
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 1.2.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
side-channel-map@1.0.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
side-channel-weakmap@1.0.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-map: 1.0.1
|
||||
|
||||
side-channel@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-list: 1.0.0
|
||||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
source-map@0.7.6: {}
|
||||
|
||||
statuses@2.0.2: {}
|
||||
|
||||
sucrase@3.35.1:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
@@ -922,6 +1634,8 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
@@ -960,10 +1674,32 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
type-is@2.0.1:
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
media-typer: 1.1.0
|
||||
mime-types: 3.0.2
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
ufo@1.6.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
ws@8.19.0: {}
|
||||
|
||||
zod-to-json-schema@3.25.1(zod@4.3.6):
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
zod@4.3.6: {}
|
||||
|
||||
@@ -1,355 +1,363 @@
|
||||
/**
|
||||
* AIPex MCP Bridge
|
||||
*
|
||||
* A stdio MCP server that bridges AI agents to the AIPex Chrome extension via WebSocket.
|
||||
* A stdio MCP server that auto-starts a shared daemon and relays tool calls
|
||||
* to the AIPex Chrome extension through it.
|
||||
*
|
||||
* Agent (MCP client) ──stdio──▶ this bridge ──WebSocket──▶ AIPex extension (MCP server)
|
||||
* Architecture:
|
||||
*
|
||||
* IDE ──stdio──▶ this bridge ──WS /bridge──▶ daemon ──WS /extension──▶ AIPex extension
|
||||
*
|
||||
* On startup:
|
||||
* 1. Try connecting to existing daemon at ws://localhost:<port>/bridge
|
||||
* 2. If no daemon running, spawn one as a detached background process
|
||||
* 3. Retry connection with backoff
|
||||
* 4. Forward all tool calls over WebSocket
|
||||
*
|
||||
* Multiple bridge instances share one daemon (multi-client support).
|
||||
*
|
||||
* Usage:
|
||||
* npx aipex-mcp-bridge [--port 9223]
|
||||
*
|
||||
* Works with any MCP client that supports stdio transport:
|
||||
* - Cursor, Claude Desktop, Claude Code, VS Code Copilot, Windsurf, Zed, etc.
|
||||
*/
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import { createInterface } from "node:readline";
|
||||
import { WebSocket, WebSocketServer } from "ws";
|
||||
import { fork } from "node:child_process"
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema
|
||||
} from "@modelcontextprotocol/sdk/types.js"
|
||||
import { WebSocket } from "ws"
|
||||
|
||||
import { toolSchemas } from "./tool-schemas.js"
|
||||
|
||||
// ── CLI args ────────────────────────────────────────────────────────────────
|
||||
|
||||
const cliArgs = process.argv.slice(2);
|
||||
const cliArgs = process.argv.slice(2)
|
||||
|
||||
if (cliArgs.includes("--help") || cliArgs.includes("-h")) {
|
||||
process.stderr.write(`
|
||||
AIPex MCP Bridge — connect AI agents to AIPex browser extension
|
||||
|
||||
Usage:
|
||||
npx aipex-mcp-bridge [--port <port>]
|
||||
npx aipex-mcp-bridge [--port <port>] [--host <host>]
|
||||
|
||||
Options:
|
||||
--port <port> WebSocket port for AIPex extension (default: 9223)
|
||||
--port <port> Daemon port (default: 9223)
|
||||
--host <host> Daemon host (default: 127.0.0.1)
|
||||
--help, -h Show this help message
|
||||
--version, -v Show version
|
||||
|
||||
After starting, open AIPex extension Options and connect to:
|
||||
ws://localhost:<port>
|
||||
`);
|
||||
process.exit(0);
|
||||
The bridge auto-starts a background daemon if one isn't already running.
|
||||
Multiple IDE instances (Cursor, Claude Code) can run simultaneously.
|
||||
|
||||
After starting, connect AIPex extension → Options → ws://localhost:<port>/extension
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (cliArgs.includes("--version") || cliArgs.includes("-v")) {
|
||||
process.stderr.write("aipex-mcp-bridge 1.0.0\n");
|
||||
process.exit(0);
|
||||
process.stderr.write("aipex-mcp-bridge 3.1.0\n")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const portIdx = cliArgs.indexOf("--port");
|
||||
const WS_PORT = portIdx !== -1 ? parseInt(cliArgs[portIdx + 1], 10) : 9223;
|
||||
|
||||
if (Number.isNaN(WS_PORT) || WS_PORT < 1 || WS_PORT > 65535) {
|
||||
process.stderr.write(`Invalid port number. Must be between 1 and 65535.\n`);
|
||||
process.exit(1);
|
||||
function getArg(name: string, fallback: string): string {
|
||||
const idx = cliArgs.indexOf(name)
|
||||
return idx !== -1 && cliArgs[idx + 1] ? cliArgs[idx + 1] : fallback
|
||||
}
|
||||
|
||||
// ── Logging (stderr only — stdout is reserved for MCP protocol) ─────────────
|
||||
const PORT = parseInt(getArg("--port", "9223"), 10)
|
||||
const HOST = getArg("--host", "127.0.0.1")
|
||||
const DAEMON_URL = `ws://${HOST}:${PORT}/bridge`
|
||||
const MAX_CONNECT_ATTEMPTS = 10
|
||||
const INITIAL_BACKOFF_MS = 300
|
||||
const TOOL_CALL_TIMEOUT_MS = 60_000
|
||||
|
||||
// ── Logging (stderr only — stdout reserved for MCP) ─────────────────────────
|
||||
|
||||
function log(msg: string) {
|
||||
process.stderr.write(`[aipex-bridge] ${msg}\n`);
|
||||
process.stderr.write(`[aipex-bridge] ${msg}\n`)
|
||||
}
|
||||
|
||||
// ── JSON-RPC types ──────────────────────────────────────────────────────────
|
||||
// ── Daemon connection ───────────────────────────────────────────────────────
|
||||
|
||||
interface JSONRPCRequest {
|
||||
jsonrpc: "2.0";
|
||||
id: number | string | null;
|
||||
method: string;
|
||||
params?: unknown;
|
||||
let daemonWs: WebSocket | undefined
|
||||
let nextReqId = 1
|
||||
|
||||
interface PendingCall {
|
||||
resolve: (result: unknown) => void
|
||||
reject: (error: Error) => void
|
||||
timer: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
interface JSONRPCResponse {
|
||||
jsonrpc: "2.0";
|
||||
id: number | string | null;
|
||||
result?: unknown;
|
||||
error?: { code: number; message: string };
|
||||
const pendingCalls = new Map<number, PendingCall>()
|
||||
|
||||
function isDaemonConnected(): boolean {
|
||||
return !!daemonWs && daemonWs.readyState === WebSocket.OPEN
|
||||
}
|
||||
|
||||
type JSONRPCMessage = JSONRPCRequest | JSONRPCResponse;
|
||||
|
||||
interface McpTool {
|
||||
name: string;
|
||||
description?: string;
|
||||
inputSchema?: unknown;
|
||||
}
|
||||
|
||||
// ── AIPex WebSocket connection state ────────────────────────────────────────
|
||||
|
||||
let aipexSocket: WebSocket | null = null;
|
||||
let aipexReady = false;
|
||||
let cachedTools: McpTool[] = [];
|
||||
|
||||
let nextAipexId = 1;
|
||||
const aipexPending = new Map<
|
||||
number | string,
|
||||
{ resolve: (v: unknown) => void; reject: (e: Error) => void }
|
||||
>();
|
||||
|
||||
// ── Respond to MCP client (stdout, JSON-RPC 2.0) ───────────────────────────
|
||||
|
||||
function respond(id: number | string | null, result: unknown) {
|
||||
const msg: JSONRPCResponse = { jsonrpc: "2.0", id, result };
|
||||
process.stdout.write(`${JSON.stringify(msg)}\n`);
|
||||
}
|
||||
|
||||
function respondError(
|
||||
id: number | string | null,
|
||||
code: number,
|
||||
message: string,
|
||||
) {
|
||||
const msg: JSONRPCResponse = { jsonrpc: "2.0", id, error: { code, message } };
|
||||
process.stdout.write(`${JSON.stringify(msg)}\n`);
|
||||
}
|
||||
|
||||
// ── Send requests to AIPex (WebSocket) ──────────────────────────────────────
|
||||
|
||||
function sendToAipex(method: string, params: unknown = {}): Promise<unknown> {
|
||||
if (!aipexSocket || aipexSocket.readyState !== WebSocket.OPEN) {
|
||||
return Promise.reject(new Error("AIPex extension not connected"));
|
||||
function sendToolCallToDaemon(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
if (!isDaemonConnected()) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"Not connected to AIPex daemon. The daemon may have stopped.\n" +
|
||||
"Restart the bridge or check if port " +
|
||||
PORT +
|
||||
" is available."
|
||||
)
|
||||
)
|
||||
}
|
||||
const id = nextAipexId++;
|
||||
const msg = { jsonrpc: "2.0", id, method, params };
|
||||
aipexSocket.send(JSON.stringify(msg));
|
||||
|
||||
const id = nextReqId++
|
||||
const msg = {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
method: "tools/call",
|
||||
params: { name: toolName, arguments: args }
|
||||
}
|
||||
daemonWs!.send(JSON.stringify(msg))
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
aipexPending.set(id, { resolve, reject });
|
||||
});
|
||||
}
|
||||
|
||||
// ── Handle messages from AIPex ──────────────────────────────────────────────
|
||||
|
||||
function handleAipexMessage(raw: string) {
|
||||
let msg: JSONRPCMessage;
|
||||
try {
|
||||
msg = JSON.parse(raw);
|
||||
} catch {
|
||||
log(`Failed to parse AIPex message: ${raw.slice(0, 100)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("result" in msg || "error" in msg) {
|
||||
const res = msg as JSONRPCResponse;
|
||||
const p = aipexPending.get(res.id!);
|
||||
if (p) {
|
||||
aipexPending.delete(res.id!);
|
||||
if (res.error) {
|
||||
p.reject(new Error(res.error.message));
|
||||
} else {
|
||||
p.resolve(res.result);
|
||||
const timer = setTimeout(() => {
|
||||
if (pendingCalls.has(id)) {
|
||||
pendingCalls.delete(id)
|
||||
reject(
|
||||
new Error(
|
||||
`Tool '${toolName}' timed out after ${TOOL_CALL_TIMEOUT_MS}ms`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}, TOOL_CALL_TIMEOUT_MS)
|
||||
pendingCalls.set(id, { resolve, reject, timer })
|
||||
})
|
||||
}
|
||||
|
||||
function handleDaemonMessage(raw: string) {
|
||||
let msg: Record<string, unknown>
|
||||
try {
|
||||
msg = JSON.parse(raw)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const id = msg.id as number | undefined
|
||||
if (id == null) return
|
||||
|
||||
const pending = pendingCalls.get(id)
|
||||
if (!pending) return
|
||||
|
||||
clearTimeout(pending.timer)
|
||||
pendingCalls.delete(id)
|
||||
|
||||
if (msg.error) {
|
||||
const err = msg.error as { message?: string }
|
||||
pending.reject(new Error(err.message || "Daemon returned an error"))
|
||||
} else {
|
||||
pending.resolve(msg.result)
|
||||
}
|
||||
}
|
||||
|
||||
// ── MCP handshake (runs automatically when AIPex connects) ──────────────────
|
||||
|
||||
async function doAipexHandshake(socket: WebSocket) {
|
||||
log("Starting MCP handshake with AIPex...");
|
||||
|
||||
const initResult = (await sendToAipex("initialize", {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "aipex-mcp-bridge", version: "1.0.0" },
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
const serverInfo = initResult?.serverInfo as
|
||||
| Record<string, string>
|
||||
| undefined;
|
||||
log(
|
||||
`AIPex server: ${serverInfo?.name ?? "?"} v${serverInfo?.version ?? "?"}`,
|
||||
);
|
||||
|
||||
socket.send(
|
||||
JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
||||
);
|
||||
|
||||
const toolsResult = (await sendToAipex("tools/list")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
cachedTools = (toolsResult?.tools as McpTool[]) ?? [];
|
||||
aipexReady = true;
|
||||
|
||||
log(`Handshake complete. ${cachedTools.length} tools available.`);
|
||||
function rejectAllPending(reason: string) {
|
||||
for (const [, entry] of pendingCalls) {
|
||||
clearTimeout(entry.timer)
|
||||
entry.reject(new Error(reason))
|
||||
}
|
||||
pendingCalls.clear()
|
||||
}
|
||||
|
||||
// ── Handle MCP requests from the agent (stdin) ──────────────────────────────
|
||||
// ── Daemon lifecycle ────────────────────────────────────────────────────────
|
||||
|
||||
async function handleAgentRequest(req: JSONRPCRequest) {
|
||||
const { id, method, params } = req;
|
||||
function tryConnectToDaemon(): Promise<WebSocket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(DAEMON_URL)
|
||||
const timeout = setTimeout(() => {
|
||||
ws.terminate()
|
||||
reject(new Error("Connection timeout"))
|
||||
}, 3_000)
|
||||
|
||||
if (method === "initialize") {
|
||||
respond(id, {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "aipex-mcp-bridge", version: "1.0.0" },
|
||||
});
|
||||
return;
|
||||
ws.on("open", () => {
|
||||
clearTimeout(timeout)
|
||||
resolve(ws)
|
||||
})
|
||||
|
||||
ws.on("error", (err) => {
|
||||
clearTimeout(timeout)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function spawnDaemon() {
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
const daemonPath = join(__dirname, "daemon.js")
|
||||
|
||||
log(`Spawning daemon: ${daemonPath} --port ${PORT} --host ${HOST}`)
|
||||
|
||||
const child = fork(daemonPath, ["--port", String(PORT), "--host", HOST], {
|
||||
detached: true,
|
||||
stdio: "ignore"
|
||||
})
|
||||
child.unref()
|
||||
child.on("error", (err) => {
|
||||
log(`Failed to spawn daemon: ${err.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
async function connectWithAutoSpawn(): Promise<WebSocket> {
|
||||
// First, try connecting to an existing daemon
|
||||
try {
|
||||
const ws = await tryConnectToDaemon()
|
||||
log("Connected to existing daemon")
|
||||
return ws
|
||||
} catch {
|
||||
// No daemon running
|
||||
}
|
||||
|
||||
if (method === "notifications/initialized") {
|
||||
return;
|
||||
}
|
||||
// Spawn a new daemon
|
||||
log("No daemon running, spawning one...")
|
||||
spawnDaemon()
|
||||
|
||||
if (method === "tools/list") {
|
||||
if (aipexReady && cachedTools.length > 0) {
|
||||
respond(id, { tools: cachedTools });
|
||||
} else {
|
||||
respond(id, {
|
||||
tools: [
|
||||
{
|
||||
name: "check_aipex_connection",
|
||||
description: [
|
||||
"AIPex extension is not connected. To enable browser control:",
|
||||
`1. Open Chrome → AIPex extension → Options page`,
|
||||
`2. Set WebSocket URL to: ws://localhost:${WS_PORT}`,
|
||||
`3. Click Connect`,
|
||||
`Then reload this MCP server.`,
|
||||
].join("\n"),
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "tools/call") {
|
||||
if (
|
||||
!aipexReady ||
|
||||
!aipexSocket ||
|
||||
aipexSocket.readyState !== WebSocket.OPEN
|
||||
) {
|
||||
respondError(
|
||||
id,
|
||||
-32000,
|
||||
`AIPex extension not connected. Open AIPex Options and connect to ws://localhost:${WS_PORT}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Retry with backoff
|
||||
let backoff = INITIAL_BACKOFF_MS
|
||||
for (let attempt = 1; attempt <= MAX_CONNECT_ATTEMPTS; attempt++) {
|
||||
await sleep(backoff)
|
||||
try {
|
||||
const result = await sendToAipex(
|
||||
"tools/call",
|
||||
params as Record<string, unknown>,
|
||||
);
|
||||
respond(id, result);
|
||||
} catch (e) {
|
||||
respondError(id, -32000, e instanceof Error ? e.message : String(e));
|
||||
const ws = await tryConnectToDaemon()
|
||||
log(`Connected to daemon (attempt ${attempt})`)
|
||||
return ws
|
||||
} catch {
|
||||
backoff = Math.min(backoff * 1.5, 2_000)
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "ping") {
|
||||
if (aipexReady && aipexSocket?.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
const result = await sendToAipex("ping");
|
||||
respond(id, result);
|
||||
} catch {
|
||||
respond(id, {});
|
||||
}
|
||||
} else {
|
||||
respond(id, {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
respondError(id, -32601, `Method not found: ${method}`);
|
||||
throw new Error(
|
||||
`Failed to connect to daemon after ${MAX_CONNECT_ATTEMPTS} attempts.\n` +
|
||||
`Check if port ${PORT} is available: lsof -i :${PORT}`
|
||||
)
|
||||
}
|
||||
|
||||
// ── Read MCP requests from stdin ────────────────────────────────────────────
|
||||
function setupDaemonConnection(ws: WebSocket) {
|
||||
daemonWs = ws
|
||||
|
||||
const stdinRl = createInterface({ input: process.stdin });
|
||||
ws.on("message", (data) => handleDaemonMessage(data.toString()))
|
||||
|
||||
stdinRl.on("line", (line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return;
|
||||
ws.on("close", () => {
|
||||
log("Daemon connection lost, will reconnect on next tool call")
|
||||
rejectAllPending("Daemon connection lost")
|
||||
daemonWs = undefined
|
||||
})
|
||||
|
||||
ws.on("error", (err) => {
|
||||
log(`Daemon WebSocket error: ${err.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
async function ensureDaemonConnection() {
|
||||
if (isDaemonConnected()) return
|
||||
|
||||
log("Reconnecting to daemon...")
|
||||
const ws = await connectWithAutoSpawn()
|
||||
setupDaemonConnection(ws)
|
||||
}
|
||||
|
||||
// ── MCP Server (stdio to IDE) ───────────────────────────────────────────────
|
||||
|
||||
const server = new Server(
|
||||
{ name: "aipex-mcp-bridge", version: "3.1.0" },
|
||||
{ capabilities: { tools: {} } }
|
||||
)
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return { tools: toolSchemas }
|
||||
})
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params
|
||||
|
||||
const tool = toolSchemas.find((t) => t.name === name)
|
||||
if (!tool) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Tool "${name}" not found` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
|
||||
let req: JSONRPCRequest;
|
||||
try {
|
||||
req = JSON.parse(trimmed);
|
||||
} catch {
|
||||
log(`Failed to parse stdin: ${trimmed.slice(0, 100)}`);
|
||||
return;
|
||||
}
|
||||
await ensureDaemonConnection()
|
||||
|
||||
handleAgentRequest(req).catch((e) => {
|
||||
log(
|
||||
`Error handling request: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
if (req.id != null) {
|
||||
respondError(req.id, -32603, "Internal error");
|
||||
const result = (await sendToolCallToDaemon(
|
||||
name,
|
||||
(args ?? {}) as Record<string, unknown>
|
||||
)) as Record<string, unknown> | undefined
|
||||
|
||||
if (result && result.content) {
|
||||
return result as {
|
||||
content: Array<{
|
||||
type: string
|
||||
text?: string
|
||||
data?: string
|
||||
mimeType?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
stdinRl.on("close", () => {
|
||||
log("stdin closed, shutting down");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// ── WebSocket server (waits for AIPex extension to connect) ─────────────────
|
||||
|
||||
const httpServer = createServer();
|
||||
const wss = new WebSocketServer({ server: httpServer });
|
||||
|
||||
wss.on("connection", (socket, req) => {
|
||||
const addr = req.socket.remoteAddress ?? "unknown";
|
||||
|
||||
if (aipexSocket && aipexSocket.readyState === WebSocket.OPEN) {
|
||||
log(`New connection from ${addr}, closing previous`);
|
||||
aipexSocket.close();
|
||||
}
|
||||
|
||||
aipexSocket = socket;
|
||||
aipexReady = false;
|
||||
cachedTools = [];
|
||||
log(`AIPex extension connected from ${addr}`);
|
||||
|
||||
socket.on("message", (data) => {
|
||||
handleAipexMessage(data.toString());
|
||||
});
|
||||
|
||||
socket.on("close", () => {
|
||||
log("AIPex extension disconnected");
|
||||
if (aipexSocket === socket) {
|
||||
aipexSocket = null;
|
||||
aipexReady = false;
|
||||
cachedTools = [];
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
log(`Socket error: ${err.message}`);
|
||||
});
|
||||
|
||||
doAipexHandshake(socket).catch((err: Error) => {
|
||||
log(`Handshake failed: ${err.message}`);
|
||||
});
|
||||
});
|
||||
|
||||
wss.on("error", (err) => {
|
||||
log(`WebSocket server error: ${err.message}`);
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: String(error instanceof Error ? error.message : error)
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// ── Start ───────────────────────────────────────────────────────────────────
|
||||
|
||||
httpServer.listen(WS_PORT, () => {
|
||||
log(`AIPex MCP Bridge started`);
|
||||
log(`WebSocket server listening on ws://localhost:${WS_PORT}`);
|
||||
log(`Waiting for AIPex extension to connect...`);
|
||||
log(`Open AIPex Options → set URL to ws://localhost:${WS_PORT} → Connect`);
|
||||
});
|
||||
async function main() {
|
||||
const ws = await connectWithAutoSpawn()
|
||||
setupDaemonConnection(ws)
|
||||
|
||||
const transport = new StdioServerTransport()
|
||||
await server.connect(transport)
|
||||
|
||||
log("AIPex MCP Bridge started (stdio → daemon relay)")
|
||||
log(`Connected to daemon at ${DAEMON_URL}`)
|
||||
}
|
||||
|
||||
// ── Exit handling ───────────────────────────────────────────────────────────
|
||||
|
||||
process.stdin.on("close", async () => {
|
||||
setTimeout(() => process.exit(0), 5_000)
|
||||
rejectAllPending("Bridge shutting down")
|
||||
if (daemonWs) daemonWs.close()
|
||||
await server.close()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
log("Shutting down...");
|
||||
wss.close();
|
||||
httpServer.close();
|
||||
process.exit(0);
|
||||
});
|
||||
log("Shutting down...")
|
||||
rejectAllPending("Bridge shutting down")
|
||||
if (daemonWs) daemonWs.close()
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
main().catch((err) => {
|
||||
log(`Fatal error: ${err instanceof Error ? err.message : String(err)}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
398
mcp-bridge/src/cli.ts
Normal file
398
mcp-bridge/src/cli.ts
Normal file
@@ -0,0 +1,398 @@
|
||||
/**
|
||||
* AIPex CLI — command-line tool for controlling the browser via AIPex.
|
||||
*
|
||||
* Connects to the AIPex daemon via WebSocket at /cli endpoint.
|
||||
* Auto-spawns the daemon if it's not running (same as bridge.ts).
|
||||
*
|
||||
* Usage:
|
||||
* aipex-cli <tool_name> [--param value ...]
|
||||
* aipex-cli --list
|
||||
* aipex-cli --help <tool_name>
|
||||
* aipex-cli --json '{"name":"create_new_tab","arguments":{"url":"..."}}'
|
||||
*/
|
||||
|
||||
import { fork, spawn } from "node:child_process"
|
||||
import { existsSync } from "node:fs"
|
||||
import { dirname, join } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { WebSocket } from "ws"
|
||||
|
||||
import { toolSchemas, type ToolSchema } from "./tool-schemas.js"
|
||||
|
||||
const DEFAULT_WS_URL = "ws://localhost:9223/cli"
|
||||
const ENTRYPOINT_PATH = "/entrypoint.sh"
|
||||
const CALL_TIMEOUT_MS = 60_000
|
||||
const MAX_RETRY_TIMEOUT_MS = parseInt(
|
||||
process.env.AIPEX_CONNECT_TIMEOUT ?? "60000",
|
||||
10
|
||||
)
|
||||
const INITIAL_BACKOFF_MS = 500
|
||||
const MAX_BACKOFF_MS = 5_000
|
||||
|
||||
// ── CLI argument parsing ─────────────────────────────────────────────────────
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length === 0 || args[0] === "-h") {
|
||||
printUsage()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (args[0] === "--list") {
|
||||
printToolList()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (args[0] === "--help") {
|
||||
const toolName = args[1]
|
||||
if (!toolName) {
|
||||
process.stderr.write("Usage: aipex-cli --help <tool_name>\n")
|
||||
process.exit(1)
|
||||
}
|
||||
printToolHelp(toolName)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (args[0] === "--json") {
|
||||
const jsonStr = args[1]
|
||||
if (!jsonStr) {
|
||||
process.stderr.write(
|
||||
'Usage: aipex-cli --json \'{"name":"...","arguments":{...}}\'\n'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
runTool(parsed.name, parsed.arguments ?? {})
|
||||
} catch (e) {
|
||||
process.stderr.write(
|
||||
`Invalid JSON: ${e instanceof Error ? e.message : String(e)}\n`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
const toolName = args[0]
|
||||
const toolArgs = parseToolArgs(args.slice(1), toolName)
|
||||
runTool(toolName, toolArgs)
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function printUsage(): void {
|
||||
process.stderr.write(`
|
||||
AIPex CLI — control the browser from the command line
|
||||
|
||||
Usage:
|
||||
aipex-cli <tool_name> [--param value ...]
|
||||
aipex-cli --list List all available tools
|
||||
aipex-cli --help <tool_name> Show tool parameters
|
||||
aipex-cli --json '{"name":"...","arguments":{...}}'
|
||||
|
||||
Examples:
|
||||
aipex-cli get_all_tabs
|
||||
aipex-cli create_new_tab --url https://google.com
|
||||
aipex-cli click --tabId 123 --uid btn-42
|
||||
aipex-cli search_elements --tabId 123 --query "button*"
|
||||
aipex-cli capture_screenshot
|
||||
|
||||
Environment:
|
||||
AIPEX_WS_URL Daemon WebSocket URL (default: ws://localhost:9223/cli)
|
||||
AIPEX_CONNECT_TIMEOUT Max ms to wait for daemon (default: 60000)
|
||||
`)
|
||||
}
|
||||
|
||||
function printToolList(): void {
|
||||
const maxLen = Math.max(...toolSchemas.map((t) => t.name.length))
|
||||
for (const tool of toolSchemas) {
|
||||
const desc = tool.description.split("\n")[0].slice(0, 80)
|
||||
process.stdout.write(` ${tool.name.padEnd(maxLen + 2)}${desc}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
function printToolHelp(name: string): void {
|
||||
const tool = toolSchemas.find((t) => t.name === name)
|
||||
if (!tool) {
|
||||
process.stderr.write(`Unknown tool: ${name}\n`)
|
||||
process.stderr.write(`Run 'aipex-cli --list' to see available tools.\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
process.stdout.write(`${tool.name}\n`)
|
||||
process.stdout.write(` ${tool.description.split("\n")[0]}\n\n`)
|
||||
const props = tool.inputSchema.properties
|
||||
const required = new Set(tool.inputSchema.required ?? [])
|
||||
if (Object.keys(props).length === 0) {
|
||||
process.stdout.write(" No parameters.\n")
|
||||
return
|
||||
}
|
||||
process.stdout.write(" Parameters:\n")
|
||||
for (const [key, schema] of Object.entries(props)) {
|
||||
const s = schema as Record<string, unknown>
|
||||
const typeStr = (s.type as string) ?? "any"
|
||||
const reqStr = required.has(key) ? " (required)" : ""
|
||||
const desc = (s.description as string) ?? ""
|
||||
process.stdout.write(` --${key} <${typeStr}>${reqStr}\n`)
|
||||
if (desc) {
|
||||
process.stdout.write(` ${desc.split("\n")[0]}\n`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseToolArgs(
|
||||
rawArgs: string[],
|
||||
toolName: string
|
||||
): Record<string, unknown> {
|
||||
const tool = toolSchemas.find((t) => t.name === toolName)
|
||||
const props = tool?.inputSchema.properties ?? {}
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
for (let i = 0; i < rawArgs.length; i++) {
|
||||
const arg = rawArgs[i]
|
||||
if (!arg.startsWith("--")) {
|
||||
process.stderr.write(`Unexpected argument: ${arg}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
const key = arg.slice(2)
|
||||
const value = rawArgs[++i]
|
||||
if (value === undefined) {
|
||||
process.stderr.write(`Missing value for --${key}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
result[key] = coerceValue(value, key, props)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function coerceValue(
|
||||
value: string,
|
||||
key: string,
|
||||
props: Record<string, unknown>
|
||||
): unknown {
|
||||
const schema = props[key] as Record<string, unknown> | undefined
|
||||
const type = schema?.type as string | undefined
|
||||
|
||||
switch (type) {
|
||||
case "number": {
|
||||
const num = Number(value)
|
||||
if (isNaN(num)) {
|
||||
process.stderr.write(`--${key} expects a number, got: ${value}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
return num
|
||||
}
|
||||
case "boolean":
|
||||
return value === "true" || value === "1"
|
||||
case "array":
|
||||
case "object":
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
process.stderr.write(`--${key} expects JSON (${type}), got: ${value}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// ── WebSocket tool call ─────────────────────────────────────────────────────
|
||||
|
||||
function isRetryableError(msg: string): boolean {
|
||||
const lower = msg.toLowerCase()
|
||||
return (
|
||||
lower.includes("not connected") ||
|
||||
lower.includes("extension is not connected") ||
|
||||
lower.includes("no extension") ||
|
||||
lower.includes("econnrefused") ||
|
||||
lower.includes("fetch failed")
|
||||
)
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((r) => setTimeout(r, ms))
|
||||
}
|
||||
|
||||
function attemptWsToolCall(
|
||||
wsUrl: string,
|
||||
name: string,
|
||||
args: Record<string, unknown>
|
||||
): Promise<{ retry: boolean; code: number }> {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false
|
||||
const ws = new WebSocket(wsUrl)
|
||||
|
||||
const connectTimer = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
ws.terminate()
|
||||
resolve({ retry: true, code: 1 })
|
||||
}
|
||||
}, 5_000)
|
||||
|
||||
ws.on("error", () => {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
clearTimeout(connectTimer)
|
||||
resolve({ retry: true, code: 1 })
|
||||
}
|
||||
})
|
||||
|
||||
ws.on("close", () => {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
clearTimeout(connectTimer)
|
||||
resolve({ retry: true, code: 1 })
|
||||
}
|
||||
})
|
||||
|
||||
ws.on("open", () => {
|
||||
clearTimeout(connectTimer)
|
||||
const msg = {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/call",
|
||||
params: { name, arguments: args }
|
||||
}
|
||||
ws.send(JSON.stringify(msg))
|
||||
|
||||
const callTimer = setTimeout(() => {
|
||||
if (!settled) {
|
||||
settled = true
|
||||
process.stderr.write(
|
||||
`Tool '${name}' timed out after ${CALL_TIMEOUT_MS}ms\n`
|
||||
)
|
||||
ws.close()
|
||||
resolve({ retry: false, code: 1 })
|
||||
}
|
||||
}, CALL_TIMEOUT_MS)
|
||||
|
||||
ws.on("message", (data) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(callTimer)
|
||||
|
||||
try {
|
||||
const response = JSON.parse(data.toString())
|
||||
|
||||
if (response.error) {
|
||||
const errMsg =
|
||||
response.error.message ?? JSON.stringify(response.error)
|
||||
if (isRetryableError(errMsg)) {
|
||||
ws.close()
|
||||
resolve({ retry: true, code: 1 })
|
||||
return
|
||||
}
|
||||
process.stderr.write(`Error: ${errMsg}\n`)
|
||||
ws.close()
|
||||
resolve({ retry: false, code: 1 })
|
||||
return
|
||||
}
|
||||
|
||||
const result = response.result
|
||||
if (result?.content && Array.isArray(result.content)) {
|
||||
for (const item of result.content) {
|
||||
if (item.type === "text" && item.text) {
|
||||
try {
|
||||
const parsed = JSON.parse(item.text)
|
||||
process.stdout.write(JSON.stringify(parsed, null, 2) + "\n")
|
||||
} catch {
|
||||
process.stdout.write(item.text + "\n")
|
||||
}
|
||||
} else if (item.type === "image") {
|
||||
process.stdout.write(
|
||||
`[Image: ${item.mimeType ?? "image/png"}, ${(((item.data?.length ?? 0) * 0.75) / 1024).toFixed(1)}KB]\n`
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (result !== undefined) {
|
||||
process.stdout.write(JSON.stringify(result, null, 2) + "\n")
|
||||
}
|
||||
} catch (e) {
|
||||
process.stderr.write(
|
||||
`Failed to parse response: ${data.toString().slice(0, 200)}\n`
|
||||
)
|
||||
}
|
||||
|
||||
ws.close()
|
||||
resolve({ retry: false, code: 0 })
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function spawnDaemon(): void {
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
const daemonPath = join(__dirname, "daemon.js")
|
||||
|
||||
try {
|
||||
const child = fork(daemonPath, ["--port", "9223", "--host", "127.0.0.1"], {
|
||||
detached: true,
|
||||
stdio: "ignore"
|
||||
})
|
||||
child.unref()
|
||||
child.on("error", () => {})
|
||||
} catch {
|
||||
// ignore spawn errors
|
||||
}
|
||||
}
|
||||
|
||||
async function runTool(
|
||||
name: string,
|
||||
args: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
const wsUrl = process.env.AIPEX_WS_URL ?? DEFAULT_WS_URL
|
||||
const deadline = Date.now() + MAX_RETRY_TIMEOUT_MS
|
||||
let backoff = INITIAL_BACKOFF_MS
|
||||
let attempt = 0
|
||||
let daemonSpawned = false
|
||||
|
||||
while (true) {
|
||||
attempt++
|
||||
|
||||
const result = await attemptWsToolCall(wsUrl, name, args)
|
||||
|
||||
if (!result.retry) {
|
||||
process.exit(result.code)
|
||||
}
|
||||
|
||||
if (Date.now() >= deadline) {
|
||||
process.stderr.write(
|
||||
`Gave up after ${MAX_RETRY_TIMEOUT_MS / 1000}s — daemon not ready at ${wsUrl}\n`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (attempt === 1) {
|
||||
// In Docker, try the entrypoint script
|
||||
if (existsSync(ENTRYPOINT_PATH)) {
|
||||
process.stderr.write(
|
||||
`[aipex-cli] Auto-starting services via ${ENTRYPOINT_PATH} ...\n`
|
||||
)
|
||||
const child = spawn(ENTRYPOINT_PATH, [], {
|
||||
detached: true,
|
||||
stdio: "ignore",
|
||||
shell: true,
|
||||
env: { ...process.env, DISPLAY: ":99" }
|
||||
})
|
||||
child.on("error", () => {})
|
||||
child.unref()
|
||||
} else if (!daemonSpawned) {
|
||||
process.stderr.write("[aipex-cli] Spawning daemon...\n")
|
||||
spawnDaemon()
|
||||
daemonSpawned = true
|
||||
}
|
||||
process.stderr.write(
|
||||
`[aipex-cli] Waiting for AIPex daemon + extension ...\n`
|
||||
)
|
||||
}
|
||||
|
||||
const remaining = Math.max(0, deadline - Date.now())
|
||||
const wait = Math.min(backoff, remaining, MAX_BACKOFF_MS)
|
||||
process.stderr.write(
|
||||
`[aipex-cli] Retry #${attempt} in ${(wait / 1000).toFixed(1)}s\n`
|
||||
)
|
||||
await sleep(wait)
|
||||
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS)
|
||||
}
|
||||
}
|
||||
441
mcp-bridge/src/daemon.ts
Normal file
441
mcp-bridge/src/daemon.ts
Normal file
@@ -0,0 +1,441 @@
|
||||
/**
|
||||
* AIPex MCP Daemon
|
||||
*
|
||||
* A background WebSocket relay that bridges multiple MCP bridge instances
|
||||
* to a single AIPex Chrome extension connection.
|
||||
*
|
||||
* Architecture:
|
||||
*
|
||||
* bridge.ts #1 ──WS /bridge──┐
|
||||
* bridge.ts #2 ──WS /bridge──┤── this daemon ──WS /extension──▶ AIPex extension
|
||||
* aipex-cli ──WS /cli─────┘
|
||||
*
|
||||
* Spawned automatically by bridge.ts when no daemon is running.
|
||||
* Self-terminates after IDLE_TIMEOUT_MS with no connections.
|
||||
*/
|
||||
|
||||
import { unlinkSync, writeFileSync } from "node:fs"
|
||||
import { createServer } from "node:http"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { WebSocket, WebSocketServer } from "ws"
|
||||
|
||||
import { toolSchemas } from "./tool-schemas.js"
|
||||
|
||||
// ── CLI args ────────────────────────────────────────────────────────────────
|
||||
|
||||
const cliArgs = process.argv.slice(2)
|
||||
|
||||
function getArg(name: string, fallback: string): string {
|
||||
const idx = cliArgs.indexOf(name)
|
||||
return idx !== -1 && cliArgs[idx + 1] ? cliArgs[idx + 1] : fallback
|
||||
}
|
||||
|
||||
const PORT = parseInt(getArg("--port", "9223"), 10)
|
||||
const HOST = getArg("--host", "127.0.0.1")
|
||||
const PID_FILE = join(homedir(), ".aipex-daemon.pid")
|
||||
const IDLE_TIMEOUT_MS = 30_000
|
||||
const TOOL_CALL_TIMEOUT_MS = 60_000
|
||||
const PING_INTERVAL_MS = 15_000
|
||||
const PING_TIMEOUT_MS = 5_000
|
||||
|
||||
// ── Logging ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function log(msg: string) {
|
||||
process.stderr.write(`[aipex-daemon] ${msg}\n`)
|
||||
}
|
||||
|
||||
// ── Extension connection ────────────────────────────────────────────────────
|
||||
|
||||
let extensionWs: WebSocket | undefined
|
||||
let nextExtId = 1
|
||||
|
||||
interface PendingExtCall {
|
||||
bridgeSocket: WebSocket
|
||||
bridgeReqId: number | string
|
||||
timer: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
const pendingExtCalls = new Map<number, PendingExtCall>()
|
||||
let extPingInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function isExtensionConnected(): boolean {
|
||||
return !!extensionWs && extensionWs.readyState === WebSocket.OPEN
|
||||
}
|
||||
|
||||
function setExtensionSocket(ws: WebSocket) {
|
||||
if (extensionWs && extensionWs.readyState === WebSocket.OPEN) {
|
||||
extensionWs.close()
|
||||
}
|
||||
rejectAllPendingExt("New extension connection replaced previous one")
|
||||
extensionWs = ws
|
||||
|
||||
ws.on("message", (data) => handleExtensionMessage(data.toString()))
|
||||
|
||||
ws.on("close", () => {
|
||||
if (extensionWs === ws) {
|
||||
log("Extension disconnected")
|
||||
stopExtPing()
|
||||
rejectAllPendingExt("Extension disconnected")
|
||||
extensionWs = undefined
|
||||
resetIdleTimer()
|
||||
}
|
||||
})
|
||||
|
||||
ws.on("error", (err) => {
|
||||
log(`Extension WebSocket error: ${err.message}`)
|
||||
})
|
||||
|
||||
startExtPing()
|
||||
resetIdleTimer()
|
||||
log("Extension connected")
|
||||
}
|
||||
|
||||
function handleExtensionMessage(raw: string) {
|
||||
let msg: Record<string, unknown>
|
||||
try {
|
||||
msg = JSON.parse(raw)
|
||||
} catch {
|
||||
log(`Failed to parse extension message: ${raw.slice(0, 200)}`)
|
||||
return
|
||||
}
|
||||
|
||||
const id = msg.id as number | undefined
|
||||
if (id == null) return
|
||||
|
||||
const pending = pendingExtCalls.get(id)
|
||||
if (!pending) return
|
||||
|
||||
clearTimeout(pending.timer)
|
||||
pendingExtCalls.delete(id)
|
||||
|
||||
const response: Record<string, unknown> = {
|
||||
jsonrpc: "2.0",
|
||||
id: pending.bridgeReqId
|
||||
}
|
||||
|
||||
if (msg.error) {
|
||||
response.error = msg.error
|
||||
} else {
|
||||
response.result = msg.result
|
||||
}
|
||||
|
||||
if (pending.bridgeSocket.readyState === WebSocket.OPEN) {
|
||||
pending.bridgeSocket.send(JSON.stringify(response))
|
||||
}
|
||||
}
|
||||
|
||||
function forwardToolCall(
|
||||
bridgeSocket: WebSocket,
|
||||
bridgeReqId: number | string,
|
||||
toolName: string,
|
||||
args: Record<string, unknown>
|
||||
) {
|
||||
if (!isExtensionConnected()) {
|
||||
const errResp = {
|
||||
jsonrpc: "2.0",
|
||||
id: bridgeReqId,
|
||||
error: {
|
||||
code: -1,
|
||||
message:
|
||||
"AIPex extension is not connected. To connect:\n" +
|
||||
"1. Open Chrome → AIPex extension → Options page\n" +
|
||||
`2. Set WebSocket URL to ws://localhost:${PORT}/extension\n` +
|
||||
"3. Click Connect"
|
||||
}
|
||||
}
|
||||
if (bridgeSocket.readyState === WebSocket.OPEN) {
|
||||
bridgeSocket.send(JSON.stringify(errResp))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const extId = nextExtId++
|
||||
const msg = {
|
||||
jsonrpc: "2.0",
|
||||
id: extId,
|
||||
method: "tools/call",
|
||||
params: { name: toolName, arguments: args }
|
||||
}
|
||||
extensionWs!.send(JSON.stringify(msg))
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (pendingExtCalls.has(extId)) {
|
||||
pendingExtCalls.delete(extId)
|
||||
const timeoutResp = {
|
||||
jsonrpc: "2.0",
|
||||
id: bridgeReqId,
|
||||
error: {
|
||||
code: -1,
|
||||
message: `Tool '${toolName}' timed out after ${TOOL_CALL_TIMEOUT_MS}ms`
|
||||
}
|
||||
}
|
||||
if (bridgeSocket.readyState === WebSocket.OPEN) {
|
||||
bridgeSocket.send(JSON.stringify(timeoutResp))
|
||||
}
|
||||
}
|
||||
}, TOOL_CALL_TIMEOUT_MS)
|
||||
|
||||
pendingExtCalls.set(extId, { bridgeSocket, bridgeReqId, timer })
|
||||
}
|
||||
|
||||
function rejectAllPendingExt(reason: string) {
|
||||
for (const [, entry] of pendingExtCalls) {
|
||||
clearTimeout(entry.timer)
|
||||
const errResp = {
|
||||
jsonrpc: "2.0",
|
||||
id: entry.bridgeReqId,
|
||||
error: { code: -1, message: reason }
|
||||
}
|
||||
if (entry.bridgeSocket.readyState === WebSocket.OPEN) {
|
||||
entry.bridgeSocket.send(JSON.stringify(errResp))
|
||||
}
|
||||
}
|
||||
pendingExtCalls.clear()
|
||||
}
|
||||
|
||||
function startExtPing() {
|
||||
stopExtPing()
|
||||
extPingInterval = setInterval(() => {
|
||||
if (!isExtensionConnected()) {
|
||||
stopExtPing()
|
||||
return
|
||||
}
|
||||
const id = nextExtId++
|
||||
const msg = { jsonrpc: "2.0", id, method: "ping" }
|
||||
extensionWs!.send(JSON.stringify(msg))
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (pendingExtCalls.has(id)) {
|
||||
pendingExtCalls.delete(id)
|
||||
log("Extension ping timeout, closing connection")
|
||||
if (extensionWs) extensionWs.close()
|
||||
}
|
||||
}, PING_TIMEOUT_MS)
|
||||
|
||||
pendingExtCalls.set(id, {
|
||||
bridgeSocket: extensionWs!,
|
||||
bridgeReqId: `ping-${id}`,
|
||||
timer
|
||||
})
|
||||
}, PING_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopExtPing() {
|
||||
if (extPingInterval) {
|
||||
clearInterval(extPingInterval)
|
||||
extPingInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bridge client handling ──────────────────────────────────────────────────
|
||||
|
||||
const bridgeClients = new Set<WebSocket>()
|
||||
|
||||
function handleBridgeMessage(socket: WebSocket, raw: string) {
|
||||
let msg: Record<string, unknown>
|
||||
try {
|
||||
msg = JSON.parse(raw)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const id = msg.id as number | string | undefined
|
||||
const method = msg.method as string | undefined
|
||||
|
||||
if (method === "tools/call") {
|
||||
const params = (msg.params ?? {}) as Record<string, unknown>
|
||||
const name = params.name as string
|
||||
const args = (params.arguments ?? {}) as Record<string, unknown>
|
||||
forwardToolCall(socket, id ?? 0, name, args)
|
||||
return
|
||||
}
|
||||
|
||||
if (method === "tools/list") {
|
||||
socket.send(
|
||||
JSON.stringify({ jsonrpc: "2.0", id, result: { tools: toolSchemas } })
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (method === "ping") {
|
||||
socket.send(JSON.stringify({ jsonrpc: "2.0", id, result: {} }))
|
||||
return
|
||||
}
|
||||
|
||||
if (method === "status") {
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
extensionConnected: isExtensionConnected(),
|
||||
bridgeClients: bridgeClients.size
|
||||
}
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ── CLI client handling (backward compat) ───────────────────────────────────
|
||||
|
||||
function handleCliMessage(socket: WebSocket, raw: string) {
|
||||
handleBridgeMessage(socket, raw)
|
||||
}
|
||||
|
||||
// ── Idle auto-shutdown ──────────────────────────────────────────────────────
|
||||
|
||||
let idleTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function resetIdleTimer() {
|
||||
if (idleTimer) {
|
||||
clearTimeout(idleTimer)
|
||||
idleTimer = null
|
||||
}
|
||||
|
||||
if (bridgeClients.size === 0 && !isExtensionConnected()) {
|
||||
idleTimer = setTimeout(() => {
|
||||
log(`No connections for ${IDLE_TIMEOUT_MS / 1000}s, shutting down`)
|
||||
shutdown()
|
||||
}, IDLE_TIMEOUT_MS)
|
||||
}
|
||||
}
|
||||
|
||||
// ── HTTP + WebSocket Server ─────────────────────────────────────────────────
|
||||
|
||||
const httpServer = createServer((req, res) => {
|
||||
if (req.url === "/health" && req.method === "GET") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" })
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
status: "ok",
|
||||
extensionConnected: isExtensionConnected(),
|
||||
bridgeClients: bridgeClients.size,
|
||||
version: "3.1.0"
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
res.writeHead(404).end("Not found")
|
||||
})
|
||||
|
||||
const extensionWss = new WebSocketServer({ noServer: true })
|
||||
const bridgeWss = new WebSocketServer({ noServer: true })
|
||||
const cliWss = new WebSocketServer({ noServer: true })
|
||||
|
||||
httpServer.on("upgrade", (req, socket, head) => {
|
||||
const pathname = new URL(req.url ?? "/", "http://localhost").pathname
|
||||
|
||||
if (pathname === "/extension" || pathname === "/") {
|
||||
extensionWss.handleUpgrade(req, socket, head, (ws) => {
|
||||
extensionWss.emit("connection", ws, req)
|
||||
})
|
||||
} else if (pathname === "/bridge") {
|
||||
bridgeWss.handleUpgrade(req, socket, head, (ws) => {
|
||||
bridgeWss.emit("connection", ws, req)
|
||||
})
|
||||
} else if (pathname === "/cli") {
|
||||
cliWss.handleUpgrade(req, socket, head, (ws) => {
|
||||
cliWss.emit("connection", ws, req)
|
||||
})
|
||||
} else {
|
||||
socket.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
extensionWss.on("connection", (socket, req) => {
|
||||
const addr = req.socket.remoteAddress ?? "unknown"
|
||||
log(`Extension connected from ${addr}`)
|
||||
setExtensionSocket(socket)
|
||||
})
|
||||
|
||||
bridgeWss.on("connection", (socket) => {
|
||||
bridgeClients.add(socket)
|
||||
resetIdleTimer()
|
||||
log(`Bridge client connected (total: ${bridgeClients.size})`)
|
||||
|
||||
socket.on("message", (data) => handleBridgeMessage(socket, data.toString()))
|
||||
|
||||
socket.on("close", () => {
|
||||
bridgeClients.delete(socket)
|
||||
resetIdleTimer()
|
||||
log(`Bridge client disconnected (total: ${bridgeClients.size})`)
|
||||
})
|
||||
|
||||
socket.on("error", (err) => {
|
||||
log(`Bridge client error: ${err.message}`)
|
||||
})
|
||||
})
|
||||
|
||||
cliWss.on("connection", (socket) => {
|
||||
bridgeClients.add(socket)
|
||||
resetIdleTimer()
|
||||
|
||||
socket.on("message", (data) => handleCliMessage(socket, data.toString()))
|
||||
|
||||
socket.on("close", () => {
|
||||
bridgeClients.delete(socket)
|
||||
resetIdleTimer()
|
||||
})
|
||||
})
|
||||
|
||||
// ── PID file ────────────────────────────────────────────────────────────────
|
||||
|
||||
function writePidFile() {
|
||||
try {
|
||||
writeFileSync(PID_FILE, String(process.pid))
|
||||
} catch {
|
||||
// non-critical
|
||||
}
|
||||
}
|
||||
|
||||
function removePidFile() {
|
||||
try {
|
||||
unlinkSync(PID_FILE)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// ── Start ───────────────────────────────────────────────────────────────────
|
||||
|
||||
httpServer.listen(PORT, HOST, () => {
|
||||
writePidFile()
|
||||
log(`AIPex MCP Daemon started (v3.1.0) pid=${process.pid}`)
|
||||
log(`Extension WS: ws://${HOST}:${PORT}/extension`)
|
||||
log(`Bridge WS: ws://${HOST}:${PORT}/bridge`)
|
||||
log(`CLI WS: ws://${HOST}:${PORT}/cli`)
|
||||
log(`Health: http://${HOST}:${PORT}/health`)
|
||||
resetIdleTimer()
|
||||
})
|
||||
|
||||
httpServer.on("error", (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
log(`Port ${PORT} already in use — another daemon is likely running`)
|
||||
process.exit(0)
|
||||
}
|
||||
log(`Server error: ${err.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
// ── Shutdown ────────────────────────────────────────────────────────────────
|
||||
|
||||
function shutdown() {
|
||||
stopExtPing()
|
||||
rejectAllPendingExt("Daemon shutting down")
|
||||
if (extensionWs) {
|
||||
extensionWs.close()
|
||||
extensionWs = undefined
|
||||
}
|
||||
extensionWss.close()
|
||||
bridgeWss.close()
|
||||
cliWss.close()
|
||||
httpServer.close()
|
||||
removePidFile()
|
||||
if (idleTimer) clearTimeout(idleTimer)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown)
|
||||
process.on("SIGTERM", shutdown)
|
||||
738
mcp-bridge/src/tool-schemas.ts
Normal file
738
mcp-bridge/src/tool-schemas.ts
Normal file
@@ -0,0 +1,738 @@
|
||||
/**
|
||||
* Static MCP tool schemas for the bridge.
|
||||
*
|
||||
* These mirror the definitions in src/mcp/tools/unified-tool-definitions.ts
|
||||
* but are expressed as plain JSON Schema so the bridge has zero dependency
|
||||
* on the extension runtime (no Zod, no Chrome APIs).
|
||||
*
|
||||
* When tools are added/removed in the extension, update this file accordingly.
|
||||
*/
|
||||
|
||||
export interface ToolSchema {
|
||||
name: string
|
||||
description: string
|
||||
inputSchema: {
|
||||
type: "object"
|
||||
properties: Record<string, unknown>
|
||||
required?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const toolSchemas: ToolSchema[] = [
|
||||
// ===== Browser Tools =====
|
||||
{
|
||||
name: "get_all_tabs",
|
||||
description:
|
||||
"Get all open tabs across all windows with their IDs, titles, and URLs",
|
||||
inputSchema: { type: "object", properties: {}, required: [] }
|
||||
},
|
||||
{
|
||||
name: "get_current_tab",
|
||||
description: "Get information about the currently active tab",
|
||||
inputSchema: { type: "object", properties: {}, required: [] }
|
||||
},
|
||||
{
|
||||
name: "switch_to_tab",
|
||||
description: "Switch to a specific tab by ID",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: { type: "number", description: "The ID of the tab to switch to" }
|
||||
},
|
||||
required: ["tabId"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "create_new_tab",
|
||||
description: "Create a new tab with the specified URL",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: { type: "string", description: "The URL to open in the new tab" }
|
||||
},
|
||||
required: ["url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "get_tab_info",
|
||||
description: "Get detailed information about a specific tab",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: { type: "number", description: "The ID of the tab" }
|
||||
},
|
||||
required: ["tabId"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "close_tab",
|
||||
description: "Close a specific tab",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: { type: "number", description: "The ID of the tab to close" }
|
||||
},
|
||||
required: ["tabId"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "organize_tabs",
|
||||
description: "Use AI to automatically group tabs by topic/purpose",
|
||||
inputSchema: { type: "object", properties: {}, required: [] }
|
||||
},
|
||||
{
|
||||
name: "ungroup_tabs",
|
||||
description: "Remove all tab groups in the current window",
|
||||
inputSchema: { type: "object", properties: {}, required: [] }
|
||||
},
|
||||
|
||||
// ===== UI Tools =====
|
||||
{
|
||||
name: "search_elements",
|
||||
description: `[FAST - USE FIRST] Search for elements in the current page using glob/grep patterns against the DOM snapshot. Returns matching elements with their UIDs for direct UID-based interaction.
|
||||
|
||||
GLOB SYNTAX:
|
||||
- * matches any characters (e.g. button* finds all buttons)
|
||||
- ? matches exactly one character
|
||||
- [abc] matches any of those characters
|
||||
- {a,b,c} matches any of those alternatives (e.g. {button,input}* finds all buttons and inputs)
|
||||
- Patterns are case-sensitive by default; use [Ll] to match both cases
|
||||
|
||||
STARTER QUERIES:
|
||||
- Broad scan: {button,link,input,StaticText}*
|
||||
- All interactive: {button,input,textarea,select,a}*
|
||||
- Login/auth: *[Ll]ogin*, *[Ss]ign*
|
||||
- Submit/save: *[Ss]ubmit*, *[Ss]ave*, *[Cc]onfirm*
|
||||
- Search boxes: *[Ss]earch*, {input,textarea}*
|
||||
- Navigation: {nav,link,a}*
|
||||
|
||||
WORKFLOW:
|
||||
1. Call search_elements with a broad pattern to discover elements
|
||||
2. Elements in the result have uid= attributes (e.g. uid=btn-42)
|
||||
3. Pass that UID to click(tabId, uid) or fill_element_by_uid(tabId, uid, value)
|
||||
4. If 0 results after 2 different patterns, fall back to capture_screenshot(sendToLLM=true)
|
||||
|
||||
This is the PREFERRED first step — much faster and cheaper than screenshots.`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: {
|
||||
type: "number",
|
||||
description: "The ID of the tab to search the elements in"
|
||||
},
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query string with grep/glob pattern support"
|
||||
},
|
||||
contextLevels: {
|
||||
type: "number",
|
||||
description: "Number of context lines to include"
|
||||
}
|
||||
},
|
||||
required: ["tabId", "query"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "click",
|
||||
description: "Click an element using its unique UID from a snapshot",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: { type: "number", description: "The ID of the tab to click on" },
|
||||
uid: {
|
||||
type: "string",
|
||||
description:
|
||||
"The unique identifier of an element from the page snapshot"
|
||||
},
|
||||
dblClick: {
|
||||
type: "boolean",
|
||||
description: "Set to true for double clicks"
|
||||
}
|
||||
},
|
||||
required: ["tabId", "uid"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "fill_element_by_uid",
|
||||
description: "Fill an input element using its unique UID from a snapshot",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: {
|
||||
type: "number",
|
||||
description: "The ID of the tab to fill the element in"
|
||||
},
|
||||
uid: {
|
||||
type: "string",
|
||||
description: "The unique identifier of the element to fill"
|
||||
},
|
||||
value: {
|
||||
type: "string",
|
||||
description: "The value to fill into the element"
|
||||
}
|
||||
},
|
||||
required: ["tabId", "uid", "value"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "get_editor_value",
|
||||
description:
|
||||
"Get the complete content from a code editor (Monaco, CodeMirror, ACE) or textarea without truncation. Use this before filling to avoid data loss.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: { type: "number", description: "The ID of the tab" },
|
||||
uid: {
|
||||
type: "string",
|
||||
description:
|
||||
"The unique identifier of the editor element from snapshot"
|
||||
}
|
||||
},
|
||||
required: ["tabId", "uid"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "fill_form",
|
||||
description:
|
||||
"Fill multiple form elements at once using their UIDs from a snapshot",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: {
|
||||
type: "number",
|
||||
description: "The ID of the tab to fill the elements in"
|
||||
},
|
||||
elements: {
|
||||
type: "array",
|
||||
description: "Array of elements to fill with their UIDs and values"
|
||||
}
|
||||
},
|
||||
required: ["tabId", "elements"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "hover_element_by_uid",
|
||||
description: "Hover over an element using its unique UID from a snapshot",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: {
|
||||
type: "number",
|
||||
description: "The ID of the tab to hover over"
|
||||
},
|
||||
uid: {
|
||||
type: "string",
|
||||
description: "The unique identifier of the element to hover over"
|
||||
}
|
||||
},
|
||||
required: ["tabId", "uid"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "upload_file_to_input",
|
||||
description: `Upload a pre-attached file to a file input element (<input type="file">) on the page.
|
||||
|
||||
PREREQUISITES:
|
||||
- The user must have already attached a file using the attachment button in the AIPex sidebar BEFORE sending the message
|
||||
- The file content is NEVER sent to the AI (privacy guaranteed)
|
||||
|
||||
WORKFLOW:
|
||||
1. Call this tool with just the tabId — the tool automatically finds the file input (including hidden ones)
|
||||
2. If the page has multiple file inputs, use input_index to select which one (0 = first)
|
||||
3. Optionally provide uid from a snapshot if you know the exact element
|
||||
|
||||
NOTE: Most websites hide the actual <input type="file"> behind a styled button. This tool handles both visible and hidden file inputs automatically — no need to find the element UID first.
|
||||
|
||||
AFTER UPLOAD: take a screenshot to verify the file was accepted, then proceed to submit the form.`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: {
|
||||
type: "number",
|
||||
description: "The ID of the tab containing the file input element"
|
||||
},
|
||||
uid: {
|
||||
type: "string",
|
||||
description:
|
||||
"UID of the <input type='file'> element from the page snapshot. OPTIONAL — if omitted or the element is hidden, the tool automatically finds the file input by CSS selector."
|
||||
},
|
||||
input_index: {
|
||||
type: "number",
|
||||
description:
|
||||
"0-based index to select which file input to target when the page has multiple. Defaults to 0. Only used when uid is not provided or not found."
|
||||
},
|
||||
file_id: {
|
||||
type: "string",
|
||||
description:
|
||||
"ID of the specific attached file to use (the 'ref' value from the [Attached file...] message). Omit to use the most recently attached file."
|
||||
},
|
||||
file_path: {
|
||||
type: "string",
|
||||
description:
|
||||
"Absolute local file path to upload directly (e.g. '/Users/me/resume.pdf'). Uses CDP DOM.setFileInputFiles — no file content is read into memory. Takes priority over file_id and pre-attached files when provided."
|
||||
}
|
||||
},
|
||||
required: ["tabId"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "computer",
|
||||
description: `[HIGH-COST FALLBACK] Coordinate-based mouse/keyboard interaction using screenshot pixels.
|
||||
|
||||
PREFER UID-BASED TOOLS FIRST: For clicking buttons, filling forms, or hovering elements, use search_elements to get UIDs, then use click/fill_element_by_uid/hover_element_by_uid. These are faster and more reliable.
|
||||
|
||||
USE THIS TOOL ONLY WHEN:
|
||||
- search_elements returned 0 matches after trying 2 different query patterns
|
||||
- UID-based actions failed twice (element not interactable)
|
||||
- The goal requires visual/pixel-level interaction: canvas apps, drag-and-drop, sliders, charts, hover-only menus
|
||||
|
||||
PREREQUISITE: If you choose coordinate actions, you MUST first call capture_screenshot(sendToLLM=true). Coordinates are in screenshot pixel space.
|
||||
|
||||
* Click element centers, not edges. Adjust if clicks miss.`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
action: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"left_click",
|
||||
"right_click",
|
||||
"type",
|
||||
"scroll",
|
||||
"key",
|
||||
"left_click_drag",
|
||||
"double_click",
|
||||
"triple_click",
|
||||
"scroll_to",
|
||||
"hover"
|
||||
],
|
||||
description: `The action to perform:
|
||||
* \`left_click\`: Click the left mouse button at the specified coordinates.
|
||||
* \`right_click\`: Click the right mouse button at the specified coordinates to open context menus.
|
||||
* \`double_click\`: Double-click the left mouse button at the specified coordinates.
|
||||
* \`triple_click\`: Triple-click the left mouse button at the specified coordinates.
|
||||
* \`type\`: Type a string of text at the current cursor position.
|
||||
* \`scroll\`: Scroll up, down, left, or right at the specified coordinates.
|
||||
* \`key\`: Press a specific keyboard key or key combination.
|
||||
* \`left_click_drag\`: Drag from start_coordinate to coordinate.
|
||||
* \`scroll_to\`: Scroll an element into view using its element UID from snapshot.
|
||||
* \`hover\`: Move the mouse cursor to the specified coordinates without clicking. Useful for revealing tooltips, dropdown menus, or triggering hover states.`
|
||||
},
|
||||
coordinate: {
|
||||
type: "array",
|
||||
description:
|
||||
"(x, y): The x (pixels from the left edge) and y (pixels from the top edge) coordinates in screenshot pixel space. Required for left_click, right_click, double_click, triple_click, scroll, and hover. For left_click_drag, this is the end position."
|
||||
},
|
||||
text: {
|
||||
type: "string",
|
||||
description:
|
||||
'The text to type (for type action) or the key(s) to press (for key action). For key action: Provide space-separated keys (e.g., "Backspace Backspace Delete"). Supports keyboard shortcuts using the platform modifier key (use "cmd" on Mac, "ctrl" on Windows/Linux, e.g., "cmd+a" for select all). Common keys: Enter, Tab, Escape, ArrowUp/Down/Left/Right, Backspace, Delete.'
|
||||
},
|
||||
start_coordinate: {
|
||||
type: "array",
|
||||
description:
|
||||
"Starting coordinates for left_click_drag action in screenshot pixel space."
|
||||
},
|
||||
scroll_direction: {
|
||||
type: "string",
|
||||
enum: ["up", "down", "left", "right"],
|
||||
description: "Direction to scroll for scroll action."
|
||||
},
|
||||
scroll_amount: {
|
||||
type: "number",
|
||||
description:
|
||||
"Number of pixels to scroll. Defaults to ~2 viewport heights for standard scrolling."
|
||||
},
|
||||
tabId: {
|
||||
type: "number",
|
||||
description:
|
||||
"The ID of the tab to operate on. Defaults to current active tab."
|
||||
},
|
||||
uid: {
|
||||
type: "string",
|
||||
description: "Element UID from snapshot for scroll_to action."
|
||||
}
|
||||
},
|
||||
required: ["action"]
|
||||
}
|
||||
},
|
||||
|
||||
// ===== Page Tools =====
|
||||
{
|
||||
name: "get_page_metadata",
|
||||
description:
|
||||
"Get page metadata including title, description, keywords, etc.",
|
||||
inputSchema: { type: "object", properties: {}, required: [] }
|
||||
},
|
||||
{
|
||||
name: "scroll_to_element",
|
||||
description: "Scroll to a DOM element and center it in the viewport",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: {
|
||||
type: "string",
|
||||
description: "CSS selector of the element to scroll to"
|
||||
}
|
||||
},
|
||||
required: ["selector"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "highlight_element",
|
||||
description: "Permanently highlight DOM elements with drop shadow effect",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: {
|
||||
type: "string",
|
||||
description: "CSS selector of the element to highlight"
|
||||
},
|
||||
color: {
|
||||
type: "string",
|
||||
description: "Shadow color (e.g., '#00d4ff')"
|
||||
},
|
||||
duration: {
|
||||
type: "number",
|
||||
description: "Duration in milliseconds (0 = permanent)"
|
||||
},
|
||||
intensity: {
|
||||
type: "string",
|
||||
enum: ["subtle", "normal", "strong"]
|
||||
},
|
||||
persist: {
|
||||
type: "boolean",
|
||||
description: "Whether to keep the highlight permanently"
|
||||
}
|
||||
},
|
||||
required: ["selector"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "highlight_text_inline",
|
||||
description:
|
||||
"Highlight specific words or phrases within text content using inline styling",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: {
|
||||
type: "string",
|
||||
description:
|
||||
"CSS selector of the element(s) containing the text to search"
|
||||
},
|
||||
searchText: {
|
||||
type: "string",
|
||||
description: "The text or phrase to highlight"
|
||||
},
|
||||
caseSensitive: { type: "boolean" },
|
||||
wholeWords: { type: "boolean" },
|
||||
highlightColor: { type: "string" },
|
||||
backgroundColor: { type: "string" },
|
||||
fontWeight: { type: "string" },
|
||||
persist: { type: "boolean" }
|
||||
},
|
||||
required: ["selector", "searchText"]
|
||||
}
|
||||
},
|
||||
|
||||
// ===== Screenshot Tools =====
|
||||
{
|
||||
name: "capture_screenshot",
|
||||
description: `[HIGH-COST FALLBACK] Capture screenshot of current visible tab.
|
||||
|
||||
TRY search_elements FIRST: For most interactions (clicking, filling, reading), use search_elements + UID-based tools. They are faster and don't send images to LLM.
|
||||
|
||||
USE THIS ONLY WHEN:
|
||||
- search_elements cannot find the target after 2 query attempts
|
||||
- You need to see visual layout, images, charts, or canvas content
|
||||
- The page uses non-standard rendering that snapshots miss
|
||||
|
||||
When sendToLLM=true: Sends image to LLM (higher latency/cost, may capture sensitive on-screen data) and enables the computer tool for coordinate-based actions.`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
sendToLLM: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Whether to send the screenshot to LLM for visual analysis. When true, enables computer tool for coordinate actions. Use sparingly - adds latency and token cost."
|
||||
}
|
||||
},
|
||||
required: []
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "capture_tab_screenshot",
|
||||
description: `[HIGH-COST FALLBACK] Capture screenshot of a specific tab by ID.
|
||||
|
||||
TRY search_elements FIRST: For most interactions, use search_elements + UID-based tools instead.
|
||||
|
||||
USE THIS ONLY WHEN: Visual verification is essential, search_elements failed, or you need to see images/charts/canvas.
|
||||
|
||||
When sendToLLM=true: Sends image to LLM (higher latency/cost) and enables coordinate-based actions.`,
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
tabId: {
|
||||
type: "number",
|
||||
description: "The ID of the tab to capture"
|
||||
},
|
||||
sendToLLM: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Whether to send the screenshot to LLM for visual analysis. When true, enables computer tool. Use sparingly."
|
||||
}
|
||||
},
|
||||
required: ["tabId"]
|
||||
}
|
||||
},
|
||||
|
||||
// ===== Download Tools =====
|
||||
{
|
||||
name: "download_text_as_markdown",
|
||||
description:
|
||||
"Download text content as a markdown file to the user's local filesystem",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
text: {
|
||||
type: "string",
|
||||
description: "The text content to download as markdown"
|
||||
},
|
||||
filename: {
|
||||
type: "string",
|
||||
description:
|
||||
"Descriptive filename for the download (without .md extension)"
|
||||
},
|
||||
folderPath: {
|
||||
type: "string",
|
||||
description: "Optional folder path for organizing downloads"
|
||||
},
|
||||
displayResults: {
|
||||
type: "boolean",
|
||||
description: "Whether to display the download results"
|
||||
}
|
||||
},
|
||||
required: ["text"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "download_image",
|
||||
description:
|
||||
"Download an image from base64 data to the user's local filesystem",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
imageData: {
|
||||
type: "string",
|
||||
description: "The base64 image data URL (data:image/...)"
|
||||
},
|
||||
filename: {
|
||||
type: "string",
|
||||
description:
|
||||
"Descriptive filename for the download (without extension)"
|
||||
},
|
||||
folderPath: {
|
||||
type: "string",
|
||||
description: "Optional folder path for organizing downloads"
|
||||
}
|
||||
},
|
||||
required: ["imageData"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "download_chat_images",
|
||||
description:
|
||||
"Download multiple images from chat messages to the user's local filesystem",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
messages: {
|
||||
type: "array",
|
||||
description: "Array of chat messages containing images"
|
||||
},
|
||||
folderPrefix: {
|
||||
type: "string",
|
||||
description: "Descriptive folder name for organizing downloads"
|
||||
},
|
||||
filenamingStrategy: {
|
||||
type: "string",
|
||||
enum: ["descriptive", "sequential", "timestamp"]
|
||||
},
|
||||
displayResults: {
|
||||
type: "boolean",
|
||||
description: "Whether to display the download results"
|
||||
}
|
||||
},
|
||||
required: ["messages"]
|
||||
}
|
||||
},
|
||||
|
||||
// ===== Intervention Tools =====
|
||||
{
|
||||
name: "list_interventions",
|
||||
description:
|
||||
"List all available human intervention tools. Use this to discover what types of human input you can request.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabledOnly: {
|
||||
type: "boolean",
|
||||
description: "If true, only return enabled interventions"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "get_intervention_info",
|
||||
description:
|
||||
"Get detailed information about a specific intervention type, including its input/output schema and examples.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: {
|
||||
type: "string",
|
||||
description:
|
||||
'The type of intervention to get information about (e.g., "monitor-operation", "voice-input", "user-selection")'
|
||||
}
|
||||
},
|
||||
required: ["type"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "request_intervention",
|
||||
description:
|
||||
"Request human intervention during task execution. This allows you to ask the user to click on an element, provide voice input, or make a selection. IMPORTANT: Only use this when absolutely necessary and when the current conversation mode allows interventions.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: {
|
||||
type: "string",
|
||||
description:
|
||||
'The type of intervention to request (e.g., "monitor-operation", "voice-input", "user-selection")'
|
||||
},
|
||||
params: {
|
||||
description: "Type-specific parameters for the intervention"
|
||||
},
|
||||
timeout: {
|
||||
type: "number",
|
||||
description: "Timeout in seconds (default: 300)"
|
||||
},
|
||||
reason: {
|
||||
type: "string",
|
||||
description:
|
||||
"A clear explanation to the user about why you need their input"
|
||||
}
|
||||
},
|
||||
required: ["type"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "cancel_intervention",
|
||||
description: "Cancel the currently active intervention request.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
description:
|
||||
"Optional intervention ID to cancel. If not provided, cancels the current active intervention."
|
||||
}
|
||||
},
|
||||
required: []
|
||||
}
|
||||
},
|
||||
|
||||
// ===== Skill Tools =====
|
||||
{
|
||||
name: "load_skill",
|
||||
description:
|
||||
"Load the main content (SKILL.md) of a skill. Use this to understand what a skill does, its capabilities, available scripts, and how to use it.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "The name of the skill to load" }
|
||||
},
|
||||
required: ["name"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "execute_skill_script",
|
||||
description:
|
||||
"Execute a script that belongs to a skill. Scripts are located in the scripts/ directory of the skill package and can perform various operations.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
skillName: { type: "string", description: "The name of the skill" },
|
||||
scriptPath: {
|
||||
type: "string",
|
||||
description:
|
||||
'The path to the script file (e.g., "scripts/init_skill.js"), MUST start with "scripts/"'
|
||||
},
|
||||
args: { description: "Arguments to pass to the script" }
|
||||
},
|
||||
required: ["skillName", "scriptPath"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "read_skill_reference",
|
||||
description:
|
||||
"Read a reference document from a skill. Reference files are located in the references/ directory and contain additional documentation, guides, or examples.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
skillName: { type: "string", description: "The name of the skill" },
|
||||
refPath: {
|
||||
type: "string",
|
||||
description:
|
||||
'The path to the reference file (e.g., "references/guide.md"), MUST start with "references/"'
|
||||
}
|
||||
},
|
||||
required: ["skillName", "refPath"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "get_skill_asset",
|
||||
description:
|
||||
"Get an asset file from a skill. Assets are located in the assets/ directory and can be images, data files, or other resources.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
skillName: { type: "string", description: "The name of the skill" },
|
||||
assetPath: {
|
||||
type: "string",
|
||||
description:
|
||||
'The path to the asset file (e.g., "assets/icon.png"), MUST start with "assets/"'
|
||||
}
|
||||
},
|
||||
required: ["skillName", "assetPath"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "list_skills",
|
||||
description:
|
||||
"List all available skills in the system. Shows enabled skills by default, or all skills if specified.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabledOnly: {
|
||||
type: "boolean",
|
||||
description: "If true, only show enabled skills. Default: false"
|
||||
}
|
||||
},
|
||||
required: []
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "get_skill_info",
|
||||
description:
|
||||
"Get detailed information about a specific skill, including its scripts, references, assets, and metadata.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
skillName: { type: "string", description: "The name of the skill" }
|
||||
},
|
||||
required: ["skillName"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,11 +1,11 @@
|
||||
import { defineConfig } from "tsup";
|
||||
import { defineConfig } from "tsup"
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/bridge.ts"],
|
||||
entry: ["src/bridge.ts", "src/daemon.ts", "src/cli.ts"],
|
||||
format: ["esm"],
|
||||
target: "node18",
|
||||
clean: true,
|
||||
banner: {
|
||||
js: "#!/usr/bin/env node",
|
||||
},
|
||||
});
|
||||
js: "#!/usr/bin/env node"
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user