Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 16 additions & 22 deletions packages/playground/mcp/src/bridge-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ import { createToolClient } from './tools/tool-executors';
import type { ToolClient } from './tools/tool-executors';

/**
* Shared configuration for the MCP bridge client and WebMCP.
*
* Both transports need the same callbacks to interact with
* the Playground site list and active client.
* Configuration accepted by `startMcpBridge`. Only includes the
* methods the bridge actually calls — callers can pass any wider
* object (e.g. the full site-management API) and TypeScript will
* accept it structurally.
*/
export interface PlaygroundConfig {
getSites: () => Array<{
export interface PlaygroundBridgeConfig {
list(): Array<{
slug: string;
name: string;
storage: string;
isActive: boolean;
}>;
getPlaygroundClient: (siteSlug: string) => PlaygroundClient | undefined;
renameSite?: (siteSlug: string, newName: string) => Promise<void>;
saveSite?: (siteSlug: string) => Promise<{ slug: string; storage: string }>;
getClient(): PlaygroundClient | undefined;
rename(newName: string): Promise<void>;
saveInBrowser(): Promise<{ slug: string; storage: string }>;
onConnect?: () => void;
}

Expand All @@ -29,7 +29,7 @@ export interface McpBridgeHandle {
const RECONNECT_INTERVAL_MS = 5000;

export function startMcpBridge(
config: PlaygroundConfig,
config: PlaygroundBridgeConfig,
port: number
): McpBridgeHandle {
const tabId = crypto.randomUUID();
Expand All @@ -39,7 +39,7 @@ export function startMcpBridge(
let stopped = false;

function sendSitesRegistration(socket: WebSocket) {
const sites = config.getSites();
const sites = config.list();
const serialized = JSON.stringify(sites);
if (serialized === previousSitesSerialized) {
return;
Expand Down Expand Up @@ -149,13 +149,13 @@ export function startMcpBridge(
}

async function handleCommand(
config: PlaygroundConfig,
config: PlaygroundBridgeConfig,
method: string,
args: unknown[],
siteSlug: string,
port: number
): Promise<unknown> {
if (method === '__open_site') {
if (method === '__open_site_in_new_tab') {
const url = new URL(window.location.href);
url.searchParams.set('mcp', 'yes');
url.searchParams.set('mcp-port', String(port));
Expand All @@ -171,22 +171,16 @@ async function handleCommand(
}

if (method === '__rename_site') {
if (!config.renameSite) {
throw new Error('renameSite not configured');
}
const [newName] = args as [string];
await config.renameSite(siteSlug, newName);
await config.rename(newName);
return true;
}

if (method === '__save_site') {
if (!config.saveSite) {
throw new Error('saveSite not configured');
}
return await config.saveSite(siteSlug);
return await config.saveInBrowser();
}

const playgroundClient = config.getPlaygroundClient(siteSlug);
const playgroundClient = config.getClient();
if (!playgroundClient) {
throw new Error(`No active client for site: ${siteSlug}`);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/playground/mcp/src/bridge-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createServer as createHttpServer } from 'node:http';
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { AddressInfo } from 'node:net';
import { randomUUID } from 'node:crypto';
import { presentStorage } from './tools/tool-definitions';
import { formatStorageLabel } from './tools/tool-definitions';

export interface SiteRegistration {
slug: string;
Expand Down Expand Up @@ -344,7 +344,7 @@ export class PlaygroundBridge {
let targetTabId: string;

if (isBrowserCommand) {
// Browser-level commands (e.g. __open_site, __rename_site)
// Browser-level commands (e.g. __open_site_in_new_tab, __rename_site)
// don't require the site to be active — just any connected
// tab, preferring one that reported this site.
if (this.connections.size === 0) {
Expand All @@ -362,7 +362,7 @@ export class PlaygroundBridge {
return Promise.reject(
new Error(
`Site "${site.siteName}" (${siteId}) is not ` +
`active in any tab. Use open_site to ` +
`active in any tab. Use open_site_in_new_tab to ` +
`activate it.`
)
);
Expand Down Expand Up @@ -448,7 +448,7 @@ export class PlaygroundBridge {
return [...this.sites.entries()].map(([siteId, site]) => ({
siteId,
name: site.siteName,
storage: presentStorage(site.storage),
storage: formatStorageLabel(site.storage),
isActive: site.activeInTabs.length > 0,
}));
}
Expand Down
3 changes: 1 addition & 2 deletions packages/playground/mcp/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { startMcpBridge } from './bridge-client';
export type { PlaygroundConfig } from './bridge-client';
export type { McpBridgeHandle } from './bridge-client';
export type { PlaygroundBridgeConfig, McpBridgeHandle } from './bridge-client';
export { registerWebMCPTools } from './webmcp';
4 changes: 2 additions & 2 deletions packages/playground/mcp/src/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ export function createServer(): McpServer {
or server required. You are automatically authenticated as an admin user.\n\n\
PREREQUISITE: Call playground_list_sites first. If no browser is connected, \
call playground_get_website_url to get the exact URL and ask the user to open it. \n\n\
Typical workflow: playground_list_sites → playground_save_site \
Typical workflow: playground_list_sites → playground_save_in_browser \
→ filesystem/PHP operations → playground_navigate to verify results.\n\n\
Capabilities: execute arbitrary PHP with full WordPress access, read/write files in the virtual filesystem \
(WordPress root: /wordpress/), make HTTP requests to the site, navigate the browser, \
and manage multiple Playground sites simultaneously.\n\n\
Important: sites are temporary by default and not persisted between sessions. \
Call playground_save_site early in any multi-step workflow where losing progress would be costly.\n\n\
Call playground_save_in_browser early in any multi-step workflow where losing progress would be costly.\n\n\
Error handling: tool failures are returned as thrown exceptions with descriptive messages, \
not as silent failures.`,
});
Expand Down
23 changes: 12 additions & 11 deletions packages/playground/mcp/src/tools/register-mcp-server-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
toolDefinitions,
getSiteToolDefinitions,
playgroundUrl,
presentStorage,
formatStorageLabel,
stringifyError,
} from './tool-definitions';
import type { ToolParam } from './tool-definitions';
Expand Down Expand Up @@ -138,20 +138,21 @@ export function registerMcpServerTools(
}
);

const openSite = siteToolDefinitions['playground_open_site'];
const openSiteInNewTab =
siteToolDefinitions['playground_open_site_in_new_tab'];
server.registerTool(
'playground_open_site',
'playground_open_site_in_new_tab',
{
title: openSite.title,
description: openSite.description,
title: openSiteInNewTab.title,
description: openSiteInNewTab.description,
inputSchema: {
siteId: siteIdSchema,
},
annotations: openSite.annotations,
annotations: openSiteInNewTab.annotations,
},
async ({ siteId }) => {
try {
await bridge.sendCommand(siteId, '__open_site');
await bridge.sendCommand(siteId, '__open_site_in_new_tab');
const site = await bridge.waitForSiteActive(siteId, 30000);
return {
content: [
Expand All @@ -166,7 +167,7 @@ export function registerMcpServerTools(
],
};
} catch (error) {
return errorResult(openSite.errorPrefix, error);
return errorResult(openSiteInNewTab.errorPrefix, error);
}
}
);
Expand Down Expand Up @@ -220,9 +221,9 @@ export function registerMcpServerTools(
})
);

const saveSite = siteToolDefinitions['playground_save_site'];
const saveSite = siteToolDefinitions['playground_save_in_browser'];
server.registerTool(
'playground_save_site',
'playground_save_in_browser',
{
title: saveSite.title,
description: saveSite.description,
Expand Down Expand Up @@ -270,7 +271,7 @@ export function registerMcpServerTools(
alreadySaved: false,
siteId,
name: site.name,
storage: presentStorage(result.storage),
storage: formatStorageLabel(result.storage),
}),
},
],
Expand Down
14 changes: 7 additions & 7 deletions packages/playground/mcp/src/tools/tool-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,17 +418,17 @@ export function getSiteToolDefinitions(): Record<string, ToolDefinition> {

Returns site names and storage type. "temporary"
sites are lost on page reload, "opfs" sites persist
across reloads. Call playground_save_site to persist
across reloads. Call playground_save_in_browser to persist
a temporary site.`,
annotations: {
readOnlyHint: true,
destructiveHint: false,
},
params: [],
},
playground_open_site: {
title: 'Open Site in Browser',
errorPrefix: 'Error opening site',
playground_open_site_in_new_tab: {
title: 'Open Site in New Tab',
errorPrefix: 'Error opening site in new tab',
description: `Open a WordPress Playground site in a new
browser tab. The site must appear in
playground_list_sites.
Expand Down Expand Up @@ -461,8 +461,8 @@ export function getSiteToolDefinitions(): Record<string, ToolDefinition> {
},
],
},
playground_save_site: {
title: 'Save Site',
playground_save_in_browser: {
title: 'Save in Browser',
errorPrefix: 'Error saving site',
description: `Save a temporary WordPress Playground site
to browser storage so it survives page reloads.
Expand Down Expand Up @@ -516,7 +516,7 @@ export function stringifyError(error: unknown): string {
/**
* Translate internal Playground storage types to user-facing names.
*/
export function presentStorage(raw: string): string {
export function formatStorageLabel(raw: string): string {
return raw === 'none' ? 'temporary' : raw;
}

Expand Down
Loading
Loading