BMAD-METHOD/tools/mcp/chrome-devtools-client.js

153 lines
3.9 KiB
JavaScript

const fs = require('node:fs');
const path = require('node:path');
let ClientModulePromise;
let StdioModulePromise;
async function loadSdkModules() {
if (!ClientModulePromise) {
ClientModulePromise = import('@modelcontextprotocol/sdk/client/index.js');
}
if (!StdioModulePromise) {
StdioModulePromise = import('@modelcontextprotocol/sdk/client/stdio.js');
}
const [{ Client }, { StdioClientTransport }] = await Promise.all([
ClientModulePromise,
StdioModulePromise,
]);
return { Client, StdioClientTransport };
}
class ChromeDevToolsMcpClient {
constructor(options = {}) {
this.options = options;
this.client = null;
this.transport = null;
this.stderrBuffer = '';
}
async connect() {
if (this.client) {
return;
}
const { Client, StdioClientTransport } = await loadSdkModules();
const transport = new StdioClientTransport(this.buildServerParameters());
if (transport.stderr) {
transport.stderr.on('data', (chunk) => {
const message = chunk.toString();
this.stderrBuffer += message;
if (this.options.logFile) {
fs.appendFileSync(this.options.logFile, message);
}
});
}
const client = new Client(
{
name: this.options.clientName ?? 'bmad-cli-chrome-mcp',
version: this.options.clientVersion ?? '1.0.0',
},
{
capabilities: {
tools: {},
logging: {},
},
},
);
try {
await client.connect(transport);
await client.listTools({});
this.client = client;
this.transport = transport;
} catch (error) {
await transport.close();
const diagnostic = this.stderrBuffer.trim();
const message =
diagnostic.length > 0 ? `${error.message}\n${diagnostic}` : error.message;
throw new Error(`Failed to connect to chrome-devtools-mcp: ${message}`);
}
}
async disconnect() {
if (!this.client || !this.transport) {
return;
}
await this.client.close();
await this.transport.close();
this.client = null;
this.transport = null;
}
async listTools() {
if (!this.client) {
throw new Error('MCP client is not connected');
}
const result = await this.client.listTools({});
return result.tools;
}
async callTool(name, args) {
if (!this.client) {
throw new Error('MCP client is not connected');
}
return this.client.callTool({ name, arguments: args });
}
buildServerParameters() {
const args = ['chrome-devtools-mcp@latest'];
if (this.options.browserUrl) {
args.push(`--browser-url=${this.options.browserUrl}`);
} else {
const headless =
this.options.headless === undefined ? true : Boolean(this.options.headless);
const isolated =
this.options.isolated === undefined ? true : Boolean(this.options.isolated);
const viewport = this.options.viewport || '1280x720';
args.push(`--headless=${headless}`);
args.push(`--isolated=${isolated}`);
args.push(`--viewport=${viewport}`);
if (this.options.channel) {
args.push(`--channel=${this.options.channel}`);
}
if (this.options.acceptInsecureCerts) {
args.push('--acceptInsecureCerts=true');
}
if (this.options.executablePath) {
args.push(`--executablePath=${this.options.executablePath}`);
}
if (Array.isArray(this.options.extraChromeArgs)) {
for (const chromeArg of this.options.extraChromeArgs) {
args.push(`--chromeArg=${chromeArg}`);
}
}
}
const logFile = this.options.logFile
? path.resolve(this.options.logFile)
: undefined;
if (logFile) {
args.push(`--logFile=${logFile}`);
}
return {
command: 'npx',
args: ['-y', ...args],
stderr: 'pipe',
env: this.options.env,
cwd: this.options.cwd,
};
}
}
module.exports = {
ChromeDevToolsMcpClient,
};