/** * Bridge Microsoft Playwright MCP tools into pi.dev as custom tools. * * This extension spawns `@playwright/mcp` as a local stdio MCP server, * lists the tools it exposes, and registers each one with pi so the agent * can call them like any other tool. */ import { createRequire } from "node:module"; import path from "node:path"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type, type TSchema } from "typebox"; const require = createRequire(import.meta.url); function jsonSchemaToTypeBox(schema: unknown): TSchema { if (!schema || typeof schema !== "object") { return Type.Any(); } const s = schema as Record; switch (s.type) { case "object": { const properties: Record = {}; const required = new Set(s.required ?? []); for (const [key, value] of Object.entries(s.properties ?? {})) { const child = jsonSchemaToTypeBox(value); properties[key] = required.has(key) ? child : Type.Optional(child); } return Type.Object(properties); } case "array": return Type.Array(jsonSchemaToTypeBox(s.items)); case "string": return Type.String({ description: s.description }); case "number": case "integer": return Type.Number({ description: s.description }); case "boolean": return Type.Boolean({ description: s.description }); case "null": return Type.Null(); default: break; } if (Array.isArray(s.enum)) { return Type.Union(s.enum.map((value: unknown) => Type.Literal(value))); } return Type.Any({ description: s.description }); } export default async function (pi: ExtensionAPI) { let serverPath: string; try { const pkgPath = require.resolve("@playwright/mcp/package.json"); serverPath = path.join(path.dirname(pkgPath), "cli.js"); } catch { console.warn( "[playwright-mcp] @playwright/mcp is not installed. " + "Run `npm install` in the Dino package directory." ); return; } const transport = new StdioClientTransport({ command: process.execPath, args: [serverPath], stderr: "inherit", }); const client = new Client({ name: "dino-playwright-mcp", version: "0.1.0", }); try { await client.connect(transport); } catch (err) { console.warn("[playwright-mcp] Could not connect to Playwright MCP server:", err); return; } let tools; try { const result = await client.listTools(); tools = result.tools; } catch (err) { console.warn("[playwright-mcp] Could not list Playwright MCP tools:", err); return; } for (const tool of tools) { pi.registerTool({ name: tool.name, label: tool.name, description: tool.description ?? ``, promptSnippet: tool.name.startsWith("browser_") ? `Browser automation via Playwright MCP: ${tool.description ?? tool.name}` : `Playwright MCP tool: ${tool.description ?? tool.name}`, parameters: jsonSchemaToTypeBox(tool.inputSchema), async execute(_toolCallId, params) { const result = await client.callTool({ name: tool.name, arguments: (params ?? {}) as Record, }); const content = result.content.map((item) => { if (item.type === "text") { return { type: "text" as const, text: item.text }; } if (item.type === "image") { return { type: "image" as const, data: item.data, mimeType: item.mimeType, }; } return { type: "text" as const, text: JSON.stringify(item) }; }); return { content, details: result }; }, }); } console.log(`[playwright-mcp] Registered ${tools.length} Playwright MCP tool(s).`); const cleanup = async () => { try { await transport.close(); await client.close(); } catch { // ignore cleanup errors } }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); pi.on("session_shutdown", cleanup); }