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 { z } from "zod" // This MCP server wraps the existing external WP testing implementation used by the chat app. // It is intentionally kept small and env-configurable so host apps can load it only when needed. const ToolInput = { plugin_path: z.string().min(1), plugin_slug: z.string().optional(), session_id: z.string().optional(), test_mode: z.enum(["cli", "visual", "both"]).optional().default("cli"), required_plugins: z .array( z.object({ plugin_slug: z.string().min(1), source: z.enum(["wordpress.org", "url", "local"]).optional(), source_url: z.string().optional(), activate: z.boolean().optional(), }), ) .optional(), test_scenarios: z .array( z.object({ name: z.string().optional(), type: z.string().optional(), url: z.string().optional(), selector: z.string().optional(), expected_text: z.string().optional(), wp_cli_command: z.string().optional(), shortcode: z.string().optional(), hook: z.string().optional(), ajax_action: z.string().optional(), method: z.string().optional(), body: z.string().optional(), assertions: z .object({ status_code: z.number().optional(), contains: z.array(z.string()).optional(), not_contains: z.array(z.string()).optional(), wp_cli_success: z.boolean().optional(), }) .optional(), }), ) .optional(), config_overrides: z.record(z.any()).optional(), } function toJsonSchema(schema) { // zod v4 exposes toJSONSchema under z.* in OpenCode, but in plain node usage // we keep schema definitions explicit below for MCP. // This helper exists only for future extension. void schema return { type: "object", additionalProperties: false, } } const TestToolInputJsonSchema = { type: "object", additionalProperties: false, required: ["plugin_path"], properties: { plugin_path: { type: "string", description: "Local filesystem path to plugin root" }, plugin_slug: { type: "string", description: "Plugin slug (defaults to folder name)" }, session_id: { type: "string", description: "Optional session id for isolation" }, test_mode: { type: "string", enum: ["cli", "visual", "both"], description: "Testing mode (only 'cli' is implemented in this server)", }, required_plugins: { type: "array", description: "Plugins to install/activate before running scenarios", items: { type: "object", additionalProperties: false, required: ["plugin_slug"], properties: { plugin_slug: { type: "string" }, source: { type: "string", enum: ["wordpress.org", "url", "local"] }, source_url: { type: "string" }, activate: { type: "boolean" }, }, }, }, test_scenarios: { type: "array", description: "Scenario list; if omitted, defaults are generated", items: { type: "object", additionalProperties: true, }, }, config_overrides: { type: "object", description: "Overrides for external WP testing configuration (see docs/env vars)", additionalProperties: true, }, }, } const server = new Server( { name: "wp-cli-testing", version: "1.0.0", }, { capabilities: { tools: {}, }, }, ) server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "test_plugin_external_wp", description: "Run WP-CLI based verification against an externally hosted WordPress test site (over SSH).", inputSchema: TestToolInputJsonSchema, }, { name: "external_wp_testing_config", description: "Return the resolved external WP testing config (from env vars) and highlight missing/invalid settings.", inputSchema: { type: "object", additionalProperties: false, properties: { config_overrides: { type: "object", additionalProperties: true, description: "Optional config overrides merged on top of env defaults", }, }, }, }, ], } }) server.setRequestHandler(CallToolRequestSchema, async (req) => { const toolName = req.params.name const args = (req.params.arguments ?? {}) // Lazy-load to avoid paying cost unless tool is actually invoked. // Note: external-wp-testing.js is CommonJS; import default gives module.exports. const externalTesting = await import("../../../chat/external-wp-testing.js") const mod = externalTesting.default ?? externalTesting const createExternalWpTester = mod?.createExternalWpTester const getExternalTestingConfig = mod?.getExternalTestingConfig if (typeof createExternalWpTester !== "function" || typeof getExternalTestingConfig !== "function") { return { content: [ { type: "text", text: "wp-cli-testing MCP server misconfigured: could not load chat/external-wp-testing.js exports.", }, ], isError: true, } } if (toolName === "external_wp_testing_config") { const overrides = args && typeof args === "object" && (args.config_overrides && typeof args.config_overrides === "object") ? args.config_overrides : {} const config = getExternalTestingConfig(overrides) const missing = [] if (!config.wpHost) missing.push("TEST_WP_HOST (or EXTERNAL_WP_HOST)") if (!config.wpSshUser) missing.push("TEST_WP_SSH_USER (or EXTERNAL_WP_SSH_USER)") if (!config.wpSshKey) missing.push("TEST_WP_SSH_KEY (or EXTERNAL_WP_SSH_KEY)") const safeConfig = { ...config, // avoid leaking filesystem layout too much; keep key path but don’t include any private key contents. wpSshKey: config.wpSshKey, } return { content: [ { type: "text", text: JSON.stringify( { ok: missing.length === 0, missing, config: safeConfig, }, null, 2, ), }, ], } } if (toolName === "test_plugin_external_wp") { const parsed = z .object(ToolInput) .safeParse(args && typeof args === "object" ? args : {}) if (!parsed.success) { return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: parsed.error.message }, null, 2) }], isError: true, } } if (parsed.data.test_mode !== "cli") { return { content: [ { type: "text", text: JSON.stringify( { ok: false, error: "Only test_mode='cli' is implemented by this MCP server. Use cli mode or extend the server.", }, null, 2, ), }, ], isError: true, } } const tester = createExternalWpTester({}) const result = await tester.runTest( { plugin_path: parsed.data.plugin_path, plugin_slug: parsed.data.plugin_slug, session_id: parsed.data.session_id, required_plugins: parsed.data.required_plugins ?? [], test_scenarios: parsed.data.test_scenarios ?? [], test_mode: "cli", }, { configOverrides: parsed.data.config_overrides && typeof parsed.data.config_overrides === "object" ? parsed.data.config_overrides : {}, }, ) return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], } } return { content: [{ type: "text", text: `Unknown tool: ${toolName}` }], isError: true, } }) const transport = new StdioServerTransport() await server.connect(transport)