Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f8c55a1
feat(ai): add MCP settings admin page for Jetpack AI
eoigal Apr 10, 2026
801fc86
fix(ai): align MCP settings UI with Figma design
eoigal Apr 11, 2026
bfa02ee
fix(ai): address Copilot review feedback on MCP settings
eoigal Apr 11, 2026
e249326
fix(ai): address second round of Copilot review feedback on MCP settings
eoigal Apr 11, 2026
b8e82c2
fix(ai): proxy MCP settings reads/writes to wpcom/v2/sites/{id}/mcp-a…
eoigal Apr 14, 2026
92efbd0
fix(ai): fix PHP 7.3 compat and Phan type error in MCP settings endpoint
eoigal Apr 14, 2026
abfe039
fix(ai): use WPCOM user_overrides for per-tool abilities, fix stale d…
eoigal Apr 15, 2026
d7a660f
fix(ai): center AI admin page content using standard Jetpack .wrap la…
eoigal Apr 15, 2026
fb06a86
fix(ai): use AdminPage component for consistent layout and footer
eoigal Apr 15, 2026
249b0e8
fix(ai): remove double header/footer by skipping wrap_ui
eoigal Apr 15, 2026
d3f627d
fix(ai): fix footer bleed, missing footer, and card width
eoigal Apr 15, 2026
02b74ae
fix(ai): clip horizontal overflow caused by AdminPage margin-left com…
eoigal Apr 15, 2026
231c1df
fix(ai): fix content padding asymmetry caused by AdminPage margin-lef…
eoigal Apr 15, 2026
3f31969
fix(ai): remove overflow-x hack, use border-box to prevent card overflow
eoigal Apr 15, 2026
e9576ec
fix(ai): white background full height, 960px max-width for content
eoigal Apr 15, 2026
b23040e
fix(ai): center content within 960px max-width
eoigal Apr 15, 2026
fe13d5d
fix(ai): fix footer position and align badge icon colors with Calypso
eoigal Apr 16, 2026
9d88027
feat(ai): add MCP upsell and fix wpcom compatibility
eoigal Apr 16, 2026
d89353b
fix(ai): use has_mcp_plan field from wpcom for upsell detection
eoigal Apr 16, 2026
497f6ea
feat(jetpack-mu-wpcom): load Jetpack AI MCP settings page and endpoin…
eoigal Apr 16, 2026
001f625
fix(ai): remove duplicate MCP endpoint and double menu registration f…
eoigal Apr 17, 2026
2feea9b
fix(ai): add changelog entry for class.jetpack-admin.php cleanup
eoigal Apr 17, 2026
987efa9
fix(ai): suppress Phan undeclared-class warnings for runtime-loaded J…
eoigal Apr 17, 2026
4177f46
Fix submenu item initialization.
sergeymitr Apr 19, 2026
09e00bd
fix(ai): address PR review feedback on MCP settings UI
eoigal Apr 21, 2026
d2ae891
fix(ai): stage remaining index.jsx and changelog changes
eoigal Apr 21, 2026
81d8101
fix(ai): change page slug from 'jetpack-ai' to 'ai' to match wpcom co…
eoigal Apr 21, 2026
b889c24
refactor(ai): replace HStack/VStack with Stack from @wordpress/ui acr…
eoigal Apr 21, 2026
7c94923
refactor(ai): replace HStack/VStack with Stack from @wordpress/ui in …
eoigal Apr 21, 2026
86906c6
fix(ai): move MCP settings REST endpoint from jetpack-mu-wpcom to jet…
eoigal Apr 21, 2026
74a871b
fix(ai): add MCP settings REST endpoint to jetpack plugin and update …
eoigal Apr 21, 2026
5b2dcb5
fix(ai): register AI admin page on self-hosted sites
eoigal Apr 21, 2026
10faa5b
revert: remove jetpack-mu-wpcom changes — to be handled in a separate PR
eoigal Apr 21, 2026
523567e
fix(ai): address style.scss review comments — use Text component and …
eoigal Apr 21, 2026
51ef622
refactor(ai): move Button to @wordpress/ui across all MCP views
eoigal Apr 21, 2026
74a4056
fix(ai): bundle @wordpress/private-apis and theme for AI admin enqueue
eoigal Apr 21, 2026
42a6bf2
fix(ai): address UI regressions and improve MCP settings UX
eoigal Apr 22, 2026
00e53f3
fix(ai): address PR review feedback — breadcrumbs, max-width, hide wh…
eoigal Apr 22, 2026
c763400
fix(ai): fix breadcrumb layout and add Jetpack logo
eoigal Apr 22, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,8 @@ phpcs.xml
# Claude setting files
.claude/settings.local.json

