diff --git a/client/dashboard/sites/settings-mcp/index.tsx b/client/dashboard/sites/settings-mcp/index.tsx index 5530bb08ff28..3168a7c80c2c 100644 --- a/client/dashboard/sites/settings-mcp/index.tsx +++ b/client/dashboard/sites/settings-mcp/index.tsx @@ -1,6 +1,7 @@ import { isAutomatticianQuery, siteBySlugQuery, + siteSettingsQuery, userSettingsQuery, userSettingsMutation, } from '@automattic/api-queries'; @@ -13,12 +14,14 @@ import { ToggleControl, ExternalLink, __experimentalText as Text, + __experimentalHeading as Heading, } from '@wordpress/components'; import { useDispatch } from '@wordpress/data'; import { createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; -import { useState, useMemo, useCallback, createElement } from 'react'; +import { useState, useMemo, useCallback } from 'react'; +import { ButtonStack } from '../../components/button-stack'; import PageLayout from '../../components/page-layout'; import SettingsPageHeader from '../settings-page-header'; import { getSiteMcpAbilities, createSiteSpecificApiPayload } from './utils'; @@ -29,6 +32,7 @@ function SettingsMcpComponent( { siteSlug }: { siteSlug: string } ) { const { data: site } = useSuspenseQuery( siteBySlugQuery( siteSlug ) ); const { data: isAutomattician } = useQuery( isAutomatticianQuery() ); const { data: userSettings } = useSuspenseQuery( userSettingsQuery() ); + const { data: siteSettings } = useQuery( siteSettingsQuery( site.ID ) ); // Use the standard userSettingsMutation (now supports mcp_abilities) const saveMcpMutation = useMutation( { ...userSettingsMutation(), @@ -42,18 +46,29 @@ function SettingsMcpComponent( { siteSlug }: { siteSlug: string } ) { // Get tools from user settings using the new nested structure const availableTools = useMemo( (): [ string, SiteMcpAbilities[ string ] ][] => { - const abilities = getSiteMcpAbilities( userSettings, site.ID ); + const abilities = getSiteMcpAbilities( userSettings, site.ID, siteSettings as any ); return Object.entries( abilities ); - }, [ userSettings, site.ID ] ); + }, [ userSettings, site.ID, siteSettings ] ); const hasTools = availableTools.length > 0; - const [ formData, setFormData ] = useState< SiteMcpAbilities >( () => - getSiteMcpAbilities( userSettings, site.ID ) - ); + const [ formData, setFormData ] = useState< any >( () => { + const abilities = getSiteMcpAbilities( userSettings, site.ID, siteSettings as any ); + return { + ...abilities, + }; + } ); // Calculate if any tools are enabled in form data (for master toggle state) - const anyToolsEnabled = hasTools && Object.values( formData ).some( ( tool ) => tool.enabled ); + const anyToolsEnabled = + hasTools && + Object.entries( formData ).some( + ( [ key, value ] ) => + key !== 'accountToolsEnabled' && + typeof value === 'object' && + value && + ( value as any ).enabled + ); const handleSubmit = useCallback( ( e: React.FormEvent ) => { @@ -69,33 +84,36 @@ function SettingsMcpComponent( { siteSlug }: { siteSlug: string } ) { createErrorNotice( __( 'Failed to save MCP tools.' ), { type: 'snackbar' } ); } }, - [ formData, userSettings, site.ID, saveMcpMutation, createErrorNotice ] + [ formData, userSettings, site.ID, siteSettings, saveMcpMutation, createErrorNotice ] ); const handleMasterToggle = useCallback( ( enabled: boolean ) => { // Get the complete list of available tools from userSettings - const currentAbilities = getSiteMcpAbilities( userSettings, site.ID ); - const updatedTools: SiteMcpAbilities = {}; + const currentAbilities = getSiteMcpAbilities( userSettings, site.ID, siteSettings as any ); + const updatedTools: any = {}; // Update all available tools to the same enabled state Object.entries( currentAbilities ).forEach( ( [ toolId, tool ] ) => { updatedTools[ toolId ] = { - ...tool, + ...( tool as any ), enabled, }; } ); + // Preserve the accountToolsEnabled setting + updatedTools.accountToolsEnabled = formData.accountToolsEnabled; + setFormData( updatedTools ); }, - [ userSettings, site.ID ] + [ userSettings, site.ID, formData.accountToolsEnabled ] ); const handleToolChange = useCallback( ( toolId: string, enabled: boolean ) => { - setFormData( ( prev ) => ( { + setFormData( ( prev: any ) => ( { ...prev, [ toolId ]: { - ...prev[ toolId ], + ...( prev[ toolId ] as any ), enabled, }, } ) ); @@ -107,7 +125,7 @@ function SettingsMcpComponent( { siteSlug }: { siteSlug: string } ) { toolId, { ...tool, - enabled: formData[ toolId ]?.enabled ?? tool.enabled, + enabled: ( formData[ toolId ] as any )?.enabled ?? ( tool as any ).enabled, }, ] ); }, [ availableTools, formData ] ); @@ -117,48 +135,6 @@ function SettingsMcpComponent( { siteSlug }: { siteSlug: string } ) { return null; } - // Group tools by type first, then by category - const groupedByType: Record< - string, - Record< string, [ string, SiteMcpAbilities[ string ] ][] > - > = tools.reduce( - ( - typeGroups: Record< string, Record< string, [ string, SiteMcpAbilities[ string ] ][] > >, - [ toolId, tool ] - ) => { - const type = tool.type || 'tool'; // Default to 'tool' instead of 'other' - const category = tool.category || 'General'; - - // Only include the three main types - if ( ! [ 'tool', 'resource', 'prompt' ].includes( type ) ) { - return typeGroups; - } - - if ( ! typeGroups[ type ] ) { - typeGroups[ type ] = {}; - } - if ( ! typeGroups[ type ][ category ] ) { - typeGroups[ type ][ category ] = []; - } - typeGroups[ type ][ category ].push( [ toolId, tool ] ); - return typeGroups; - }, - {} as Record< string, Record< string, [ string, SiteMcpAbilities[ string ] ][] > > - ); - - // Type descriptions - const typeDescriptions: Record< string, string > = { - tool: __( - 'Tools allow AI assistants to read and search your WordPress.com data. These are view-only capabilities that cannot modify your content or settings.' - ), - resource: __( - 'Resources provide AI assistants with read-only access to your data, such as site statistics or user information.' - ), - prompt: __( - 'Prompts help AI assistants understand context and provide better responses to your queries.' - ), - }; - const renderContent = () => { if ( ! hasTools ) { return ( @@ -173,9 +149,9 @@ function SettingsMcpComponent( { siteSlug }: { siteSlug: string } ) { } return ( -
- - + + +
) }
-
-
-
- { hasTools && anyToolsEnabled && ( - <> - { Object.entries( groupedByType ).map( ( [ type, typeCategories ] ) => { - const typeKey = type as 'tool' | 'resource' | 'prompt'; - return ( -
- { __( 'Site-level MCP Tools' ) } - - { typeDescriptions[ typeKey ] } + { hasTools && anyToolsEnabled && ( + + { __( 'Site-specific MCP Tools' ) } + + { __( 'Control which MCP tools are available for this site.' ) } - - - - { Object.entries( typeCategories ).map( ( [ category, categoryTools ] ) => ( - - - { category } - - { categoryTools.map( ( [ toolId, tool ] ) => ( - - handleToolChange( toolId, checked ) } - label={ tool.title } - help={ tool.description } - /> - - ) ) } - - ) ) } - - - -
- ); - } ) } - - ) } + + { tools + .filter( ( [ toolId ] ) => toolId !== 'accountToolsEnabled' ) + .map( ( [ toolId, tool ] ) => ( + handleToolChange( toolId, checked ) } + label={ tool.title } + help={ tool.description } + /> + ) ) } + + + ) } - { anyToolsEnabled && ( - <> -
- { __( 'Account-level MCP Tools' ) } - - { createInterpolateElement( - __( - 'Account-level MCP tools are available across all your sites, manage account MCP settings.' - ), - { - a: createElement( 'a', { href: '/me/mcp', target: '_blank' } ), - } - ) } - -
- - ) } + { anyToolsEnabled && ( + + { __( 'Account-level MCP Tools' ) } + + { createInterpolateElement( + __( + 'Account-level tools work across all sites. Manage account MCP settings.' + ), + { + a: , + } + ) } + + + setFormData( ( prev: any ) => ( { ...prev, accountToolsEnabled: checked } ) ) + } + label={ __( 'Account-level MCP tools' ) } + help={ __( + 'When enabled, account-level MCP tools will be available on this site.' + ) } + /> + + ) } - { hasTools && ( -
- -
- ) } - + { hasTools && ( + + + + ) } + + +
+
); }; diff --git a/client/dashboard/sites/settings-mcp/utils.ts b/client/dashboard/sites/settings-mcp/utils.ts index ee2c9422cd65..504543242725 100644 --- a/client/dashboard/sites/settings-mcp/utils.ts +++ b/client/dashboard/sites/settings-mcp/utils.ts @@ -13,8 +13,9 @@ export type McpAbilitiesApiStructure = { */ export function getSiteMcpAbilities( userSettings: { mcp_abilities?: McpAbilities } | null | undefined, - siteId: string | number -): SiteMcpAbilities { + siteId: string | number, + siteSettings?: { default_tools?: boolean } | null | undefined +): SiteMcpAbilities & { accountToolsEnabled?: boolean } { const siteIdStr = String( siteId ); const mcpData = userSettings?.mcp_abilities; @@ -50,7 +51,25 @@ export function getSiteMcpAbilities( } } ); - return mergedAbilities; + // Determine if account tools should be enabled + // Check if there are any enabled account-level abilities + const hasEnabledAccountTools = + mcpData.account && Object.values( mcpData.account ).some( ( ability ) => ability.enabled ); + + // Check if site has default tools enabled (from site settings) + const hasDefaultSiteTools = siteSettings?.default_tools === true; + + // Set accountToolsEnabled to true if: + // 1. Explicitly set in site overrides, OR + // 2. There are enabled account-level tools, OR + // 3. Site has default tools available + const accountToolsEnabled = + siteOverrides.accountToolsEnabled ?? ( hasEnabledAccountTools || hasDefaultSiteTools ); + + return { + ...mergedAbilities, + accountToolsEnabled, + }; } /** @@ -165,7 +184,7 @@ export function convertAbilitiesFromApi( export function createSiteSpecificApiPayload( userSettings: { mcp_abilities?: McpAbilities } | null | undefined, siteId: string | number, - abilities: SiteMcpAbilities + abilities: SiteMcpAbilities & { accountToolsEnabled?: boolean } ): { mcp_abilities: McpAbilitiesApiStructure } { const siteIdNum = Number( siteId ); const mcpData = userSettings?.mcp_abilities; @@ -180,6 +199,11 @@ export function createSiteSpecificApiPayload( // Find only the abilities that differ from defaults const siteOverrides: Record< string, boolean > = {}; Object.entries( abilities ).forEach( ( [ abilityName, ability ] ) => { + // Skip the accountToolsEnabled field + if ( abilityName === 'accountToolsEnabled' ) { + return; + } + const defaultAbility = defaultSiteAbilities[ abilityName ]; // Only store if it differs from the default @@ -188,6 +212,11 @@ export function createSiteSpecificApiPayload( } } ); + // Add accountToolsEnabled if it's set (and not the default true) + if ( abilities.accountToolsEnabled !== undefined && abilities.accountToolsEnabled !== true ) { + siteOverrides.accountToolsEnabled = abilities.accountToolsEnabled; + } + // Create the optimized payload (only include site-specific overrides) const payload: McpAbilitiesApiStructure = {}; diff --git a/client/me/mcp/main.jsx b/client/me/mcp/main.jsx index d7f25851b18f..fd1f65b92c6f 100644 --- a/client/me/mcp/main.jsx +++ b/client/me/mcp/main.jsx @@ -4,14 +4,14 @@ import { Button, __experimentalVStack as VStack, __experimentalText as Text, + __experimentalHeading as Heading, Card, CardBody, - CardHeader, ToggleControl, } from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; import { useTranslate } from 'i18n-calypso'; -import { useState, useEffect, createElement } from 'react'; +import { useState, useEffect } from 'react'; import { connect, useDispatch as useReduxDispatch } from 'react-redux'; import DocumentHead from 'calypso/components/data/document-head'; import FormButton from 'calypso/components/forms/form-button'; @@ -22,6 +22,7 @@ import PageViewTracker from 'calypso/lib/analytics/page-view-tracker'; import getUserSettings from 'calypso/state/selectors/get-user-settings'; import { saveUserSettings } from 'calypso/state/user-settings/actions'; import { isUpdatingUserSettings } from 'calypso/state/user-settings/selectors'; +import { ButtonStack } from '../../dashboard/components/button-stack'; import { getAccountMcpAbilities, createAccountApiPayload } from './utils'; function McpComponent( { path, userSettings, isUpdating } ) { @@ -114,62 +115,12 @@ function McpComponent( { path, userSettings, isUpdating } ) { }, ] ); - // Group tools by type first, then by category - const groupedByType = tools.reduce( ( typeGroups, [ toolId, tool ] ) => { - const type = tool.type || 'tool'; // Default to 'tool' instead of 'other' - const category = tool.category || 'General'; - - // Only include the three main types - if ( ! [ 'tool', 'resource', 'prompt' ].includes( type ) ) { - return typeGroups; - } - - if ( ! typeGroups[ type ] ) { - typeGroups[ type ] = {}; - } - if ( ! typeGroups[ type ][ category ] ) { - typeGroups[ type ][ category ] = []; - } - typeGroups[ type ][ category ].push( [ toolId, tool ] ); - return typeGroups; - }, {} ); - - // Type descriptions - const typeDescriptions = { - tool: translate( - 'Tools allow AI assistants to read and search your WordPress.com data. These are view-only capabilities that cannot modify your content or settings.' - ), - resource: translate( - 'Resources provide AI assistants with read-only access to your data, such as site statistics or user information.' - ), - prompt: translate( - 'Prompts help AI assistants understand context and provide better responses to your queries.' - ), - }; - - // Type display names - const typeDisplayNames = { - tool: translate( 'Tools' ), - resource: translate( 'Resources' ), - prompt: translate( 'Prompts' ), - }; - return ( -
- - - - { translate( 'Account-level MCP tools' ) } - - { translate( - 'These tools are available across all your sites. You can enable or disable them here to control access globally.' - ) } - - - - - { hasTools ? ( - + + + + + { hasTools ? (
) }
-
- ) : ( - - { translate( 'No MCP tools are currently available.' ) } - - ) } -
-
- - { hasTools && anyToolsEnabled && ( - <> - { Object.entries( groupedByType ).map( ( [ type, typeCategories ] ) => ( -
- - - - { typeDisplayNames[ type ] } - - { typeDescriptions[ type ] } - - - - - - { Object.entries( typeCategories ).map( ( [ category, categoryTools ] ) => ( - - - { category } - - - { categoryTools.map( ( [ toolId, tool ] ) => ( - - handleToolChange( toolId, checked ) } - label={ tool.title } - help={ tool.description } - disabled={ ! anyToolsEnabled } - /> - - ) ) } - - - ) ) } - - - -
- ) ) } - - ) } - - { hasTools && anyToolsEnabled && ( - <> -
- - - - - { translate( 'Site-specific MCP settings' ) } - - - { createInterpolateElement( - translate( - 'Account-level MCP tools are available on all your sites. You can manage site-specific MCP access and overrides in individual site settings.' - ), - { - a: createElement( 'a', { - href: '/sites', - target: '_blank', - style: { textDecoration: 'underline' }, - } ), - } - ) } - + ) : ( + + { translate( 'No MCP tools are currently available.' ) } + + ) } + + { hasTools && anyToolsEnabled && ( + + { translate( 'Account-level MCP Tools' ) } + + { translate( 'Control which MCP tools are available across all your sites.' ) } + + + { tools.map( ( [ toolId, tool ] ) => ( + handleToolChange( toolId, checked ) } + label={ tool.title } + help={ tool.description } + disabled={ ! anyToolsEnabled } + /> + ) ) } - - -
- - ) } - - { hasTools && ( -
- - { isUpdating ? translate( 'Saving…' ) : translate( 'Save MCP tools' ) } - -
- ) } -
+ + ) } + + { hasTools && anyToolsEnabled && ( + + { translate( 'Site-specific MCP settings' ) } + + { createInterpolateElement( + translate( + 'Account-level tools work across all sites. Manage site-specific MCP settings to control which tools are available on a specific site.' + ), + { + a: , + } + ) } + + + ) } + + { hasTools && ( + + + { isUpdating ? translate( 'Saving…' ) : translate( 'Save MCP tools' ) } + + + ) } + + + + ); };