chore(chrome-qwen-bridge): connect & them

This commit is contained in:
yiliang114
2025-12-20 18:51:56 +08:00
parent cc3cfb5d65
commit 84e190fd9d
80 changed files with 38140 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
#!/usr/bin/env node
/**
* Browser MCP Server
* Provides browser tools (read_page, capture_screenshot, etc.) to Qwen CLI
* Communicates with Native Host via HTTP to get browser data
*/
const http = require('http');
const readline = require('readline');
const BRIDGE_URL = 'http://127.0.0.1:18765';
// MCP Protocol version
const PROTOCOL_VERSION = '2024-11-05';
// Available tools
const TOOLS = [
{
name: 'browser_read_page',
description: 'Read the content of the current browser page. Returns URL, title, text content, links, and images.',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'browser_capture_screenshot',
description: 'Capture a screenshot of the current browser tab. Returns a base64-encoded PNG image.',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'browser_get_network_logs',
description: 'Get network request logs from the current browser tab. Useful for debugging API calls.',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'browser_get_console_logs',
description: 'Get console logs (log, error, warn, info) from the current browser tab.',
inputSchema: {
type: 'object',
properties: {},
required: []
}
}
];
// Send request to Native Host HTTP bridge
async function callBridge(method, params = {}) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({ method, params });
const req = http.request(BRIDGE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data)
}
}, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try {
const result = JSON.parse(body);
if (result.success) {
resolve(result.data);
} else {
reject(new Error(result.error || 'Unknown error'));
}
} catch (err) {
reject(new Error(`Failed to parse response: ${err.message}`));
}
});
});
req.on('error', (err) => {
reject(new Error(`Bridge connection failed: ${err.message}. Make sure Chrome extension is running.`));
});
req.write(data);
req.end();
});
}
// Handle MCP tool calls
async function handleToolCall(name, args) {
switch (name) {
case 'browser_read_page': {
const data = await callBridge('read_page');
return {
content: [
{
type: 'text',
text: JSON.stringify({
url: data.url,
title: data.title,
content: data.content?.text || data.content?.markdown || '',
linksCount: data.links?.length || 0,
imagesCount: data.images?.length || 0
}, null, 2)
}
]
};
}
case 'browser_capture_screenshot': {
const data = await callBridge('capture_screenshot');
return {
content: [
{
type: 'image',
data: data.dataUrl?.replace(/^data:image\/png;base64,/, '') || '',
mimeType: 'image/png'
}
]
};
}
case 'browser_get_network_logs': {
const data = await callBridge('get_network_logs');
const logs = data.logs || [];
const summary = logs.slice(-20).map(log => ({
method: log.method,
url: log.params?.request?.url || log.params?.documentURL,
status: log.params?.response?.status,
timestamp: log.timestamp
}));
return {
content: [
{
type: 'text',
text: `Network logs (last ${summary.length} entries):\n${JSON.stringify(summary, null, 2)}`
}
]
};
}
case 'browser_get_console_logs': {
const data = await callBridge('get_console_logs');
const logs = data.logs || [];
const formatted = logs.slice(-50).map(log =>
`[${log.type}] ${log.message}`
).join('\n');
return {
content: [
{
type: 'text',
text: `Console logs (last ${Math.min(logs.length, 50)} entries):\n${formatted || '(no logs captured)'}`
}
]
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// Send JSON-RPC response
function sendResponse(id, result) {
const response = {
jsonrpc: '2.0',
id,
result
};
console.log(JSON.stringify(response));
}
// Send JSON-RPC error
function sendError(id, code, message) {
const response = {
jsonrpc: '2.0',
id,
error: { code, message }
};
console.log(JSON.stringify(response));
}
// Handle incoming JSON-RPC messages
async function handleMessage(message) {
const { id, method, params } = message;
try {
switch (method) {
case 'initialize':
sendResponse(id, {
protocolVersion: PROTOCOL_VERSION,
capabilities: {
tools: {}
},
serverInfo: {
name: 'browser-mcp-server',
version: '1.0.0'
}
});
break;
case 'notifications/initialized':
// No response needed for notifications
break;
case 'tools/list':
sendResponse(id, { tools: TOOLS });
break;
case 'tools/call':
try {
const result = await handleToolCall(params.name, params.arguments || {});
sendResponse(id, result);
} catch (err) {
sendResponse(id, {
content: [
{
type: 'text',
text: `Error: ${err.message}`
}
],
isError: true
});
}
break;
case 'ping':
sendResponse(id, {});
break;
default:
if (id !== undefined) {
sendError(id, -32601, `Method not found: ${method}`);
}
}
} catch (err) {
if (id !== undefined) {
sendError(id, -32603, err.message);
}
}
}
// Main: Read JSON-RPC messages from stdin
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', async (line) => {
try {
const message = JSON.parse(line);
await handleMessage(message);
} catch (err) {
// Ignore parse errors
}
});
rl.on('close', () => {
process.exit(0);
});
// Handle errors
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
});

View File

@@ -0,0 +1,8 @@
#!/bin/bash
exec 2>> /tmp/qwen-wrapper-error.log
echo "$(date): Wrapper started" >> /tmp/qwen-wrapper-error.log
echo "$(date): PWD=$PWD" >> /tmp/qwen-wrapper-error.log
echo "$(date): Node=$(which node)" >> /tmp/qwen-wrapper-error.log
# 运行实际的 host.js
exec /usr/local/bin/node /Users/yiliang/projects/temp/qwen-code/packages/chrome-qwen-bridge/native-host/host.js