mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 17:57:46 +00:00
chore(chrome-qwen-bridge): connect & them
This commit is contained in:
271
packages/chrome-qwen-bridge/native-host/browser-mcp-server.js
Executable file
271
packages/chrome-qwen-bridge/native-host/browser-mcp-server.js
Executable 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);
|
||||
});
|
||||
8
packages/chrome-qwen-bridge/native-host/wrapper.sh
Executable file
8
packages/chrome-qwen-bridge/native-host/wrapper.sh
Executable 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
|
||||
Reference in New Issue
Block a user