Complete Claude Code plugin with: - 9 skills (/pw:init, generate, review, fix, migrate, coverage, testrail, browserstack, report) - 3 specialized agents (test-architect, test-debugger, migration-planner) - 55 test case templates across 11 categories (auth, CRUD, checkout, search, forms, dashboard, settings, onboarding, notifications, API, accessibility) - TestRail MCP server (TypeScript) — 8 tools for bidirectional sync - BrowserStack MCP server (TypeScript) — 7 tools for cross-browser testing - Smart hooks (auto-validate tests, auto-detect Playwright projects) - 6 curated reference docs (golden rules, locators, assertions, fixtures, pitfalls, flaky tests) - Leverages Claude Code built-ins (/batch, /debug, Explore subagent) - Zero-config for core features; TestRail/BrowserStack via env vars - Both TypeScript and JavaScript support throughout Co-authored-by: Leo <leo@openclaw.ai>
184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
#!/usr/bin/env npx tsx
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import {
|
|
CallToolRequestSchema,
|
|
ListToolsRequestSchema,
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
import { BrowserStackClient } from './client.js';
|
|
import type { BrowserStackSessionUpdate } from './types.js';
|
|
|
|
const config = {
|
|
username: process.env.BROWSERSTACK_USERNAME ?? '',
|
|
accessKey: process.env.BROWSERSTACK_ACCESS_KEY ?? '',
|
|
};
|
|
|
|
if (!config.username || !config.accessKey) {
|
|
console.error(
|
|
'Missing BrowserStack configuration. Set BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const client = new BrowserStackClient(config);
|
|
|
|
const server = new Server(
|
|
{ name: 'pw-browserstack', version: '1.0.0' },
|
|
{ capabilities: { tools: {} } },
|
|
);
|
|
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
tools: [
|
|
{
|
|
name: 'browserstack_get_plan',
|
|
description: 'Get BrowserStack Automate plan details including parallel session limits',
|
|
inputSchema: { type: 'object', properties: {} },
|
|
},
|
|
{
|
|
name: 'browserstack_get_browsers',
|
|
description: 'List all available browser and OS combinations for Playwright testing',
|
|
inputSchema: { type: 'object', properties: {} },
|
|
},
|
|
{
|
|
name: 'browserstack_get_builds',
|
|
description: 'List recent test builds with status',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
limit: { type: 'number', description: 'Max builds to return (default 10)' },
|
|
status: {
|
|
type: 'string',
|
|
enum: ['running', 'done', 'failed', 'timeout'],
|
|
description: 'Filter by status',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'browserstack_get_sessions',
|
|
description: 'List test sessions within a build',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
build_id: { type: 'string', description: 'Build hashed ID' },
|
|
limit: { type: 'number', description: 'Max sessions to return' },
|
|
},
|
|
required: ['build_id'],
|
|
},
|
|
},
|
|
{
|
|
name: 'browserstack_get_session',
|
|
description: 'Get detailed session info including video URL, logs, and screenshots',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
session_id: { type: 'string', description: 'Session hashed ID' },
|
|
},
|
|
required: ['session_id'],
|
|
},
|
|
},
|
|
{
|
|
name: 'browserstack_update_session',
|
|
description: 'Update session status (mark as passed/failed) and name',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
session_id: { type: 'string', description: 'Session hashed ID' },
|
|
status: {
|
|
type: 'string',
|
|
enum: ['passed', 'failed'],
|
|
description: 'Test result status',
|
|
},
|
|
name: { type: 'string', description: 'Updated session name' },
|
|
reason: { type: 'string', description: 'Reason for failure' },
|
|
},
|
|
required: ['session_id'],
|
|
},
|
|
},
|
|
{
|
|
name: 'browserstack_get_logs',
|
|
description: 'Get text logs for a specific test session',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
session_id: { type: 'string', description: 'Session hashed ID' },
|
|
},
|
|
required: ['session_id'],
|
|
},
|
|
},
|
|
],
|
|
}));
|
|
|
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
const { name, arguments: args } = request.params;
|
|
|
|
try {
|
|
switch (name) {
|
|
case 'browserstack_get_plan': {
|
|
const plan = await client.getPlan();
|
|
return { content: [{ type: 'text', text: JSON.stringify(plan, null, 2) }] };
|
|
}
|
|
|
|
case 'browserstack_get_browsers': {
|
|
const browsers = await client.getBrowsers();
|
|
const playwrightBrowsers = browsers.filter(
|
|
(b) =>
|
|
['chrome', 'firefox', 'playwright-chromium', 'playwright-firefox', 'playwright-webkit'].includes(
|
|
b.browser?.toLowerCase() ?? '',
|
|
) || b.browser?.toLowerCase().includes('playwright'),
|
|
);
|
|
const summary = playwrightBrowsers.length > 0 ? playwrightBrowsers : browsers.slice(0, 50);
|
|
return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] };
|
|
}
|
|
|
|
case 'browserstack_get_builds': {
|
|
const builds = await client.getBuilds(
|
|
(args?.limit as number) ?? 10,
|
|
args?.status as string | undefined,
|
|
);
|
|
return { content: [{ type: 'text', text: JSON.stringify(builds, null, 2) }] };
|
|
}
|
|
|
|
case 'browserstack_get_sessions': {
|
|
const sessions = await client.getSessions(
|
|
args!.build_id as string,
|
|
args?.limit as number | undefined,
|
|
);
|
|
return { content: [{ type: 'text', text: JSON.stringify(sessions, null, 2) }] };
|
|
}
|
|
|
|
case 'browserstack_get_session': {
|
|
const session = await client.getSession(args!.session_id as string);
|
|
return { content: [{ type: 'text', text: JSON.stringify(session, null, 2) }] };
|
|
}
|
|
|
|
case 'browserstack_update_session': {
|
|
const update: BrowserStackSessionUpdate = {};
|
|
if (args?.status) update.status = args.status as 'passed' | 'failed';
|
|
if (args?.name) update.name = args.name as string;
|
|
if (args?.reason) update.reason = args.reason as string;
|
|
const updated = await client.updateSession(args!.session_id as string, update);
|
|
return { content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }] };
|
|
}
|
|
|
|
case 'browserstack_get_logs': {
|
|
const logs = await client.getSessionLogs(args!.session_id as string);
|
|
return { content: [{ type: 'text', text: logs }] };
|
|
}
|
|
|
|
default:
|
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
}
|
|
});
|
|
|
|
async function main() {
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
}
|
|
|
|
main().catch(console.error);
|