MCP Browser Tool: Connect Your QA Agent, Read-Only First

Your explorer already reads pages through a simple interface. Now you will add a second implementation that works through MCP, so your agent can use tools published as servers. This MCP browser tool starts with one strict rule: it can navigate and read, and nothing else.

Advertisement

📖 What You’ll Learn

  • How your agent acts as an MCP client
  • How to restrict which MCP tools can be called
  • How to keep your secrets from the server
  • In what order to add MCP tools

💡 Why Add an MCP Browser Tool?

Explain Like I’m 5: A Second Remote

Imagine: You have a new remote for the same television. The buttons you press are still “on” and “volume”.

What it really means: An interface lets you swap how the browser is reached.

In testing: The explorer does not need to change at all.

You learned the theory in What Is an MCP Server? and Playwright MCP Workflow for Testers. Here, your own code is the client.


🔍 Install the Packages

npm install @modelcontextprotocol/client @playwright/mcp

Installing @playwright/mcp as a project dependency pins its version in your lockfile. This is safer than downloading the latest version on every run. This project was tested with version 0.0.85.


🧩 Build the Read-Only Tool

Explain Like I’m 5: Looking through the Window

Imagine: You can see everything in the shop through the window, but you cannot touch anything.

What it really means: The agent can look at pages but cannot change them.

In testing: Clicking and typing tools are not on the allowed list.

Create src/tools/mcp-browser.tool.ts.

import { Client } from '@modelcontextprotocol/client';
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { GuardrailError, redact } from '../core/guardrails.js';
import type { BrowserTool, PageSnapshot } from './browser.tool.js';

// Navigation and reading only. Clicking and typing are not on this list.
const READ_ONLY_TOOLS = ['browser_navigate', 'browser_snapshot'];

type ToolText = { content?: Array<{ type: string; text?: string }>; isError?: boolean };

export class McpBrowserTool implements BrowserTool {
  constructor(private readonly allowedOrigin: string) {}

  private async callTool(
    client: Client,
    name: string,
    args: Record<string, unknown>,
  ): Promise<string> {
    if (!READ_ONLY_TOOLS.includes(name)) {
      throw new GuardrailError(`MCP tool is not allowed: ${name}`);
    }
    const startedAt = Date.now();
    const result = (await client.callTool({ name, arguments: args })) as ToolText;
    const text = (result.content ?? []).map((item) => item.text ?? '').join('\n');
    console.error(
      `[mcp] ${name} ${redact(JSON.stringify(args))} ${Date.now() - startedAt}ms ` +
        `${result.isError ? 'error' : 'ok'}`,
    );
    if (result.isError) {
      throw new Error(`MCP tool ${name} failed: ${text}`);
    }
    return text;
  }

  async inspect(url: string): Promise<PageSnapshot> {
    const client = new Client({ name: 'qa-agent', version: '1.0.0' });
    const browsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH;
    const transport = new StdioClientTransport({
      // The server gets a minimal environment, so it can never see your API key.
      env: {
        ...getDefaultEnvironment(),
        ...(browsersPath ? { PLAYWRIGHT_BROWSERS_PATH: browsersPath } : {}),
      },
      command: process.execPath,
      args: [
        'node_modules/@playwright/mcp/cli.js',
        '--browser', 'chromium',
        '--headless',
        '--isolated',
        '--allowed-origins', this.allowedOrigin,
      ],
    });
    await client.connect(transport);
    try {
      await this.callTool(client, 'browser_navigate', { url });
      const text = await this.callTool(client, 'browser_snapshot', {});
      return {
        url: text.match(/Page URL: (\S+)/)?.[1] ?? url,
        title: text.match(/Page Title: (.+)/)?.[1]?.trim() ?? '',
        ariaSnapshot: text,
      };
    } finally {
      await client.close();
    }
  }
}
ControlWhy
READ_ONLY_TOOLSEnforced in your code, whatever the server offers
--allowed-originsTells the browser where it may go
--isolatedUses a clean, temporary profile
Minimal envThe server cannot see your API key
Logging to stderrRecords each call with secrets removed

The origin list is not a security wall

The Playwright MCP documentation says it is not a security boundary. Your own host checks, in the explorer, still run after every page load.


🛠️ Try It

Create scripts/try-mcp.ts.

import { parseAriaSnapshot } from '../src/agents/explorer.agent.js';
import { McpBrowserTool } from '../src/tools/mcp-browser.tool.js';

