first commit

This commit is contained in:
nasir@endelospay.com
2025-08-12 02:54:17 +05:00
commit d97cad1736
225 changed files with 137626 additions and 0 deletions

147
app/native-server/README.md Normal file
View File

@@ -0,0 +1,147 @@
# Fastify Chrome Native Messaging服务
这是一个基于Fastify的TypeScript项目用于与Chrome扩展进行原生通信。
## 功能特性
- 通过Chrome Native Messaging协议与Chrome扩展进行双向通信
- 提供RESTful API服务
- 完全使用TypeScript开发
- 包含完整的测试套件
- 遵循代码质量最佳实践
## 开发环境设置
### 前置条件
- Node.js 14+
- npm 6+
### 安装
```bash
git clone https://github.com/your-username/fastify-chrome-native.git
cd fastify-chrome-native
npm install
```
### 开发
1. 本地构建注册native server
```bash
cd app/native-server
npm run dev
```
2. 启动chrome extension
```bash
cd app/chrome-extension
npm run dev
```
### 构建
```bash
npm run build
```
### 注册Native Messaging主机
全局安装后会自动注册
```bash
npm i -g mcp-chrome-bridge
```
### 与Chrome扩展集成
以下是Chrome扩展中如何使用此服务的简单示例
```javascript
// background.js
let nativePort = null;
let serverRunning = false;
// 启动Native Messaging服务
function startServer() {
if (nativePort) {
console.log('已连接到Native Messaging主机');
return;
}
try {
nativePort = chrome.runtime.connectNative('com.yourcompany.fastify_native_host');
nativePort.onMessage.addListener(message => {
console.log('收到Native消息:', message);
if (message.type === 'started') {
serverRunning = true;
console.log(`服务已启动,端口: ${message.payload.port}`);
} else if (message.type === 'stopped') {
serverRunning = false;
console.log('服务已停止');
} else if (message.type === 'error') {
console.error('Native错误:', message.payload.message);
}
});
nativePort.onDisconnect.addListener(() => {
console.log('Native连接断开:', chrome.runtime.lastError);
nativePort = null;
serverRunning = false;
});
// 启动服务器
nativePort.postMessage({ type: 'start', payload: { port: 3000 } });
} catch (error) {
console.error('启动Native Messaging时出错:', error);
}
}
// 停止服务器
function stopServer() {
if (nativePort && serverRunning) {
nativePort.postMessage({ type: 'stop' });
}
}
// 测试与服务器的通信
async function testPing() {
try {
const response = await fetch('http://localhost:3000/ping');
const data = await response.json();
console.log('Ping响应:', data);
return data;
} catch (error) {
console.error('Ping失败:', error);
return null;
}
}
// 在扩展启动时连接Native主机
chrome.runtime.onStartup.addListener(startServer);
// 导出供popup或内容脚本使用的API
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'startServer') {
startServer();
sendResponse({ success: true });
} else if (message.action === 'stopServer') {
stopServer();
sendResponse({ success: true });
} else if (message.action === 'testPing') {
testPing().then(sendResponse);
return true; // 指示我们将异步发送响应
}
});
```
### 测试
```bash
npm run test
```
### 许可证
MIT

View File

@@ -0,0 +1,64 @@
#!/bin/bash
# 获取脚本所在的绝对目录
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_DIR="/Users/hang/code/tencent/ai/chrome-mcp-server/app/native-server/dist/logs" # 或者你选择的、确定有写入权限的目录
# 获取当前时间戳用于日志文件名,避免覆盖
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
WRAPPER_LOG="${LOG_DIR}/native_host_wrapper_${TIMESTAMP}.log"
# Node.js 脚本的实际路径
NODE_SCRIPT="${SCRIPT_DIR}/index.js"
# 确保日志目录存在
mkdir -p "${LOG_DIR}"
# 记录 wrapper 脚本被调用的信息
echo "Wrapper script called at $(date)" > "${WRAPPER_LOG}"
echo "SCRIPT_DIR: ${SCRIPT_DIR}" >> "${WRAPPER_LOG}"
echo "LOG_DIR: ${LOG_DIR}" >> "${WRAPPER_LOG}"
echo "NODE_SCRIPT: ${NODE_SCRIPT}" >> "${WRAPPER_LOG}"
echo "Initial PATH: ${PATH}" >> "${WRAPPER_LOG}"
# 动态查找 Node.js 可执行文件
NODE_EXEC=""
# 1. 尝试用 which (它会使用当前环境的 PATH, 但 Chrome 的 PATH 可能不完整)
if command -v node &>/dev/null; then
NODE_EXEC=$(command -v node)
echo "Found node using 'command -v node': ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
# 2. 如果 which 找不到,尝试一些 macOS 上常见的 Node.js 安装路径
if [ -z "${NODE_EXEC}" ]; then
COMMON_NODE_PATHS=(
"/usr/local/bin/node" # Homebrew on Intel Macs / direct install
"/opt/homebrew/bin/node" # Homebrew on Apple Silicon
"$HOME/.nvm/versions/node/$(ls -t $HOME/.nvm/versions/node | head -n 1)/bin/node" # NVM (latest installed)
# 你可以根据需要添加更多你环境中可能存在的路径
)
for path_to_node in "${COMMON_NODE_PATHS[@]}"; do
if [ -x "${path_to_node}" ]; then
NODE_EXEC="${path_to_node}"
echo "Found node at common path: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
break
fi
done
fi
# 3. 如果还是找不到,记录错误并退出
if [ -z "${NODE_EXEC}" ]; then
echo "ERROR: Node.js executable not found!" >> "${WRAPPER_LOG}"
echo "Please ensure Node.js is installed and its path is accessible or configured in this script." >> "${WRAPPER_LOG}"
# 对于 Native Host它需要保持运行以接收消息直接退出可能不是最佳
# 但如果node都找不到也无法执行目标脚本
# 这里可以考虑输出一个符合 Native Messaging 协议的错误消息给扩展(如果可以的话)
# 或者就让它失败Chrome会报告 Native Host Exited.
exit 1 # 必须退出,否则下面的 exec 会失败
fi
echo "Using Node executable: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
echo "Node version found by script: $(${NODE_EXEC} -v)" >> "${WRAPPER_LOG}"
echo "Executing: ${NODE_EXEC} ${NODE_SCRIPT}" >> "${WRAPPER_LOG}"
echo "PWD: $(pwd)" >> "${WRAPPER_LOG}" # PWD 记录一下,有时有用
exec "${NODE_EXEC}" "${NODE_SCRIPT}" 2>> "${LOG_DIR}/native_host_stderr_${TIMESTAMP}.log"

View File

