Skip to content

Replace monolithic MCP server with a layered proxy - #45

Open
smartlabsAT wants to merge 1 commit into
epic/35-architecture-cleanupfrom
feature/39-modularize-mcp-server
Open

Replace monolithic MCP server with a layered proxy#45
smartlabsAT wants to merge 1 commit into
epic/35-architecture-cleanupfrom
feature/39-modularize-mcp-server

Conversation

@smartlabsAT

Copy link
Copy Markdown
Owner

Summary

Replaces the 2223-LOC monolithic src/mcp/server.ts with a thin routing layer over an upstream MCP execution layer. Pure architectural refactor — no new tool logic. Tool delegation, browser lifecycle, and signal handling all move out of the monolith into a small focused module.

New src/proxy/ module

File LOC Purpose
proxy-server.ts 121 MCP Server (low-level), setRequestHandler for ListToolsRequestSchema + CallToolRequestSchema, inline local-tool registry, signal cleanup
upstream-client.ts 230 Wraps @playwright/mcp@0.0.75 as a subprocess via Client + StdioClientTransport. Single-flight start, auto-restart with rate limit, restartWith(options) exposed for Epic 3
version.ts 18 Reads PACKAGE_NAME / PACKAGE_VERSION from package.json for MCP handshake metadata

Other changes

  • src/mcp/server.ts becomes a 3-line shim importing the proxy — existing .mcp.json configs pointing at dist/mcp/server.cjs keep working
  • @playwright/mcp@0.0.75 added as exact-pinned dependency
  • engines.node bumped >=16>=18
  • tsup.config.ts gains the proxy entry; @playwright/mcp becomes external in both MCP and proxy bundles
  • 3 protocol-validation modules deleted (~933 LOC): protocol-validator.ts, protocol-validation-layer.ts, protocol-error-recovery.ts
  • 3 protocol tests deleted
  • 4 protocol re-exports removed from src/index.ts
  • 4 docs (MCP_SERVER.md, INTELLIGENT_TESTING.md, CACHING.md, ISSUE_24_STABILITY.md) get a "v0.2.0-alpha.1 status" banner so users understand session/test/cache MCP tools are temporarily unavailable
  • src/commands/mcp.ts CLI hint refreshed to list real upstream tools instead of removed ones

Stats

18 files changed: +436 / −3807 (netto −3371 LOC in src/tests).

