# Microsoft Playwright MCP Comparison

## Overview

[Microsoft's Playwright MCP](https://github.com/microsoft/playwright-mcp) (32K stars, TypeScript) is the highest-starred TypeScript MCP server implementation. It exposes browser automation capabilities (navigation, clicking, typing, screenshotting) as MCP tools. Unlike our proxy-based architecture, Playwright MCP executes tools directly in-process using the Playwright browser engine.

---

## How Playwright MCP works

1. **Multi-layer factory architecture**: CLI entry point → server factory → per-domain tool files. Each tool domain (navigation, interaction, snapshot, pdf, etc.) has its own file with `defineTool()` calls. The server aggregates tools from all domains at startup.

2. **Capability-gated tools**: Tools are gated by browser capabilities. For example, PDF tools only register when the `--pdf` flag is passed. This is compile-time gating based on server configuration, similar to our domain filtering.

3. **Multi-transport**: Supports both stdio and Streamable HTTP (`--port` flag). The Streamable HTTP transport enables remote access and multi-client scenarios. This is the most production-ready TypeScript multi-transport implementation.

4. **Session state**: The server maintains browser state between tool calls — a page opened in one call persists for the next. This is fundamentally different from our stateless proxy model.

5. **Snapshot-based rendering**: Tool results include page snapshots (accessibility tree representation) rather than raw HTML. This is a domain-specific optimization for browser content.

---

## How our system differs

| Aspect                  | Playwright MCP                      | Coda MCP Server                        |
| ----------------------- | ----------------------------------- | -------------------------------------- |
| **Domain**              | Browser automation                  | Royalties platform data                |
| **Execution**           | In-process (Playwright engine)      | HTTP proxy to Express API              |
| **State**               | Stateful (browser session persists) | Stateless (each call independent)      |
| **Transport**           | stdio + Streamable HTTP             | stdio only                             |
| **Tool registration**   | `defineTool()` per domain file      | Static `TOOL_DEFINITIONS` array        |
| **Tool gating**         | Capability flags (`--pdf`, etc.)    | Domain filtering + exclusion set       |
| **Auth**                | None (local browser)                | Bearer token forwarded to Express      |
| **Error handling**      | Playwright error propagation        | `ToolCallOutcome` tagged union + hints |
| **Observability**       | Basic logging                       | OTel tracing + metrics + MCP logging   |
| **Response truncation** | Snapshot size limits                | 100K structural array truncation       |
| **Testing**             | E2E via MCP client                  | Unit + integration (InMemoryTransport) |

---

## What we are NOT adopting and why

1. **Stateful sessions**: Our tools are stateless by design — each tool call is independent, and the Express server handles all state management. Adding browser-like session state would create lifecycle complexity and break the clean proxy model.

2. **In-process execution**: Same rationale as Stripe — our tools require the Express middleware stack.

3. **Snapshot rendering**: Domain-specific to browser content. Our tool results are structured JSON (accounts, contracts, revenue data), not DOM trees.

---

## What we are adopting

### 1. Multi-transport support (future — bundle with SDK v2)

Playwright's `--port` flag for Streamable HTTP is the reference pattern for TypeScript MCP servers. When we add remote transport (SDK v2), we should follow this pattern: a CLI flag that switches between stdio and HTTP, with the server code unchanged.

### 2. Per-domain tool files

Playwright organizes tools into domain-specific files (`navigation.ts`, `interaction.ts`, `snapshot.ts`). Our tool definitions already follow this pattern (`account/definitions.ts`, `graphql/definitions.ts`, etc.) — this validates our approach.

### 3. E2E testing via MCP client

Playwright tests tools by connecting a real MCP client, issuing tool calls, and asserting on results. Our `integration.test.ts` uses `InMemoryTransport` for this, which is the same pattern without the overhead of spawning a child process.

---

## Validation of our approach

Playwright validates our deterministic tool ordering (`.sort()` by name) — their tools are also registered in a deterministic order. Both implementations align with the upcoming spec requirement for deterministic `tools/list` responses.

The key lesson from Playwright is that the transport layer (stdio vs HTTP) should be a deployment concern, not an architectural one. Our MCP modules are transport-agnostic — they operate on `ToolCallOutcome` values from `McpHttpClient`, not on transport-level details.

---

## References

- [microsoft/playwright-mcp](https://github.com/microsoft/playwright-mcp) (GitHub)
- MCP spec 2025-03-26: Streamable HTTP transport

---

_Comparison conducted 2026-05-10._
