Skip to content
Closed
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
9 changes: 7 additions & 2 deletions apps/cli/ai/mcp-server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createStudioTools } from 'cli/ai/tools';
import { type McpTelemetryGroup } from 'cli/lib/types/bump-stats';

export async function startMcpStdioServer(): Promise< void > {
const studioMcp = createStudioTools();
type McpServerOptions = {
telemetryGroup?: McpTelemetryGroup;
};

export async function startMcpStdioServer( options: McpServerOptions = {} ): Promise< void > {
const studioMcp = createStudioTools( options );
const transport = new StdioServerTransport();

const shutdown = async () => {
Expand Down
33 changes: 32 additions & 1 deletion apps/cli/ai/tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {
} from 'cli/commands/preview/delete';
import { runCommand as runListPreviewCommand } from 'cli/commands/preview/list';
import { runCommand as runUpdatePreviewCommand } from 'cli/commands/preview/update';
import { bumpStat } from 'cli/lib/bump-stat';
import { readCliConfig } from 'cli/lib/cli-config/core';
import { getSiteByFolder } from 'cli/lib/cli-config/sites';
import { isServerRunning, sendWpCliCommand } from 'cli/lib/wordpress-server-manager';
import { getProgressCallback, setProgressCallback } from 'cli/logger';
import { studioToolDefinitions } from '../tools';
import { createStudioToolDefinitions, studioToolDefinitions } from '../tools';

vi.mock( 'cli/ai/block-validator', () => ( {
validateBlocks: vi.fn(),
Expand Down Expand Up @@ -74,6 +75,12 @@ vi.mock( 'cli/lib/cli-config/sites', async () => ( {
getSiteByFolder: vi.fn(),
} ) );

vi.mock( 'cli/lib/bump-stat', () => ( {
bumpStat: vi.fn(),
bumpAggregatedUniqueStat: vi.fn(),
getPlatformMetric: vi.fn(),
} ) );

vi.mock( 'cli/lib/daemon-client', () => ( {
connectToDaemon: vi.fn(),
disconnectFromDaemon: vi.fn(),
Expand Down Expand Up @@ -130,6 +137,30 @@ describe( 'Studio AI MCP tools', () => {
);
} );

it( 'only includes workflow telemetry when a plugin group is configured', () => {
expect( studioToolDefinitions.map( ( tool ) => tool.name ) ).not.toContain(
'record_workflow_event'
);
expect( createStudioToolDefinitions( 'codex-plugin' ).map( ( tool ) => tool.name ) ).toContain(
'record_workflow_event'
);
} );

it( 'records workflow telemetry for a configured plugin group', async () => {
const tool = createStudioToolDefinitions( 'codex-plugin' ).find(
( definition ) => definition.name === 'record_workflow_event'
);
expect( tool ).toBeDefined();

const result = await tool!.handler(
{ workflow: 'theme-build', stage: 'started' } as never,
null
);

expect( bumpStat ).toHaveBeenCalledWith( 'codex-plugin', 'theme-build-started' );
expect( getTextContent( result ) ).toContain( 'codex-plugin theme-build-started' );
} );

it( 'creates previews for a resolved local site', async () => {
const result = await getTool( 'preview_create' ).handler(
{ nameOrPath: 'My Site' } as never,
Expand Down
84 changes: 64 additions & 20 deletions apps/cli/ai/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@ import { runCommand as runListSitesCommand } from 'cli/commands/site/list';
import { runCommand as runStartSiteCommand } from 'cli/commands/site/start';
import { runCommand as runStatusCommand } from 'cli/commands/site/status';
import { runCommand as runStopSiteCommand, Mode as StopMode } from 'cli/commands/site/stop';
import { bumpStat } from 'cli/lib/bump-stat';
import { readCliConfig, type SiteData } from 'cli/lib/cli-config/core';
import { getSiteByFolder, getSiteUrl } from 'cli/lib/cli-config/sites';
import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client';
import { getUnsupportedWpCliPostContentMessage } from 'cli/lib/rewrite-wp-cli-post-content';
import { STUDIO_SITES_ROOT } from 'cli/lib/site-paths';
import {
MCP_WORKFLOWS,
MCP_WORKFLOW_STAGES,
type McpTelemetryGroup,
type McpWorkflowStat,
} from 'cli/lib/types/bump-stats';
import { normalizeHostname } from 'cli/lib/utils';
import { isServerRunning, sendWpCliCommand } from 'cli/lib/wordpress-server-manager';
import { getProgressCallback, setProgressCallback, emitProgress } from 'cli/logger';
Expand Down Expand Up @@ -128,6 +135,33 @@ function textResult( text: string ) {
};
}

function createRecordWorkflowEventTool( telemetryGroup?: McpTelemetryGroup ) {
return tool(
'record_workflow_event',
'Records a workflow telemetry event for the active AI plugin integration, such as theme-build started or completed.',
{
workflow: z
.enum( MCP_WORKFLOWS )
.describe( 'Workflow name, such as theme-build or site-build' ),
stage: z
.enum( MCP_WORKFLOW_STAGES )
.describe( 'Workflow stage to record: started, completed, or failed' ),
},
async ( args ) => {
const stat = `${ args.workflow }-${ args.stage }` as McpWorkflowStat;

if ( ! telemetryGroup ) {
return textResult(
`Workflow telemetry skipped for ${ stat }: no plugin group configured.`
);
}

bumpStat( telemetryGroup, stat );
return textResult( `Workflow telemetry recorded: ${ telemetryGroup } ${ stat }.` );
}
);
}

function formatInvalidBlocks( report: ValidationReport ): string[] {
const lines: string[] = [];
for ( const result of report.results ) {
Expand Down Expand Up @@ -761,28 +795,38 @@ const auditPerformanceTool = tool(
}
);

export const studioToolDefinitions = [
createSiteTool,
listSitesTool,
getSiteInfoTool,
startSiteTool,
stopSiteTool,
deleteSiteTool,
createPreviewTool,
listPreviewsTool,
updatePreviewTool,
deletePreviewTool,
runWpCliTool,
validateBlocksTool,
takeScreenshotTool,
installTaxonomyScriptsTool,
auditPerformanceTool,
];

export function createStudioTools() {
export function createStudioToolDefinitions( telemetryGroup?: McpTelemetryGroup ) {
const tools = [
createSiteTool,
listSitesTool,
getSiteInfoTool,
startSiteTool,
stopSiteTool,
deleteSiteTool,
createPreviewTool,
listPreviewsTool,
updatePreviewTool,
deletePreviewTool,
runWpCliTool,
validateBlocksTool,
takeScreenshotTool,
installTaxonomyScriptsTool,
auditPerformanceTool,
];

if ( telemetryGroup ) {
return [ ...tools, createRecordWorkflowEventTool( telemetryGroup ) ];
}

return tools;
}

export const studioToolDefinitions = createStudioToolDefinitions();

export function createStudioTools( options: { telemetryGroup?: McpTelemetryGroup } = {} ) {
return createSdkMcpServer( {
name: 'studio',
version: '1.0.0',
tools: studioToolDefinitions,
tools: createStudioToolDefinitions( options.telemetryGroup ),
} );
}
6 changes: 6 additions & 0 deletions apps/cli/ai/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ const toolDisplayNames: Record< string, string > = {
mcp__studio__wp_cli: __( 'Run WP-CLI' ),
mcp__studio__validate_blocks: __( 'Validate blocks' ),
mcp__studio__take_screenshot: __( 'Take screenshot' ),
mcp__studio__record_workflow_event: __( 'Record workflow telemetry' ),
Read: __( 'Read' ),
Write: __( 'Write' ),
Edit: __( 'Edit' ),
Expand Down Expand Up @@ -277,6 +278,11 @@ function getToolDetail( name: string, input: Record< string, unknown > ): string
return __( 'inline content' );
case 'mcp__studio__take_screenshot':
return typeof input.url === 'string' ? input.url : '';
case 'mcp__studio__record_workflow_event':
if ( typeof input.workflow === 'string' && typeof input.stage === 'string' ) {
return `${ input.workflow } ${ input.stage }`;
}
return '';
case 'Read':
case 'Write':
case 'Edit': {
Expand Down
22 changes: 18 additions & 4 deletions apps/cli/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '@studio/common/lib/mcp-config';
import { __ } from '@wordpress/i18n';
import { startMcpStdioServer } from 'cli/ai/mcp-server';
import { MCP_TELEMETRY_GROUPS, type McpTelemetryGroup } from 'cli/lib/types/bump-stats';
import { Logger, LoggerError } from 'cli/logger';
import { StudioArgv } from 'cli/types';

Expand Down Expand Up @@ -63,29 +64,42 @@ function printInstallationInstructions(): void {
}
}

export async function runCommand(): Promise< void > {
type McpCommandOptions = {
telemetryGroup?: McpTelemetryGroup;
};

export async function runCommand( options: McpCommandOptions = {} ): Promise< void > {
if ( process.stdin.isTTY && process.stdout.isTTY ) {
printInstallationInstructions();
return;
}

await startMcpStdioServer();
await startMcpStdioServer( options );
}

export const registerCommand = ( yargs: StudioArgv ) => {
return yargs.command( {
command: 'mcp',
describe: __( 'MCP server for AI assistants' ),
builder: ( yargs ) => {
return yargs.help( false ).option( 'help', { type: 'boolean' } );
return yargs
.help( false )
.option( 'help', { type: 'boolean' } )
.option( 'telemetry-group', {
type: 'string',
hidden: true,
Copy link
Copy Markdown
Member

@sejas sejas Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that this telemetry group is a hidden option, so users won’t get confused about it 👍 .

choices: [ ...MCP_TELEMETRY_GROUPS ],
} );
},
handler: async ( argv ) => {
if ( argv.help ) {
printInstallationInstructions();
return;
}
try {
await runCommand();
await runCommand( {
telemetryGroup: argv.telemetryGroup as McpTelemetryGroup | undefined,
} );
} catch ( error ) {
if ( error instanceof LoggerError ) {
logger.reportError( error );
Expand Down
9 changes: 7 additions & 2 deletions apps/cli/lib/bump-stat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import {
saveCliConfig,
unlockCliConfig,
} from 'cli/lib/cli-config/core';
import { StatsGroup, StatsMetric } from 'cli/lib/types/bump-stats';
import {
type BumpStatGroup,
type BumpStatMetric,
StatsGroup,
StatsMetric,
} from 'cli/lib/types/bump-stats';

const lastBumpStatsProvider: LastBumpStatsProvider = {
load: async () => {
Expand All @@ -28,7 +33,7 @@ const lastBumpStatsProvider: LastBumpStatsProvider = {
},
};

export function bumpStat( group: StatsGroup, stat: StatsMetric, bumpInDev = false ) {
export function bumpStat( group: BumpStatGroup, stat: BumpStatMetric, bumpInDev = false ) {
return __bumpStat( group, stat, bumpInDev );
}

Expand Down
19 changes: 19 additions & 0 deletions apps/cli/lib/types/bump-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ export enum StatsGroup {
STUDIO_CLI_WEEKLY_UNIQUE_APP = 'studio-cli-weekly-unq-app',
}

export const MCP_TELEMETRY_GROUPS = [ 'claude-code-plugin', 'codex-plugin' ] as const;
export type McpTelemetryGroup = ( typeof MCP_TELEMETRY_GROUPS )[ number ];

export const MCP_WORKFLOWS = [
'site-build',
'theme-build',
'block-build',
'plugin-build',
'auditing',
] as const;
export type McpWorkflow = ( typeof MCP_WORKFLOWS )[ number ];

export const MCP_WORKFLOW_STAGES = [ 'started', 'completed', 'failed' ] as const;
export type McpWorkflowStage = ( typeof MCP_WORKFLOW_STAGES )[ number ];
export type McpWorkflowStat = `${ McpWorkflow }-${ McpWorkflowStage }`;

export type BumpStatGroup = StatsGroup | McpTelemetryGroup;
export type BumpStatMetric = StatsMetric | McpWorkflowStat;

export enum StatsMetric {
SUCCESS = 'success',
FAILURE = 'failure',
Expand Down
Loading