@@ -0,0 +1,325 @@
# Chrome MCP Bridge 安装指南
本文档详细说明了 Chrome MCP Bridge 的安装和注册流程。
## 安装流程概述
Chrome MCP Bridge 的安装和注册流程如下:
```
npm install -g chrome-mcp-bridge
└─ postinstall.js
├─ 复制可执行文件到 npm_prefix/bin ← 总是可写用户或root权限
├─ 尝试用户级别注册 ← 无需sudo大多数情况下成功
└─ 如果失败 ➜ 提示用户运行 chrome-mcp-bridge register --system
└─ 使用sudo-prompt自动提权 → 写入系统级清单文件
```
上面的流程图展示了从全局安装开始,到最终完成注册的完整过程。
## 详细安装步骤
### 1. 全局安装
```bash
npm install -g chrome-mcp-bridge
```
安装完成后,系统会自动尝试在用户目录中注册 Native Messaging 主机。这不需要管理员权限,是推荐的安装方式。
### 2. 用户级别注册
用户级别注册会在以下位置创建清单文件:
```
清单文件位置
├─ 用户级别(无需管理员权限)
│ ├─ Windows: %APPDATA%\Google\Chrome\NativeMessagingHosts\
│ ├─ macOS: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
│ └─ Linux: ~/.config/google-chrome/NativeMessagingHosts/
└─ 系统级别(需要管理员权限)
├─ Windows: %ProgramFiles%\Google\Chrome\NativeMessagingHosts\
├─ macOS: /Library/Google/Chrome/NativeMessagingHosts/
└─ Linux: /etc/opt/chrome/native-messaging-hosts/
```
如果自动注册失败,或者您想手动注册,可以运行:
```bash
chrome-mcp-bridge register
```
### 3. 系统级别注册
如果用户级别注册失败(例如,由于权限问题),您可以尝试系统级别注册。系统级别注册需要管理员权限,但我们提供了两种便捷的方式来完成这一过程。
系统级别注册有两种方式:
#### 方式一:使用 `--system` 参数(推荐)
```bash
chrome-mcp-bridge register --system
```
这将使用 `sudo-prompt` 自动提升权限,无需手动输入 `sudo` 命令。
#### 方式二:直接使用管理员权限
**Windows**
以管理员身份运行命令提示符或 PowerShell然后执行
```
chrome-mcp-bridge register
```
**macOS/Linux**
使用 sudo 命令:
```
sudo chrome-mcp-bridge register
```
## 注册流程详解
### 注册流程图
```
注册流程
├─ 用户级别注册 (chrome-mcp-bridge register)
│ ├─ 获取用户级别清单路径
│ ├─ 创建用户目录
│ ├─ 生成清单内容
│ ├─ 写入清单文件
│ └─ Windows平台创建用户级注册表项
└─ 系统级别注册 (chrome-mcp-bridge register --system)
├─ 检查是否有管理员权限
│ ├─ 有权限 → 直接创建系统目录和写入清单
│ └─ 无权限 → 使用sudo-prompt提权
│ ├─ 创建临时清单文件
│ └─ 复制到系统目录
└─ Windows平台创建系统级注册表项
```
### 清单文件结构
```
manifest.json
├─ name: "com.chrome-mcp.native-host"
├─ description: "Node.js Host for Browser Bridge Extension"
├─ path: "/path/to/node" ← Node.js可执行文件路径
├─ type: "stdio" ← 通信类型
├─ allowed_origins: [ ← 允许连接的扩展
│ "chrome-extension://扩展ID/"
└─ args: [ ← 启动参数
"/path/to/chrome-mcp-bridge",
"native"
]
```
### 用户级别注册流程
1. 确定用户级别清单文件路径
2. 创建必要的目录
3. 生成清单内容,包括:
- 主机名称
- 描述
- Node.js 可执行文件路径
- 通信类型stdio
- 允许的扩展 ID
- 启动参数
4. 写入清单文件
5. 在 Windows 上,还会创建相应的注册表项
### 系统级别注册流程
1. 检测是否已有管理员权限
2. 如果已有管理员权限:
- 直接创建系统级目录
- 写入清单文件
- 设置适当的权限
- 在 Windows 上创建系统级注册表项
3. 如果没有管理员权限:
- 使用 `sudo-prompt` 提升权限
- 创建临时清单文件
- 复制到系统目录
- 在 Windows 上创建系统级注册表项
## 验证安装
### 验证流程图
```
验证安装
├─ 检查清单文件
│ ├─ 文件存在 → 检查内容是否正确
│ └─ 文件不存在 → 重新安装
├─ 检查Chrome扩展
│ ├─ 扩展已安装 → 检查扩展权限
│ └─ 扩展未安装 → 安装扩展
└─ 测试连接
├─ 连接成功 → 安装完成
└─ 连接失败 → 检查错误日志 → 参考故障排除
```
### 验证步骤
安装完成后,您可以通过以下方式验证安装是否成功:
1. 检查清单文件是否存在于相应目录
- 用户级别:检查用户目录下的清单文件
- 系统级别:检查系统目录下的清单文件
- 确认清单文件内容是否正确
2. 在 Chrome 中安装对应的扩展
- 确保扩展已正确安装
- 确保扩展有 `nativeMessaging` 权限
3. 尝试通过扩展连接到本地服务
- 使用扩展的测试功能尝试连接
- 检查 Chrome 的扩展日志是否有错误信息
## 故障排除
### 故障排除流程图
```
故障排除
├─ 权限问题
│ ├─ 检查用户权限
│ │ ├─ 有足够权限 → 检查目录权限
│ │ └─ 无足够权限 → 尝试系统级别安装
│ │
│ ├─ 执行权限问题 (macOS/Linux)
│ │ ├─ "Permission denied" 错误
│ │ ├─ "Native host has exited" 错误
│ │ └─ 运行 chrome-mcp-bridge fix-permissions
│ │
│ └─ 尝试 chrome-mcp-bridge register --system
├─ 路径问题
│ ├─ 检查Node.js安装 (node -v)
│ └─ 检查全局NPM路径 (npm root -g)
├─ 注册表问题 (Windows)
│ ├─ 检查注册表访问权限
│ └─ 尝试手动创建注册表项
└─ 其他问题
├─ 检查控制台错误信息
└─ 提交Issue到项目仓库
```
### 常见问题解决步骤
如果安装过程中遇到问题,请尝试以下步骤:
1. 确保 Node.js 已正确安装
- 运行 `node -v``npm -v` 检查版本
- 确保 Node.js 版本 >= 14.x
2. 检查是否有足够的权限创建文件和目录
- 用户级别安装需要对用户目录有写入权限
- 系统级别安装需要管理员/root权限
3. **修复执行权限问题**
**macOS/Linux 平台**
**问题描述**
- npm 安装通常会保留文件权限,但 pnpm 可能不会
- 可能遇到 "Permission denied" 或 "Native host has exited" 错误
- Chrome 扩展无法启动 native host 进程
**解决方案**
a) **使用内置修复命令(推荐)**
```bash
chrome-mcp-bridge fix-permissions
```
b) **手动设置权限**
```bash
# 查找安装路径
npm list -g chrome-mcp-bridge
# 或者对于 pnpm
pnpm list -g chrome-mcp-bridge
# 设置执行权限(替换为实际路径)
chmod +x /path/to/node_modules/chrome-mcp-bridge/run_host.sh
chmod +x /path/to/node_modules/chrome-mcp-bridge/index.js
chmod +x /path/to/node_modules/chrome-mcp-bridge/cli.js
```
**Windows 平台**
**问题描述**
- Windows 上 `.bat` 文件通常不需要执行权限,但可能遇到其他问题
- 文件可能被标记为只读
- 可能遇到 "Access denied" 或文件无法执行的错误
**解决方案**
a) **使用内置修复命令(推荐)**
```cmd
chrome-mcp-bridge fix-permissions
```
b) **手动检查文件属性**
```cmd
# 查找安装路径
npm list -g chrome-mcp-bridge
# 检查文件属性(在文件资源管理器中右键 -> 属性)
# 确保 run_host.bat 不是只读文件
```
c) **重新安装并强制权限**
```bash
# 卸载
npm uninstall -g chrome-mcp-bridge
# 或 pnpm uninstall -g chrome-mcp-bridge
# 重新安装
npm install -g chrome-mcp-bridge
# 或 pnpm install -g chrome-mcp-bridge
# 如果仍有问题,运行权限修复
chrome-mcp-bridge fix-permissions
```
4. 在 Windows 上,确保注册表访问没有被限制
- 检查是否可以访问 `HKCU\Software\Google\Chrome\NativeMessagingHosts\`
- 对于系统级别,检查 `HKLM\Software\Google\Chrome\NativeMessagingHosts\`
5. 尝试使用系统级别安装
- 使用 `chrome-mcp-bridge register --system` 命令
- 或直接使用管理员权限运行
6. 检查控制台输出的错误信息
- 详细的错误信息通常会指出问题所在
- 可以添加 `--verbose` 参数获取更多日志信息
如果问题仍然存在,请提交 issue 到项目仓库,并附上以下信息:
- 操作系统版本
- Node.js 版本
- 安装命令
- 错误信息
- 尝试过的解决方法

View File

@@ -0,0 +1,17 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/scripts/**/*'],
coverageDirectory: 'coverage',
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80,
},
},
};

View File

@@ -0,0 +1,78 @@
{
"name": "mcp-chrome-bridge",
"version": "1.0.29",
"description": "Chrome Native-Messaging host (Node)",
"main": "dist/index.js",
"bin": {
"mcp-chrome-bridge": "./dist/cli.js",
"mcp-chrome-stdio": "./dist/mcp/mcp-server-stdio.js"
},
"scripts": {
"dev": "nodemon --watch src --ext ts,js,json --ignore dist/ --exec \"npm run build && npm run register:dev\"",
"build": "ts-node src/scripts/build.ts",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint 'src/**/*.{js,ts}'",
"lint:fix": "eslint 'src/**/*.{js,ts}' --fix",
"format": "prettier --write 'src/**/*.{js,ts,json}'",
"register:dev": "node dist/scripts/register-dev.js",
"postinstall": "node dist/scripts/postinstall.js"
},
"files": [
"dist"
],
"engines": {
"node": ">=14.0.0"
},
"preferGlobal": true,
"keywords": [
"mcp",
"chrome",
"browser"
],
"author": "hangye",
"license": "MIT",
"dependencies": {
"@fastify/cors": "^11.0.1",
"@modelcontextprotocol/sdk": "^1.11.0",
"chalk": "^5.4.1",
"chrome-mcp-shared": "workspace:*",
"commander": "^13.1.0",
"fastify": "^5.3.2",
"is-admin": "^4.0.0",
"pino": "^9.6.0",
"uuid": "^11.1.0"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/chrome": "^0.0.318",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.3",
"@types/supertest": "^6.0.3",
"@typescript-eslint/parser": "^8.31.1",
"cross-env": "^7.0.3",
"husky": "^9.1.7",
"jest": "^29.7.0",
"lint-staged": "^15.5.1",
"nodemon": "^3.1.10",
"pino-pretty": "^13.0.0",
"rimraf": "^6.0.1",
"supertest": "^7.1.0",
"ts-jest": "^29.3.2",
"ts-node": "^10.9.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,ts}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env node
import { program } from 'commander';
import * as fs from 'fs';
import * as path from 'path';
import {
tryRegisterUserLevelHost,
colorText,
registerWithElevatedPermissions,
ensureExecutionPermissions,
} from './scripts/utils';
// Import writeNodePath from postinstall
async function writeNodePath(): Promise<void> {
try {
const nodePath = process.execPath;
const nodePathFile = path.join(__dirname, 'node_path.txt');
console.log(colorText(`Writing Node.js path: ${nodePath}`, 'blue'));
fs.writeFileSync(nodePathFile, nodePath, 'utf8');
console.log(colorText('✓ Node.js path written for run_host scripts', 'green'));
} catch (error: any) {
console.warn(colorText(`⚠️ Failed to write Node.js path: ${error.message}`, 'yellow'));
}
}
program
.version(require('../package.json').version)
.description('Mcp Chrome Bridge - Local service for communicating with Chrome extension');
// Register Native Messaging host
program
.command('register')
.description('Register Native Messaging host')
.option('-f, --force', 'Force re-registration')
.option('-s, --system', 'Use system-level installation (requires administrator/sudo privileges)')
.action(async (options) => {
try {
// Write Node.js path for run_host scripts
await writeNodePath();
// Detect if running with root/administrator privileges
const isRoot = process.getuid && process.getuid() === 0; // Unix/Linux/Mac
let isAdmin = false;
if (process.platform === 'win32') {
try {
isAdmin = require('is-admin')(); // Windows requires additional package
} catch (error) {
console.warn(
colorText('Warning: Unable to detect administrator privileges on Windows', 'yellow'),
);
isAdmin = false;
}
}
const hasElevatedPermissions = isRoot || isAdmin;
// If --system option is specified or running with root/administrator privileges
if (options.system || hasElevatedPermissions) {
await registerWithElevatedPermissions();
console.log(
colorText('System-level Native Messaging host registered successfully!', 'green'),
);
console.log(
colorText(
'You can now use connectNative in Chrome extension to connect to this service.',
'blue',
),
);
} else {
// Regular user-level installation
console.log(colorText('Registering user-level Native Messaging host...', 'blue'));
const success = await tryRegisterUserLevelHost();
if (success) {
console.log(colorText('Native Messaging host registered successfully!', 'green'));
console.log(
colorText(
'You can now use connectNative in Chrome extension to connect to this service.',
'blue',
),
);
} else {
console.log(
colorText(
'User-level registration failed, please try the following methods:',
'yellow',
),
);
console.log(colorText(' 1. sudo mcp-chrome-bridge register', 'yellow'));
console.log(colorText(' 2. mcp-chrome-bridge register --system', 'yellow'));
process.exit(1);
}
}
} catch (error: any) {
console.error(colorText(`Registration failed: ${error.message}`, 'red'));
process.exit(1);
}
});
// Fix execution permissions
program
.command('fix-permissions')
.description('Fix execution permissions for native host files')
.action(async () => {
try {
console.log(colorText('Fixing execution permissions...', 'blue'));
await ensureExecutionPermissions();
console.log(colorText('✓ Execution permissions fixed successfully!', 'green'));
} catch (error: any) {
console.error(colorText(`Failed to fix permissions: ${error.message}`, 'red'));
process.exit(1);
}
});
// Update port in stdio-config.json
program
.command('update-port <port>')
.description('Update the port number in stdio-config.json')
.action(async (port: string) => {
try {
const portNumber = parseInt(port, 10);
if (isNaN(portNumber) || portNumber < 1 || portNumber > 65535) {
console.error(colorText('Error: Port must be a valid number between 1 and 65535', 'red'));
process.exit(1);
}
const configPath = path.join(__dirname, 'mcp', 'stdio-config.json');
if (!fs.existsSync(configPath)) {
console.error(colorText(`Error: Configuration file not found at ${configPath}`, 'red'));
process.exit(1);
}
const configData = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(configData);
const currentUrl = new URL(config.url);
currentUrl.port = portNumber.toString();
config.url = currentUrl.toString();
fs.writeFileSync(configPath, JSON.stringify(config, null, 4));
console.log(colorText(`✓ Port updated successfully to ${portNumber}`, 'green'));
console.log(colorText(`Updated URL: ${config.url}`, 'blue'));
} catch (error: any) {
console.error(colorText(`Failed to update port: ${error.message}`, 'red'));
process.exit(1);
}
});
program.parse(process.argv);
// If no command provided, show help
if (!process.argv.slice(2).length) {
program.outputHelp();
}

View File

@@ -0,0 +1,47 @@
export enum NATIVE_MESSAGE_TYPE {
START = 'start',
STARTED = 'started',
STOP = 'stop',
STOPPED = 'stopped',
PING = 'ping',
PONG = 'pong',
ERROR = 'error',
}
export const NATIVE_SERVER_PORT = 56889;
// Timeout constants (in milliseconds)
export const TIMEOUTS = {
DEFAULT_REQUEST_TIMEOUT: 15000,
EXTENSION_REQUEST_TIMEOUT: 20000,
PROCESS_DATA_TIMEOUT: 20000,
} as const;
// Server configuration
export const SERVER_CONFIG = {
HOST: '127.0.0.1',
CORS_ORIGIN: true,
LOGGER_ENABLED: false,
} as const;
// HTTP Status codes
export const HTTP_STATUS = {
OK: 200,
NO_CONTENT: 204,
BAD_REQUEST: 400,
INTERNAL_SERVER_ERROR: 500,
GATEWAY_TIMEOUT: 504,
} as const;
// Error messages
export const ERROR_MESSAGES = {
NATIVE_HOST_NOT_AVAILABLE: 'Native host connection not established.',
SERVER_NOT_RUNNING: 'Server is not actively running.',
REQUEST_TIMEOUT: 'Request to extension timed out.',
INVALID_MCP_REQUEST: 'Invalid MCP request or session.',
INVALID_SESSION_ID: 'Invalid or missing MCP session ID.',
INTERNAL_SERVER_ERROR: 'Internal Server Error',
MCP_SESSION_DELETION_ERROR: 'Internal server error during MCP session deletion.',
MCP_REQUEST_PROCESSING_ERROR: 'Internal server error during MCP request processing.',
INVALID_SSE_SESSION: 'Invalid or missing MCP session ID for SSE.',
} as const;

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env node
import serverInstance from './server';
import nativeMessagingHostInstance from './native-messaging-host';
try {
serverInstance.setNativeHost(nativeMessagingHostInstance); // Server needs setNativeHost method
nativeMessagingHostInstance.setServer(serverInstance); // NativeHost needs setServer method
nativeMessagingHostInstance.start();
} catch (error) {
process.exit(1);
}
process.on('error', (error) => {
process.exit(1);
});
// Handle process signals and uncaught exceptions
process.on('SIGINT', () => {
process.exit(0);
});
process.on('SIGTERM', () => {
process.exit(0);
});
process.on('exit', (code) => {
});
process.on('uncaughtException', (error) => {
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
// Don't exit immediately, let the program continue running
});

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import {
CallToolRequestSchema,
CallToolResult,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { TOOL_SCHEMAS } from 'chrome-mcp-shared';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import * as fs from 'fs';
import * as path from 'path';
let stdioMcpServer: Server | null = null;
let mcpClient: Client | null = null;
// Read configuration from stdio-config.json
const loadConfig = () => {
try {
const configPath = path.join(__dirname, 'stdio-config.json');
const configData = fs.readFileSync(configPath, 'utf8');
return JSON.parse(configData);
} catch (error) {
console.error('Failed to load stdio-config.json:', error);
throw new Error('Configuration file stdio-config.json not found or invalid');
}
};
export const getStdioMcpServer = () => {
if (stdioMcpServer) {
return stdioMcpServer;
}
stdioMcpServer = new Server(
{
name: 'StdioChromeMcpServer',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
},
);
setupTools(stdioMcpServer);
return stdioMcpServer;
};
export const ensureMcpClient = async () => {
try {
if (mcpClient) {
const pingResult = await mcpClient.ping();
if (pingResult) {
return mcpClient;
}
}
const config = loadConfig();
mcpClient = new Client({ name: 'Mcp Chrome Proxy', version: '1.0.0' }, { capabilities: {} });
const transport = new StreamableHTTPClientTransport(new URL(config.url), {});
await mcpClient.connect(transport);
return mcpClient;
} catch (error) {
mcpClient?.close();
mcpClient = null;
console.error('Failed to connect to MCP server:', error);
}
};
export const setupTools = (server: Server) => {
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments || {}),
);
};
const handleToolCall = async (name: string, args: any): Promise<CallToolResult> => {
try {
const client = await ensureMcpClient();
if (!client) {
throw new Error('Failed to connect to MCP server');
}
const result = await client.callTool({ name, arguments: args }, undefined, {
timeout: 2 * 6 * 1000, // Default timeout of 2 minute
});
return result as CallToolResult;
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error calling tool: ${error.message}`,
},
],
isError: true,
};
}
};
async function main() {
const transport = new StdioServerTransport();
await getStdioMcpServer().connect(transport);
}
main().catch((error) => {
console.error('Fatal error Chrome MCP Server main():', error);
process.exit(1);
});

