diff --git a/packages/playground/mcp/src/tools/register-mcp-server-tools.ts b/packages/playground/mcp/src/tools/register-mcp-server-tools.ts index 8160b5835f3..ea94d5337b2 100644 --- a/packages/playground/mcp/src/tools/register-mcp-server-tools.ts +++ b/packages/playground/mcp/src/tools/register-mcp-server-tools.ts @@ -55,6 +55,9 @@ function paramsToZodSchema(params: ToolParam[]): Record { case 'object': zodType = z.record(z.string(), z.string()); break; + case 'string_or_object': + zodType = z.union([z.string(), z.record(z.string(), z.unknown())]); + break; default: throw new Error( `Unknown param type "${param.type}" for "${param.name}"` diff --git a/packages/playground/mcp/src/tools/tool-definitions.ts b/packages/playground/mcp/src/tools/tool-definitions.ts index 5c0629096e0..dd26405db6a 100644 --- a/packages/playground/mcp/src/tools/tool-definitions.ts +++ b/packages/playground/mcp/src/tools/tool-definitions.ts @@ -13,7 +13,7 @@ export interface ToolAnnotations { openWorldHint?: boolean; } -export type ToolParamType = 'string' | 'boolean' | 'object'; +export type ToolParamType = 'string' | 'boolean' | 'object' | 'string_or_object'; export interface ToolParam { name: string; @@ -90,19 +90,17 @@ export const toolDefinitions: Record = { auth needed. Tool selection guide: - 1. Use this tool (playground_request) with the - REST API for standard content CRUD — posts, - pages, users, terms, comments, settings, etc. - The REST API handles serialization, pagination, - and field filtering for you. + 1. Use this tool with the REST API for content + CRUD (posts, pages, users, settings, etc.) + and for the Abilities API + (/wp-json/wp-abilities/v1/abilities) which + exposes structured actions from plugins and + WordPress core. 2. Use playground_execute_php when the data you - need is not exposed by the REST API (e.g. - raw options, direct database queries, or - custom table access). + need is not exposed by the REST API. 3. Use this tool as a plain HTTP request (non-REST) - when the HTTP layer itself matters: verifying - redirects, status codes, cookies, or response - headers. + when the HTTP layer itself matters (redirects, + status codes, cookies, headers). The response JSON contains three fields: - "text": the response body as a string @@ -147,8 +145,8 @@ export const toolDefinitions: Record = { }, { name: 'body', - type: 'string', - description: 'Request body (for POST/PUT requests)', + type: 'string_or_object', + description: 'Request body (for POST/PUT requests). Accepts a JSON string or an object.', required: false, }, ], @@ -531,10 +529,13 @@ export function paramsToJsonSchema( const required: string[] = []; for (const param of params) { - const prop: Record = { - type: param.type, - description: param.description, - }; + const prop: Record = {}; + if (param.type === 'string_or_object') { + prop['oneOf'] = [{ type: 'string' }, { type: 'object' }]; + } else { + prop['type'] = param.type; + } + prop['description'] = param.description; if (param.additionalProperties !== undefined) { prop['additionalProperties'] = param.additionalProperties; } diff --git a/packages/playground/mcp/src/tools/tool-executors.ts b/packages/playground/mcp/src/tools/tool-executors.ts index 0b3a9b6a4bf..cbfa8800990 100644 --- a/packages/playground/mcp/src/tools/tool-executors.ts +++ b/packages/playground/mcp/src/tools/tool-executors.ts @@ -97,7 +97,11 @@ export const toolExecutors: Record< const headers = { ...((input['headers'] as Record) ?? {}), }; - const body = input['body'] as string | undefined; + const rawBody = input['body']; + const body = + typeof rawBody === 'object' && rawBody !== null + ? JSON.stringify(rawBody) + : (rawBody as string | undefined); try { const parsedUrl = new URL(url, 'http://localhost'); diff --git a/packages/playground/mcp/tests/e2e/mcp-tools.spec.ts b/packages/playground/mcp/tests/e2e/mcp-tools.spec.ts index 3ea8c126918..57eb971b395 100644 --- a/packages/playground/mcp/tests/e2e/mcp-tools.spec.ts +++ b/packages/playground/mcp/tests/e2e/mcp-tools.spec.ts @@ -762,6 +762,39 @@ test.describe.serial('playground_request REST API auth', () => { expect(JSON.parse(gone.text).code).toBe('rest_post_invalid_id'); }); + test('accepts object body without JSON.stringify', async ({ + mcpClient, + siteId, + }) => { + const createResult = await mcpClient.callTool({ + name: 'playground_request', + arguments: { + siteId, + url: '/wp-json/wp/v2/posts', + method: 'POST', + body: { + title: 'Object Body Test', + content: 'Created with object body', + status: 'publish', + }, + }, + }); + const created = JSON.parse(resultText(createResult)); + expect(created.httpStatusCode).toBe(201); + const post = JSON.parse(created.text); + expect(post.title.raw).toBe('Object Body Test'); + + // Clean up + await mcpClient.callTool({ + name: 'playground_request', + arguments: { + siteId, + url: `/wp-json/wp/v2/posts/${post.id}?force=true`, + method: 'DELETE', + }, + }); + }); + test('logged-out user cannot create posts', async ({ mcpClient, siteId,