# Cursor / Playwright MCP local logs and snapshots
.playwright-mcp/

.pnpm-debug.log
.pnpm-error.log
32 changes: 32 additions & 0 deletions projects/plugins/jetpack/_inc/client/ai-admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Entry point for the Jetpack AI admin page.
*
* Mounts the React app into the #jetpack-ai-root div rendered by Jetpack_AI_Page::page_render().
*/

import apiFetch from '@wordpress/api-fetch';
import * as WPElement from '@wordpress/element';
import App from './ai/main';
import './ai/style.scss';

const { apiRoot, apiNonce } = window?.jetpackAiSettings ?? {};

if ( apiRoot ) {
apiFetch.use( apiFetch.createRootURLMiddleware( apiRoot ) );
}
if ( apiNonce ) {
apiFetch.use( apiFetch.createNonceMiddleware( apiNonce ) );
}

/**
* Mount the React app into the page root element.
*/
function render() {
const container = document.getElementById( 'jetpack-ai-root' );
if ( ! container ) {
return;
}
WPElement.createRoot( container ).render( <App /> );
}

render();
168 changes: 168 additions & 0 deletions projects/plugins/jetpack/_inc/client/ai/main.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* Root component for the Jetpack AI admin page.
*
* Manages the view stack (hub → read | write | setup) and owns the MCP settings state.
*/

import { AdminPage, JetpackLogo } from '@automattic/jetpack-components';
import { Spinner } from '@wordpress/components';
import { useCallback, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Notice, Stack } from '@wordpress/ui';
import McpHub from './mcp/index';
import McpRead from './mcp/read';
import McpSetup from './mcp/setup';
import McpUpsell from './mcp/upsell';
import { useMcpSettings } from './mcp/use-mcp-settings';
import McpWrite from './mcp/write';

const { blogId, apiRoot, apiNonce } = window?.jetpackAiSettings ?? {};

const VIEW_TITLES = {
hub: __( 'AI', 'jetpack' ),
read: __( 'Read', 'jetpack' ),
Comment thread
eoigal marked this conversation as resolved.
write: __( 'Write', 'jetpack' ),
setup: __( 'Connect external AI agent', 'jetpack' ),
};

const VIEW_DESCRIPTIONS = {
hub: __( 'Control how AI agents interact with your site.', 'jetpack' ),
read: __( 'View your site\u2019s content.', 'jetpack' ),
write: __( 'Create, update, and manage content on your site.', 'jetpack' ),
setup: __( 'Get instructions for connecting your external AI assistant.', 'jetpack' ),
};

/**
* Breadcrumb nav shown on sub-views: "AI / Read", "AI / Write", etc.
* Replaces both the page title and the ← Back button.
*
* @param {object} props - Component props.
* @param {string} props.view - Current sub-view key.
* @param {Function} props.onNavigate - Called with no args to go back to hub.
* @return {object} Component markup.
*/
function Breadcrumbs( { view, onNavigate } ) {
Comment thread
eoigal marked this conversation as resolved.
return (
<nav aria-label={ __( 'Breadcrumbs', 'jetpack' ) }>
<ul className="admin-ui-breadcrumbs__list jetpack-ai-admin__breadcrumbs">
Comment thread
eoigal marked this conversation as resolved.
<li>
<button
type="button"
className="jetpack-ai-admin__breadcrumb-link"
onClick={ onNavigate }
>
<JetpackLogo showText={ false } height={ 20 } />
{ __( 'AI', 'jetpack' ) }
</button>
</li>
<li>
<span className="jetpack-ai-admin__breadcrumb-current">{ VIEW_TITLES[ view ] }</span>
</li>
</ul>
</nav>
);
}

