Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ function paramsToZodSchema(params: ToolParam[]): Record<string, z.ZodType> {
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}"`
Expand Down
37 changes: 19 additions & 18 deletions packages/playground/mcp/src/tools/tool-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,19 +90,17 @@ export const toolDefinitions: Record<string, ToolDefinition> = {
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
Expand Down Expand Up @@ -147,8 +145,8 @@ export const toolDefinitions: Record<string, ToolDefinition> = {
},
{
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,
},
],
Expand Down Expand Up @@ -531,10 +529,13 @@ export function paramsToJsonSchema(
const required: string[] = [];

for (const param of params) {
const prop: Record<string, unknown> = {
type: param.type,
description: param.description,
};
const prop: Record<string, unknown> = {};
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;
}
Expand Down
6 changes: 5 additions & 1 deletion packages/playground/mcp/src/tools/tool-executors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ export const toolExecutors: Record<
const headers = {
...((input['headers'] as Record<string, string>) ?? {}),
};
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');
Expand Down
33 changes: 33 additions & 0 deletions packages/playground/mcp/tests/e2e/mcp-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading