Universal MCP Client
Connect to any MCP server without bloating context with tool definitions.
⚠️ PLAYWRIGHT USERS: READ "CRITICAL: Playwright Browser Session Behavior" SECTION BELOW!
Each MCP call = new browser session. Browser CLOSES after each call. You CANNOT navigate in one call and click in another. Use
browser_run_codefor ANY multi-step operation. If you need to return to a state (e.g., logged in), you MUST redo ALL steps from scratch.
How It Works
Instead of loading all MCP tool schemas into context, this client:
- Lists available servers from config
- Queries tool schemas on-demand
- Executes tools with JSON arguments
Configuration
Config location priority:
MCP_CONFIG_PATHenvironment variable.claude/skills/mcp-client/references/mcp-config.json.mcp.jsonin current directory~/.claude.json
Commands
bash1# List configured servers 2python scripts/mcp_client.py servers 3 4# List tools from a specific server 5python scripts/mcp_client.py tools playwright 6 7# Call a tool 8python scripts/mcp_client.py call playwright browser_navigate '{"url": "https://example.com"}'
CRITICAL: Playwright Browser Session Behavior
⚠️ The Session Problem
Each MCP call creates a NEW browser session. The browser CLOSES after each call.
This means:
bash1# ❌ WRONG - These run in SEPARATE browser sessions! 2python scripts/mcp_client.py call playwright browser_navigate '{"url": "https://example.com"}' 3python scripts/mcp_client.py call playwright browser_click '{"element": "Accept cookies"}' 4python scripts/mcp_client.py call playwright browser_snapshot '{}' 5# ^ The snapshot captures a FRESH page, not the page after clicking!
✅ The Solution: browser_run_code
Use browser_run_code to run multiple Playwright steps in ONE browser session:
bash1python scripts/mcp_client.py call playwright browser_run_code '{ 2 "code": " 3 await page.goto(\"https://example.com\"); 4 5 // Wait for and click cookie banner 6 const acceptBtn = page.getByRole(\"button\", { name: /accept/i }); 7 if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) { 8 await acceptBtn.click(); 9 await page.waitForTimeout(1000); 10 } 11 12 // Wait for page to stabilize 13 await page.waitForLoadState(\"networkidle\"); 14 15 // Return snapshot data for analysis 16 const snapshot = await page.accessibility.snapshot(); 17 return JSON.stringify(snapshot, null, 2); 18 " 19}'
When to Use Each Approach
| Scenario | Tool | Why |
|---|---|---|
| Simple page load + snapshot | browser_navigate | Returns snapshot automatically |
| Multi-step interaction | browser_run_code | Keeps session alive |
| Click then observe result | browser_run_code | Session persists |
| Fill form and submit | browser_run_code | Session persists |
| Hover to reveal menu | browser_run_code | Session persists |
Playwright Workflows for Test Discovery
1. Basic Page Exploration (Single Step)
browser_navigate returns both navigation result AND accessibility snapshot:
bash1python scripts/mcp_client.py call playwright browser_navigate '{"url": "https://example.com"}'
Output includes:
- Page URL and title
- Full accessibility tree (all visible elements with roles, names, states)
- Element references for further interaction
Use this when: Simple page load without interactions
2. Page with Cookie Banner (Multi-Step)
bash1python scripts/mcp_client.py call playwright browser_run_code '{ 2 "code": " 3 await page.goto(\"https://www.olx.ro\"); 4 5 // Handle cookie consent 6 try { 7 const cookieBtn = page.getByRole(\"button\", { name: \"Accept\" }); 8 await cookieBtn.click({ timeout: 5000 }); 9 await page.waitForTimeout(1000); 10 } catch (e) { 11 // No cookie banner 12 } 13 14 // Get accessibility snapshot 15 const snapshot = await page.accessibility.snapshot({ interestingOnly: false }); 16 return JSON.stringify(snapshot, null, 2); 17 " 18}'
3. Navigate to Subpage (Multi-Step)
bash1python scripts/mcp_client.py call playwright browser_run_code '{ 2 "code": " 3 await page.goto(\"https://www.olx.ro\"); 4 5 // Dismiss cookies 6 const acceptBtn = page.getByRole(\"button\", { name: \"Accept\" }); 7 if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) { 8 await acceptBtn.click(); 9 await page.waitForTimeout(500); 10 } 11 12 // Navigate to login 13 await page.goto(\"https://www.olx.ro/cont/\"); 14 15 // Wait for redirect to login domain 16 await page.waitForURL(/login\\.olx\\.ro/, { timeout: 10000 }); 17 18 // Get form structure 19 const snapshot = await page.accessibility.snapshot(); 20 return JSON.stringify({ url: page.url(), snapshot }, null, 2); 21 " 22}'
4. Explore Element Interactions (Multi-Step)
Use this to understand how menus/dropdowns behave:
bash1python scripts/mcp_client.py call playwright browser_run_code '{ 2 "code": " 3 await page.goto(\"https://www.olx.ro\"); 4 5 // Dismiss cookies 6 const acceptBtn = page.getByRole(\"button\", { name: \"Accept\" }); 7 if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) { 8 await acceptBtn.click(); 9 } 10 11 // Click on category to see what happens 12 const categoryLink = page.getByRole(\"link\", { name: /Auto, moto/i }).first(); 13 await categoryLink.click(); 14 15 // Wait to see result 16 await page.waitForTimeout(1500); 17 18 // Capture state after click 19 const snapshot = await page.accessibility.snapshot(); 20 return JSON.stringify({ 21 url: page.url(), 22 didNavigate: page.url().includes(\"auto\"), 23 snapshot: snapshot 24 }, null, 2); 25 " 26}'
5. Fill Form and Capture State
bash1python scripts/mcp_client.py call playwright browser_run_code '{ 2 "code": " 3 await page.goto(\"https://login.olx.ro\"); 4 5 // Fill login form 6 await page.locator(\"input[type=email]\").fill(\"test@example.com\"); 7 await page.locator(\"input[type=password]\").fill(\"test123\"); 8 9 // Click login button 10 await page.getByTestId(\"login-submit-button\").click(); 11 12 // Wait for response 13 await page.waitForTimeout(3000); 14 15 // Capture any error messages 16 const errors = await page.locator(\"[class*=error], [role=alert]\").allTextContents(); 17 const snapshot = await page.accessibility.snapshot(); 18 19 return JSON.stringify({ 20 url: page.url(), 21 errors: errors, 22 snapshot: snapshot 23 }, null, 2); 24 " 25}'
Gathering Selectors for Page Objects
Best Practices
1. Use Accessibility Tree First
The snapshot from browser_navigate or browser_run_code provides:
- Role: button, link, textbox, combobox, etc.
- Name: accessible name (from label, aria-label, text content)
- State: disabled, checked, expanded, etc.
Map these to Playwright locators:
typescript1// From snapshot: { role: "button", name: "Căutare" } 2page.getByRole('button', { name: /Căutare/i }) 3 4// From snapshot: { role: "textbox", name: "Ce anume cauți?" } 5page.getByRole('textbox', { name: /Ce anume cauți/i }) 6 7// From snapshot: { role: "link", name: "Auto, moto și ambarcațiuni" } 8page.getByRole('link', { name: /Auto, moto/i })
2. Selector Priority
| Priority | Method | Use When |
|---|---|---|
| 1 | getByRole() | Element has semantic role + accessible name |
| 2 | getByTestId() | Element has data-testid attribute |
| 3 | getByText() | Unique text content |
| 4 | getByPlaceholder() | Input with placeholder |
| 5 | locator('[attr="value"]') | CSS attribute selector |
| 6 | locator('.class') | CSS class (fragile, avoid) |
3. Handling Multiple Matches
typescript1// Use .first() when multiple match 2page.getByRole('link', { name: 'Category' }).first() 3 4// Use parent context 5page.locator('nav').getByRole('link', { name: 'Category' }) 6 7// Use filter 8page.getByRole('button').filter({ hasText: /submit/i })
4. Get Full DOM for Complex Cases
When accessibility tree isn't enough, get raw HTML:
bash1python scripts/mcp_client.py call playwright browser_run_code '{ 2 "code": " 3 await page.goto(\"https://example.com\"); 4 5 // Get specific element HTML 6 const formHtml = await page.locator(\"form\").first().innerHTML(); 7 8 // Or get all buttons with their attributes 9 const buttons = await page.locator(\"button\").evaluateAll(btns => 10 btns.map(b => ({ 11 text: b.textContent, 12 testid: b.dataset.testid, 13 class: b.className, 14 type: b.type 15 })) 16 ); 17 18 return JSON.stringify({ formHtml, buttons }, null, 2); 19 " 20}'
Quick Reference: Playwright MCP Tools
| Tool | Session Behavior | Use Case |
|---|---|---|
browser_navigate | New session, returns snapshot | Simple page load |
browser_run_code | Single session, custom script | Multi-step operations |
browser_click | New session | Single click (usually not useful alone) |
browser_type | New session | Single type (usually not useful alone) |
browser_snapshot | Reuses if session exists | Get current page state |
browser_screenshot | Reuses if session exists | Visual capture |
Tool Arguments
browser_navigate
json1{"url": "https://example.com"}
browser_run_code
json1{ 2 "code": "await page.goto('https://example.com'); return await page.title();" 3}
The code must be valid JavaScript that:
- Uses
pageobject (Playwright Page) - Uses
awaitfor async operations - Returns the data you want (use
JSON.stringifyfor objects)
browser_click
json1{"element": "Submit button", "ref": "optional-element-ref"}
browser_type
json1{"element": "Email input", "text": "user@example.com"}
Error Handling
| Error | Cause | Fix |
|---|---|---|
| "No MCP config found" | Missing config file | Create mcp-config.json |
| "Server not found" | Server not in config | Add server to config |
| "Connection failed" | Server not running | Start the MCP server |
| "Invalid JSON" | Bad tool arguments | Check argument format |
| "Timeout" | Page too slow | Increase timeout in code |
| "Element not found" | Wrong selector | Check snapshot for actual names |
Setup
-
Copy the example config:
bash1cp .claude/skills/mcp-client/references/mcp-config.example.json \ 2 .claude/skills/mcp-client/references/mcp-config.json -
The config should contain:
json1{ 2 "mcpServers": { 3 "playwright": { 4 "command": "npx", 5 "args": ["@playwright/mcp@latest"] 6 } 7 } 8} -
Install dependencies:
bash1pip install mcp fastmcp
Config Example
See references/mcp-config.example.json
Available Servers
See references/mcp-servers.md for:
- Playwright (browser automation)
- GitHub (repository operations)
- Filesystem (file access)
- Sequential Thinking (reasoning)
- And more...
Dependencies
bash1pip install mcp fastmcp