/**
* Root App component for the Jetpack AI admin page.
*
* @return {object} Component markup.
*/
export default function App() {
const [ view, setView ] = useState( 'hub' );
const [ saveError, setSaveError ] = useState( null );
const { isLoading, savingToolIds, mcpAbilities, hasMcpAccess, error, updateMcpAbilities } =
useMcpSettings();

const handleUpdate = useCallback(
update => {
setSaveError( null );
return updateMcpAbilities( update ).catch( () => {
setSaveError( __( 'Failed to save MCP settings. Please try again.', 'jetpack' ) );
} );
},
[ updateMcpAbilities ]
);

const dismissSaveError = useCallback( () => setSaveError( null ), [] );
const navigateBack = useCallback( () => setView( 'hub' ), [] );

const isSubView = view !== 'hub';

return (
<AdminPage
title={ isSubView ? undefined : VIEW_TITLES.hub }
subTitle={ VIEW_DESCRIPTIONS[ view ] }
breadcrumbs={
isSubView ? <Breadcrumbs view={ view } onNavigate={ navigateBack } /> : undefined
}
apiRoot={ apiRoot }
apiNonce={ apiNonce }
>
<div className="jetpack-ai-admin__content">
{ isLoading && (
<div className="jetpack-ai-admin__loading">
<Spinner />
</div>
) }

{ ! isLoading && error && (
<Notice.Root intent="error">
<Notice.Description>{ error }</Notice.Description>
</Notice.Root>
) }

{ ! isLoading && saveError && (
<Notice.Root intent="error">
<Notice.Description>{ saveError }</Notice.Description>
<Notice.CloseIcon label={ __( 'Dismiss', 'jetpack' ) } onClick={ dismissSaveError } />
</Notice.Root>
) }

{ ! isLoading && ! error && ! blogId && (
<Notice.Root intent="warning">
<Notice.Description>
{ __(
'This site is not connected to WordPress.com. Please connect Jetpack to manage MCP settings.',
'jetpack'
) }
</Notice.Description>
</Notice.Root>
) }

{ ! isLoading && ! error && !! blogId && ! hasMcpAccess && <McpUpsell /> }

{ ! isLoading && ! error && !! blogId && hasMcpAccess && (
<Stack direction="column" gap="md">
{ view === 'hub' && (
<McpHub
mcpAbilities={ mcpAbilities }
blogId={ blogId }
savingToolIds={ savingToolIds }
onNavigate={ setView }
onUpdate={ handleUpdate }
/>
) }
{ view === 'read' && (
<McpRead
mcpAbilities={ mcpAbilities }
blogId={ blogId }
savingToolIds={ savingToolIds }
onUpdate={ handleUpdate }
/>
) }
{ view === 'write' && (
<McpWrite
mcpAbilities={ mcpAbilities }
blogId={ blogId }
savingToolIds={ savingToolIds }
onUpdate={ handleUpdate }
/>
) }
{ view === 'setup' && <McpSetup /> }
</Stack>
) }
</div>
</AdminPage>
);
}
142 changes: 142 additions & 0 deletions projects/plugins/jetpack/_inc/client/ai/mcp/categories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* MCP Tools Category Mapping.
*
* Ported from client/dashboard/me/mcp/categories.ts in wp-calypso.
* Maps API category values to display categories and sub-categories for the MCP settings page.
*/

import { __ } from '@wordpress/i18n';

export const DISPLAY_CATEGORIES = {
POSTS: __( 'Posts', 'jetpack' ),
PAGES: __( 'Pages', 'jetpack' ),
DESIGN: __( 'Design', 'jetpack' ),
SITES: __( 'Sites', 'jetpack' ),
ACCOUNT: __( 'Account', 'jetpack' ),
DOMAINS: __( 'Domains', 'jetpack' ),
DEVELOPER_TESTING: __( 'Developer & testing', 'jetpack' ),
UNCATEGORIZED: __( 'Uncategorized', 'jetpack' ),
};

export const CATEGORY_ORDER = [
DISPLAY_CATEGORIES.SITES,
DISPLAY_CATEGORIES.POSTS,
DISPLAY_CATEGORIES.PAGES,
DISPLAY_CATEGORIES.DESIGN,
DISPLAY_CATEGORIES.DOMAINS,
DISPLAY_CATEGORIES.ACCOUNT,
DISPLAY_CATEGORIES.DEVELOPER_TESTING,
DISPLAY_CATEGORIES.UNCATEGORIZED,
];

const SUB_CATEGORIES = {
POSTS: __( 'Posts', 'jetpack' ),
COMMENTS: __( 'Comments', 'jetpack' ),
CATEGORIES_TAGS: __( 'Categories & tags', 'jetpack' ),
SITES: __( 'Sites', 'jetpack' ),
MEDIA: __( 'Media', 'jetpack' ),
SITE_SETTINGS: __( 'Site settings', 'jetpack' ),
ANALYTICS: __( 'Analytics', 'jetpack' ),
ACCOUNT: __( 'Account', 'jetpack' ),
NOTIFICATIONS: __( 'Notifications', 'jetpack' ),
};

