Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

import type {TargetUniverse} from './DevtoolsUtils.js';
Expand Down Expand Up @@ -47,6 +46,7 @@ import {
ExtensionRegistry,
type InstalledExtension,
} from './utils/ExtensionRegistry.js';
import {saveTemporaryFile} from './utils/files.js';
import {WaitForHelper} from './WaitForHelper.js';

export type {
Expand Down Expand Up @@ -799,18 +799,7 @@ export class McpContext implements Context {
data: Uint8Array<ArrayBufferLike>,
filename: string,
): Promise<{filepath: string}> {
try {
const dir = await fs.mkdtemp(
path.join(os.tmpdir(), 'chrome-devtools-mcp-'),
);

const filepath = path.join(dir, filename);
await fs.writeFile(filepath, data);
return {filepath};
} catch (err) {
this.logger(err);
throw new Error('Could not save a file', {cause: err});
}
return await saveTemporaryFile(data, filename);
}
async saveFile(
data: Uint8Array<ArrayBufferLike>,
Expand Down
6 changes: 3 additions & 3 deletions src/bin/chrome-devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ for (const [commandName, commandDef] of Object.entries(commands)) {
commandStr,
commandDef.description,
y => {
y.option('format', {
y.option('output-format', {
choices: ['md', 'json'],
default: 'md',
});
Expand Down Expand Up @@ -171,9 +171,9 @@ for (const [commandName, commandDef] of Object.entries(commands)) {

if (response.success) {
console.log(
handleResponse(
await handleResponse(
JSON.parse(response.result) as unknown as CallToolResult,
argv['format'] as 'json' | 'md',
argv['output-format'] as 'json' | 'md',
),
);
} else {
Expand Down
24 changes: 21 additions & 3 deletions src/daemon/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import net from 'node:net';
import {logger} from '../logger.js';
import type {CallToolResult} from '../third_party/index.js';
import {PipeTransport} from '../third_party/index.js';
import {saveTemporaryFile} from '../utils/files.js';

import type {DaemonMessage, DaemonResponse} from './types.js';
import {
Expand Down Expand Up @@ -144,10 +145,10 @@ export async function stopDaemon() {
await waitForFile(pidFilePath, /*removed=*/ true);
}

export function handleResponse(
export async function handleResponse(
response: CallToolResult,
format: 'json' | 'md',
): string {
): Promise<string> {
if (response.isError) {
return JSON.stringify(response.content);
}
Expand All @@ -161,9 +162,26 @@ export function handleResponse(
for (const content of response.content) {
if (content.type === 'text') {
chunks.push(content.text);
} else if (content.type === 'image') {
const imageData = content.data;
const mimeType = content.mimeType;
let extension = '.png';
switch (mimeType) {
case 'image/jpg':
case 'image/jpeg':
extension = '.jpeg';
break;
case 'webp':
extension = '.webp';
break;
}
const data = Buffer.from(imageData, 'base64');
const name = crypto.randomUUID();
const {filepath} = await saveTemporaryFile(data, `${name}${extension}`);
chunks.push(`Saved to ${filepath}.`);
} else {
throw new Error('Not supported response content type');
}
}
return format === 'md' ? chunks.join('') : JSON.stringify(chunks);
return format === 'md' ? chunks.join(' ') : JSON.stringify(chunks);
}
26 changes: 26 additions & 0 deletions src/utils/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

export async function saveTemporaryFile(
data: Uint8Array<ArrayBufferLike>,
filename: string,
): Promise<{filepath: string}> {
try {
const dir = await fs.mkdtemp(
path.join(os.tmpdir(), 'chrome-devtools-mcp-'),
Comment thread
OrKoN marked this conversation as resolved.
);

const filepath = path.join(dir, filename);
await fs.writeFile(filepath, data);
return {filepath};
} catch (err) {
throw new Error('Could not save a file', {cause: err});
}
}
25 changes: 10 additions & 15 deletions tests/daemon/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('daemon client', () => {
describe('parsing', () => {
it('handles MCP response with text format', async () => {
const textResponse = {content: [{type: 'text' as const, text: 'test'}]};
assert.strictEqual(handleResponse(textResponse, 'md'), 'test');
assert.strictEqual(await handleResponse(textResponse, 'md'), 'test');
});

it('handles JSON response', async () => {
Expand All @@ -67,7 +67,7 @@ describe('daemon client', () => {
},
};
assert.strictEqual(
handleResponse(jsonResponse, 'json'),
await handleResponse(jsonResponse, 'json'),
JSON.stringify(jsonResponse.structuredContent),
);
});
Expand All @@ -78,7 +78,7 @@ describe('daemon client', () => {
content: [{type: 'text' as const, text: 'Something went wrong'}],
};
assert.strictEqual(
handleResponse(errorResponse, 'md'),
await handleResponse(errorResponse, 'md'),
JSON.stringify(errorResponse.content),
);
});
Expand All @@ -88,29 +88,24 @@ describe('daemon client', () => {
content: [{type: 'text' as const, text: 'Fall through text'}],
};
assert.deepStrictEqual(
handleResponse(textResponse, 'json'),
await handleResponse(textResponse, 'json'),
JSON.stringify(['Fall through text']),
);
});

it('throws error for unsupported content type', async () => {
it('supports images', async () => {
const unsupportedContentResponse = {
content: [
{
type: 'resource' as const,
resource: {
uri: 'data:image/png;base64,base64data',
blob: 'base64data',
mimeType: 'image/png',
},
type: 'image' as const,
data: 'base64data',
mimeType: 'image/png',
},
],
structuredContent: {},
};
assert.throws(
() => handleResponse(unsupportedContentResponse, 'md'),
new Error('Not supported response content type'),
);
const response = await handleResponse(unsupportedContentResponse, 'md');
assert.ok(response.includes('.png'));
});
});
});
20 changes: 20 additions & 0 deletions tests/e2e/chrome-devtools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,26 @@ describe('chrome-devtools', () => {
);
});

it('can take screenshot', async () => {
const startResult = spawnSync('node', [CLI_PATH, 'start', ...START_ARGS]);
assert.strictEqual(
startResult.status,
0,
`start command failed: ${startResult.stderr.toString()}`,
);

const result = spawnSync('node', [CLI_PATH, 'take_screenshot']);
assert.strictEqual(
result.status,
0,
`take_screenshot command failed: ${result.stderr.toString()}`,
);
assert(
result.stdout.toString().includes('.png'),
'take_screenshot output is unexpected',
);
});

it('forwards disclaimers to stderr on start', () => {
const result = spawnSync('node', [CLI_PATH, 'start', ...START_ARGS]);
assert.strictEqual(
Expand Down
Loading