View File

@@ -0,0 +1,24 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { setupTools } from './register-tools';
export let mcpServer: Server | null = null;
export const getMcpServer = () => {
if (mcpServer) {
return mcpServer;
}
mcpServer = new Server(
{
name: 'ChromeMcpServer',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
},
);
setupTools(mcpServer);
return mcpServer;
};

View File

@@ -0,0 +1,55 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
CallToolResult,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import nativeMessagingHostInstance from '../native-messaging-host';
import { NativeMessageType, TOOL_SCHEMAS } from 'chrome-mcp-shared';
export const setupTools = (server: Server) => {
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments || {}),
);
};
const handleToolCall = async (name: string, args: any): Promise<CallToolResult> => {
try {
// 发送请求到Chrome扩展并等待响应
const response = await nativeMessagingHostInstance.sendRequestToExtensionAndWait(
{
name,
args,
},
NativeMessageType.CALL_TOOL,
30000, // 30秒超时
);
if (response.status === 'success') {
return response.data;
} else {
return {
content: [
{
type: 'text',
text: `Error calling tool: ${response.error}`,
},
],
isError: true,
};
}
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error calling tool: ${error.message}`,
},
],
isError: true,
};
}
};

View File

@@ -0,0 +1,3 @@
{
"url": "http://127.0.0.1:12306/mcp"
}

View File

@@ -0,0 +1,268 @@
import { stdin, stdout } from 'process';
import { Server } from './server';
import { v4 as uuidv4 } from 'uuid';
import { NativeMessageType } from 'chrome-mcp-shared';
import { TIMEOUTS } from './constant';
interface PendingRequest {
resolve: (value: any) => void;
reject: (reason?: any) => void;
timeoutId: NodeJS.Timeout;
}
export class NativeMessagingHost {
private associatedServer: Server | null = null;
private pendingRequests: Map<string, PendingRequest> = new Map();
public setServer(serverInstance: Server): void {
this.associatedServer = serverInstance;
}
// add message handler to wait for start server
public start(): void {
try {
this.setupMessageHandling();
} catch (error: any) {
process.exit(1);
}
}
private setupMessageHandling(): void {
let buffer = Buffer.alloc(0);
let expectedLength = -1;
stdin.on('readable', () => {
let chunk;
while ((chunk = stdin.read()) !== null) {
buffer = Buffer.concat([buffer, chunk]);
if (expectedLength === -1 && buffer.length >= 4) {
expectedLength = buffer.readUInt32LE(0);
buffer = buffer.slice(4);
}
if (expectedLength !== -1 && buffer.length >= expectedLength) {
const messageBuffer = buffer.slice(0, expectedLength);
buffer = buffer.slice(expectedLength);
try {
const message = JSON.parse(messageBuffer.toString());
this.handleMessage(message);
} catch (error: any) {
this.sendError(`Failed to parse message: ${error.message}`);
}
expectedLength = -1; // reset to get next data
}
}
});
stdin.on('end', () => {
this.cleanup();
});
stdin.on('error', () => {
this.cleanup();
});
}
private async handleMessage(message: any): Promise<void> {
if (!message || typeof message !== 'object') {
this.sendError('Invalid message format');
return;
}
if (message.responseToRequestId) {
const requestId = message.responseToRequestId;
const pending = this.pendingRequests.get(requestId);
if (pending) {
clearTimeout(pending.timeoutId);
if (message.error) {
pending.reject(new Error(message.error));
} else {
pending.resolve(message.payload);
}
this.pendingRequests.delete(requestId);
} else {
// just ignore
}
return;
}
// Handle directive messages from Chrome
try {
switch (message.type) {
case NativeMessageType.START:
await this.startServer(message.payload?.port || 3000);
break;
case NativeMessageType.STOP:
await this.stopServer();
break;
// Keep ping/pong for simple liveness detection, but this differs from request-response pattern
case 'ping_from_extension':
this.sendMessage({ type: 'pong_to_extension' });
break;
default:
// Double check when message type is not supported
if (!message.responseToRequestId) {
this.sendError(
`Unknown message type or non-response message: ${message.type || 'no type'}`,
);
}
}
} catch (error: any) {
this.sendError(`Failed to handle directive message: ${error.message}`);
}
}
/**
* Send request to Chrome and wait for response
* @param messagePayload Data to send to Chrome
* @param timeoutMs Timeout for waiting response (milliseconds)
* @returns Promise, resolves to Chrome's returned payload on success, rejects on failure
*/
public sendRequestToExtensionAndWait(
messagePayload: any,
messageType: string = 'request_data',
timeoutMs: number = TIMEOUTS.DEFAULT_REQUEST_TIMEOUT,
): Promise<any> {
return new Promise((resolve, reject) => {
const requestId = uuidv4(); // Generate unique request ID
const timeoutId = setTimeout(() => {
this.pendingRequests.delete(requestId); // Remove from Map after timeout
reject(new Error(`Request timed out after ${timeoutMs}ms`));
}, timeoutMs);
// Store request's resolve/reject functions and timeout ID
this.pendingRequests.set(requestId, { resolve, reject, timeoutId });
// Send message with requestId to Chrome
this.sendMessage({
type: messageType, // Define a request type, e.g. 'request_data'
payload: messagePayload,
requestId: requestId, // <--- Key: include request ID
});
});
}
/**
* Start Fastify server (now accepts Server instance)
*/
private async startServer(port: number): Promise<void> {
if (!this.associatedServer) {
this.sendError('Internal error: server instance not set');
return;
}
try {
if (this.associatedServer.isRunning) {
this.sendMessage({
type: NativeMessageType.ERROR,
payload: { message: 'Server is already running' },
});
return;
}
await this.associatedServer.start(port, this);
this.sendMessage({
type: NativeMessageType.SERVER_STARTED,
payload: { port },
});
} catch (error: any) {
this.sendError(`Failed to start server: ${error.message}`);
}
}
/**
* Stop Fastify server
*/
private async stopServer(): Promise<void> {
if (!this.associatedServer) {
this.sendError('Internal error: server instance not set');
return;
}
try {
// Check status through associatedServer
if (!this.associatedServer.isRunning) {
this.sendMessage({
type: NativeMessageType.ERROR,
payload: { message: 'Server is not running' },
});
return;
}
await this.associatedServer.stop();
// this.serverStarted = false; // Server should update its own status after successful stop
this.sendMessage({ type: NativeMessageType.SERVER_STOPPED }); // Distinguish from previous 'stopped'
} catch (error: any) {
this.sendError(`Failed to stop server: ${error.message}`);
}
}
/**
* Send message to Chrome extension
*/
public sendMessage(message: any): void {
try {
const messageString = JSON.stringify(message);
const messageBuffer = Buffer.from(messageString);
const headerBuffer = Buffer.alloc(4);
headerBuffer.writeUInt32LE(messageBuffer.length, 0);
// Ensure atomic write
stdout.write(Buffer.concat([headerBuffer, messageBuffer]), (err) => {
if (err) {
// Consider how to handle write failure, may affect request completion
} else {
// Message sent successfully, no action needed
}
});
} catch (error: any) {
// Catch JSON.stringify or Buffer operation errors
// If preparation stage fails, associated request may never be sent
// Need to consider whether to reject corresponding Promise (if called within sendRequestToExtensionAndWait)
}
}
/**
* Send error message to Chrome extension (mainly for sending non-request-response type errors)
*/
private sendError(errorMessage: string): void {
this.sendMessage({
type: NativeMessageType.ERROR_FROM_NATIVE_HOST, // Use more explicit type
payload: { message: errorMessage },
});
}
/**
* Clean up resources
*/
private cleanup(): void {
// Reject all pending requests
this.pendingRequests.forEach((pending) => {
clearTimeout(pending.timeoutId);
pending.reject(new Error('Native host is shutting down or Chrome disconnected.'));
});
this.pendingRequests.clear();
if (this.associatedServer && this.associatedServer.isRunning) {
this.associatedServer
.stop()
.then(() => {
process.exit(0);
})
.catch(() => {
process.exit(1);
});
} else {
process.exit(0);
}
}
}
const nativeMessagingHostInstance = new NativeMessagingHost();
export default nativeMessagingHostInstance;

View File

@@ -0,0 +1,121 @@
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
const distDir = path.join(__dirname, '..', '..', 'dist');
// 清理上次构建
console.log('清理上次构建...');
try {
fs.rmSync(distDir, { recursive: true, force: true });
} catch (err) {
// 忽略目录不存在的错误
console.log(err);
}
// 创建dist目录
fs.mkdirSync(distDir, { recursive: true });
fs.mkdirSync(path.join(distDir, 'logs'), { recursive: true }); // 创建logs目录
console.log('dist 和 dist/logs 目录已创建/确认存在');
// 编译TypeScript
console.log('编译TypeScript...');
execSync('tsc', { stdio: 'inherit' });
// 复制配置文件
console.log('复制配置文件...');
const configSourcePath = path.join(__dirname, '..', 'mcp', 'stdio-config.json');
const configDestPath = path.join(distDir, 'mcp', 'stdio-config.json');
try {
// 确保目标目录存在
fs.mkdirSync(path.dirname(configDestPath), { recursive: true });
if (fs.existsSync(configSourcePath)) {
fs.copyFileSync(configSourcePath, configDestPath);
console.log(`已将 stdio-config.json 复制到 ${configDestPath}`);
} else {
console.error(`错误: 配置文件未找到: ${configSourcePath}`);
}
} catch (error) {
console.error('复制配置文件时出错:', error);
}
// 复制package.json并更新其内容
console.log('准备package.json...');
const packageJson = require('../../package.json');
// 创建安装说明
const readmeContent = `# ${packageJson.name}
本程序为Chrome扩展的Native Messaging主机端。
## 安装说明
1. 确保已安装Node.js
2. 全局安装本程序:
\`\`\`
npm install -g ${packageJson.name}
\`\`\`
3. 注册Native Messaging主机:
\`\`\`
# 用户级别安装(推荐)
${packageJson.name} register
# 如果用户级别安装失败,可以尝试系统级别安装
${packageJson.name} register --system
# 或者使用管理员权限
sudo ${packageJson.name} register
\`\`\`
## 使用方法
此应用程序由Chrome扩展自动启动无需手动运行。
`;
fs.writeFileSync(path.join(distDir, 'README.md'), readmeContent);
console.log('复制包装脚本...');
const scriptsSourceDir = path.join(__dirname, '.');
const macOsWrapperSourcePath = path.join(scriptsSourceDir, 'run_host.sh');
const windowsWrapperSourcePath = path.join(scriptsSourceDir, 'run_host.bat');
const macOsWrapperDestPath = path.join(distDir, 'run_host.sh');
const windowsWrapperDestPath = path.join(distDir, 'run_host.bat');
try {
if (fs.existsSync(macOsWrapperSourcePath)) {
fs.copyFileSync(macOsWrapperSourcePath, macOsWrapperDestPath);
console.log(`已将 ${macOsWrapperSourcePath} 复制到 ${macOsWrapperDestPath}`);
} else {
console.error(`错误: macOS 包装脚本源文件未找到: ${macOsWrapperSourcePath}`);
}
if (fs.existsSync(windowsWrapperSourcePath)) {
fs.copyFileSync(windowsWrapperSourcePath, windowsWrapperDestPath);
console.log(`已将 ${windowsWrapperSourcePath} 复制到 ${windowsWrapperDestPath}`);
} else {
console.error(`错误: Windows 包装脚本源文件未找到: ${windowsWrapperSourcePath}`);
}
} catch (error) {
console.error('复制包装脚本时出错:', error);
}
// 为关键JavaScript文件和macOS包装脚本添加可执行权限
console.log('添加可执行权限...');
const filesToMakeExecutable = ['index.js', 'cli.js', 'run_host.sh']; // cli.js 假设在 dist 根目录
filesToMakeExecutable.forEach((file) => {
const filePath = path.join(distDir, file); // filePath 现在是目标路径
try {
if (fs.existsSync(filePath)) {
fs.chmodSync(filePath, '755');
console.log(`已为 ${file} 添加可执行权限 (755)`);
} else {
console.warn(`警告: ${filePath} 不存在,无法添加可执行权限`);
}
} catch (error) {
console.error(`${file} 添加可执行权限时出错:`, error);
}
});
console.log('✅ 构建完成');

View File

@@ -0,0 +1,4 @@
export const COMMAND_NAME = 'chrome-mcp-bridge';
export const EXTENSION_ID = 'hbdgbgagpkpjffpklnamcljpakneikee';
export const HOST_NAME = 'com.chromemcp.nativehost';
export const DESCRIPTION = 'Node.js Host for Browser Bridge Extension';

View File

@@ -0,0 +1,295 @@
#!/usr/bin/env node
import fs from 'fs';
import os from 'os';
import path from 'path';
import { COMMAND_NAME } from './constant';
import { colorText, tryRegisterUserLevelHost } from './utils';
// Check if this script is run directly
const isDirectRun = require.main === module;
// Detect global installation for both npm and pnpm
function detectGlobalInstall(): boolean {
// npm uses npm_config_global
if (process.env.npm_config_global === 'true') {
return true;
}
// pnpm detection methods
// Method 1: Check if PNPM_HOME is set and current path contains it
if (process.env.PNPM_HOME && __dirname.includes(process.env.PNPM_HOME)) {
return true;
}
// Method 2: Check if we're in a global pnpm directory structure
// pnpm global packages are typically installed in ~/.local/share/pnpm/global/5/node_modules
// Windows: %APPDATA%\pnpm\global\5\node_modules
const globalPnpmPatterns =
process.platform === 'win32'
? ['\\pnpm\\global\\', '\\pnpm-global\\', '\\AppData\\Roaming\\pnpm\\']
: ['/pnpm/global/', '/.local/share/pnpm/', '/pnpm-global/'];
if (globalPnpmPatterns.some((pattern) => __dirname.includes(pattern))) {
return true;
}
// Method 3: Check npm_config_prefix for pnpm
if (process.env.npm_config_prefix && __dirname.includes(process.env.npm_config_prefix)) {
return true;
}
// Method 4: Windows-specific global installation paths
if (process.platform === 'win32') {
const windowsGlobalPatterns = [
'\\npm\\node_modules\\',
'\\AppData\\Roaming\\npm\\node_modules\\',
'\\Program Files\\nodejs\\node_modules\\',
'\\nodejs\\node_modules\\',
];
if (windowsGlobalPatterns.some((pattern) => __dirname.includes(pattern))) {
return true;
}
}
return false;
}
const isGlobalInstall = detectGlobalInstall();
/**
* Write Node.js path for run_host scripts to avoid fragile relative paths
*/
async function writeNodePath(): Promise<void> {
try {
const nodePath = process.execPath;
const nodePathFile = path.join(__dirname, '..', 'node_path.txt');
console.log(colorText(`Writing Node.js path: ${nodePath}`, 'blue'));
fs.writeFileSync(nodePathFile, nodePath, 'utf8');
console.log(colorText('✓ Node.js path written for run_host scripts', 'green'));
} catch (error: any) {
console.warn(colorText(`⚠️ Failed to write Node.js path: ${error.message}`, 'yellow'));
}
}
/**
* 确保执行权限(无论是否为全局安装)
*/
async function ensureExecutionPermissions(): Promise<void> {
if (process.platform === 'win32') {
// Windows 平台处理
await ensureWindowsFilePermissions();
return;
}
// Unix/Linux 平台处理
const filesToCheck = [
path.join(__dirname, '..', 'index.js'),
path.join(__dirname, '..', 'run_host.sh'),
path.join(__dirname, '..', 'cli.js'),
];
for (const filePath of filesToCheck) {
if (fs.existsSync(filePath)) {
try {
fs.chmodSync(filePath, '755');
console.log(
colorText(`✓ Set execution permissions for ${path.basename(filePath)}`, 'green'),
);
} catch (err: any) {
console.warn(
colorText(
`⚠️ Unable to set execution permissions for ${path.basename(filePath)}: ${err.message}`,
'yellow',
),
);
}
} else {
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
}
}
}
/**
* Windows 平台文件权限处理
*/
async function ensureWindowsFilePermissions(): Promise<void> {
const filesToCheck = [
path.join(__dirname, '..', 'index.js'),
path.join(__dirname, '..', 'run_host.bat'),
path.join(__dirname, '..', 'cli.js'),
];
for (const filePath of filesToCheck) {
if (fs.existsSync(filePath)) {
try {
// 检查文件是否为只读,如果是则移除只读属性
const stats = fs.statSync(filePath);
if (!(stats.mode & parseInt('200', 8))) {
// 检查写权限
// 尝试移除只读属性
fs.chmodSync(filePath, stats.mode | parseInt('200', 8));
console.log(
colorText(`✓ Removed read-only attribute from ${path.basename(filePath)}`, 'green'),
);
}
// 验证文件可读性
fs.accessSync(filePath, fs.constants.R_OK);
console.log(
colorText(`✓ Verified file accessibility for ${path.basename(filePath)}`, 'green'),
);
} catch (err: any) {
console.warn(
colorText(
`⚠️ Unable to verify file permissions for ${path.basename(filePath)}: ${err.message}`,
'yellow',
),
);
}
} else {
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
}
}
}
async function tryRegisterNativeHost(): Promise<void> {
try {
console.log(colorText('Attempting to register Chrome Native Messaging host...', 'blue'));
// Always ensure execution permissions, regardless of installation type
await ensureExecutionPermissions();
if (isGlobalInstall) {
// First try user-level installation (no elevated permissions required)
const userLevelSuccess = await tryRegisterUserLevelHost();
if (!userLevelSuccess) {
// User-level installation failed, suggest using register command
console.log(
colorText(
'User-level installation failed, system-level installation may be needed',
'yellow',
),
);
console.log(
colorText('Please run the following command for system-level installation:', 'blue'),
);
console.log(` ${COMMAND_NAME} register --system`);
printManualInstructions();
}
} else {
// Local installation mode, don't attempt automatic registration
console.log(
colorText('Local installation detected, skipping automatic registration', 'yellow'),
);
printManualInstructions();
}
} catch (error) {
console.log(
colorText(
`注册过程中出现错误: ${error instanceof Error ? error.message : String(error)}`,
'red',
),
);
printManualInstructions();
}
}
/**
* 打印手动安装指南
*/
function printManualInstructions(): void {
console.log('\n' + colorText('===== Manual Registration Guide =====', 'blue'));
console.log(colorText('1. Try user-level installation (recommended):', 'yellow'));
if (isGlobalInstall) {
console.log(` ${COMMAND_NAME} register`);
} else {
console.log(` npx ${COMMAND_NAME} register`);
}
console.log(
colorText('\n2. If user-level installation fails, try system-level installation:', 'yellow'),
);
console.log(colorText(' Use --system parameter (auto-elevate permissions):', 'yellow'));
if (isGlobalInstall) {
console.log(` ${COMMAND_NAME} register --system`);
} else {
console.log(` npx ${COMMAND_NAME} register --system`);
}
console.log(colorText('\n Or use administrator privileges directly:', 'yellow'));
if (os.platform() === 'win32') {
console.log(
colorText(
' Please run Command Prompt or PowerShell as administrator and execute:',
'yellow',
),
);
if (isGlobalInstall) {
console.log(` ${COMMAND_NAME} register`);
} else {
console.log(` npx ${COMMAND_NAME} register`);
}
} else {
console.log(colorText(' Please run the following command in terminal:', 'yellow'));
if (isGlobalInstall) {
console.log(` sudo ${COMMAND_NAME} register`);
} else {
console.log(` sudo npx ${COMMAND_NAME} register`);
}
}
console.log(
'\n' +
colorText(
'Ensure Chrome extension is installed and refresh the extension to connect to local service.',
'blue',
),
);
}
/**
* 主函数
*/
async function main(): Promise<void> {
console.log(colorText(`Installing ${COMMAND_NAME}...`, 'green'));
// Debug information
console.log(colorText('Installation environment debug info:', 'blue'));
console.log(` __dirname: ${__dirname}`);
console.log(` npm_config_global: ${process.env.npm_config_global}`);
console.log(` PNPM_HOME: ${process.env.PNPM_HOME}`);
console.log(` npm_config_prefix: ${process.env.npm_config_prefix}`);
console.log(` isGlobalInstall: ${isGlobalInstall}`);
// Always ensure execution permissions first
await ensureExecutionPermissions();
// Write Node.js path for run_host scripts to use
await writeNodePath();
// If global installation, try automatic registration
if (isGlobalInstall) {
await tryRegisterNativeHost();
} else {
console.log(colorText('Local installation detected', 'yellow'));
printManualInstructions();
}
}
// Only execute main function when running this script directly
if (isDirectRun) {
main().catch((error) => {
console.error(
colorText(
`Installation script error: ${error instanceof Error ? error.message : String(error)}`,
'red',
),
);
});
}

View File

@@ -0,0 +1,3 @@
import { tryRegisterUserLevelHost } from './utils';
tryRegisterUserLevelHost();

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env node
import { COMMAND_NAME } from './constant';
import { colorText, registerWithElevatedPermissions } from './utils';
/**
* 主函数
*/
async function main(): Promise<void> {
console.log(colorText(`正在注册 ${COMMAND_NAME} Native Messaging主机...`, 'blue'));
try {
await registerWithElevatedPermissions();
console.log(
colorText('注册成功现在Chrome扩展可以通过Native Messaging与本地服务通信。', 'green'),
);
} catch (error: any) {
console.error(colorText(`注册失败: ${error.message}`, 'red'));
process.exit(1);
}
}
// 执行主函数
main();

View File

@@ -0,0 +1,95 @@
@echo off
setlocal enabledelayedexpansion
REM Setup paths
set "SCRIPT_DIR=%~dp0"
if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
set "LOG_DIR=%SCRIPT_DIR%\logs"
set "NODE_SCRIPT=%SCRIPT_DIR%\index.js"
if not exist "%LOG_DIR%" md "%LOG_DIR%"
REM Generate timestamp
for /f %%i in ('powershell -NoProfile -Command "Get-Date -Format 'yyyyMMdd_HHmmss'"') do set "TIMESTAMP=%%i"
set "WRAPPER_LOG=%LOG_DIR%\native_host_wrapper_windows_%TIMESTAMP%.log"
set "STDERR_LOG=%LOG_DIR%\native_host_stderr_windows_%TIMESTAMP%.log"
REM Initial logging
echo Wrapper script called at %DATE% %TIME% > "%WRAPPER_LOG%"
echo SCRIPT_DIR: %SCRIPT_DIR% >> "%WRAPPER_LOG%"
echo LOG_DIR: %LOG_DIR% >> "%WRAPPER_LOG%"
echo NODE_SCRIPT: %NODE_SCRIPT% >> "%WRAPPER_LOG%"
echo Initial PATH: %PATH% >> "%WRAPPER_LOG%"
echo User: %USERNAME% >> "%WRAPPER_LOG%"
echo Current PWD: %CD% >> "%WRAPPER_LOG%"
REM Node.js discovery
set "NODE_EXEC="
REM Priority 1: Installation-time node path
set "NODE_PATH_FILE=%SCRIPT_DIR%\node_path.txt"
echo Checking installation-time node path >> "%WRAPPER_LOG%"
if exist "%NODE_PATH_FILE%" (
set /p EXPECTED_NODE=<"%NODE_PATH_FILE%"
if exist "!EXPECTED_NODE!" (
set "NODE_EXEC=!EXPECTED_NODE!"
echo Found installation-time node at !NODE_EXEC! >> "%WRAPPER_LOG%"
)
)
REM Priority 1.5: Fallback to relative path
if not defined NODE_EXEC (
set "EXPECTED_NODE=%SCRIPT_DIR%\..\..\..\node.exe"
echo Checking relative path >> "%WRAPPER_LOG%"
if exist "%EXPECTED_NODE%" (
set "NODE_EXEC=%EXPECTED_NODE%"
echo Found node at relative path: !NODE_EXEC! >> "%WRAPPER_LOG%"
)
)
REM Priority 2: where command
if not defined NODE_EXEC (
echo Trying 'where node.exe' >> "%WRAPPER_LOG%"
for /f "delims=" %%i in ('where node.exe 2^>nul') do (
if not defined NODE_EXEC (
set "NODE_EXEC=%%i"
echo Found node using 'where': !NODE_EXEC! >> "%WRAPPER_LOG%"
)
)
)
REM Priority 3: Common paths
if not defined NODE_EXEC (
if exist "%ProgramFiles%\nodejs\node.exe" (
set "NODE_EXEC=%ProgramFiles%\nodejs\node.exe"
echo Found node at !NODE_EXEC! >> "%WRAPPER_LOG%"
) else if exist "%ProgramFiles(x86)%\nodejs\node.exe" (
set "NODE_EXEC=%ProgramFiles(x86)%\nodejs\node.exe"
echo Found node at !NODE_EXEC! >> "%WRAPPER_LOG%"
) else if exist "%LOCALAPPDATA%\Programs\nodejs\node.exe" (
set "NODE_EXEC=%LOCALAPPDATA%\Programs\nodejs\node.exe"
echo Found node at !NODE_EXEC! >> "%WRAPPER_LOG%"
)
)
REM Validation
if not defined NODE_EXEC (
echo ERROR: Node.js executable not found! >> "%WRAPPER_LOG%"
exit /B 1
)
echo Using Node executable: %NODE_EXEC% >> "%WRAPPER_LOG%"
call "%NODE_EXEC%" -v >> "%WRAPPER_LOG%" 2>>&1
if not exist "%NODE_SCRIPT%" (
echo ERROR: Node.js script not found at %NODE_SCRIPT% >> "%WRAPPER_LOG%"
exit /B 1
)
echo Executing: "%NODE_EXEC%" "%NODE_SCRIPT%" >> "%WRAPPER_LOG%"
call "%NODE_EXEC%" "%NODE_SCRIPT%" 2>> "%STDERR_LOG%"
set "EXIT_CODE=%ERRORLEVEL%"
echo Exit code: %EXIT_CODE% >> "%WRAPPER_LOG%"
endlocal
exit /B %EXIT_CODE%

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# Configuration
ENABLE_LOG_ROTATION="true"
LOG_RETENTION_COUNT=5
# Setup paths
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_DIR="${SCRIPT_DIR}/logs"
mkdir -p "${LOG_DIR}"
# Log rotation
if [ "${ENABLE_LOG_ROTATION}" = "true" ]; then
ls -tp "${LOG_DIR}/native_host_wrapper_macos_"* 2>/dev/null | tail -n +$((LOG_RETENTION_COUNT + 1)) | xargs -I {} rm -- {}
ls -tp "${LOG_DIR}/native_host_stderr_macos_"* 2>/dev/null | tail -n +$((LOG_RETENTION_COUNT + 1)) | xargs -I {} rm -- {}
fi
# Logging setup
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
WRAPPER_LOG="${LOG_DIR}/native_host_wrapper_macos_${TIMESTAMP}.log"
STDERR_LOG="${LOG_DIR}/native_host_stderr_macos_${TIMESTAMP}.log"
NODE_SCRIPT="${SCRIPT_DIR}/index.js"
# Initial logging
{
echo "--- Wrapper script called at $(date) ---"
echo "SCRIPT_DIR: ${SCRIPT_DIR}"
echo "LOG_DIR: ${LOG_DIR}"
echo "NODE_SCRIPT: ${NODE_SCRIPT}"
echo "Initial PATH: ${PATH}"
echo "User: $(whoami)"
echo "Current PWD: $(pwd)"
} > "${WRAPPER_LOG}"
# Node.js discovery
NODE_EXEC=""
# Priority 1: Installation-time node path
NODE_PATH_FILE="${SCRIPT_DIR}/node_path.txt"
echo "Searching for Node.js..." >> "${WRAPPER_LOG}"
echo "[Priority 1] Checking installation-time node path" >> "${WRAPPER_LOG}"
if [ -f "${NODE_PATH_FILE}" ]; then
EXPECTED_NODE=$(cat "${NODE_PATH_FILE}" 2>/dev/null | tr -d '\n\r')
if [ -n "${EXPECTED_NODE}" ] && [ -x "${EXPECTED_NODE}" ]; then
NODE_EXEC="${EXPECTED_NODE}"
echo "Found installation-time node at ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
# Priority 1.5: Fallback to relative path
if [ -z "${NODE_EXEC}" ]; then
EXPECTED_NODE="${SCRIPT_DIR}/../../../bin/node"
echo "[Priority 1.5] Checking relative path" >> "${WRAPPER_LOG}"
if [ -x "${EXPECTED_NODE}" ]; then
NODE_EXEC="${EXPECTED_NODE}"
echo "Found node at relative path: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
# Priority 2: NVM
if [ -z "${NODE_EXEC}" ]; then
echo "[Priority 2] Checking NVM" >> "${WRAPPER_LOG}"
NVM_DIR="$HOME/.nvm"
if [ -d "${NVM_DIR}" ]; then
# Try default version first
if [ -L "${NVM_DIR}/alias/default" ]; then
NVM_DEFAULT_VERSION=$(readlink "${NVM_DIR}/alias/default")
NVM_DEFAULT_NODE="${NVM_DIR}/versions/node/${NVM_DEFAULT_VERSION}/bin/node"
if [ -x "${NVM_DEFAULT_NODE}" ]; then
NODE_EXEC="${NVM_DEFAULT_NODE}"
echo "Found NVM default node: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
# Fallback to latest version
if [ -z "${NODE_EXEC}" ]; then
LATEST_NVM_VERSION_PATH=$(ls -d ${NVM_DIR}/versions/node/v* 2>/dev/null | sort -V | tail -n 1)
if [ -n "${LATEST_NVM_VERSION_PATH}" ] && [ -x "${LATEST_NVM_VERSION_PATH}/bin/node" ]; then
NODE_EXEC="${LATEST_NVM_VERSION_PATH}/bin/node"
echo "Found NVM latest node: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
fi
fi
# Priority 3: Common paths
if [ -z "${NODE_EXEC}" ]; then
echo "[Priority 3] Checking common paths" >> "${WRAPPER_LOG}"
COMMON_NODE_PATHS=(
"/opt/homebrew/bin/node"
"/usr/local/bin/node"
)
for path_to_node in "${COMMON_NODE_PATHS[@]}"; do
if [ -x "${path_to_node}" ]; then
NODE_EXEC="${path_to_node}"
echo "Found node at: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
break
fi
done
fi
# Priority 4: command -v
if [ -z "${NODE_EXEC}" ]; then
echo "[Priority 4] Trying 'command -v node'" >> "${WRAPPER_LOG}"
if command -v node &>/dev/null; then
NODE_EXEC=$(command -v node)
echo "Found node using 'command -v': ${NODE_EXEC}" >> "${WRAPPER_LOG}"
fi
fi
# Priority 5: PATH search
if [ -z "${NODE_EXEC}" ]; then
echo "[Priority 5] Searching PATH" >> "${WRAPPER_LOG}"
OLD_IFS=$IFS
IFS=:
for path_in_env in $PATH; do
if [ -x "${path_in_env}/node" ]; then
NODE_EXEC="${path_in_env}/node"
echo "Found node in PATH: ${NODE_EXEC}" >> "${WRAPPER_LOG}"
break
fi
done
IFS=$OLD_IFS
fi
# Execution
if [ -z "${NODE_EXEC}" ]; then
{
echo "ERROR: Node.js executable not found!"
echo "Searched: installation path, relative path, NVM, common paths, command -v, PATH"
} >> "${WRAPPER_LOG}"
exit 1
fi
{
echo "Using Node executable: ${NODE_EXEC}"
echo "Node version: $(${NODE_EXEC} -v)"
echo "Executing: ${NODE_EXEC} ${NODE_SCRIPT}"
} >> "${WRAPPER_LOG}"
exec "${NODE_EXEC}" "${NODE_SCRIPT}" 2>> "${STDERR_LOG}"

View File

@@ -0,0 +1,433 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
import { execSync } from 'child_process';
import { promisify } from 'util';
import { COMMAND_NAME, DESCRIPTION, EXTENSION_ID, HOST_NAME } from './constant';
export const access = promisify(fs.access);
export const mkdir = promisify(fs.mkdir);
export const writeFile = promisify(fs.writeFile);
/**
* 打印彩色文本
*/
export function colorText(text: string, color: string): string {
const colors: Record<string, string> = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
reset: '\x1b[0m',
};
return colors[color] + text + colors.reset;
}
/**
* Get user-level manifest file path
*/
export function getUserManifestPath(): string {
if (os.platform() === 'win32') {
// Windows: %APPDATA%\Google\Chrome\NativeMessagingHosts\
return path.join(
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
} else if (os.platform() === 'darwin') {
// macOS: ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/
return path.join(
os.homedir(),
'Library',
'Application Support',
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
} else {
// Linux: ~/.config/google-chrome/NativeMessagingHosts/
return path.join(
os.homedir(),
'.config',
'google-chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
}
}
/**
* Get system-level manifest file path
*/
export function getSystemManifestPath(): string {
if (os.platform() === 'win32') {
// Windows: %ProgramFiles%\Google\Chrome\NativeMessagingHosts\
return path.join(
process.env.ProgramFiles || 'C:\\Program Files',
'Google',
'Chrome',
'NativeMessagingHosts',
`${HOST_NAME}.json`,
);
} else if (os.platform() === 'darwin') {
// macOS: /Library/Google/Chrome/NativeMessagingHosts/
return path.join('/Library', 'Google', 'Chrome', 'NativeMessagingHosts', `${HOST_NAME}.json`);
} else {
// Linux: /etc/opt/chrome/native-messaging-hosts/
return path.join('/etc', 'opt', 'chrome', 'native-messaging-hosts', `${HOST_NAME}.json`);
}
}
/**
* Get native host startup script file path
*/
export async function getMainPath(): Promise<string> {
try {
const packageDistDir = path.join(__dirname, '..');
const wrapperScriptName = process.platform === 'win32' ? 'run_host.bat' : 'run_host.sh';
const absoluteWrapperPath = path.resolve(packageDistDir, wrapperScriptName);
return absoluteWrapperPath;
} catch (error) {
console.log(colorText('Cannot find global package path, using current directory', 'yellow'));
throw error;
}
}
/**
* 确保关键文件具有执行权限
*/
export async function ensureExecutionPermissions(): Promise<void> {
try {
const packageDistDir = path.join(__dirname, '..');
if (process.platform === 'win32') {
// Windows 平台处理
await ensureWindowsFilePermissions(packageDistDir);
return;
}
// Unix/Linux 平台处理
const filesToCheck = [
path.join(packageDistDir, 'index.js'),
path.join(packageDistDir, 'run_host.sh'),
path.join(packageDistDir, 'cli.js'),
];
for (const filePath of filesToCheck) {
if (fs.existsSync(filePath)) {
try {
fs.chmodSync(filePath, '755');
console.log(
colorText(`✓ Set execution permissions for ${path.basename(filePath)}`, 'green'),
);
} catch (err: any) {
console.warn(
colorText(
`⚠️ Unable to set execution permissions for ${path.basename(filePath)}: ${err.message}`,
'yellow',
),
);
}
} else {
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
}
}
} catch (error: any) {
console.warn(colorText(`⚠️ Error ensuring execution permissions: ${error.message}`, 'yellow'));
}
}
/**
* Windows 平台文件权限处理
*/
async function ensureWindowsFilePermissions(packageDistDir: string): Promise<void> {
const filesToCheck = [
path.join(packageDistDir, 'index.js'),
path.join(packageDistDir, 'run_host.bat'),
path.join(packageDistDir, 'cli.js'),
];
for (const filePath of filesToCheck) {
if (fs.existsSync(filePath)) {
try {
// 检查文件是否为只读,如果是则移除只读属性
const stats = fs.statSync(filePath);
if (!(stats.mode & parseInt('200', 8))) {
// 检查写权限
// 尝试移除只读属性
fs.chmodSync(filePath, stats.mode | parseInt('200', 8));
console.log(
colorText(`✓ Removed read-only attribute from ${path.basename(filePath)}`, 'green'),
);
}
// 验证文件可读性
fs.accessSync(filePath, fs.constants.R_OK);
console.log(
colorText(`✓ Verified file accessibility for ${path.basename(filePath)}`, 'green'),
);
} catch (err: any) {
console.warn(
colorText(
`⚠️ Unable to verify file permissions for ${path.basename(filePath)}: ${err.message}`,
'yellow',
),
);
}
} else {
console.warn(colorText(`⚠️ File not found: ${filePath}`, 'yellow'));
}
}
}
/**
* Create Native Messaging host manifest content
*/
export async function createManifestContent(): Promise<any> {
const mainPath = await getMainPath();
return {
name: HOST_NAME,
description: DESCRIPTION,
path: mainPath, // Node.js可执行文件路径
type: 'stdio',
allowed_origins: [`chrome-extension://${EXTENSION_ID}/`],
};
}
/**
* 验证Windows注册表项是否存在
*/
function verifyWindowsRegistryEntry(registryKey: string, expectedPath: string): boolean {
if (os.platform() !== 'win32') {
return true; // 非Windows平台跳过验证
}
try {
const result = execSync(`reg query "${registryKey}" /ve`, { encoding: 'utf8', stdio: 'pipe' });
const lines = result.split('\n');
for (const line of lines) {
if (line.includes('REG_SZ') && line.includes(expectedPath.replace(/\\/g, '\\\\'))) {
return true;
}
}
return false;
} catch (error) {
return false;
}
}
/**
* 尝试注册用户级别的Native Messaging主机
*/
export async function tryRegisterUserLevelHost(): Promise<boolean> {
try {
console.log(colorText('Attempting to register user-level Native Messaging host...', 'blue'));
// 1. 确保执行权限
await ensureExecutionPermissions();
// 2. 确定清单文件路径
const manifestPath = getUserManifestPath();
// 3. 确保目录存在
await mkdir(path.dirname(manifestPath), { recursive: true });
// 4. 创建清单内容
const manifest = await createManifestContent();
console.log('manifest path==>', manifest, manifestPath);
// 5. 写入清单文件
await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
if (os.platform() === 'win32') {
const registryKey = `HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`;
try {
// 确保路径使用正确的转义格式
const escapedPath = manifestPath.replace(/\\/g, '\\\\');
const regCommand = `reg add "${registryKey}" /ve /t REG_SZ /d "${escapedPath}" /f`;
console.log(colorText(`Executing registry command: ${regCommand}`, 'blue'));
execSync(regCommand, { stdio: 'pipe' });
// 验证注册表项是否创建成功
if (verifyWindowsRegistryEntry(registryKey, manifestPath)) {
console.log(colorText('✓ Successfully created Windows registry entry', 'green'));
} else {
console.log(colorText('⚠️ Registry entry created but verification failed', 'yellow'));
}
} catch (error: any) {
console.log(
colorText(`⚠️ Unable to create Windows registry entry: ${error.message}`, 'yellow'),
);
console.log(colorText(`Registry key: ${registryKey}`, 'yellow'));
console.log(colorText(`Manifest path: ${manifestPath}`, 'yellow'));
return false; // Windows上如果注册表项创建失败整个注册过程应该视为失败
}
}
console.log(colorText('Successfully registered user-level Native Messaging host!', 'green'));
return true;
} catch (error) {
console.log(
colorText(
`User-level registration failed: ${error instanceof Error ? error.message : String(error)}`,
'yellow',
),
);
return false;
}
}
// 导入is-admin包仅在Windows平台使用
let isAdmin: () => boolean = () => false;
if (process.platform === 'win32') {
try {
isAdmin = require('is-admin');
} catch (error) {
console.warn('缺少is-admin依赖Windows平台下可能无法正确检测管理员权限');
console.warn(error);
}
}
/**
* 使用提升权限注册系统级清单
*/
export async function registerWithElevatedPermissions(): Promise<void> {
try {
console.log(colorText('Attempting to register system-level manifest...', 'blue'));
// 1. 确保执行权限
await ensureExecutionPermissions();
// 2. 准备清单内容
const manifest = await createManifestContent();
// 3. 获取系统级清单路径
const manifestPath = getSystemManifestPath();
// 4. 创建临时清单文件
const tempManifestPath = path.join(os.tmpdir(), `${HOST_NAME}.json`);
await writeFile(tempManifestPath, JSON.stringify(manifest, null, 2));
// 5. 检测是否已经有管理员权限
const isRoot = process.getuid && process.getuid() === 0; // Unix/Linux/Mac
const hasAdminRights = process.platform === 'win32' ? isAdmin() : false; // Windows平台检测管理员权限
const hasElevatedPermissions = isRoot || hasAdminRights;
// 准备命令
const command =
os.platform() === 'win32'
? `if not exist "${path.dirname(manifestPath)}" mkdir "${path.dirname(manifestPath)}" && copy "${tempManifestPath}" "${manifestPath}"`
: `mkdir -p "${path.dirname(manifestPath)}" && cp "${tempManifestPath}" "${manifestPath}" && chmod 644 "${manifestPath}"`;
if (hasElevatedPermissions) {
// 已经有管理员权限,直接执行命令
try {
// 创建目录
if (!fs.existsSync(path.dirname(manifestPath))) {
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
}
// 复制文件
fs.copyFileSync(tempManifestPath, manifestPath);
// 设置权限非Windows平台
if (os.platform() !== 'win32') {
fs.chmodSync(manifestPath, '644');
}
console.log(colorText('System-level manifest registration successful!', 'green'));
} catch (error: any) {
console.error(
colorText(`System-level manifest installation failed: ${error.message}`, 'red'),
);
throw error;
}
} else {
// 没有管理员权限,打印手动操作提示
console.log(
colorText('⚠️ Administrator privileges required for system-level installation', 'yellow'),
);
console.log(
colorText(
'Please run one of the following commands with administrator privileges:',
'blue',
),
);
if (os.platform() === 'win32') {
console.log(colorText(' 1. Open Command Prompt as Administrator and run:', 'blue'));
console.log(colorText(` ${command}`, 'cyan'));
} else {
console.log(colorText(' 1. Run with sudo:', 'blue'));
console.log(colorText(` sudo ${command}`, 'cyan'));
}
console.log(
colorText(' 2. Or run the registration command with elevated privileges:', 'blue'),
);
console.log(colorText(` sudo ${COMMAND_NAME} register --system`, 'cyan'));
throw new Error('Administrator privileges required for system-level installation');
}
// 6. Windows特殊处理 - 设置系统级注册表
if (os.platform() === 'win32') {
const registryKey = `HKLM\\Software\\Google\\Chrome\\NativeMessagingHosts\\${HOST_NAME}`;
// 确保路径使用正确的转义格式
const escapedPath = manifestPath.replace(/\\/g, '\\\\');
const regCommand = `reg add "${registryKey}" /ve /t REG_SZ /d "${escapedPath}" /f`;
console.log(colorText(`Creating system registry entry: ${registryKey}`, 'blue'));
console.log(colorText(`Manifest path: ${manifestPath}`, 'blue'));
if (hasElevatedPermissions) {
// 已经有管理员权限,直接执行注册表命令
try {
execSync(regCommand, { stdio: 'pipe' });
// 验证注册表项是否创建成功
if (verifyWindowsRegistryEntry(registryKey, manifestPath)) {
console.log(colorText('Windows registry entry created successfully!', 'green'));
} else {
console.log(colorText('⚠️ Registry entry created but verification failed', 'yellow'));
}
} catch (error: any) {
console.error(
colorText(`Windows registry entry creation failed: ${error.message}`, 'red'),
);
console.error(colorText(`Command: ${regCommand}`, 'red'));
throw error;
}
} else {
// 没有管理员权限,打印手动操作提示
console.log(
colorText(
'⚠️ Administrator privileges required for Windows registry modification',
'yellow',
),
);
console.log(colorText('Please run the following command as Administrator:', 'blue'));
console.log(colorText(` ${regCommand}`, 'cyan'));
console.log(colorText('Or run the registration command with elevated privileges:', 'blue'));
console.log(
colorText(
` Run Command Prompt as Administrator and execute: ${COMMAND_NAME} register --system`,
'cyan',
),
);
throw new Error('Administrator privileges required for Windows registry modification');
}
}
} catch (error: any) {
console.error(colorText(`注册失败: ${error.message}`, 'red'));
throw error;
}
}

View File

@@ -0,0 +1,282 @@
import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import cors from '@fastify/cors';
import {
NATIVE_SERVER_PORT,
TIMEOUTS,
SERVER_CONFIG,
HTTP_STATUS,
ERROR_MESSAGES,
} from '../constant';
import { NativeMessagingHost } from '../native-messaging-host';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { randomUUID } from 'node:crypto';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { getMcpServer } from '../mcp/mcp-server';
// Define request body type (if data needs to be retrieved from HTTP requests)
interface ExtensionRequestPayload {
data?: any; // Data you want to pass to the extension
}
export class Server {
private fastify: FastifyInstance;
public isRunning = false; // Changed to public or provide a getter
private nativeHost: NativeMessagingHost | null = null;
private transportsMap: Map<string, StreamableHTTPServerTransport | SSEServerTransport> =
new Map();
constructor() {
this.fastify = Fastify({ logger: SERVER_CONFIG.LOGGER_ENABLED });
this.setupPlugins();
this.setupRoutes();
}
/**
* Associate NativeMessagingHost instance
*/
public setNativeHost(nativeHost: NativeMessagingHost): void {
this.nativeHost = nativeHost;
}
private async setupPlugins(): Promise<void> {
await this.fastify.register(cors, {
origin: SERVER_CONFIG.CORS_ORIGIN,
});
}
private setupRoutes(): void {
// for ping
this.fastify.get(
'/ask-extension',
async (request: FastifyRequest<{ Body: ExtensionRequestPayload }>, reply: FastifyReply) => {
if (!this.nativeHost) {
return reply
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.send({ error: ERROR_MESSAGES.NATIVE_HOST_NOT_AVAILABLE });
}
if (!this.isRunning) {
return reply
.status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.send({ error: ERROR_MESSAGES.SERVER_NOT_RUNNING });
}
try {
// wait from extension message
const extensionResponse = await this.nativeHost.sendRequestToExtensionAndWait(
request.query,
'process_data',
TIMEOUTS.EXTENSION_REQUEST_TIMEOUT,
);
return reply.status(HTTP_STATUS.OK).send({ status: 'success', data: extensionResponse });
} catch (error: any) {
if (error.message.includes('timed out')) {
return reply
.status(HTTP_STATUS.GATEWAY_TIMEOUT)
.send({ status: 'error', message: ERROR_MESSAGES.REQUEST_TIMEOUT });
} else {
return reply.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).send({
status: 'error',
message: `Failed to get response from extension: ${error.message}`,
});
}
}
},
);
// Compatible with SSE
this.fastify.get('/sse', async (_, reply) => {
try {
// Set SSE headers
reply.raw.writeHead(HTTP_STATUS.OK, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
// Create SSE transport
const transport = new SSEServerTransport('/messages', reply.raw);
this.transportsMap.set(transport.sessionId, transport);
reply.raw.on('close', () => {
this.transportsMap.delete(transport.sessionId);
});
const server = getMcpServer();
await server.connect(transport);
// Keep connection open
reply.raw.write(':\n\n');
} catch (error) {
if (!reply.sent) {
reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
}
}
});
// Compatible with SSE
this.fastify.post('/messages', async (req, reply) => {
try {
const { sessionId } = req.query as any;
const transport = this.transportsMap.get(sessionId) as SSEServerTransport;
if (!sessionId || !transport) {
reply.code(HTTP_STATUS.BAD_REQUEST).send('No transport found for sessionId');
return;
}
await transport.handlePostMessage(req.raw, reply.raw, req.body);
} catch (error) {
if (!reply.sent) {
reply.code(HTTP_STATUS.INTERNAL_SERVER_ERROR).send(ERROR_MESSAGES.INTERNAL_SERVER_ERROR);
}
}
});
// POST /mcp: Handle client-to-server messages
this.fastify.post('/mcp', async (request, reply) => {
const sessionId = request.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport | undefined = this.transportsMap.get(
sessionId || '',
) as StreamableHTTPServerTransport;
if (transport) {
// transport found, do nothing
} else if (!sessionId && isInitializeRequest(request.body)) {
const newSessionId = randomUUID(); // Generate session ID
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => newSessionId, // Use pre-generated ID
onsessioninitialized: (initializedSessionId) => {
// Ensure transport instance exists and session ID matches
if (transport && initializedSessionId === newSessionId) {
this.transportsMap.set(initializedSessionId, transport);
}
},
});
transport.onclose = () => {
if (transport?.sessionId && this.transportsMap.get(transport.sessionId)) {
this.transportsMap.delete(transport.sessionId);
}
};
await getMcpServer().connect(transport);
} else {
reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_MCP_REQUEST });
return;
}
try {
await transport.handleRequest(request.raw, reply.raw, request.body);
} catch (error) {
if (!reply.sent) {
reply
.code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.send({ error: ERROR_MESSAGES.MCP_REQUEST_PROCESSING_ERROR });
}
}
});
this.fastify.get('/mcp', async (request, reply) => {
const sessionId = request.headers['mcp-session-id'] as string | undefined;
const transport = sessionId
? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
: undefined;
if (!transport) {
reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SSE_SESSION });
return;
}
reply.raw.setHeader('Content-Type', 'text/event-stream');
reply.raw.setHeader('Cache-Control', 'no-cache');
reply.raw.setHeader('Connection', 'keep-alive');
reply.raw.flushHeaders(); // Ensure headers are sent immediately
try {
// transport.handleRequest will take over the response stream
await transport.handleRequest(request.raw, reply.raw);
if (!reply.sent) {
// If transport didn't send anything (unlikely for SSE initial handshake)
reply.hijack(); // Prevent Fastify from automatically sending response
}
} catch (error) {
if (!reply.raw.writableEnded) {
reply.raw.end();
}
}
request.socket.on('close', () => {
request.log.info(`SSE client disconnected for session: ${sessionId}`);
// transport's onclose should handle its own cleanup
});
});
this.fastify.delete('/mcp', async (request, reply) => {
const sessionId = request.headers['mcp-session-id'] as string | undefined;
const transport = sessionId
? (this.transportsMap.get(sessionId) as StreamableHTTPServerTransport)
: undefined;
if (!transport) {
reply.code(HTTP_STATUS.BAD_REQUEST).send({ error: ERROR_MESSAGES.INVALID_SESSION_ID });
return;
}
try {
await transport.handleRequest(request.raw, reply.raw);
// Assume transport.handleRequest will send response or transport.onclose will cleanup
if (!reply.sent) {
reply.code(HTTP_STATUS.NO_CONTENT).send();
}
} catch (error) {
if (!reply.sent) {
reply
.code(HTTP_STATUS.INTERNAL_SERVER_ERROR)
.send({ error: ERROR_MESSAGES.MCP_SESSION_DELETION_ERROR });
}
}
});
}
public async start(port = NATIVE_SERVER_PORT, nativeHost: NativeMessagingHost): Promise<void> {
if (!this.nativeHost) {
this.nativeHost = nativeHost; // Ensure nativeHost is set
} else if (this.nativeHost !== nativeHost) {
this.nativeHost = nativeHost; // Update to the passed instance
}
if (this.isRunning) {
return;
}
try {
await this.fastify.listen({ port, host: SERVER_CONFIG.HOST });
this.isRunning = true; // Update running status
// No need to return, Promise resolves void by default
} catch (err) {
this.isRunning = false; // Startup failed, reset status
// Throw error instead of exiting directly, let caller (possibly NativeHost) handle
throw err; // or return Promise.reject(err);
// process.exit(1); // Not recommended to exit directly here
}
}
public async stop(): Promise<void> {
if (!this.isRunning) {
return;
}
// this.nativeHost = null; // Not recommended to nullify here, association relationship may still be needed
try {
await this.fastify.close();
this.isRunning = false; // Update running status
} catch (err) {
// Even if closing fails, mark as not running, but log the error
this.isRunning = false;
throw err; // Throw error
}
}
public getInstance(): FastifyInstance {
return this.fastify;
}
}
const serverInstance = new Server();
export default serverInstance;

