-
Notifications
You must be signed in to change notification settings - Fork 881
feat(ai): Add Jetpack AI admin page with MCP settings #48048
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 801fc86
fix(ai): align MCP settings UI with Figma design
eoigal bfa02ee
fix(ai): address Copilot review feedback on MCP settings
eoigal e249326
fix(ai): address second round of Copilot review feedback on MCP settings
eoigal b8e82c2
fix(ai): proxy MCP settings reads/writes to wpcom/v2/sites/{id}/mcp-a…
eoigal 92efbd0
fix(ai): fix PHP 7.3 compat and Phan type error in MCP settings endpoint
eoigal abfe039
fix(ai): use WPCOM user_overrides for per-tool abilities, fix stale d…
eoigal d7a660f
fix(ai): center AI admin page content using standard Jetpack .wrap la…
eoigal fb06a86
fix(ai): use AdminPage component for consistent layout and footer
eoigal 249b0e8
fix(ai): remove double header/footer by skipping wrap_ui
eoigal d3f627d
fix(ai): fix footer bleed, missing footer, and card width
eoigal 02b74ae
fix(ai): clip horizontal overflow caused by AdminPage margin-left com…
eoigal 231c1df
fix(ai): fix content padding asymmetry caused by AdminPage margin-lef…
eoigal 3f31969
fix(ai): remove overflow-x hack, use border-box to prevent card overflow
eoigal e9576ec
fix(ai): white background full height, 960px max-width for content
eoigal b23040e
fix(ai): center content within 960px max-width
eoigal fe13d5d
fix(ai): fix footer position and align badge icon colors with Calypso
eoigal 9d88027
feat(ai): add MCP upsell and fix wpcom compatibility
eoigal d89353b
fix(ai): use has_mcp_plan field from wpcom for upsell detection
eoigal 497f6ea
feat(jetpack-mu-wpcom): load Jetpack AI MCP settings page and endpoin…
eoigal 001f625
fix(ai): remove duplicate MCP endpoint and double menu registration f…
eoigal 2feea9b
fix(ai): add changelog entry for class.jetpack-admin.php cleanup
eoigal 987efa9
fix(ai): suppress Phan undeclared-class warnings for runtime-loaded J…
eoigal 4177f46
Fix submenu item initialization.
sergeymitr 09e00bd
fix(ai): address PR review feedback on MCP settings UI
eoigal d2ae891
fix(ai): stage remaining index.jsx and changelog changes
eoigal 81d8101
fix(ai): change page slug from 'jetpack-ai' to 'ai' to match wpcom co…
eoigal b889c24
refactor(ai): replace HStack/VStack with Stack from @wordpress/ui acr…
eoigal 7c94923
refactor(ai): replace HStack/VStack with Stack from @wordpress/ui in …
eoigal 86906c6
fix(ai): move MCP settings REST endpoint from jetpack-mu-wpcom to jet…
eoigal 74a871b
fix(ai): add MCP settings REST endpoint to jetpack plugin and update …
eoigal 5b2dcb5
fix(ai): register AI admin page on self-hosted sites
eoigal 10faa5b
revert: remove jetpack-mu-wpcom changes — to be handled in a separate PR
eoigal 523567e
fix(ai): address style.scss review comments — use Text component and …
eoigal 51ef622
refactor(ai): move Button to @wordpress/ui across all MCP views
eoigal 74a4056
fix(ai): bundle @wordpress/private-apis and theme for AI admin enqueue
eoigal 42a6bf2
fix(ai): address UI regressions and improve MCP settings UX
eoigal 00e53f3
fix(ai): address PR review feedback — breadcrumbs, max-width, hide wh…
eoigal c763400
fix(ai): fix breadcrumb layout and add Jetpack logo
eoigal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' ), | ||
| 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 } ) { | ||
|
eoigal marked this conversation as resolved.
|
||
| return ( | ||
| <nav aria-label={ __( 'Breadcrumbs', 'jetpack' ) }> | ||
| <ul className="admin-ui-breadcrumbs__list jetpack-ai-admin__breadcrumbs"> | ||
|
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
142
projects/plugins/jetpack/_inc/client/ai/mcp/categories.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.