mirror of
https://github.com/hangwin/mcp-chrome.git
synced 2026-08-28 17:45:23 +08:00
feat: add console tool & support mcp stdio type
This commit is contained in:
@@ -61,9 +61,11 @@ npm install -g mcp-chrome-bridge
|
||||
pnpm
|
||||
|
||||
```bash
|
||||
pnpm install -g mcp-chrome-bridge
|
||||
pnpm install -g mcp-chrome-bridge --unsafe-perm
|
||||
```
|
||||
|
||||
> Note: When using pnpm, the `--unsafe-perm` flag is required to ensure post-installation scripts run properly, which is necessary for registering the Native Messaging host.
|
||||
|
||||
3. **Load Chrome Extension**
|
||||
- Open Chrome and go to `chrome://extensions/`
|
||||
- Enable "Developer mode"
|
||||
@@ -91,12 +93,14 @@ Add the following configuration to Claude Desktop's MCP configuration:
|
||||
Complete tool list: [Complete Tool List](docs/TOOLS.md)
|
||||
|
||||
<details>
|
||||
<summary><strong>📊 Browser Management (4 tools)</strong></summary>
|
||||
<summary><strong>📊 Browser Management (6 tools)</strong></summary>
|
||||
|
||||
- `get_windows_and_tabs` - List all browser windows and tabs
|
||||
- `chrome_navigate` - Navigate to URLs and control viewport
|
||||
- `chrome_close_tabs` - Close specific tabs or windows
|
||||
- `chrome_go_back_or_forward` - Browser navigation control
|
||||
- `chrome_inject_script` - Inject content scripts into web pages
|
||||
- `chrome_send_command_to_inject_script` - Send commands to injected content scripts
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -114,11 +118,12 @@ Complete tool list: [Complete Tool List](docs/TOOLS.md)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🔍 Content Analysis (3 tools)</strong></summary>
|
||||
<summary><strong>🔍 Content Analysis (4 tools)</strong></summary>
|
||||
|
||||
- `search_tabs_content` - AI-powered semantic search across browser tabs
|
||||
- `chrome_get_web_content` - Extract HTML/text content from pages
|
||||
- `chrome_get_interactive_elements` - Find clickable elements
|
||||
- `chrome_console` - Capture and retrieve console output from browser tabs
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -148,6 +153,7 @@ Instruction: Help me summarize the current page content, then draw a diagram to
|
||||
https://github.com/user-attachments/assets/fd17209b-303d-48db-9e5e-3717141df183
|
||||
|
||||
### After analyzing the content of the image, the LLM automatically controls Excalidraw to replicate the image
|
||||
|
||||
prompt: [excalidraw-prompt](prompt/excalidraw-prompt.md)|[content-analize](prompt/content-analize.md)
|
||||
Instruction: First, analyze the content of the image, and then replicate the image by combining the analysis with the content of the image.
|
||||
|
||||
|
||||
+8
-4
@@ -61,15 +61,17 @@ npm install -g mcp-chrome-bridge
|
||||
pnpm
|
||||
|
||||
```bash
|
||||
pnpm install -g mcp-chrome-bridge
|
||||
pnpm install -g mcp-chrome-bridge --unsafe-perm
|
||||
```
|
||||
|
||||
> 注意:使用 pnpm 安装时需要添加 `--unsafe-perm` 参数以确保安装后脚本能正常执行,这对于注册 Native Messaging 主机是必要的。
|
||||
|
||||
3. **加载 Chrome 扩展**
|
||||
- 打开 Chrome 并访问 `chrome://extensions/`
|
||||
- 启用"开发者模式"
|
||||
- 点击"加载已解压的扩展程序",选择 `your/dowloaded/extension/folder`
|
||||
- 点击插件图标打开插件,点击连接即可看到mcp的配置
|
||||
<img width="475" alt="截屏2025-06-09 15 52 06" src="https://github.com/user-attachments/assets/241e57b8-c55f-41a4-9188-0367293dc5bc" />
|
||||
<img width="475" alt="截屏2025-06-09 15 52 06" src="https://github.com/user-attachments/assets/241e57b8-c55f-41a4-9188-0367293dc5bc" />
|
||||
|
||||
### 在 Claude Desktop 中使用
|
||||
|
||||
@@ -91,7 +93,7 @@ pnpm install -g mcp-chrome-bridge
|
||||
完整工具列表:[完整工具列表](docs/TOOLS_zh.md)
|
||||
|
||||
<details>
|
||||
<summary><strong>📊 浏览器管理 (4个工具)</strong></summary>
|
||||
<summary><strong>📊 浏览器管理 (6个工具)</strong></summary>
|
||||
|
||||
- `get_windows_and_tabs` - 列出所有浏览器窗口和标签页
|
||||
- `chrome_navigate` - 导航到 URL 并控制视口
|
||||
@@ -116,11 +118,12 @@ pnpm install -g mcp-chrome-bridge
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🔍 内容分析 (3个工具)</strong></summary>
|
||||
<summary><strong>🔍 内容分析 (4个工具)</strong></summary>
|
||||
|
||||
- `search_tabs_content` - AI 驱动的浏览器标签页语义搜索
|
||||
- `chrome_get_web_content` - 从页面提取 HTML/文本内容
|
||||
- `chrome_get_interactive_elements` - 查找可点击元素
|
||||
- `chrome_console` - 捕获和获取浏览器标签页的控制台输出
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -150,6 +153,7 @@ prompt: [excalidraw-prompt](prompt/excalidraw-prompt.md)
|
||||
https://github.com/user-attachments/assets/f14f79a6-9390-4821-8296-06d020bcfc07
|
||||
|
||||
### ai先分析图片的内容元素,然后再自动控制excalidraw把图片模仿出来
|
||||
|
||||
prompt: [excalidraw-prompt](prompt/excalidraw-prompt.md)|[content-analize](prompt/content-analize.md)
|
||||
指令:先看下图片是否能用excalidraw画出来,如果则列出所需的步骤和元素,然后画出来
|
||||
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
import { createErrorResponse, ToolResult } from '@/common/tool-handler';
|
||||
import { BaseBrowserToolExecutor } from '../base-browser';
|
||||
import { TOOL_NAMES } from 'chrome-mcp-shared';
|
||||
|
||||
const DEBUGGER_PROTOCOL_VERSION = '1.3';
|
||||
const DEFAULT_MAX_MESSAGES = 100;
|
||||
|
||||
interface ConsoleToolParams {
|
||||
url?: string;
|
||||
includeExceptions?: boolean;
|
||||
maxMessages?: number;
|
||||
}
|
||||
|
||||
interface ConsoleMessage {
|
||||
timestamp: number;
|
||||
level: string;
|
||||
text: string;
|
||||
args?: any[];
|
||||
source?: string;
|
||||
url?: string;
|
||||
lineNumber?: number;
|
||||
stackTrace?: any;
|
||||
}
|
||||
|
||||
interface ConsoleException {
|
||||
timestamp: number;
|
||||
text: string;
|
||||
url?: string;
|
||||
lineNumber?: number;
|
||||
columnNumber?: number;
|
||||
stackTrace?: any;
|
||||
}
|
||||
|
||||
interface ConsoleResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
tabId: number;
|
||||
tabUrl: string;
|
||||
tabTitle: string;
|
||||
captureStartTime: number;
|
||||
captureEndTime: number;
|
||||
totalDurationMs: number;
|
||||
messages: ConsoleMessage[];
|
||||
exceptions: ConsoleException[];
|
||||
messageCount: number;
|
||||
exceptionCount: number;
|
||||
messageLimitReached: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool for capturing console output from browser tabs
|
||||
*/
|
||||
class ConsoleTool extends BaseBrowserToolExecutor {
|
||||
name = TOOL_NAMES.BROWSER.CONSOLE;
|
||||
|
||||
async execute(args: ConsoleToolParams): Promise<ToolResult> {
|
||||
const { url, includeExceptions = true, maxMessages = DEFAULT_MAX_MESSAGES } = args;
|
||||
|
||||
let targetTab: chrome.tabs.Tab;
|
||||
|
||||
try {
|
||||
if (url) {
|
||||
// Navigate to the specified URL
|
||||
targetTab = await this.navigateToUrl(url);
|
||||
} else {
|
||||
// Use current active tab
|
||||
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (!activeTab?.id) {
|
||||
return createErrorResponse('No active tab found and no URL provided.');
|
||||
}
|
||||
targetTab = activeTab;
|
||||
}
|
||||
|
||||
if (!targetTab?.id) {
|
||||
return createErrorResponse('Failed to identify target tab.');
|
||||
}
|
||||
|
||||
const tabId = targetTab.id;
|
||||
|
||||
// Capture console messages (one-time capture)
|
||||
const result = await this.captureConsoleMessages(tabId, {
|
||||
includeExceptions,
|
||||
maxMessages,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('ConsoleTool: Critical error during execute:', error);
|
||||
return createErrorResponse(`Error in ConsoleTool: ${error.message || String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async navigateToUrl(url: string): Promise<chrome.tabs.Tab> {
|
||||
// Check if URL is already open
|
||||
const existingTabs = await chrome.tabs.query({ url });
|
||||
|
||||
if (existingTabs.length > 0 && existingTabs[0]?.id) {
|
||||
const tab = existingTabs[0];
|
||||
// Activate the existing tab
|
||||
await chrome.tabs.update(tab.id!, { active: true });
|
||||
await chrome.windows.update(tab.windowId, { focused: true });
|
||||
return tab;
|
||||
} else {
|
||||
// Create new tab with the URL
|
||||
const newTab = await chrome.tabs.create({ url, active: true });
|
||||
// Wait for tab to be ready
|
||||
await this.waitForTabReady(newTab.id!);
|
||||
return newTab;
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForTabReady(tabId: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const checkTab = async () => {
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
if (tab.status === 'complete') {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkTab, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
// Tab might be closed, resolve anyway
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
checkTab();
|
||||
});
|
||||
}
|
||||
|
||||
private formatConsoleArgs(args: any[]): string {
|
||||
if (!args || args.length === 0) return '';
|
||||
|
||||
return args
|
||||
.map((arg) => {
|
||||
if (arg.type === 'string') {
|
||||
return arg.value || '';
|
||||
} else if (arg.type === 'number') {
|
||||
return String(arg.value || '');
|
||||
} else if (arg.type === 'boolean') {
|
||||
return String(arg.value || '');
|
||||
} else if (arg.type === 'object') {
|
||||
return arg.description || '[Object]';
|
||||
} else if (arg.type === 'undefined') {
|
||||
return 'undefined';
|
||||
} else if (arg.type === 'function') {
|
||||
return arg.description || '[Function]';
|
||||
} else {
|
||||
return arg.description || arg.value || String(arg);
|
||||
}
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
private async captureConsoleMessages(
|
||||
tabId: number,
|
||||
options: {
|
||||
includeExceptions: boolean;
|
||||
maxMessages: number;
|
||||
},
|
||||
): Promise<ConsoleResult> {
|
||||
const { includeExceptions, maxMessages } = options;
|
||||
const startTime = Date.now();
|
||||
const messages: ConsoleMessage[] = [];
|
||||
const exceptions: ConsoleException[] = [];
|
||||
let limitReached = false;
|
||||
|
||||
try {
|
||||
// Get tab information
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
|
||||
// Check if debugger is already attached
|
||||
const targets = await chrome.debugger.getTargets();
|
||||
const existingTarget = targets.find(
|
||||
(t) => t.tabId === tabId && t.attached && t.type === 'page',
|
||||
);
|
||||
if (existingTarget && !existingTarget.extensionId) {
|
||||
throw new Error(
|
||||
`Debugger is already attached to tab ${tabId} by another tool (e.g., DevTools).`,
|
||||
);
|
||||
}
|
||||
|
||||
// Attach debugger
|
||||
try {
|
||||
await chrome.debugger.attach({ tabId }, DEBUGGER_PROTOCOL_VERSION);
|
||||
} catch (error: any) {
|
||||
if (error.message?.includes('Cannot attach to the target with an attached client')) {
|
||||
throw new Error(
|
||||
`Debugger is already attached to tab ${tabId}. This might be DevTools or another extension.`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Set up event listener to collect messages
|
||||
const collectedMessages: any[] = [];
|
||||
const collectedExceptions: any[] = [];
|
||||
|
||||
const eventListener = (source: chrome.debugger.Debuggee, method: string, params?: any) => {
|
||||
if (source.tabId !== tabId) return;
|
||||
|
||||
if (method === 'Log.entryAdded' && params?.entry) {
|
||||
collectedMessages.push(params.entry);
|
||||
} else if (method === 'Runtime.consoleAPICalled' && params) {
|
||||
// Convert Runtime.consoleAPICalled to Log.entryAdded format
|
||||
const logEntry = {
|
||||
timestamp: params.timestamp,
|
||||
level: params.type || 'log',
|
||||
text: this.formatConsoleArgs(params.args || []),
|
||||
source: 'console-api',
|
||||
url: params.stackTrace?.callFrames?.[0]?.url,
|
||||
lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber,
|
||||
stackTrace: params.stackTrace,
|
||||
args: params.args,
|
||||
};
|
||||
collectedMessages.push(logEntry);
|
||||
} else if (
|
||||
method === 'Runtime.exceptionThrown' &&
|
||||
includeExceptions &&
|
||||
params?.exceptionDetails
|
||||
) {
|
||||
collectedExceptions.push(params.exceptionDetails);
|
||||
}
|
||||
};
|
||||
|
||||
chrome.debugger.onEvent.addListener(eventListener);
|
||||
|
||||
try {
|
||||
// Enable Runtime domain first to capture console API calls and exceptions
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable');
|
||||
|
||||
// Also enable Log domain to capture other log entries
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Log.enable');
|
||||
|
||||
// Wait for all messages to be flushed
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Process collected messages
|
||||
for (const entry of collectedMessages) {
|
||||
if (messages.length >= maxMessages) {
|
||||
limitReached = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const message: ConsoleMessage = {
|
||||
timestamp: entry.timestamp,
|
||||
level: entry.level || 'log',
|
||||
text: entry.text || '',
|
||||
source: entry.source,
|
||||
url: entry.url,
|
||||
lineNumber: entry.lineNumber,
|
||||
};
|
||||
|
||||
if (entry.stackTrace) {
|
||||
message.stackTrace = entry.stackTrace;
|
||||
}
|
||||
|
||||
if (entry.args && Array.isArray(entry.args)) {
|
||||
message.args = entry.args;
|
||||
}
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
// Process collected exceptions
|
||||
for (const exceptionDetails of collectedExceptions) {
|
||||
const exception: ConsoleException = {
|
||||
timestamp: Date.now(),
|
||||
text:
|
||||
exceptionDetails.text ||
|
||||
exceptionDetails.exception?.description ||
|
||||
'Unknown exception',
|
||||
url: exceptionDetails.url,
|
||||
lineNumber: exceptionDetails.lineNumber,
|
||||
columnNumber: exceptionDetails.columnNumber,
|
||||
};
|
||||
|
||||
if (exceptionDetails.stackTrace) {
|
||||
exception.stackTrace = exceptionDetails.stackTrace;
|
||||
}
|
||||
|
||||
exceptions.push(exception);
|
||||
}
|
||||
} finally {
|
||||
// Clean up
|
||||
chrome.debugger.onEvent.removeListener(eventListener);
|
||||
|
||||
try {
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Runtime.disable');
|
||||
} catch (e) {
|
||||
console.warn(`ConsoleTool: Error disabling Runtime for tab ${tabId}:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Log.disable');
|
||||
} catch (e) {
|
||||
console.warn(`ConsoleTool: Error disabling Log for tab ${tabId}:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
await chrome.debugger.detach({ tabId });
|
||||
} catch (e) {
|
||||
console.warn(`ConsoleTool: Error detaching debugger for tab ${tabId}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
// Sort messages by timestamp
|
||||
messages.sort((a, b) => a.timestamp - b.timestamp);
|
||||
exceptions.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Console capture completed for tab ${tabId}. ${messages.length} messages, ${exceptions.length} exceptions captured.`,
|
||||
tabId,
|
||||
tabUrl: tab.url || '',
|
||||
tabTitle: tab.title || '',
|
||||
captureStartTime: startTime,
|
||||
captureEndTime: endTime,
|
||||
totalDurationMs: endTime - startTime,
|
||||
messages,
|
||||
exceptions,
|
||||
messageCount: messages.length,
|
||||
exceptionCount: exceptions.length,
|
||||
messageLimitReached: limitReached,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`ConsoleTool: Error capturing console messages for tab ${tabId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const consoleTool = new ConsoleTool();
|
||||
@@ -11,3 +11,4 @@ export { keyboardTool } from './keyboard';
|
||||
export { historyTool } from './history';
|
||||
export { bookmarkSearchTool, bookmarkAddTool, bookmarkDeleteTool } from './bookmark';
|
||||
export { injectScriptTool, sendCommandToInjectScriptTool } from './inject-script';
|
||||
export { consoleTool } from './console';
|
||||
|
||||
@@ -51,15 +51,4 @@ class WindowTool extends BaseBrowserToolExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
interface TabContentResult {
|
||||
tabId: number;
|
||||
url: string;
|
||||
title: string;
|
||||
textContent?: string;
|
||||
error?: string;
|
||||
matchScore?: number;
|
||||
semanticScore?: number;
|
||||
matchedSnippets?: string[];
|
||||
}
|
||||
|
||||
export const windowTool = new WindowTool();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "a chrome extension to use your own chrome as a mcp server",
|
||||
"author": "hangye",
|
||||
"private": true,
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "mcp-chrome-bridge",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.23",
|
||||
"description": "Chrome Native-Messaging host (Node)",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"mcp-chrome-bridge": "/dist/cli.js"
|
||||
"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\"",
|
||||
@@ -35,13 +36,12 @@
|
||||
"@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",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"uuid": "^11.1.0",
|
||||
"chrome-mcp-shared": "workspace:*"
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.7.0",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { program } from 'commander';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
tryRegisterUserLevelHost,
|
||||
colorText,
|
||||
@@ -95,6 +97,42 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/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);
|
||||
console.log('🚀 Chrome MCP Server running on type stdio');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error Chrome MCP Server main():', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"url": "http://127.0.0.1:12306/mcp"
|
||||
}
|
||||
@@ -21,26 +21,29 @@ console.log('dist 和 dist/logs 目录已创建/确认存在');
|
||||
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 distPackageJson = {
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
description: packageJson.description,
|
||||
author: packageJson.author,
|
||||
license: packageJson.license,
|
||||
main: 'index.js',
|
||||
dependencies: packageJson.dependencies,
|
||||
scripts: {
|
||||
start: 'node index.js',
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(distDir, 'package.json'), JSON.stringify(distPackageJson, null, 2));
|
||||
|
||||
// 创建安装说明
|
||||
const readmeContent = `# ${packageJson.name}
|
||||
|
||||
|
||||
@@ -284,16 +284,6 @@ export async function tryRegisterUserLevelHost(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用sudo-prompt提权
|
||||
let sudoPrompt: any;
|
||||
try {
|
||||
sudoPrompt = require('sudo-prompt');
|
||||
} catch (error) {
|
||||
console.error('缺少sudo-prompt依赖,请先安装:npm install sudo-prompt');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 导入is-admin包(仅在Windows平台使用)
|
||||
let isAdmin: () => boolean = () => false;
|
||||
if (process.platform === 'win32') {
|
||||
@@ -360,20 +350,31 @@ export async function registerWithElevatedPermissions(): Promise<void> {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// 没有管理员权限,使用sudo-prompt提权
|
||||
await new Promise((resolve, reject) => {
|
||||
sudoPrompt.exec(command, { name: `${COMMAND_NAME} Installer` }, (error: Error) => {
|
||||
if (error) {
|
||||
console.error(
|
||||
colorText(`Elevated permission installation failed: ${error.message}`, 'red'),
|
||||
);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log(colorText('System-level manifest registration successful!', 'green'));
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
// 没有管理员权限,打印手动操作提示
|
||||
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特殊处理 - 设置系统级注册表
|
||||
@@ -405,21 +406,24 @@ export async function registerWithElevatedPermissions(): Promise<void> {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
// 没有管理员权限,使用sudo-prompt提权
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
sudoPrompt.exec(regCommand, { name: `${COMMAND_NAME} Installer` }, (error: Error) => {
|
||||
if (error) {
|
||||
console.error(
|
||||
colorText(`Windows registry entry creation failed: ${error.message}`, 'red'),
|
||||
);
|
||||
console.error(colorText(`Command: ${regCommand}`, 'red'));
|
||||
reject(error);
|
||||
} else {
|
||||
console.log(colorText('Windows registry entry created successfully!', 'green'));
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
// 没有管理员权限,打印手动操作提示
|
||||
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) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chrome-mcp-shared",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"author": "hangye",
|
||||
"main": "dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -24,6 +24,7 @@ export const TOOL_NAMES = {
|
||||
BOOKMARK_DELETE: 'chrome_bookmark_delete',
|
||||
INJECT_SCRIPT: 'chrome_inject_script',
|
||||
SEND_COMMAND_TO_INJECT_SCRIPT: 'chrome_send_command_to_inject_script',
|
||||
CONSOLE: 'chrome_console',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -509,4 +510,28 @@ export const TOOL_SCHEMAS: Tool[] = [
|
||||
required: ['eventName'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: TOOL_NAMES.BROWSER.CONSOLE,
|
||||
description:
|
||||
'Capture and retrieve all console output from the current active browser tab/page. This captures console messages that existed before the tool was called.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
description:
|
||||
'URL to navigate to and capture console from. If not provided, uses the current active tab',
|
||||
},
|
||||
includeExceptions: {
|
||||
type: 'boolean',
|
||||
description: 'Include uncaught exceptions in the output (default: true)',
|
||||
},
|
||||
maxMessages: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of console messages to capture (default: 100)',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Generated
-9
@@ -123,9 +123,6 @@ importers:
|
||||
pino:
|
||||
specifier: ^9.6.0
|
||||
version: 9.7.0
|
||||
sudo-prompt:
|
||||
specifier: ^9.2.1
|
||||
version: 9.2.1
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
@@ -4040,10 +4037,6 @@ packages:
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
hasBin: true
|
||||
|
||||
sudo-prompt@9.2.1:
|
||||
resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
superagent@10.2.1:
|
||||
resolution: {integrity: sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
@@ -8855,8 +8848,6 @@ snapshots:
|
||||
pirates: 4.0.7
|
||||
ts-interface-checker: 0.1.13
|
||||
|
||||
sudo-prompt@9.2.1: {}
|
||||
|
||||
superagent@10.2.1:
|
||||
dependencies:
|
||||
component-emitter: 1.3.1
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user