Skip to content

Commit a2cb85f

Browse files
youknowriadclaudelezama
authored
Add WordPress.com REST API support for remote sites in AI command (#2992)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Miguel Lezama <lezama@gmail.com>
1 parent f53a0b3 commit a2cb85f

9 files changed

Lines changed: 334 additions & 28 deletions

File tree

apps/cli/ai/agent.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import path from 'path';
22
import { query, type Query } from '@anthropic-ai/claude-agent-sdk';
33
import {
44
ALLOWED_TOOLS,
5+
ALLOWED_TOOLS_REMOTE,
56
STUDIO_ROOT,
67
createPathApprovalSession,
78
promptForApproval,
89
type AskUserQuestion,
910
} from 'cli/ai/security';
1011
import { buildSystemPrompt } from 'cli/ai/system-prompt';
11-
import { createStudioTools } from 'cli/ai/tools';
12+
import { createRemoteSiteTools, createStudioTools } from 'cli/ai/tools';
13+
import type { SiteInfo } from 'cli/ai/ui';
1214

1315
export type { AskUserQuestion } from 'cli/ai/security';
1416

@@ -18,6 +20,8 @@ export interface AiAgentConfig {
1820
model?: AiModelId;
1921
maxTurns?: number;
2022
resume?: string;
23+
activeSite?: SiteInfo | null;
24+
wpcomAccessToken?: string;
2125
onAskUser?: ( questions: AskUserQuestion[] ) => Promise< Record< string, string > >;
2226
}
2327

@@ -48,25 +52,55 @@ process.on( 'unhandledRejection', ( reason ) => {
4852
* Caller can iterate messages with `for await` and call `interrupt()` to stop.
4953
*/
5054
export function startAiAgent( config: AiAgentConfig ): Query {
51-
const { prompt, env, model = DEFAULT_MODEL, maxTurns = 50, resume, onAskUser } = config;
55+
const {
56+
prompt,
57+
env,
58+
model = DEFAULT_MODEL,
59+
maxTurns = 50,
60+
resume,
61+
activeSite,
62+
wpcomAccessToken,
63+
onAskUser,
64+
} = config;
5265
const resolvedEnv = env ?? { ...( process.env as Record< string, string > ) };
5366

67+
const isRemoteSite = activeSite?.remote && activeSite?.wpcomSiteId && wpcomAccessToken;
68+
69+
// Configure MCP servers based on site type:
70+
// Remote sites get WP.com REST API tools + screenshot; local sites get the full Studio toolset.
71+
const mcpServers = {
72+
studio: isRemoteSite
73+
? createRemoteSiteTools( wpcomAccessToken, activeSite.wpcomSiteId! )
74+
: createStudioTools(),
75+
};
76+
77+
const allowedTools = isRemoteSite ? [ ...ALLOWED_TOOLS_REMOTE ] : [ ...ALLOWED_TOOLS ];
78+
79+
// Build site-aware system prompt
80+
const systemPromptOptions = isRemoteSite
81+
? {
82+
remoteSite: {
83+
name: activeSite.name,
84+
url: activeSite.url ?? '',
85+
id: activeSite.wpcomSiteId!,
86+
},
87+
}
88+
: undefined;
89+
5490
return query( {
5591
prompt,
5692
options: {
5793
env: resolvedEnv,
5894
systemPrompt: {
5995
type: 'preset',
6096
preset: 'claude_code',
61-
append: buildSystemPrompt(),
62-
},
63-
mcpServers: {
64-
studio: createStudioTools(),
97+
append: buildSystemPrompt( systemPromptOptions ),
6598
},
99+
mcpServers,
66100
maxTurns,
67101
cwd: STUDIO_ROOT,
68102
tools: { type: 'preset', preset: 'claude_code' },
69-
allowedTools: [ ...ALLOWED_TOOLS ],
103+
allowedTools,
70104
permissionMode: 'default',
71105
canUseTool: async ( toolName, input, metadata ) => {
72106
if ( toolName === 'AskUserQuestion' && onAskUser ) {

apps/cli/ai/security.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ export const ALLOWED_TOOLS = [
3939
'AskUserQuestion',
4040
] as const;
4141

42+
// Tools allowed when operating on a remote WordPress.com site
43+
export const ALLOWED_TOOLS_REMOTE = [
44+
'mcp__studio__*',
45+
'Read',
46+
'Glob',
47+
'Grep',
48+
'WebFetch',
49+
'WebSearch',
50+
'TodoRead',
51+
'NotebookRead',
52+
'AskUserQuestion',
53+
] as const;
54+
4255
// Tools that should not manipulate files outside trusted roots without permission (write access)
4356
const PATH_GATED_TOOLS = [ 'Write', 'Edit', 'Bash', 'NotebookEdit' ] as const;
4457
const PATH_INPUT_KEYS = [ 'path', 'file_path', 'filePath' ] as const;

apps/cli/ai/sessions/recorder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export class AiSessionRecorder {
8787
path: string;
8888
remote?: boolean;
8989
url?: string;
90+
wpcomSiteId?: number;
9091
} ): Promise< void > {
9192
await this.appendEvent( {
9293
type: 'site.selected',
@@ -95,6 +96,7 @@ export class AiSessionRecorder {
9596
sitePath: site.path,
9697
remote: site.remote,
9798
url: site.url,
99+
wpcomSiteId: site.wpcomSiteId,
98100
} );
99101
}
100102

apps/cli/ai/sessions/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type AiSessionEvent =
2727
sitePath: string;
2828
remote?: boolean;
2929
url?: string;
30+
wpcomSiteId?: number;
3031
}
3132
| {
3233
type: 'user.message';

apps/cli/ai/system-prompt.ts

Lines changed: 121 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,101 @@
1-
export function buildSystemPrompt(): string {
1+
interface RemoteSiteContext {
2+
name: string;
3+
url: string;
4+
id: number;
5+
}
6+
7+
export function buildSystemPrompt( options?: { remoteSite?: RemoteSiteContext } ): string {
8+
if ( options?.remoteSite ) {
9+
return `${ buildRemoteIntro( options.remoteSite ) }
10+
11+
${ REMOTE_CONTENT_GUIDELINES }
12+
13+
${ REMOTE_DESIGN_GUIDELINES }
14+
`;
15+
}
16+
17+
return `${ buildLocalIntro() }
18+
19+
${ LOCAL_CONTENT_GUIDELINES }
20+
21+
${ LOCAL_DESIGN_GUIDELINES }
22+
`;
23+
}
24+
25+
function buildRemoteIntro( site: RemoteSiteContext ): string {
26+
return `You are WordPress Studio AI, the AI assistant built into WordPress Studio CLI. Your name is "WordPress Studio AI". You manage WordPress.com sites using the WordPress.com REST API.
27+
28+
IMPORTANT: The active site is a remote WordPress.com site: "${ site.name }" (ID: ${ site.id }) at ${ site.url }.
29+
IMPORTANT: You MUST use the wpcom_request tool (prefixed with mcp__studio__) to manage this site. Do NOT use WP-CLI, file Write/Edit, Bash, or any local file operations — this site is hosted on WordPress.com and cannot be modified through the local filesystem.
30+
IMPORTANT: Before doing ANY work, you MUST first check the site's plan by calling \`GET /\` (apiNamespace: \`""\`). The \`plan.product_slug\` field indicates the plan. If the site is on a free plan (e.g. \`free_plan\`), you MUST refuse design customization requests — this includes custom CSS, inline styles, style attributes on blocks, global styles editing, custom JavaScript, animations, custom colors/fonts/layouts, and plugin management. Do NOT attempt workarounds like inline styles or style block attributes — these produce invalid blocks on WordPress.com. Instead, tell the user that design customizations require upgrading to a paid WordPress.com plan and STOP. Do not proceed with the design task.
31+
32+
## Available Tools (prefixed with mcp__studio__)
33+
34+
- **wpcom_request**: A REST API client that supports both the WordPress REST API (wp/v2) and the WordPress.com REST API (v1.1).
35+
- \`method\`: GET, POST, PUT, or DELETE
36+
- \`path\`: Relative to \`/sites/{siteId}/\` (e.g., \`/posts\`, \`/posts/123\`, \`/templates\`). Prefix with \`!\` for absolute paths (e.g., \`!/me\`).
37+
- \`query\`: Optional query parameters object
38+
- \`body\`: Optional request body for POST/PUT
39+
- \`apiNamespace\`: Defaults to \`"wp/v2"\`. Set to \`""\` (empty string) for WP.com REST API v1.1, or \`"wpcom/v2"\` for WP.com v2 endpoints.
40+
- **take_screenshot**: Take a full-page screenshot of a URL (supports desktop and mobile viewports)
41+
42+
## API Namespace Guide
43+
44+
**Prefer wp/v2** (default — standard WordPress REST API) for most resources:
45+
- Posts, pages, media, categories, tags, users, comments
46+
- Templates, template parts, navigation, global styles, block patterns
47+
- Any standard WordPress resource
48+
49+
**Use WP.com v1.1** (set \`apiNamespace: ""\`) for WP.com-specific endpoints:
50+
- Plugin management: \`/plugins\`, \`/plugins/{slug}/install\`
51+
- Theme switching: \`/themes/mine\`
52+
- Site info: \`/\` (root)
53+
- Site settings: \`/settings\`
54+
55+
## Common wp/v2 Endpoints (default apiNamespace)
56+
57+
**Posts & Pages**: \`GET /posts\`, \`GET /posts/{id}\`, \`POST /posts\`, \`POST /posts/{id}\`, \`DELETE /posts/{id}\`
58+
**Media**: \`GET /media\`, \`POST /media\`
59+
**Templates**: \`GET /templates\`, \`GET /templates/{id}\`, \`POST /templates\`, \`POST /templates/{id}\`, \`DELETE /templates/{id}\`
60+
**Template Parts**: \`GET /template-parts\`, \`GET /template-parts/{id}\`, \`POST /template-parts\`, \`POST /template-parts/{id}\`
61+
**Navigation**: \`GET /navigation\`, \`POST /navigation\`, \`POST /navigation/{id}\`
62+
**Global Styles**: \`GET /global-styles/{id}\`, \`POST /global-styles/{id}\`. To find the global styles ID, first \`GET /themes?status=active\` — the active theme's \`_links["wp:user-global-styles"][0].href\` contains the ID.
63+
**Categories/Tags**: \`GET /categories\`, \`POST /categories\`, \`GET /tags\`, \`POST /tags\`
64+
**Block Types**: \`GET /block-types\`, \`GET /block-types/{name}\`
65+
**Search**: \`GET /search?search={query}\`
66+
67+
Use \`per_page\` and \`page\` for pagination. Use \`status\` to filter by publish status. For creating/updating content, pass block markup in the \`content\` field of the body.
68+
69+
## Common WP.com v1.1 Endpoints (set apiNamespace to "")
70+
71+
**Site**: \`GET /\` (site info), \`POST /settings\`
72+
**Plugins**: \`GET /plugins\`, \`POST /plugins/{slug}/install\`, \`POST /plugins/{slug}\` (body: \`{ active: true/false }\`)
73+
**Themes**: \`GET /themes\`, \`POST /themes/mine\` (body: \`{ theme: "slug" }\`)
74+
**Media upload from URL**: \`POST /media/new\` (body: \`{ media_urls: [...] }\`)
75+
76+
## Workflow
77+
78+
1. **Check the site plan** (MANDATORY FIRST STEP): Use \`GET /\` (apiNamespace: \`""\`) to get site info and check \`plan.product_slug\`. Stop and inform the user if they request features unavailable on their plan.
79+
2. **Understand the site**: Use \`GET /posts\` to list content, \`GET /themes?status=active\` to see the active theme.
80+
3. **Make changes**: Use POST requests to create/update content, manage templates, switch themes.
81+
4. **Verify visually**: Use take_screenshot to capture the site on desktop and mobile viewports. Check spacing, alignment, colors, contrast, and layout. Fix any issues.
82+
83+
## General rules
84+
85+
- Always confirm destructive operations (deleting posts, deactivating plugins, etc.) with the user before proceeding.
86+
- When creating content, follow WordPress best practices for block-based content.
87+
- If a requested operation fails, check the error message and suggest alternatives.
88+
- Explore the API — if you're unsure about an endpoint, try a GET request first to discover available data.`;
89+
}
90+
91+
function buildLocalIntro(): string {
292
return `You are WordPress Studio AI, the AI assistant built into WordPress Studio CLI. Your name is "WordPress Studio AI". You manage and modify local WordPress sites using your Studio tools and generate content for these sites.
393
494
IMPORTANT: You MUST use your mcp__studio__ tools to manage WordPress sites. Never create, start, or stop sites using Bash commands, shell scripts, or manual file operations. Never run \`wp\` commands via Bash — always use the wp_cli tool instead. The Studio tools handle all server management, database setup, and WordPress provisioning automatically.
595
IMPORTANT: For any generated content for the site, these three principles are mandatory:
696
797
- Gorgeous design: More details on the guidelines below.
8-
- No HTML blocks and raw HTML: Check the block content guidelines below.
98+
- No HTML blocks and raw HTML: Check the block content guidelines below.
999
- No invalid block: Use the validate_blocks everytime to ensure that the blocks are 100% valid.
10100
11101
## Workflow
@@ -52,23 +142,41 @@ Then continue with:
52142
- Always add the style.css as editor styles in the functions.php of the theme to make the editor match the frontend.
53143
- For theme and page content custom CSS, put the styles in the main style.css of the theme. No custom stylesheets.
54144
- Scroll animations must use progressive enhancement: CSS defines elements in their **final visible state** by default (full opacity, final position). JavaScript on the frontend adds the initial hidden state (e.g. \`opacity: 0\`, \`transform\`) and scroll-triggered transitions. This ensures elements are fully visible in the block editor (which loads theme CSS but not custom JS).
55-
- All animations and transitions must respect \`prefers-reduced-motion\`. Add a \`@media (prefers-reduced-motion: reduce)\` block that disables or simplifies animations (e.g. \`animation: none; transition: none; scroll-behavior: auto;\`).
145+
- All animations and transitions must respect \`prefers-reduced-motion\`. Add a \`@media (prefers-reduced-motion: reduce)\` block that disables or simplifies animations (e.g. \`animation: none; transition: none; scroll-behavior: auto;\`).`;
146+
}
147+
148+
const REMOTE_CONTENT_GUIDELINES = `## Block content guidelines
149+
150+
- Use only core WordPress blocks. No custom HTML blocks except for inline SVGs.
151+
- No decorative HTML comments (e.g. \`<!-- Hero Section -->\`). Only block delimiter comments are allowed.
152+
- No emojis anywhere in generated content.`;
153+
154+
const REMOTE_DESIGN_GUIDELINES = `## Design capabilities by plan
155+
156+
**Free plans** — content only, no design customization:
157+
- CAN: Create/edit posts, pages, templates, template parts. Switch themes. Upload media.
158+
- CANNOT: Any visual/design customization including custom CSS, inline styles, style attributes on blocks, global styles, custom JavaScript, animations, custom colors, custom fonts, custom layouts, or plugin management.
159+
- ACTION: If the user requests ANY design change — even "small" ones like changing a color or font — you MUST refuse, explain it requires a paid plan, and STOP. Do not suggest inline styles, style attributes, or any other workaround. These will produce invalid blocks.
56160
57-
## Block content guidelines
161+
**Paid plans** (Personal, Premium, Business, eCommerce) — progressively more control:
162+
- Custom CSS, global styles, plugin management, and advanced customization become available.
163+
- Check the specific plan to determine exact capabilities.`;
58164

59-
- Only use \`core/html\` blocks for:
60-
- Inline SVGs
61-
- \`<form>\` elements and interactive inputs
62-
- Animation/interaction markup with no block equivalent (marquee, cursor)
63-
- A single \`<script>\` block at the bottom of the page for JS
64-
- Never use \`core/html\` to wrap text content, headings, layout sections, or lists.
165+
const LOCAL_CONTENT_GUIDELINES = `## Block content guidelines
166+
167+
- Only use \`core/html\` blocks for:
168+
- Inline SVGs
169+
- \`<form>\` elements and interactive inputs
170+
- Animation/interaction markup with no block equivalent (marquee, cursor)
171+
- A single \`<script>\` block at the bottom of the page for JS
172+
- Never use \`core/html\` to wrap text content, headings, layout sections, or lists.
65173
- No decorative HTML comments (e.g. \`<!-- Hero Section -->\`, \`<!-- Features -->\`). Only block delimiter comments are allowed.
66174
- No custom class names on inner DOM elements — only on the outermost block wrapper via the \`className\` attribute.
67175
- No inline \`style\` or \`style\` block attributes for styling. Use \`className\` + \`style.css\` instead.
68176
- Use \`core/spacer\` for empty spacing divs, not \`core/group\`.
69-
- No emojis anywhere in generated content.
177+
- No emojis anywhere in generated content.`;
70178

71-
## Design guidelines
179+
const LOCAL_DESIGN_GUIDELINES = `## Design guidelines
72180
73181
**Important**: Always use sophisticated scroll effects and add animations unless specifically asked otherwise.
74182
@@ -99,7 +207,4 @@ Interpret creatively and make unexpected choices that feel genuinely designed fo
99207
100208
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
101209
102-
Remember: You are capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
103-
104-
`;
105-
}
210+
Remember: You are capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.`;

apps/cli/ai/tools.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { z } from 'zod/v4';
66
import { validateBlocks, type ValidationReport } from 'cli/ai/block-validator';
77
import { getSharedBrowser } from 'cli/ai/browser-utils';
88
import { auditPerformance } from 'cli/ai/performance-audit';
9+
import { createWpcomToolDefinitions } from 'cli/ai/wpcom-tools';
910
import { runCommand as runCreatePreviewCommand } from 'cli/commands/preview/create';
1011
import {
1112
Mode as PreviewDeleteMode,
@@ -637,10 +638,12 @@ const takeScreenshotTool = tool(
637638
// Reduce motion to avoid capturing mid-animation states
638639
await page.emulateMedia( { reducedMotion: 'reduce' } );
639640

640-
await page.goto( args.url, { waitUntil: 'networkidle', timeout: 15000 } );
641+
await page.goto( args.url, { waitUntil: 'domcontentloaded', timeout: 30000 } );
642+
await page.waitForLoadState( 'networkidle', { timeout: 10000 } ).catch( () => {} );
641643

642644
// Scroll through the page to trigger lazy-loaded images, then wait
643-
// for all images to finish loading.
645+
// for all images to finish loading (with a timeout so we don't hang
646+
// on images that never settle).
644647
await page.evaluate( async () => {
645648
const delay = ( ms: number ) =>
646649
new Promise< void >( ( resolve ) => setTimeout( resolve, ms ) );
@@ -652,7 +655,8 @@ const takeScreenshotTool = tool(
652655
}
653656
window.scrollTo( 0, 0 );
654657

655-
await Promise.all(
658+
const timeout = new Promise< void >( ( resolve ) => setTimeout( resolve, 5000 ) );
659+
const allImages = Promise.all(
656660
Array.from( document.images )
657661
.filter( ( img ) => ! img.complete )
658662
.map(
@@ -663,6 +667,7 @@ const takeScreenshotTool = tool(
663667
} )
664668
)
665669
);
670+
await Promise.race( [ allImages, timeout ] );
666671
} );
667672

668673
// Hide WordPress admin bar and scrollbars for cleaner screenshots
@@ -797,3 +802,16 @@ export function createStudioTools() {
797802
tools: studioToolDefinitions,
798803
} );
799804
}
805+
806+
/**
807+
* Creates an MCP server for remote WordPress.com sites, combining WP.com REST API tools
808+
* with URL-based tools (screenshot) that work with any site.
809+
*/
810+
export function createRemoteSiteTools( token: string, siteId: number ) {
811+
const wpcomTools = createWpcomToolDefinitions( token, siteId );
812+
return createSdkMcpServer( {
813+
name: 'studio',
814+
version: '1.0.0',
815+
tools: [ ...wpcomTools, takeScreenshotTool ],
816+
} );
817+
}

apps/cli/ai/ui.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface SiteInfo {
5656
running: boolean;
5757
remote?: boolean;
5858
url?: string;
59+
wpcomSiteId?: number;
5960
}
6061

6162
const DEFAULT_COLLAPSE_THRESHOLD_LINES = 5;
@@ -828,6 +829,7 @@ export class AiChatUI {
828829
running: false,
829830
remote: true,
830831
url: site.url,
832+
wpcomSiteId: site.id,
831833
} ) );
832834
this.sitePickerRemoteLoading = false;
833835
this.rebuildSitePickerList();

0 commit comments

Comments
 (0)