const tool = new McpBrowserTool('http://127.0.0.1:4321');
const snapshot = await tool.inspect('http://127.0.0.1:4321/login');

console.log(`${snapshot.title} at ${snapshot.url}`);
console.log(parseAriaSnapshot(snapshot.ariaSnapshot));

Start the demo in one terminal and run this in another.

npx tsx scripts/try-mcp.ts

When we ran it, the output looked like this.

[mcp] browser_navigate {"url":"http://127.0.0.1:4321/login"} 1979ms ok
[mcp] browser_snapshot {} 13ms ok
Demo Login at http://127.0.0.1:4321/login
[
  { role: 'heading', name: 'Sign in' },
  { role: 'textbox', name: 'Email' },
  { role: 'textbox', name: 'Password' },
  { role: 'button', name: 'Sign in' }
]

The same parseAriaSnapshot function read the MCP result. To use MCP in the workflow, pass new McpBrowserTool(new URL(env.BASE_URL).origin) to the explorer in src/app.ts.


📌 The Order for Adding MCP Tools

Explain Like I’m 5: Learning to Swim

Imagine: You start in the shallow end with armbands. You go deeper only when you are ready.

What it really means: Add powerful tools only after simpler ones are proven.

In testing: Writing and running tools come after approvals work.

  1. Choose a client library and check its current documentation.
  2. Start with a read-only browser inspection tool.
  3. Expose only what the login workflow needs.
  4. Define schemas, time limits and errors for each tool.
  5. Allowlist your hosts and actions.
  6. Add write and execution tools only after approvals work.
  7. Log the tool name, redacted arguments, duration and status.
  8. Test bad input, timeouts, disconnections and strange page content.

You practised step 8 in MCP Server Testing.


🌍 Real-Life Example: The Extra Tool

A browser server offers dozens of tools, and a team exposes them all. A model uses a code-running tool to solve a small problem, and it works, so nobody notices. Later, that same tool runs something unsafe. A short allowlist would have refused the call the first time.


🧪 Try It Yourself

  1. Create the files and run npm run typecheck.
  2. Run the script against the demo.
  3. Add browser_click to a call, and confirm that it is refused.
  4. Change the allowed origin, and see what happens.
  5. Plug the MCP tool into src/app.ts, and run the agent.

📋 Ready-to-Copy Prompt

You are reviewing how a QA agent connects to an MCP server.

Code:
[paste the MCP client code with no secrets]

Check that:
- Only named tools can be called
- The server cannot read my environment secrets
- The server version is pinned
- Tool results are treated as untrusted data
- Every call is logged with secrets removed

Check my code against the current MCP SDK documentation. Say if you are unsure.

⚠️ Common Mistakes

  • Exposing every tool that a server offers
  • Passing your whole environment to a server
  • Always downloading the latest server version
  • Trusting the origin list as a security boundary
  • Adding write tools before approvals work

🎯 Key Takeaways

  • An interface lets MCP replace the browser code.
  • Enforce a tool allowlist in your own client.
  • Give MCP servers a minimal environment.
  • Add tools from least powerful to most powerful.

🎤 Interview Questions

1. How would you integrate MCP into a QA agent?

Model answer: I would place it behind an existing tool interface and connect with an MCP client. I would start read-only, with an allowlist of tools and logging.

2. Why enforce a tool allowlist in the client?

Model answer: A server may offer far more tools than I need, and they can change. A client-side list guarantees that only the approved ones can be called.

3. Why pin an MCP server version?

Model answer: It prevents unreviewed changes from reaching my system. It also reduces supply chain risk and keeps runs repeatable.

4. Why restrict a server’s environment?

Model answer: A local server inherits power from its process. Passing a minimal environment means that it cannot read API keys or other secrets.

5. What would you log for each tool call?

Model answer: I would log the tool name, redacted arguments, duration and result status. That creates an audit trail without exposing sensitive data.


📝 Assignment

Add a time limit to callTool, and show a clear error when it expires. Then add a unit test using a fake client. Finally, compare the results and speed of the Playwright and MCP versions.


❓ FAQ

Is MCP better than calling Playwright directly?

Not always. For this project, direct Playwright is simpler. MCP helps when you want to share tools across applications.

I got a “browser is not installed” error. What now?

Run the install command shown in the error message. Playwright MCP may need its own browser build.


🚀 Next Steps

Well done on finishing this lesson. Complete the assignment, then continue with AI Agent CI Pipeline: GitHub Actions and Quality Metrics.