Hybrid Strategies: Using Skills and MCP Together
Updated Aug 2026
Why Hybrid?
Skills and MCP each have strengths the other lacks:
| Capability | Skills (Playwright CLI) | MCP |
|---|---|---|
| Token efficiency | Excellent (~4x cheaper per task, measured) | Poor |
| Semantic page understanding | YAML accessibility snapshots — read on demand | Accessibility trees — streamed always |
| Speed per step | Dozens of tokens per command | Thousands of tokens per action |
| Works without a filesystem | No — disk is the whole trick | Yes — inline state |
| Discovery | snapshot + read the YAML |
Automatic (tree arrives with every action) |
The 2026 division of labor is simpler than it was in the vibe-check era: the CLI now carries semantic page understanding too (same accessibility data, on disk). What MCP retains is environments where there is no disk. A hybrid approach uses the CLI wherever the agent has a filesystem and MCP where it doesn't — or where an integration is MCP-native.
Strategy 1: Snapshot for Discovery, Refs for Execution
The Pattern
The discovery/execution split now lives inside the CLI:
Phase 1: Discovery (one snapshot read)
Agent: "What's on this page?"
→ playwright-cli snapshot (returns a file path)
→ Agent reads the YAML: textbox "Email" [ref=e3],
textbox "Password" [ref=e4], button "Sign in" [ref=e5]
Phase 2: Execution (refs, dozens of tokens each)
→ playwright-cli fill e3 "user@test.com"
→ playwright-cli fill e4 "secret"
→ playwright-cli click e5
Token Cost
Discovery phase: ~1,500-5,000 tokens (one snapshot read, sized by page)
Execution phase: ~200 tokens (3 ref commands)
Total: ~2,000-5,000 tokens
vs. Pure MCP: ~15,000-25,000 tokens (schemas every turn + a tree per action)
When to Use
- Always, basically — this is the default working rhythm of a CLI-driven agent
- UI that changes frequently — a fresh snapshot re-discovers refs automatically
- First test run of a new feature — later runs already know the flow
The equivalent MCP pattern (call browser_snapshot, then act) still applies when you're on the MCP transport — the difference is that with MCP you also pay the tree tax on every action, not just on discovery.
Strategy 2: CLI by Default, Re-snapshot as Fallback, MCP for No-Disk Environments
The Pattern
# Pseudocode for the agent's behavior
try:
# Primary: act on a known ref (fast, cheap)
playwright-cli click e5
except StaleOrMissingRef:
# Fallback: refresh understanding from disk
playwright-cli snapshot
# Agent reads the new YAML, finds "Sign in" button's new ref
playwright-cli click e9
# Separately: if this flow must also run from a sandboxed surface
# with no filesystem, route that environment through Playwright MCP.
Token Cost
Happy path (ref works): ~55 tokens
Fallback path (re-snapshot): ~2,000-5,000 tokens (one snapshot read + retry)
Average (assuming 90% success): a few hundred tokens per step
When to Use
- Mature test suites where most flows are stable
- Self-healing tests that adapt when the UI changes (see the Self-Healing Strategies chapter — Playwright's healer agent productizes exactly this loop)
- Cost optimization — pay for page state only when something breaks
Strategy 3: Different Tools for Different Test Types
The Pattern
| Test Type | Primary Tool | Why |
|---|---|---|
| Functional tests | CLI (playwright-cli) |
Known flows, ref-driven, fast |
| Accessibility audits | CLI snapshot (YAML a11y tree) |
The snapshot is an accessibility summary — grep it, reason over it |
| Visual regression | CLI + screenshot |
Files on disk, compared without entering context |
| Exploratory testing | CLI snapshot + agent reasoning |
Discovery is a read away |
| Sandboxed / hosted agents | Playwright MCP | No filesystem — inline state is the only option |
| API integration checks | CLI + eval |
playwright-cli eval for XHR/state inspection |
| Performance checks | CLI + eval |
playwright-cli eval "JSON.stringify(performance.timing)" |
Configuration
# test-framework-config.yaml
test_types:
functional:
tool: playwright-cli
session: per-test # fresh named session per test
headed: false
accessibility:
tool: playwright-cli
artifacts: snapshots # keep YAML snapshots for audit
visual:
tool: playwright-cli
screenshots: true
comparison_threshold: 0.98
hosted_exploratory:
tool: playwright-mcp # sandboxed surface, no filesystem
snapshot_mode: full
Strategy 4: Phased Framework Adoption
Phase 1: CLI + Skill Only (Week 1-2)
Start with the simplest approach:
npm install -g @playwright/cli@latest
playwright-cli install # workspace: .playwright-cli/
playwright-cli install --skills # SKILL.md for your coding agent
Write your first flows as snapshot-then-refs sequences:
# Simple test flow
playwright-cli open https://app.example.com/login
playwright-cli snapshot # read YAML → refs
playwright-cli fill e3 "test@example.com"
playwright-cli fill e4 "password"
playwright-cli click e5
playwright-cli snapshot # verify: "Welcome" heading present
Phase 2: Add MCP Where the CLI Can't Go (Week 3-4)
When flows must also run from MCP-native or sandboxed surfaces:
- Add Playwright MCP:
claude mcp add playwright -- npx @playwright/mcp@latest(it's published to the official MCP Registry each release) - Route only those environments through it; keep everything with a filesystem on the CLI
- Since Playwright 1.59,
browser.bind()can even point both transports at one shared browser
Phase 3: Intelligent Routing (Week 5+)
Build routing logic that chooses the right transport:
- Agent has a filesystem → CLI, always
- Sandboxed surface → MCP
- CI runs → CLI only (cost optimization), traces archived
- Failures → re-snapshot first; escalate to the healer agent workflow
Implementation: A Simple Hybrid Test
Here's what the working rhythm looks like in practice, as an agent would execute it:
Agent thinking: "I need to test the login flow on the redesigned page.
I don't know the new structure. Snapshot first."
Step 1: Navigate (cheap)
→ Bash: playwright-cli open https://app.example.com/login
Step 2: Discover page structure (one disk read)
→ Bash: playwright-cli snapshot → .playwright-cli/ YAML path
→ Agent reads:
- form "Login":
- textbox "Email address" [ref=e3]
- textbox "Password" [ref=e4]
- button "Sign in" [ref=e5]
- link "Forgot password?" [ref=e6]
Step 3: Execute test (refs — cheap)
→ Bash: playwright-cli fill e3 "test@example.com"
→ Bash: playwright-cli fill e4 "secret"
→ Bash: playwright-cli click e5
Step 4: Verify (one more snapshot read)
→ Bash: playwright-cli snapshot
→ Agent reads: heading "Dashboard", text "Welcome, Test User"
Step 5: Evidence (file path only — image never enters context)
→ Bash: playwright-cli screenshot
Total: 2 snapshot reads (~4K tokens) + 6 commands (~400 tokens) ≈ 4,500 tokens
vs. Pure MCP for the same flow: ~25,000-35,000 tokens
Anti-Patterns to Avoid
Don't: Use MCP for Every Click When You Have a Filesystem
# BAD: Each action via MCP from inside Claude Code
browser_click(ref=e5) # tree streamed with the response
browser_type(ref=e3, "hi") # tree streamed again
browser_click(ref=e7) # and again
# Schemas every turn + a tree per action, for no benefit
Don't: Act on Guessed or Stale Refs
# BAD: refs from a page that has since changed
playwright-cli click e5 # ref belonged to the old snapshot
playwright-cli click e12 # guessing
# Wasted attempts + error handling
# GOOD: refs always come from a current snapshot
playwright-cli snapshot # refresh
# read YAML → button "Submit" [ref=e9]
playwright-cli click e9
Do: Match the Transport to the Environment
# GOOD
Filesystem available (Claude Code, CI) → playwright-cli + SKILL.md
No filesystem (hosted/sandboxed agent) → Playwright MCP
Both needed on one browser → browser.bind() shares the instance
Interview Talking Point
"Our framework treats skills and MCP as transports, not religions — which is Microsoft's own posture, since they ship both and recommend the CLI for coding agents at about 4x fewer tokens. The default is the Playwright CLI: snapshot once to discover the page as YAML on disk, then act by element refs at a few dozen tokens per command; when a ref goes stale we re-snapshot rather than guess. Playwright MCP stays in the stack for exactly one reason — surfaces without filesystem access, where inline state is the only option. And because
browser.bind()lets both transports share one browser instance, routing per environment costs us nothing architecturally."