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
1 change: 1 addition & 0 deletions packages/playwright-core/src/tools/backend/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type ContextConfig = {
codegen?: 'typescript' | 'python' | 'java' | 'csharp' | 'none';
console?: { level?: 'error' | 'warning' | 'info' | 'debug' };
imageResponses?: 'allow' | 'omit' | 'only';
filePaths?: 'relative' | 'absolute';
network?: {
allowedOrigins?: string[];
blockedOrigins?: string[];
Expand Down
15 changes: 9 additions & 6 deletions packages/playwright-core/src/tools/backend/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export class Response {
this._raw = this._json || (options?.raw ?? false);
}

private _computeRelativeTo(fileName: string): string {
private _printablePath(fileName: string): string {
if (this._context.config.filePaths === 'absolute')
return path.resolve(fileName);
const rel = path.relative(this._clientWorkspace, fileName);
// Prefix bare filenames with `./` so they're not mistaken for living in
// the auto-named `.playwright-cli/` artifact directory.
Expand All @@ -92,7 +94,7 @@ export class Response {
else
fileName = await this._context.outputFile(template, { origin: 'llm' });
await fs.promises.mkdir(path.dirname(fileName), { recursive: true });
const relativeName = this._computeRelativeTo(fileName);
const relativeName = this._printablePath(fileName);
const printableLink = `- [${title}](${relativeName})`;
return { fileName, relativeName, printableLink };
}
Expand Down Expand Up @@ -128,7 +130,7 @@ export class Response {
}

addFileLink(title: string, fileName: string) {
const relativeName = this._computeRelativeTo(fileName);
const relativeName = this._printablePath(fileName);
this.addTextResult(`- [${title}](${relativeName})`);
}

Expand Down Expand Up @@ -285,7 +287,8 @@ export class Response {
const snapshotToFile = this._includeSnapshot !== 'explicit' || !!this._includeSnapshotFileName;
const ariaFormat = this._includeSnapshot === 'none' ? 'none' : (this._json && !snapshotToFile ? 'json' : 'text');
const updateWebMCP = this._includeSnapshot !== 'none'; // Collect the page's WebMCP tools whenever a snapshot is taken anyway.
const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotRoot, this._includeSnapshotDepth, this._includeSnapshotBoxes, this._clientWorkspace, ariaFormat, updateWebMCP) : undefined;
const logRelativeTo = this._context.config.filePaths === 'absolute' ? undefined : this._clientWorkspace;
const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._includeSnapshotRoot, this._includeSnapshotDepth, this._includeSnapshotBoxes, logRelativeTo, ariaFormat, updateWebMCP) : undefined;
const tabHeaders = await Promise.all(this._context.tabs().map(tab => tab.headerSnapshot()));
if (this._includeSnapshot !== 'none' || tabHeaders.some(header => header.changed)) {
if (tabHeaders.length !== 1)
Expand Down Expand Up @@ -320,7 +323,7 @@ export class Response {
if (event.type === 'download-start')
text.push(`- Downloading file ${event.download.download.suggestedFilename()} ...`);
else if (event.type === 'download-finish')
text.push(`- Downloaded file ${event.download.download.suggestedFilename()} to "${this._computeRelativeTo(event.download.outputFile)}"`);
text.push(`- Downloaded file ${event.download.download.suggestedFilename()} to "${this._printablePath(event.download.outputFile)}"`);
}
}
if (text.length)
Expand All @@ -329,7 +332,7 @@ export class Response {
const pausedDetails = this._context.debugger().pausedDetails();
if (pausedDetails) {
addSection('Paused', [
`- ${pausedDetails.title} at ${this._computeRelativeTo(pausedDetails.location.file)}${pausedDetails.location.line ? ':' + pausedDetails.location.line : ''}`,
`- ${pausedDetails.title} at ${this._printablePath(pausedDetails.location.file)}${pausedDetails.location.line ? ':' + pausedDetails.location.line : ''}`,
'- Use any tools to explore and interact, resume by calling resume/step-over/pause-at',
]);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/tools/mcp/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ export type Config = {
*/
imageResponses?: 'allow' | 'omit' | 'only';

/**
* How file paths are rendered in tool results. Can be "relative" to the workspace root or "absolute". Defaults to "relative".
*/
filePaths?: 'relative' | 'absolute';

snapshot?: {
/**
* When taking snapshots for responses, specifies the mode to use.
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright-core/src/tools/mcp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type CLIOptions = {
endpoint?: string;
extension?: boolean;
executablePath?: string;
filePaths?: 'relative' | 'absolute';
grantPermissions?: string[];
headless?: boolean;
host?: string;
Expand Down Expand Up @@ -390,6 +391,7 @@ function configFromCLIOptions(cliOptions: CLIOptions): Config & { configFile?: s
outputDir: cliOptions.outputDir,
outputMaxSize: cliOptions.outputMaxSize,
imageResponses: cliOptions.imageResponses,
filePaths: cliOptions.filePaths,
testIdAttribute: cliOptions.testIdAttribute,
timeouts: {
action: cliOptions.timeoutAction,
Expand Down Expand Up @@ -428,6 +430,8 @@ export function configFromEnv(env?: NodeJS.ProcessEnv): Config & { configFile?:
options.device = envToString(e.PLAYWRIGHT_MCP_DEVICE);
options.executablePath = envToString(e.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
options.extension = envToBoolean(e.PLAYWRIGHT_MCP_EXTENSION);
if (e.PLAYWRIGHT_MCP_FILE_PATHS)
options.filePaths = enumParser<'relative' | 'absolute'>('--file-paths', ['relative', 'absolute'], e.PLAYWRIGHT_MCP_FILE_PATHS);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
options.filePaths = enumParser<'relative' | 'absolute'>('--file-paths', ['relative', 'absolute'], e.PLAYWRIGHT_MCP_FILE_PATHS);
options.filePaths = enumParser<'relative' | 'absolute'>('PLAYWRIGHT_MCP_FILE_PATHS', ['relative', 'absolute'], e.PLAYWRIGHT_MCP_FILE_PATHS);

options.grantPermissions = commaSeparatedList(e.PLAYWRIGHT_MCP_GRANT_PERMISSIONS);
options.headless = envToBoolean(e.PLAYWRIGHT_MCP_HEADLESS);
options.host = envToString(e.PLAYWRIGHT_MCP_HOST);
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/tools/mcp/configIni.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ const longhandTypes: Record<string, LonghandType> = {
'outputDir': 'string',
'outputMaxSize': 'number',
'imageResponses': 'string',
'filePaths': 'string',
'allowUnrestrictedFileAccess': 'boolean',
'codegen': 'string',
'testIdAttribute': 'string',
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/tools/mcp/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function decorateMCPCommand(command: Command) {
.option('--executable-path <path>', 'path to the browser executable.')
.option('--extension', 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright Extension" to be installed.')
.option('--endpoint <endpoint>', 'Bound browser endpoint to connect to.')
.option('--file-paths <mode>', 'how file paths are rendered in tool results, "relative" to the workspace root or "absolute". Default is "relative".', enumParser.bind(null, '--file-paths', ['relative', 'absolute']))
.option('--grant-permissions <permissions...>', 'List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".', commaSeparatedList)
.option('--headless', 'run browser in headless mode, headed by default')
.option('--host <host>', 'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.')
Expand Down
46 changes: 46 additions & 0 deletions tests/mcp/file-paths.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import path from 'path';
import { test, expect, parseResponse } from './fixtures';

test('--file-paths=absolute', async ({ startClient, server }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const { client } = await startClient({
args: ['--file-paths=absolute'],
config: { outputDir },
});
server.setContent('/', `<script>console.error('hello')</script>`, 'text/html');

const navigate = parseResponse(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
}));
expect(navigate.events).toContain(`New console entries: ${path.join(outputDir, 'console-')}`);

expect(await client.callTool({
name: 'browser_take_screenshot',
})).toHaveResponse({
result: expect.stringContaining(`[Screenshot of viewport](${path.join(outputDir, 'page-')}`),
});

expect(await client.callTool({
name: 'browser_take_screenshot',
arguments: { filename: 'screenshot.png' },
})).toHaveResponse({
result: expect.stringContaining(`[Screenshot of viewport](${testInfo.outputPath('screenshot.png')})`),
});
});
Loading