export const SUB_CATEGORY_ORDER = {
[ DISPLAY_CATEGORIES.POSTS ]: [
SUB_CATEGORIES.POSTS,
SUB_CATEGORIES.COMMENTS,
SUB_CATEGORIES.CATEGORIES_TAGS,
],
[ DISPLAY_CATEGORIES.SITES ]: [
SUB_CATEGORIES.SITES,
SUB_CATEGORIES.SITE_SETTINGS,
SUB_CATEGORIES.MEDIA,
SUB_CATEGORIES.ANALYTICS,
],
[ DISPLAY_CATEGORIES.ACCOUNT ]: [ SUB_CATEGORIES.ACCOUNT, SUB_CATEGORIES.NOTIFICATIONS ],
};

const API_CATEGORY_TO_DISPLAY = {
posts: DISPLAY_CATEGORIES.POSTS,
comments: DISPLAY_CATEGORIES.POSTS,
'categories-tags': DISPLAY_CATEGORIES.POSTS,
pages: DISPLAY_CATEGORIES.PAGES,
design: DISPLAY_CATEGORIES.DESIGN,
sites: DISPLAY_CATEGORIES.SITES,
media: DISPLAY_CATEGORIES.SITES,
users: DISPLAY_CATEGORIES.SITES,
plugins: DISPLAY_CATEGORIES.SITES,
'site-settings': DISPLAY_CATEGORIES.SITES,
analytics: DISPLAY_CATEGORIES.SITES,
account: DISPLAY_CATEGORIES.ACCOUNT,
notifications: DISPLAY_CATEGORIES.ACCOUNT,
billing: DISPLAY_CATEGORIES.ACCOUNT,
domains: DISPLAY_CATEGORIES.DOMAINS,
'developer-testing': DISPLAY_CATEGORIES.DEVELOPER_TESTING,
};

const API_CATEGORY_TO_SUB_CATEGORY = {
posts: SUB_CATEGORIES.POSTS,
comments: SUB_CATEGORIES.COMMENTS,
'categories-tags': SUB_CATEGORIES.CATEGORIES_TAGS,
sites: SUB_CATEGORIES.SITES,
media: SUB_CATEGORIES.MEDIA,
users: SUB_CATEGORIES.SITE_SETTINGS,
plugins: SUB_CATEGORIES.SITE_SETTINGS,
'site-settings': SUB_CATEGORIES.SITE_SETTINGS,
analytics: SUB_CATEGORIES.ANALYTICS,
account: SUB_CATEGORIES.ACCOUNT,
notifications: SUB_CATEGORIES.NOTIFICATIONS,
billing: SUB_CATEGORIES.ACCOUNT,
};

/**
* Get the display sub-category name for a tool.
*
* @param {string} toolId - Tool identifier.
* @param {object} ability - Tool descriptor from the API.
* @return {string | undefined} Sub-category display name, or undefined if none.
*/
export function getSubCategory( toolId, ability ) {
const apiCategory = ability?.category;
if ( apiCategory ) {
return API_CATEGORY_TO_SUB_CATEGORY[ apiCategory ];
}
return undefined;
}

/**
* Check whether a tool is a write (non-readonly) tool.
*
* @param {string} toolId - Tool identifier.
* @param {object} ability - Tool descriptor from the API.
* @return {boolean} True if the tool is a write tool.
*/
export function isWriteTool( toolId, ability ) {
return ability?.readonly === false;
}

/**
* Get the display category name for a tool.
*
* @param {string} toolId - Tool identifier.
* @param {object} ability - Tool descriptor from the API.
* @return {string} Display category name, falling back to Uncategorized.
*/
export function getDisplayCategory( toolId, ability ) {
const apiCategory = ability?.category;
if ( apiCategory && API_CATEGORY_TO_DISPLAY[ apiCategory ] ) {
return API_CATEGORY_TO_DISPLAY[ apiCategory ];
}
return DISPLAY_CATEGORIES.UNCATEGORIZED;
}

/**
* Pass-through sort — preserved for interface compatibility.
*
* @param {Array} tools - Tool entries to sort.
* @return {Array} The same tool entries, unchanged.
*/
export function sortTools( tools ) {
return tools;
}
Loading
Loading