Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,8 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- **Emulation** (2 tools)
- [`emulate`](docs/tool-reference.md#emulate)
- [`resize_page`](docs/tool-reference.md#resize_page)
- **Performance** (3 tools)
- **Performance** (4 tools)
- [`performance_analyze_file`](docs/tool-reference.md#performance_analyze_file)
- [`performance_analyze_insight`](docs/tool-reference.md#performance_analyze_insight)
- [`performance_start_trace`](docs/tool-reference.md#performance_start_trace)
- [`performance_stop_trace`](docs/tool-reference.md#performance_stop_trace)
Expand Down
13 changes: 12 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
- **[Emulation](#emulation)** (2 tools)
- [`emulate`](#emulate)
- [`resize_page`](#resize_page)
- **[Performance](#performance)** (3 tools)
- **[Performance](#performance)** (4 tools)
- [`performance_analyze_file`](#performance_analyze_file)
- [`performance_analyze_insight`](#performance_analyze_insight)
- [`performance_start_trace`](#performance_start_trace)
- [`performance_stop_trace`](#performance_stop_trace)
Expand Down Expand Up @@ -213,6 +214,16 @@

## Performance

### `performance_analyze_file`

**Description:** Analyzes a performance trace file from the local filesystem. This can be used to analyze traces exported from Chrome DevTools, Lighthouse, or other tools that generate Chrome trace format files.

**Parameters:**

- **filePath** (string) **(required)**: The absolute path to the trace file to analyze. Supports JSON trace files in Chrome trace format (e.g., trace.json, lighthouse-0.trace.json).

---

### `performance_analyze_insight`

**Description:** Provides more detailed information on a specific Performance Insight of an insight set that was highlighted in the results of a trace recording.
Expand Down
46 changes: 46 additions & 0 deletions src/tools/performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import fs from 'node:fs/promises';

import {logger} from '../logger.js';
import {zod} from '../third_party/index.js';
import type {Page} from '../third_party/index.js';
Expand Down Expand Up @@ -161,6 +163,50 @@ export const analyzeInsight = defineTool({
},
});

export const analyzeFile = defineTool({
name: 'performance_analyze_file',
description:
'Analyzes a performance trace file from the local filesystem. This can be used to analyze traces exported from Chrome DevTools, Lighthouse, or other tools that generate Chrome trace format files.',
annotations: {
category: ToolCategory.PERFORMANCE,
readOnlyHint: true,
},
schema: {
filePath: zod
.string()
.describe(
'The absolute path to the trace file to analyze. Supports JSON trace files in Chrome trace format (e.g., trace.json, lighthouse-0.trace.json).',
),
},
handler: async (request, response, context) => {
const {filePath} = request.params;

try {
const buffer = await fs.readFile(filePath);
const result = await parseRawTraceBuffer(new Uint8Array(buffer));

if (traceResultIsSuccess(result)) {
context.storeTraceRecording(result);
const traceSummaryText = getTraceSummary(result);
response.appendResponseLine(
`Successfully analyzed trace file: ${filePath}`,
);
response.appendResponseLine(traceSummaryText);
} else {
response.appendResponseLine(
'There was an error parsing the trace file:',
);
response.appendResponseLine(result.error);
}
} catch (e) {
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
logger(`Error reading trace file: ${errorText}`);
response.appendResponseLine('An error occurred reading the trace file:');
response.appendResponseLine(errorText);
}
},
});

async function stopTracingAndAppendOutput(
page: Page,
response: Response,
Expand Down
79 changes: 79 additions & 0 deletions tests/tools/performance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
*/

import assert from 'node:assert';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {describe, it, afterEach} from 'node:test';

import sinon from 'sinon';

import {
analyzeFile,
analyzeInsight,
startTrace,
stopTrace,
Expand Down Expand Up @@ -276,4 +280,79 @@ describe('performance', () => {
});
});
});

describe('performance_analyze_file', () => {
it('analyzes a trace file from the filesystem', async () => {
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-trace-test-'));
const tempFilePath = path.join(tempDir, 'trace.json');
fs.writeFileSync(tempFilePath, rawData);

try {
await withMcpContext(async (response, context) => {
await analyzeFile.handler(
{params: {filePath: tempFilePath}},
response,
context,
);

const output = response.responseLines.join('\n');
assert.ok(
output.includes(
`Successfully analyzed trace file: ${tempFilePath}`,
),
);
assert.ok(
output.includes('## Summary of Performance trace findings'),
);
assert.ok(output.includes('URL: https://web.dev/'));
assert.strictEqual(context.recordedTraces().length, 1);
});
} finally {
fs.rmSync(tempDir, {recursive: true});
}
});

it('returns an error if the file does not exist', async () => {
await withMcpContext(async (response, context) => {
await analyzeFile.handler(
{params: {filePath: '/nonexistent/path/trace.json'}},
response,
context,
);

assert.ok(
response.responseLines
.join('\n')
.includes('An error occurred reading the trace file:'),
);
assert.strictEqual(context.recordedTraces().length, 0);
});
});

it('returns an error if the file is not valid JSON', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-trace-test-'));
const tempFilePath = path.join(tempDir, 'invalid.json');
fs.writeFileSync(tempFilePath, 'not valid json');

try {
await withMcpContext(async (response, context) => {
await analyzeFile.handler(
{params: {filePath: tempFilePath}},
response,
context,
);

assert.ok(
response.responseLines
.join('\n')
.includes('There was an error parsing the trace file:'),
);
assert.strictEqual(context.recordedTraces().length, 0);
});
} finally {
fs.rmSync(tempDir, {recursive: true});
}
});
});
});