View File

@@ -0,0 +1,27 @@
import { describe, expect, test, afterAll, beforeAll } from '@jest/globals';
import supertest from 'supertest';
import Server from './index';
describe('服务器测试', () => {
// 启动服务器测试实例
beforeAll(async () => {
await Server.getInstance().ready();
});
// 关闭服务器
afterAll(async () => {
await Server.stop();
});
test('GET /ping 应返回正确响应', async () => {
const response = await supertest(Server.getInstance().server)
.get('/ping')
.expect(200)
.expect('Content-Type', /json/);
expect(response.body).toEqual({
status: 'ok',
message: 'pong',
});
});
});

View File

@@ -0,0 +1,45 @@
// import { stderr } from 'process';
// import * as fs from 'fs';
// import * as path from 'path';
// // 设置日志文件路径
// const LOG_DIR = path.join(
// '/Users/hang/code/ai/chrome-mcp-server/app/native-server/dist/',
// '.debug-log',
// ); // 使用不同目录区分
// const LOG_FILE = path.join(
// LOG_DIR,
// `native-host-${new Date().toISOString().replace(/:/g, '-')}.log`,
// );
// // 确保日志目录存在
// if (!fs.existsSync(LOG_DIR)) {
// try {
// fs.mkdirSync(LOG_DIR, { recursive: true });
// } catch (err) {
// stderr.write(`[ERROR] 创建日志目录失败: ${err}\n`);
// }
// }
// // 日志函数
// function writeLog(level: string, message: string): void {
// const timestamp = new Date().toISOString();
// const logMessage = `[${timestamp}] [${level}] ${message}\n`;
// // 写入到文件
// try {
// fs.appendFileSync(LOG_FILE, logMessage);
// } catch (err) {
// stderr.write(`[ERROR] 写入日志失败: ${err}\n`);
// }
// // 同时输出到stderr不影响native messaging协议
// stderr.write(logMessage);
// }
// // 日志级别函数
// export const logger = {
// debug: (message: string) => writeLog('DEBUG', message),
// info: (message: string) => writeLog('INFO', message),
// warn: (message: string) => writeLog('WARN', message),
// error: (message: string) => writeLog('ERROR', message),
// };

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2018",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2018", "DOM"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}