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 browserruncode for 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:
MCPCONFIGPATH environment variable
.claude/skills/mcp-client/references/mcp-config.json
.mcp.json in current directory
~/.claude.json
Commands
# List configured servers
python scripts/mcp_client.py servers
# List tools from a specific server
python scripts/mcp_client.py tools playwright
# Call a tool
python 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:
# ❌ WRONG - These run in SEPARATE browser sessions!
python scripts/mcp_client.py call playwright browser_navigate '{"url": "https://example.com"}'
python scripts/mcp_client.py call playwright browser_click '{"element": "Accept cookies"}'
python scripts/mcp_client.py call playwright browser_snapshot '{}'
# ^ The snapshot captures a FRESH page, not the page after clicking!
✅ The Solution: browserruncode
Use browserruncode to run multiple Playwright steps in ONE browser session:
python scripts/mcp_client.py call playwright browser_run_code '{
"code": "
await page.goto(\"https://example.com\");
// Wait for and click cookie banner
const acceptBtn = page.getByRole(\"button\", { name: /accept/i });
if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await acceptBtn.click();
await page.waitForTimeout(1000);
}
// Wait for page to stabilize
await page.waitForLoadState(\"networkidle\");
// Return snapshot data for analysis
const snapshot = await page.accessibility.snapshot();
return JSON.stringify(snapshot, null, 2);
"
}'
When to Use Each Approach
| Scenario |
Tool |
Why |
| Simple page load + snapshot |
browser_navigate |
Returns snapshot automatically |
| Multi-step interaction |
browserruncode |
Keeps session alive |
| Click then observe result |
browserruncode |
Session persists |
| Fill form and submit |
browserruncode |
Session persists |
| Hover to reveal menu |
browserruncode |
Session persists |
Playwright Workflows for Test Discovery
1. Basic Page Exploration (Single Step)
browser_navigate returns both navigation result AND accessibility snapshot:
python 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)
python scripts/mcp_client.py call playwright browser_run_code '{
"code": "
await page.goto(\"https://www.olx.ro\");
// Handle cookie consent
try {
const cookieBtn = page.getByRole(\"button\", { name: \"Accept\" });
await cookieBtn.click({ timeout: 5000 });
await page.waitForTimeout(1000);
} catch (e) {
// No cookie banner
}
// Get accessibility snapshot
const snapshot = await page.accessibility.snapshot({ interestingOnly: false });
return JSON.stringify(snapshot, null, 2);
"
}'
3. Navigate to Subpage (Multi-Step)
python scripts/mcp_client.py call playwright browser_run_code '{
"code": "
await page.goto(\"https://www.olx.ro\");
// Dismiss cookies
const acceptBtn = page.getByRole(\"button\", { name: \"Accept\" });
if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await acceptBtn.click();
await page.waitForTimeout(500);
}
// Navigate to login
await page.goto(\"https://www.olx.ro/cont/\");
// Wait for redirect to login domain
await page.waitForURL(/login\\.olx\\.ro/, { timeout: 10000 });
// Get form structure
const snapshot = await page.accessibility.snapshot();
return JSON.stringify({ url: page.url(), snapshot }, null, 2);
"
}'
4. Explore Element Interactions (Multi-Step)
Use this to understand how menus/dropdowns behave:
python scripts/mcp_client.py call playwright browser_run_code '{
"code": "
await page.goto(\"https://www.olx.ro\");
// Dismiss cookies
const acceptBtn = page.getByRole(\"button\", { name: \"Accept\" });
if (await acceptBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await acceptBtn.click();
}
// Click on category to see what happens
const categoryLink = page.getByRole(\"link\", { name: /Auto, moto/i }).first();
await categoryLink.click();
// Wait to see result
await page.waitForTimeout(1500);
// Capture state after click
const snapshot = await page.accessibility.snapshot();
return JSON.stringify({
url: page.url(),
didNavigate: page.url().includes(\"auto\"),
snapshot: snapshot
}, null, 2);
"
}'
5. Fill Form and Capture State
python scripts/mcp_client.py call playwright browser_run_code '{
"code": "
await page.goto(\"https://login.olx.ro\");
// Fill login form
await page.locator(\"input[type=email]\").fill(\"[email protected]\");
await page.locator(\"input[type=password]\").fill(\"test123\");
// Click login button
await page.getByTestId(\"login-submit-button\").click();
// Wait for response
await page.waitForTimeout(3000);
// Capture any error messages
const errors = await page.locator(\"[class*=error], [role=alert]\").allTextContents();
const snapshot = await page.accessibility.snapshot();
return JSON.stringify({
url: page.url(),
errors: errors,
snapshot: snapshot
}, null, 2);
"
}'
Gathering Selectors for Page Objects
Best Practices
1. Use Accessibility Tree First
The snapshot from browsernavigate or browserrun_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:
// From snapshot: { role: "button", name: "Căutare" }
page.getByRole('button', { name: /Căutare/i })
// From snapshot: { role: "textbox", name: "Ce anume cauți?" }
page.getByRole('textbox', { name: /Ce anume cauți/i })
// From snapshot: { role: "link", name: "Auto, moto și ambarcațiuni" }
page.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
// Use .first() when multiple match
page.getByRole('link', { name: 'Category' }).first()
// Use parent context
page.locator('nav').getByRole('link', { name: 'Category' })
// Use filter
page.getByRole('button').filter({ hasText: /submit/i })
4. Get Full DOM for Complex Cases
When accessibility tree isn't enough, get raw HTML:
python scripts/mcp_client.py call playwright browser_run_code '{
"code": "
await page.goto(\"https://example.com\");
// Get specific element HTML
const formHtml = await page.locator(\"form\").first().innerHTML();
// Or get all buttons with their attributes
const buttons = await page.locator(\"button\").evaluateAll(btns =>
btns.map(b => ({
text: b.textContent,
testid: b.dataset.testid,
class: b.className,
type: b.type
}))
);
return JSON.stringify({ formHtml, buttons }, null, 2);
"
}'
Quick Reference: Playwright MCP Tools
| Tool |
Session Behavior |
Use Case |
browser_navigate |
New session, returns snapshot |
Simple page load |
browserruncode |
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
{"url": "https://example.com"}
browserruncode
{
"code": "await page.goto('https://example.com'); return await page.title();"
}
The code must be valid JavaScript that:
- Uses
page object (Playwright Page)
- Uses
await for async operations
- Returns the data you want (use
JSON.stringify for objects)
browser_click
{"element": "Submit button", "ref": "optional-element-ref"}
browser_type
{"element": "Email input", "text": "[email protected]"}
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:
``bash cp .claude/skills/mcp-client/references/mcp-config.example.json \ .claude/skills/mcp-client/references/mcp-config.json ``
- The config should contain:
``json { "mcpServers": { "playwright": { "command": "npx", "args": ["@playwright/mcp@latest"] } } } ``
- Install dependencies:
``bash pip install mcp fastmcp ``
Config Example
See [references/mcp-config.example.json](references/mcp-config.example.json)
Available Servers
See [references/mcp-servers.md](references/mcp-servers.md) for:
- Playwright (browser automation)
- GitHub (repository operations)
- Filesystem (file access)
- Sequential Thinking (reasoning)
- And more...
Dependencies
pip install mcp fastmcp