Architecture decisions honoured (Epic #35)

  • MCP SDK Server low-level class — not McpServer
  • @playwright/mcp@0.0.75 exact pin (no caret)
  • require.resolve resolution — actually via @playwright/mcp/package.json because v0.0.75 has a strict exports field that does not expose ./cli.js; we derive the sibling path
  • Backward-compat shim at src/mcp/server.ts
  • browser_* prefix preserved (all 23 upstream tools)
  • "MECHANICAL ONLY" rule: no cache/click/type interception introduced

Out of scope (per issue, intentionally absent)

  • Interception logic for click/type (Epic 2)
  • Local tools — the registry is empty in alpha.1, Epic 3 will populate it
  • --caps flag forwarding
  • BASE_URL env-var URL rewriting (acknowledged alpha.1 regression, restored in Epic 2)

Review fixes applied before commit

After dispatching two parallel review agents (code-quality + stress-test), I applied four fixes:

  1. tools/list propagates upstream errors instead of silently returning an empty list (MCP error -32603 surfaces the real diagnosis to the client)
  2. Helpful errors for missing @playwright/mcp and invalid PLAYWRIGHT_MCP_CLI_PATH instead of Node's MODULE_NOT_FOUND stack trace
  3. stop() cleanup clears the pending restart timer and detaches the old transport's onclose — prevents a stale callback firing into a fresh client
  4. TODO marker in restartWith() documenting the Epic-3 race window between stop() and ensureStarted()

Closes #39

Test plan

  • npm run build:tsc exits 0
  • npm run build exits 0
  • npm test exits 0 (15/15: 8 CLI + 7 install)
  • dist/mcp/server.cjs and dist/proxy/proxy-server.cjs exist and are executable; both list 23 tools
  • Smoke test: spawn dist/mcp/server.cjs, connect, tools/list returns ≥ 23 tools in under 3 s (target was 3 s, observed 71 ms)
  • Real tool call: browser_navigate to about:blank followed by browser_close completes successfully (verified end-to-end)
  • Concurrent: 10 parallel tools/list calls trigger upstream startup exactly once (single-flight)
  • Auto-restart: kill -9 on the upstream subprocess; next call recovers in ~2 s
  • Rate limit: 6 successive crashes exhaust the limit and proxy exits with code 1
  • SIGINT/SIGTERM: proxy exits with code 0 in milliseconds; no zombie subprocesses
  • Bad tool input (unknown name, missing args, null args, very long name) returns isError: true from upstream, proxy stays alive
  • Missing/bogus PLAYWRIGHT_MCP_CLI_PATH surfaces the actionable error message

Collapse 2223 LOC of inline tool registrations, browser lifecycle, and
state mixing in `src/mcp/server.ts` into a thin routing layer over an
upstream MCP execution layer. Mechanical refactor — no new tool logic.

New `src/proxy/` module:
- `proxy-server.ts`  — MCP `Server` (low-level), `setRequestHandler` for
  `ListToolsRequestSchema` + `CallToolRequestSchema`, an inline (currently
  empty) local-tool registry, and SIGINT/SIGTERM cleanup.
- `upstream-client.ts` — wraps `@playwright/mcp@0.0.75` as a subprocess
  via `Client` + `StdioClientTransport`. Lazy single-flight start,
  auto-restart on `transport.onclose` with a 5-per-60s rate-limit
  exiting on overrun, and `restartWith(options)` exposed for Epic 3.
- `version.ts` — reads PACKAGE_NAME / PACKAGE_VERSION from package.json
  so MCP handshake metadata follows version bumps automatically.

The `@playwright/mcp` CLI entry is resolved via
`require.resolve('@playwright/mcp/package.json')` rather than `/cli.js`
because v0.0.75 has a strict `exports` field that does not expose
`./cli.js` — we derive the sibling path. `PLAYWRIGHT_MCP_CLI_PATH` is
honoured as an override and both code paths are guarded with helpful
errors when the package or file is missing.

`src/mcp/server.ts` becomes a 3-line shim that imports the proxy, so
existing `.mcp.json` configurations pointing at `dist/mcp/server.cjs`
continue to work without modification.

Protocol-validation modules (`protocol-validator.ts`,
`protocol-validation-layer.ts`, `protocol-error-recovery.ts`,
~933 LOC) are deleted along with their three tests; SDK-level protocol
handling now covers what they did. Their four public re-exports are
removed from `src/index.ts`.

`tsup.config.ts` gains the proxy entry and adds `@playwright/mcp` to
externals across the MCP and proxy bundles. `package.json` pins
`@playwright/mcp` at exact `0.0.75`, bumps `engines.node` from `>=16`
to `>=18`, and the bin/exports entries stay valid because the shim
still produces `dist/mcp/server.cjs`.

Out-of-scope per the issue: no cache/click/type interception (Epic 2),
no local session/test/cache tools (Epic 3 will populate the registry),
no `--caps` flag forwarding, no BASE_URL URL rewriting (acknowledged
alpha.1 regression — Epic 2 restores it).

Verified behaviour:
- `npx tsc` and `npm run build` exit 0; `npm test` 15/15 passes.
- A spawn-and-list smoke test connects in 71 ms and lists 23 upstream
  tools (well under the 3 s AC); a real `browser_navigate` round-trip
  completes in ~1.1 s including browser launch.
- Sequential load (50 calls): ~44 ms/call after warm-up, single stable
  upstream PID, no memory growth.
- Concurrent `tools/list` (10x): single-flight start verified — upstream
  spawns exactly once.
- Crash + auto-restart recovers in ~2.2 s; six successive crashes hit
  the rate limit and the proxy exits cleanly with code 1.
- SIGINT/SIGTERM: exit 0 in ~3 ms, no zombie subprocesses.
- Backward-compat: `dist/mcp/server.cjs` shim and `dist/proxy/proxy-server.cjs`
  direct entry are functionally identical (6-byte size delta from the
  source-map URL only).

Review fixes applied before commit:
- `tools/list` errors now propagate to the client (was silently returning
  an empty list, indistinguishable from "no tools available").
- Missing `@playwright/mcp` or invalid `PLAYWRIGHT_MCP_CLI_PATH` now
  produce actionable error messages instead of Node's `MODULE_NOT_FOUND`
  stack trace.
- `stop()` clears the pending restart timer and detaches the old
  transport's `onclose` to prevent a stale handler firing into a fresh
  client instance.
- `restartWith()` carries an inline TODO documenting the Epic-3 race
  window between `stop()` and `ensureStarted()`.

Docs (`docs/MCP_SERVER.md`, `docs/INTELLIGENT_TESTING.md`,
`docs/CACHING.md`, `docs/ISSUE_24_STABILITY.md`) get explicit
"v0.2.0-alpha.1 status" notices so users reading them understand that
session/test/cache MCP tools are temporarily unavailable in alpha.1
and return in a later v0.2.0 milestone (CLI equivalents work unchanged).

Closes #39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant