diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 7b86286a23..fe62db2515 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -1,5 +1,12 @@ import { lazy, Suspense } from 'react'; -import { Navigate, Route, Routes } from 'react-router-dom'; +import { + Navigate, + type Params, + Route, + Routes, + useLocation, + useParams, +} from 'react-router-dom'; import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsProtectedRouteWrapper'; import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; @@ -24,6 +31,55 @@ const SettingsRestPlayground = lazy(() => ), ); +// TODO: remove these legacy /api-webhooks redirects after 2026-08-04, once +// users have had time to update their bookmarks to the new API settings routes. +const LEGACY_API_WEBHOOKS_SETTINGS_PATHS = { + ApiWebhooks: 'api-webhooks', + NewApiKey: 'api-webhooks/apis/new', + ApiKeyDetail: 'api-webhooks/apis/:apiKeyId', + NewWebhook: 'api-webhooks/webhooks/new', + WebhookDetail: 'api-webhooks/webhooks/:webhookId', +} as const; + +type LegacySettingsPathRedirectProps = { + to: + | SettingsPath.ApiWebhooks + | SettingsPath.NewApiKey + | SettingsPath.ApiKeyDetail + | SettingsPath.NewWebhook + | SettingsPath.WebhookDetail; +}; + +const getLegacySettingsPathRedirectPathname = ( + to: LegacySettingsPathRedirectProps['to'], + params: Readonly>, +) => { + switch (to) { + case SettingsPath.ApiKeyDetail: + return getSettingsPath(SettingsPath.ApiKeyDetail, { + apiKeyId: params.apiKeyId ?? null, + }); + case SettingsPath.WebhookDetail: + return getSettingsPath(SettingsPath.WebhookDetail, { + webhookId: params.webhookId ?? null, + }); + case SettingsPath.ApiWebhooks: + case SettingsPath.NewApiKey: + case SettingsPath.NewWebhook: + return getSettingsPath(to); + } +}; + +function LegacySettingsPathRedirect({ to }: LegacySettingsPathRedirectProps) { + const location = useLocation(); + const params = useParams(); + const pathname = getLegacySettingsPathRedirectPathname(to, params); + + return ( + + ); +} + const SettingsAccountsConfiguration = lazy(() => import('~/pages/settings/accounts/SettingsAccountsConfiguration').then( (module) => ({ @@ -832,6 +888,30 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( /> } > + } + /> + } + /> + + } + /> + } + /> + + } + /> } diff --git a/packages/twenty-front/src/modules/marketplace/utils/__tests__/buildPermissionSummaryFromRoleManifest.test.ts b/packages/twenty-front/src/modules/marketplace/utils/__tests__/buildPermissionSummaryFromRoleManifest.test.ts index 0d13aaf8bf..10a96e263c 100644 --- a/packages/twenty-front/src/modules/marketplace/utils/__tests__/buildPermissionSummaryFromRoleManifest.test.ts +++ b/packages/twenty-front/src/modules/marketplace/utils/__tests__/buildPermissionSummaryFromRoleManifest.test.ts @@ -231,7 +231,7 @@ describe('buildPermissionSummaryFromRoleManifest', () => { const result = buildPermissionSummaryFromRoleManifest(role); expect(result).toHaveLength(1); - expect(result[0].label).toBe('Manage API keys and webhooks'); + expect(result[0].label).toBe('Manage MCP, API keys, and webhooks'); }); it('should ignore unknown permission flags', () => { diff --git a/packages/twenty-front/src/modules/marketplace/utils/buildPermissionSummaryFromRoleManifest.ts b/packages/twenty-front/src/modules/marketplace/utils/buildPermissionSummaryFromRoleManifest.ts index c46cdde52a..3a80f101c9 100644 --- a/packages/twenty-front/src/modules/marketplace/utils/buildPermissionSummaryFromRoleManifest.ts +++ b/packages/twenty-front/src/modules/marketplace/utils/buildPermissionSummaryFromRoleManifest.ts @@ -1,12 +1,12 @@ import { type RoleManifest } from 'twenty-shared/application'; import { isDefined } from 'twenty-shared/utils'; import { - IconCode, type IconComponent, IconCurrencyDollar, IconDatabase, IconHierarchy, IconKey, + IconPlug, IconSettings, IconSettingsAutomation, IconSparkles, @@ -112,8 +112,8 @@ export const buildPermissionSummaryFromRoleManifest = ( Icon: IconCurrencyDollar, }, [SystemPermissionFlag.API_KEYS_AND_WEBHOOKS]: { - label: 'Manage API keys and webhooks', - Icon: IconCode, + label: 'Manage MCP, API keys, and webhooks', + Icon: IconPlug, }, [SystemPermissionFlag.AI]: { label: 'Run AI agents', diff --git a/packages/twenty-front/src/modules/settings/components/SettingsEditableTitle.tsx b/packages/twenty-front/src/modules/settings/components/SettingsEditableTitle.tsx index 2daeb46840..defb1e97f5 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsEditableTitle.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsEditableTitle.tsx @@ -3,6 +3,7 @@ import { type TitleInputProps, } from '@/ui/input/components/TitleInput'; import { styled } from '@linaria/react'; +import { themeCssVariables } from 'twenty-ui/theme-constants'; const StyledTitleInputContainer = styled.div` max-width: 420px; @@ -18,6 +19,7 @@ export const SettingsEditableTitle = (props: SettingsEditableTitleProps) => ( // oxlint-disable-next-line react/jsx-props-no-spreading {...props} sizeVariant={props.sizeVariant ?? 'sm'} + textColor={props.textColor ?? themeCssVariables.font.color.primary} /> ); diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookForm.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookForm.tsx index ea6a08b212..5c4daa4664 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookForm.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookForm.tsx @@ -23,6 +23,7 @@ import { H2Title } from 'twenty-ui/typography'; import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { SETTINGS_API_WEBHOOKS_TABS } from '~/pages/settings/api-webhooks/constants/SettingsApiWebhooksTabs'; import { SettingsDatabaseEventsForm } from '@/settings/components/SettingsDatabaseEventsForm'; const DELETE_WEBHOOK_MODAL_ID = 'delete-webhook-modal'; @@ -81,8 +82,13 @@ export const SettingsDevelopersWebhookForm = ({ href: getSettingsPath(SettingsPath.General), }, { - children: t`APIs & Webhooks`, - href: getSettingsPath(SettingsPath.ApiWebhooks), + children: t`MCP & APIs`, + href: getSettingsPath( + SettingsPath.ApiWebhooks, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.WEBHOOKS, + ), }, { children: isCreationMode ? t`New` : getTitle() }, ]} @@ -90,7 +96,15 @@ export const SettingsDevelopersWebhookForm = ({ navigate(SettingsPath.ApiWebhooks)} + onCancel={() => + navigate( + SettingsPath.ApiWebhooks, + undefined, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.WEBHOOKS, + ) + } onSave={formConfig.handleSubmit(handleSave)} /> } diff --git a/packages/twenty-front/src/modules/settings/developers/hooks/useWebhookForm.ts b/packages/twenty-front/src/modules/settings/developers/hooks/useWebhookForm.ts index 95f3239bc0..ec3d9bcf50 100644 --- a/packages/twenty-front/src/modules/settings/developers/hooks/useWebhookForm.ts +++ b/packages/twenty-front/src/modules/settings/developers/hooks/useWebhookForm.ts @@ -26,6 +26,7 @@ import { UpdateWebhookDocument, } from '~/generated-metadata/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { SETTINGS_API_WEBHOOKS_TABS } from '~/pages/settings/api-webhooks/constants/SettingsApiWebhooksTabs'; type UseWebhookFormProps = { webhookId?: string; @@ -109,6 +110,11 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => { navigate( createdWebhook ? SettingsPath.WebhookDetail : SettingsPath.ApiWebhooks, createdWebhook ? { webhookId: createdWebhook.id } : undefined, + undefined, + undefined, + createdWebhook + ? undefined + : SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.WEBHOOKS, ); } catch (error) { enqueueErrorSnackBar({ @@ -197,7 +203,13 @@ export const useWebhookForm = ({ webhookId, mode }: UseWebhookFormProps) => { message: t`Webhook deleted successfully`, }); - navigate(SettingsPath.ApiWebhooks); + navigate( + SettingsPath.ApiWebhooks, + undefined, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.WEBHOOKS, + ); } catch (error) { enqueueErrorSnackBar({ apolloError: CombinedGraphQLErrors.is(error) ? error : undefined, diff --git a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx index 5243b1893f..4a795f4bd3 100644 --- a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx +++ b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx @@ -16,7 +16,7 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { t } from '@lingui/core/macro'; import { isNonEmptyString } from '@sniptt/guards'; import { - IconApi, + IconApps, IconAt, IconCalendarEvent, IconColorSwatch, @@ -148,9 +148,9 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { !isBillingEnabled || !permissionMap[PermissionFlagType.WORKSPACE], }, { - label: t`APIs & Webhooks`, + label: t`MCP & APIs`, path: SettingsPath.ApiWebhooks, - Icon: IconApi, + Icon: IconPlug, isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS], }, // TODO: Re-enable when integrations page is ready @@ -163,7 +163,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => { { label: t`Apps`, path: SettingsPath.Applications, - Icon: IconPlug, + Icon: IconApps, isHidden: !permissionMap[PermissionFlagType.APPLICATIONS], }, { diff --git a/packages/twenty-front/src/modules/settings/playground/assets/cover-dark.png b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/cover-dark.png similarity index 100% rename from packages/twenty-front/src/modules/settings/playground/assets/cover-dark.png rename to packages/twenty-front/src/modules/settings/mcp-and-apis/assets/cover-dark.png diff --git a/packages/twenty-front/src/modules/settings/playground/assets/cover-light.png b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/cover-light.png similarity index 100% rename from packages/twenty-front/src/modules/settings/playground/assets/cover-light.png rename to packages/twenty-front/src/modules/settings/mcp-and-apis/assets/cover-light.png diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/amazon-q.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/amazon-q.svg new file mode 100644 index 0000000000..927d49cfa4 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/amazon-q.svg @@ -0,0 +1,12 @@ + + + Icon-Architecture/64/Arch_Amazon-Q_64 + + + + + + + + + \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/augment-code.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/augment-code.svg new file mode 100644 index 0000000000..2438545ea4 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/augment-code.svg @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/cline.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/cline.svg new file mode 100644 index 0000000000..8dcc05786c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/cline.svg @@ -0,0 +1 @@ +Cline \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/cursor.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/cursor.svg new file mode 100644 index 0000000000..14085aac89 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/cursor.svg @@ -0,0 +1 @@ +Cursor \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/gemini-cli.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/gemini-cli.svg new file mode 100644 index 0000000000..dffc66158b --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/gemini-cli.svg @@ -0,0 +1,12 @@ + + Gemini CLI + + + + + + + + + + diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/goose.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/goose.svg new file mode 100644 index 0000000000..3088c697a3 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/goose.svg @@ -0,0 +1,4 @@ + + Goose + + diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/jetbrains.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/jetbrains.svg new file mode 100644 index 0000000000..f3fa841b6d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/jetbrains.svg @@ -0,0 +1 @@ +JetBrains \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/librechat.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/librechat.svg new file mode 100644 index 0000000000..36a536d654 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/librechat.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/lm-studio.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/lm-studio.svg new file mode 100644 index 0000000000..a2a179f7c0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/lm-studio.svg @@ -0,0 +1 @@ +LM Studio \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/raycast.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/raycast.svg new file mode 100644 index 0000000000..2ce034a0c5 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/raycast.svg @@ -0,0 +1 @@ +Raycast \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/replit.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/replit.svg new file mode 100644 index 0000000000..8689f42df6 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/replit.svg @@ -0,0 +1 @@ +Replit \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/vs-code.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/vs-code.svg new file mode 100644 index 0000000000..c453e633f3 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/vs-code.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/warp.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/warp.svg new file mode 100644 index 0000000000..8b12c83411 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/warp.svg @@ -0,0 +1 @@ +Warp \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/windsurf.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/windsurf.svg new file mode 100644 index 0000000000..e794f0c18e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/windsurf.svg @@ -0,0 +1 @@ +Windsurf \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/zed.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/zed.svg new file mode 100644 index 0000000000..44d35f7db0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-clients/zed.svg @@ -0,0 +1 @@ +Zed Industries \ No newline at end of file diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-cover-dark.png b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-cover-dark.png new file mode 100644 index 0000000000..abd258797a Binary files /dev/null and b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-cover-dark.png differ diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-cover-light.png b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-cover-light.png new file mode 100644 index 0000000000..28ab830959 Binary files /dev/null and b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/mcp-cover-light.png differ diff --git a/packages/twenty-front/src/modules/settings/playground/assets/model-context-protocol-logo.svg b/packages/twenty-front/src/modules/settings/mcp-and-apis/assets/model-context-protocol-logo.svg similarity index 100% rename from packages/twenty-front/src/modules/settings/playground/assets/model-context-protocol-logo.svg rename to packages/twenty-front/src/modules/settings/mcp-and-apis/assets/model-context-protocol-logo.svg diff --git a/packages/twenty-front/src/modules/settings/playground/components/GraphQLPlayground.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/GraphQLPlayground.tsx similarity index 89% rename from packages/twenty-front/src/modules/settings/playground/components/GraphQLPlayground.tsx rename to packages/twenty-front/src/modules/settings/mcp-and-apis/components/GraphQLPlayground.tsx index 06bbca026f..7fea4751a1 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/GraphQLPlayground.tsx +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/GraphQLPlayground.tsx @@ -1,9 +1,9 @@ -import '@/settings/playground/utils/setupGraphiqlMonacoWorkers'; +import '@/settings/mcp-and-apis/utils/setupGraphiqlMonacoWorkers'; import { isPlaygroundApiKeyFresh, playgroundApiKeyState, -} from '@/settings/playground/states/playgroundApiKeyState'; -import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas'; +} from '@/settings/mcp-and-apis/states/playgroundApiKeyState'; +import { PlaygroundSchemas } from '@/settings/mcp-and-apis/types/PlaygroundSchemas'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { styled } from '@linaria/react'; import { explorerPlugin } from '@graphiql/plugin-explorer'; diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/components/McpClientLogo.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/McpClientLogo.tsx new file mode 100644 index 0000000000..e109a4346a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/McpClientLogo.tsx @@ -0,0 +1,44 @@ +import { styled } from '@linaria/react'; + +import { IconModelClaude, IconProviderOpenai } from 'twenty-ui/icon'; +import { useTheme, useThemeColorScheme } from 'twenty-ui/theme-constants'; + +const StyledLogoImage = styled.img<{ isInverted: boolean }>` + display: block; + filter: ${({ isInverted }) => (isInverted ? 'invert(1)' : 'none')}; + height: 100%; + object-fit: contain; + width: 100%; +`; + +type McpClientLogoProps = { + invertInDarkMode?: boolean; + src: string; +}; + +export const McpClientLogo = ({ + invertInDarkMode = false, + src, +}: McpClientLogoProps) => { + const colorScheme = useThemeColorScheme(); + + return ( + + ); +}; + +export const McpClaudeLogo = () => { + const theme = useTheme(); + + return ; +}; + +export const McpOpenAiLogo = () => { + const theme = useTheme(); + + return ; +}; diff --git a/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/PlaygroundSetupForm.tsx similarity index 89% rename from packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx rename to packages/twenty-front/src/modules/settings/mcp-and-apis/components/PlaygroundSetupForm.tsx index 0770913274..d2249e95e4 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/PlaygroundSetupForm.tsx +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/PlaygroundSetupForm.tsx @@ -1,7 +1,7 @@ -import { useOpenPlayground } from '@/settings/playground/hooks/useOpenPlayground'; -import { SETTINGS_PLAYGROUND_FORM_SCHEMA_SELECT_OPTIONS } from '@/settings/playground/constants/SettingsPlaygroundFormSchemaSelectOptions'; -import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas'; -import { PlaygroundTypes } from '@/settings/playground/types/PlaygroundTypes'; +import { useOpenPlayground } from '@/settings/mcp-and-apis/hooks/useOpenPlayground'; +import { SETTINGS_PLAYGROUND_FORM_SCHEMA_SELECT_OPTIONS } from '@/settings/mcp-and-apis/constants/SettingsPlaygroundFormSchemaSelectOptions'; +import { PlaygroundSchemas } from '@/settings/mcp-and-apis/types/PlaygroundSchemas'; +import { PlaygroundTypes } from '@/settings/mcp-and-apis/types/PlaygroundTypes'; import { Select } from '@/ui/input/components/Select'; import { styled } from '@linaria/react'; import { zodResolver } from '@hookform/resolvers/zod'; diff --git a/packages/twenty-front/src/modules/settings/playground/components/RestPlayground.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/RestPlayground.tsx similarity index 92% rename from packages/twenty-front/src/modules/settings/playground/components/RestPlayground.tsx rename to packages/twenty-front/src/modules/settings/mcp-and-apis/components/RestPlayground.tsx index ac93178c04..997d15614f 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/RestPlayground.tsx +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/RestPlayground.tsx @@ -1,9 +1,9 @@ -import { RestPlaygroundSchemaFetchEffect } from '@/settings/playground/components/RestPlaygroundSchemaFetchEffect'; +import { RestPlaygroundSchemaFetchEffect } from '@/settings/mcp-and-apis/components/RestPlaygroundSchemaFetchEffect'; import { isPlaygroundApiKeyFresh, playgroundApiKeyState, -} from '@/settings/playground/states/playgroundApiKeyState'; -import { type PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas'; +} from '@/settings/mcp-and-apis/states/playgroundApiKeyState'; +import { type PlaygroundSchemas } from '@/settings/mcp-and-apis/types/PlaygroundSchemas'; import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue'; import { useContext, useState, lazy, Suspense } from 'react'; import { styled } from '@linaria/react'; diff --git a/packages/twenty-front/src/modules/settings/playground/components/RestPlaygroundSchemaFetchEffect.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/RestPlaygroundSchemaFetchEffect.tsx similarity index 92% rename from packages/twenty-front/src/modules/settings/playground/components/RestPlaygroundSchemaFetchEffect.tsx rename to packages/twenty-front/src/modules/settings/mcp-and-apis/components/RestPlaygroundSchemaFetchEffect.tsx index 4231ab7b90..79ebc3d5f2 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/RestPlaygroundSchemaFetchEffect.tsx +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/RestPlaygroundSchemaFetchEffect.tsx @@ -1,4 +1,4 @@ -import { type PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas'; +import { type PlaygroundSchemas } from '@/settings/mcp-and-apis/types/PlaygroundSchemas'; import { useEffect } from 'react'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/components/SettingsMcpSetup.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/SettingsMcpSetup.tsx new file mode 100644 index 0000000000..7e6552d148 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/SettingsMcpSetup.tsx @@ -0,0 +1,117 @@ +import { styled } from '@linaria/react'; +import { t } from '@lingui/core/macro'; +import { Fragment } from 'react'; + +import { LightCopyIconButton } from '@/object-record/record-field/ui/components/LightCopyIconButton'; +import ModelContextProtocolLogo from '@/settings/mcp-and-apis/assets/model-context-protocol-logo.svg?react'; +import { SettingsMcpSetupCard } from '@/settings/mcp-and-apis/components/SettingsMcpSetupCard'; +import { buildMcpSetupCategories } from '@/settings/mcp-and-apis/utils/buildMcpSetupCategories'; +import { + buildMcpConfig, + buildMcpServerUrl, + isHttpsUrl, +} from '@/settings/mcp-and-apis/utils/mcpSetup'; +import { CodeEditor, CoreEditorHeader } from 'twenty-ui/input'; +import { Section } from 'twenty-ui/layout'; +import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants'; +import { H2Title } from 'twenty-ui/typography'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; + +const StyledMcpEditorHeaderTitle = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[2]}; +`; + +const StyledMcpIcon = styled(ModelContextProtocolLogo)` + color: inherit; + flex-shrink: 0; + height: calc(${themeCssVariables.icon.size.md} * 1px); + width: calc(${themeCssVariables.icon.size.md} * 1px); +`; + +const StyledMcpSetupContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${themeCssVariables.spacing[10]}; +`; + +const StyledCardsGrid = styled.div` + display: grid; + gap: ${themeCssVariables.spacing[3]}; + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (max-width: ${MOBILE_VIEWPORT}px) { + grid-template-columns: 1fr; + } +`; + +export const SettingsMcpSetup = () => { + const mcpServerUrl = buildMcpServerUrl(REACT_APP_SERVER_BASE_URL); + const mcpConfig = buildMcpConfig(mcpServerUrl); + const categories = buildMcpSetupCategories({ + isHttpsInstallLinkEnabled: isHttpsUrl(mcpServerUrl), + mcpServerUrl, + }); + + return ( + + {categories.map((category) => ( + +
+ + + {category.cards.map((card) => ( + + ))} + +
+ + {category.showManualConfigurationAfter && ( +
+ + + + {t`MCP client configuration`} + , + ]} + rightNodes={[ + , + ]} + /> + +
+ )} +
+ ))} +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/components/SettingsMcpSetupCard.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/SettingsMcpSetupCard.tsx new file mode 100644 index 0000000000..6f669819de --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/SettingsMcpSetupCard.tsx @@ -0,0 +1,169 @@ +import { styled } from '@linaria/react'; +import { isDefined } from 'twenty-shared/utils'; + +import { type McpSetupCard } from '@/settings/mcp-and-apis/types/McpSetup'; +import { Pill } from 'twenty-ui/data-display'; +import { IconExternalLink } from 'twenty-ui/icon'; +import { + AppTooltip, + Card, + CardContent, + TooltipDelay, +} from 'twenty-ui/surfaces'; +import { themeCssVariables, useTheme } from 'twenty-ui/theme-constants'; + +const StyledCardContent = styled(CardContent)` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[4]}; + min-height: ${themeCssVariables.spacing[25]}; + min-width: 0; + padding: ${themeCssVariables.spacing[3]}; + padding-left: ${themeCssVariables.spacing[4]}; +`; + +const StyledLogo = styled.div` + align-items: center; + color: ${themeCssVariables.font.color.primary}; + display: flex; + flex: 0 0 ${themeCssVariables.spacing[8]}; + height: ${themeCssVariables.spacing[8]}; + justify-content: center; + width: ${themeCssVariables.spacing[8]}; +`; + +const StyledBody = styled.div` + display: flex; + flex: 1 1 0; + flex-direction: column; + gap: ${themeCssVariables.spacing[2]}; + min-width: 0; +`; + +const StyledHeader = styled.div` + align-items: center; + display: flex; + gap: ${themeCssVariables.spacing[2]}; + min-width: 0; +`; + +const StyledTitle = styled.div` + color: ${themeCssVariables.font.color.primary}; + font-size: ${themeCssVariables.font.size.md}; + font-weight: ${themeCssVariables.font.weight.medium}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const StyledActionSlot = styled.div` + display: flex; + flex: 0 0 auto; + margin-left: auto; +`; + +const StyledDescription = styled.div` + color: ${themeCssVariables.font.color.secondary}; + font-size: ${themeCssVariables.font.size.sm}; +`; + +// Install links use custom URI schemes (goose://, vscode:, lmstudio://) that +// twenty-ui Button/RawLink/RoundedLink strip through react-router or getSafeUrl, +// so the call to action has to stay a raw anchor with the untouched href. +const StyledInstallAction = styled.a` + align-items: center; + background: ${themeCssVariables.background.transparent.lighter}; + border: 1px solid ${themeCssVariables.background.transparent.medium}; + border-radius: ${themeCssVariables.border.radius.sm}; + box-sizing: border-box; + color: ${themeCssVariables.font.color.secondary}; + display: flex; + font-size: ${themeCssVariables.font.size.sm}; + font-weight: ${themeCssVariables.font.weight.medium}; + gap: ${themeCssVariables.spacing[1]}; + height: ${themeCssVariables.spacing[6]}; + padding: 0 ${themeCssVariables.spacing[2]}; + text-decoration: none; + white-space: nowrap; + + &:hover { + background: ${themeCssVariables.background.transparent.light}; + } + + &[aria-disabled='true'] { + color: ${themeCssVariables.font.color.light}; + cursor: not-allowed; + } +`; + +type SettingsMcpSetupCardActionProps = { + card: McpSetupCard; +}; + +const SettingsMcpSetupCardAction = ({ + card, +}: SettingsMcpSetupCardActionProps) => { + const theme = useTheme(); + + if (card.isDisabled === true) { + return ( + <> + + + {card.ctaLabel} + + {isDefined(card.disabledTooltip) && isDefined(card.tooltipId) && ( + + )} + + ); + } + + if (!isDefined(card.href)) { + return null; + } + + return ( + + + {card.ctaLabel} + + ); +}; + +type SettingsMcpSetupCardProps = { + card: McpSetupCard; +}; + +export const SettingsMcpSetupCard = ({ card }: SettingsMcpSetupCardProps) => ( + + + {card.logo} + + + {card.title} + + + + + + {card.description} + + + +); diff --git a/packages/twenty-front/src/modules/settings/playground/components/__stories__/GraphQLPlayground.stories.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/GraphQLPlayground.stories.tsx similarity index 88% rename from packages/twenty-front/src/modules/settings/playground/components/__stories__/GraphQLPlayground.stories.tsx rename to packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/GraphQLPlayground.stories.tsx index 47e997d5f5..4b705e8f06 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/__stories__/GraphQLPlayground.stories.tsx +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/GraphQLPlayground.stories.tsx @@ -1,5 +1,5 @@ -import { GraphQLPlayground } from '@/settings/playground/components/GraphQLPlayground'; -import { playgroundApiKeyState } from '@/settings/playground/states/playgroundApiKeyState'; +import { GraphQLPlayground } from '@/settings/mcp-and-apis/components/GraphQLPlayground'; +import { playgroundApiKeyState } from '@/settings/mcp-and-apis/states/playgroundApiKeyState'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { type Meta, type StoryObj } from '@storybook/react-vite'; import { useEffect } from 'react'; diff --git a/packages/twenty-front/src/modules/settings/playground/components/__stories__/PlaygroundSetupForm.stories.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/PlaygroundSetupForm.stories.tsx similarity index 89% rename from packages/twenty-front/src/modules/settings/playground/components/__stories__/PlaygroundSetupForm.stories.tsx rename to packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/PlaygroundSetupForm.stories.tsx index aacfbd9d99..9838e32db2 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/__stories__/PlaygroundSetupForm.stories.tsx +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/PlaygroundSetupForm.stories.tsx @@ -1,4 +1,4 @@ -import { PlaygroundSetupForm } from '@/settings/playground/components/PlaygroundSetupForm'; +import { PlaygroundSetupForm } from '@/settings/mcp-and-apis/components/PlaygroundSetupForm'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { type Meta, type StoryObj } from '@storybook/react-vite'; import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing'; diff --git a/packages/twenty-front/src/modules/settings/playground/components/__stories__/RestPlayground.stories.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/RestPlayground.stories.tsx similarity index 92% rename from packages/twenty-front/src/modules/settings/playground/components/__stories__/RestPlayground.stories.tsx rename to packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/RestPlayground.stories.tsx index 3ede3a59dd..54664b2b39 100644 --- a/packages/twenty-front/src/modules/settings/playground/components/__stories__/RestPlayground.stories.tsx +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/RestPlayground.stories.tsx @@ -1,6 +1,6 @@ -import { RestPlayground } from '@/settings/playground/components/RestPlayground'; -import { playgroundApiKeyState } from '@/settings/playground/states/playgroundApiKeyState'; -import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas'; +import { RestPlayground } from '@/settings/mcp-and-apis/components/RestPlayground'; +import { playgroundApiKeyState } from '@/settings/mcp-and-apis/states/playgroundApiKeyState'; +import { PlaygroundSchemas } from '@/settings/mcp-and-apis/types/PlaygroundSchemas'; import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState'; import { type Meta, type StoryObj } from '@storybook/react-vite'; import { HttpResponse, http } from 'msw'; diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/SettingsMcpSetup.stories.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/SettingsMcpSetup.stories.tsx new file mode 100644 index 0000000000..4ec4a0f7c4 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/components/__stories__/SettingsMcpSetup.stories.tsx @@ -0,0 +1,24 @@ +import { SettingsMcpSetup } from '@/settings/mcp-and-apis/components/SettingsMcpSetup'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; +import { type Meta, type StoryObj } from '@storybook/react-vite'; +import { ComponentDecorator } from 'twenty-ui/testing'; + +const meta: Meta = { + title: 'Modules/Settings/Playground/SettingsMcpSetup', + component: SettingsMcpSetup, + decorators: [ComponentDecorator, SnackBarDecorator], + parameters: { + docs: { + description: { + component: + 'SettingsMcpSetup lists the MCP clients that can connect to the workspace, with quick-install and manual configuration options.', + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/constants/McpSetup.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/constants/McpSetup.ts new file mode 100644 index 0000000000..d136be64cf --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/constants/McpSetup.ts @@ -0,0 +1,30 @@ +export const MCP_SETUP = { + chatGptTwentyAppUrl: + 'https://chatgpt.com/apps/twenty/asdk_app_6a0ac8d7e28c8191a58ea65bb0ca3d5c', + tooltipIds: { + claudeInstallDisabled: 'mcp-claude-install-disabled', + replitInstallDisabled: 'mcp-replit-install-disabled', + }, + authorizationHeader: { + key: 'Authorization', + value: 'Bearer ', + }, + server: { + name: 'twenty', + displayName: 'Twenty', + }, + clientDocsUrls: { + augment: 'https://docs.augmentcode.com/setup-augment/mcp', + amazonQ: + 'https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/qdev-mcp.html', + cline: 'https://docs.cline.bot/mcp/mcp-overview', + geminiCli: 'https://geminicli.com/docs/tools/mcp-server/', + jetbrains: 'https://www.jetbrains.com/help/ai-assistant/mcp.html', + libreChat: 'https://www.librechat.ai/docs/features/mcp', + lmStudio: 'https://lmstudio.ai/docs/app/mcp', + raycast: 'https://manual.raycast.com/ai/model-context-protocol', + warp: 'https://docs.warp.dev/agent-platform/capabilities/mcp/', + windsurf: 'https://docs.devin.ai/desktop/cascade/mcp', + zed: 'https://zed.dev/docs/ai/mcp', + }, +} as const; diff --git a/packages/twenty-front/src/modules/settings/playground/constants/SettingsPlaygroundFormSchemaSelectOptions.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/constants/SettingsPlaygroundFormSchemaSelectOptions.ts similarity index 81% rename from packages/twenty-front/src/modules/settings/playground/constants/SettingsPlaygroundFormSchemaSelectOptions.ts rename to packages/twenty-front/src/modules/settings/mcp-and-apis/constants/SettingsPlaygroundFormSchemaSelectOptions.ts index 7e2a5755ba..2e083f58c6 100644 --- a/packages/twenty-front/src/modules/settings/playground/constants/SettingsPlaygroundFormSchemaSelectOptions.ts +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/constants/SettingsPlaygroundFormSchemaSelectOptions.ts @@ -1,4 +1,4 @@ -import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas'; +import { PlaygroundSchemas } from '@/settings/mcp-and-apis/types/PlaygroundSchemas'; import { msg } from '@lingui/core/macro'; import { IconBracketsAngle, IconFolderRoot } from 'twenty-ui/icon'; export const SETTINGS_PLAYGROUND_FORM_SCHEMA_SELECT_OPTIONS = [ diff --git a/packages/twenty-front/src/modules/settings/playground/hooks/useOpenPlayground.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/hooks/useOpenPlayground.ts similarity index 89% rename from packages/twenty-front/src/modules/settings/playground/hooks/useOpenPlayground.ts rename to packages/twenty-front/src/modules/settings/mcp-and-apis/hooks/useOpenPlayground.ts index fdfa87243c..d48c3baaed 100644 --- a/packages/twenty-front/src/modules/settings/playground/hooks/useOpenPlayground.ts +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/hooks/useOpenPlayground.ts @@ -7,9 +7,9 @@ import { isDefined } from 'twenty-shared/utils'; import { isPlaygroundApiKeyFresh, playgroundApiKeyState, -} from '@/settings/playground/states/playgroundApiKeyState'; -import { type PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas'; -import { PlaygroundTypes } from '@/settings/playground/types/PlaygroundTypes'; +} from '@/settings/mcp-and-apis/states/playgroundApiKeyState'; +import { type PlaygroundSchemas } from '@/settings/mcp-and-apis/types/PlaygroundSchemas'; +import { PlaygroundTypes } from '@/settings/mcp-and-apis/types/PlaygroundTypes'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; diff --git a/packages/twenty-front/src/modules/settings/playground/states/openAPIReference.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/states/openAPIReference.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/playground/states/openAPIReference.ts rename to packages/twenty-front/src/modules/settings/mcp-and-apis/states/openAPIReference.ts diff --git a/packages/twenty-front/src/modules/settings/playground/states/playgroundApiKeyState.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/states/playgroundApiKeyState.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/playground/states/playgroundApiKeyState.ts rename to packages/twenty-front/src/modules/settings/mcp-and-apis/states/playgroundApiKeyState.ts diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/types/McpSetup.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/types/McpSetup.ts new file mode 100644 index 0000000000..60a7ade322 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/types/McpSetup.ts @@ -0,0 +1,20 @@ +import { type ReactNode } from 'react'; + +export type McpSetupCard = { + badge: string; + ctaLabel: string; + description: string; + disabledTooltip?: string; + href?: string; + isDisabled?: boolean; + logo: ReactNode; + title: string; + tooltipId?: string; +}; + +export type McpSetupCategory = { + cards: McpSetupCard[]; + description: string; + showManualConfigurationAfter?: boolean; + title: string; +}; diff --git a/packages/twenty-front/src/modules/settings/playground/types/PlaygroundSchemas.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/types/PlaygroundSchemas.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/playground/types/PlaygroundSchemas.ts rename to packages/twenty-front/src/modules/settings/mcp-and-apis/types/PlaygroundSchemas.ts diff --git a/packages/twenty-front/src/modules/settings/playground/types/PlaygroundTypes.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/types/PlaygroundTypes.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/playground/types/PlaygroundTypes.ts rename to packages/twenty-front/src/modules/settings/mcp-and-apis/types/PlaygroundTypes.ts diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/__tests__/mcpSetup.test.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/__tests__/mcpSetup.test.ts new file mode 100644 index 0000000000..fd176d48a8 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/__tests__/mcpSetup.test.ts @@ -0,0 +1,154 @@ +import { + buildClaudeInstallLink, + buildCursorInstallLink, + buildGooseInstallLink, + buildLmStudioInstallLink, + buildMcpConfig, + buildMcpServerUrl, + buildReplitInstallLink, + buildRemoteMcpServerConfig, + buildVsCodeInstallLink, + isHttpsUrl, +} from '@/settings/mcp-and-apis/utils/mcpSetup'; + +const mcpServerUrl = 'https://api.twenty.com/mcp'; + +const decodeBase64JsonParam = (link: string, paramName: string) => { + const params = new URLSearchParams(link.split('?')[1]); + const encodedValue = params.get(paramName); + + if (encodedValue === null) { + throw new Error(`Missing ${paramName} param`); + } + + return JSON.parse(Buffer.from(encodedValue, 'base64').toString('utf-8')); +}; + +describe('buildMcpServerUrl', () => { + it('appends /mcp after trimming trailing slashes', () => { + expect(buildMcpServerUrl('https://api.twenty.com///')).toBe(mcpServerUrl); + }); +}); + +describe('isHttpsUrl', () => { + it('returns true only for valid HTTPS urls', () => { + expect(isHttpsUrl(mcpServerUrl)).toBe(true); + expect(isHttpsUrl('http://api.twenty.com/mcp')).toBe(false); + expect(isHttpsUrl('not a url')).toBe(false); + }); +}); + +describe('buildMcpConfig', () => { + it('builds the remote MCP JSON config', () => { + expect(JSON.parse(buildMcpConfig(mcpServerUrl))).toEqual({ + mcpServers: { + twenty: { + url: mcpServerUrl, + headers: { + Authorization: 'Bearer ', + }, + }, + }, + }); + }); +}); + +describe('buildRemoteMcpServerConfig', () => { + it('builds the common remote server config', () => { + expect(buildRemoteMcpServerConfig(mcpServerUrl)).toEqual({ + url: mcpServerUrl, + headers: { + Authorization: 'Bearer ', + }, + }); + }); +}); + +describe('buildClaudeInstallLink', () => { + it('builds a Claude custom connector link', () => { + const link = buildClaudeInstallLink(mcpServerUrl); + const params = new URL(link).searchParams; + + expect(link.startsWith('https://claude.ai/customize/connectors?')).toBe( + true, + ); + expect(params.get('modal')).toBe('add-custom-connector'); + expect(params.get('connectorName')).toBe('Twenty'); + expect(params.get('connectorUrl')).toBe(mcpServerUrl); + }); +}); + +describe('buildCursorInstallLink', () => { + it('base64-encodes the remote MCP config', () => { + const link = buildCursorInstallLink(mcpServerUrl); + + expect(link.startsWith('https://cursor.com/en/install-mcp?')).toBe(true); + expect(new URLSearchParams(link.split('?')[1]).get('name')).toBe('twenty'); + expect(decodeBase64JsonParam(link, 'config')).toEqual( + buildRemoteMcpServerConfig(mcpServerUrl), + ); + }); +}); + +describe('buildVsCodeInstallLink', () => { + it('URI-encodes the VS Code MCP install payload', () => { + const payload = JSON.parse( + decodeURIComponent( + buildVsCodeInstallLink(mcpServerUrl).replace('vscode:mcp/install?', ''), + ), + ); + + expect(payload).toEqual({ + name: 'twenty', + type: 'http', + url: mcpServerUrl, + headers: { + Authorization: 'Bearer ', + }, + }); + }); +}); + +describe('buildGooseInstallLink', () => { + it('builds a Goose streamable HTTP extension link', () => { + const link = buildGooseInstallLink(mcpServerUrl); + const params = new URLSearchParams(link.split('?')[1]); + + expect(link.startsWith('goose://extension?')).toBe(true); + expect(params.get('type')).toBe('streamable_http'); + expect(params.get('id')).toBe('twenty'); + expect(params.get('name')).toBe('Twenty'); + expect(params.get('url')).toBe(mcpServerUrl); + expect(params.get('header')).toBe('Authorization=Bearer '); + }); +}); + +describe('buildReplitInstallLink', () => { + it('base64-encodes the Replit MCP payload', () => { + const link = buildReplitInstallLink(mcpServerUrl); + + expect(link.startsWith('https://replit.com/integrations?')).toBe(true); + expect(decodeBase64JsonParam(link, 'mcp')).toEqual({ + displayName: 'Twenty', + baseUrl: mcpServerUrl, + headers: [ + { + key: 'Authorization', + value: 'Bearer ', + }, + ], + }); + }); +}); + +describe('buildLmStudioInstallLink', () => { + it('base64-encodes the remote MCP config', () => { + const link = buildLmStudioInstallLink(mcpServerUrl); + + expect(link.startsWith('lmstudio://add_mcp?')).toBe(true); + expect(new URLSearchParams(link.split('?')[1]).get('name')).toBe('twenty'); + expect(decodeBase64JsonParam(link, 'config')).toEqual( + buildRemoteMcpServerConfig(mcpServerUrl), + ); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/buildMcpSetupCategories.tsx b/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/buildMcpSetupCategories.tsx new file mode 100644 index 0000000000..065f5944fa --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/buildMcpSetupCategories.tsx @@ -0,0 +1,202 @@ +import { t } from '@lingui/core/macro'; + +import AmazonQLogo from '@/settings/mcp-and-apis/assets/mcp-clients/amazon-q.svg'; +import AugmentCodeLogo from '@/settings/mcp-and-apis/assets/mcp-clients/augment-code.svg'; +import ClineLogo from '@/settings/mcp-and-apis/assets/mcp-clients/cline.svg'; +import CursorLogo from '@/settings/mcp-and-apis/assets/mcp-clients/cursor.svg'; +import GeminiCliLogo from '@/settings/mcp-and-apis/assets/mcp-clients/gemini-cli.svg'; +import GooseLogo from '@/settings/mcp-and-apis/assets/mcp-clients/goose.svg'; +import JetBrainsLogo from '@/settings/mcp-and-apis/assets/mcp-clients/jetbrains.svg'; +import LibreChatLogo from '@/settings/mcp-and-apis/assets/mcp-clients/librechat.svg'; +import LmStudioLogo from '@/settings/mcp-and-apis/assets/mcp-clients/lm-studio.svg'; +import RaycastLogo from '@/settings/mcp-and-apis/assets/mcp-clients/raycast.svg'; +import ReplitLogo from '@/settings/mcp-and-apis/assets/mcp-clients/replit.svg'; +import VsCodeLogo from '@/settings/mcp-and-apis/assets/mcp-clients/vs-code.svg'; +import WarpLogo from '@/settings/mcp-and-apis/assets/mcp-clients/warp.svg'; +import WindsurfLogo from '@/settings/mcp-and-apis/assets/mcp-clients/windsurf.svg'; +import ZedLogo from '@/settings/mcp-and-apis/assets/mcp-clients/zed.svg'; +import { + McpClaudeLogo, + McpClientLogo, + McpOpenAiLogo, +} from '@/settings/mcp-and-apis/components/McpClientLogo'; +import { MCP_SETUP } from '@/settings/mcp-and-apis/constants/McpSetup'; +import { type McpSetupCategory } from '@/settings/mcp-and-apis/types/McpSetup'; +import { + buildClaudeInstallLink, + buildCursorInstallLink, + buildGooseInstallLink, + buildLmStudioInstallLink, + buildReplitInstallLink, + buildVsCodeInstallLink, +} from '@/settings/mcp-and-apis/utils/mcpSetup'; + +type BuildMcpSetupCategoriesParams = { + isHttpsInstallLinkEnabled: boolean; + mcpServerUrl: string; +}; + +export const buildMcpSetupCategories = ({ + isHttpsInstallLinkEnabled, + mcpServerUrl, +}: BuildMcpSetupCategoriesParams): McpSetupCategory[] => [ + { + title: t`Quick install`, + description: t`Open a maintained integration or prefill clients that accept MCP install links.`, + showManualConfigurationAfter: true, + cards: [ + { + title: t`ChatGPT`, + badge: t`Official app`, + description: t`Open Twenty's official ChatGPT integration for your workspace.`, + ctaLabel: t`Open`, + href: MCP_SETUP.chatGptTwentyAppUrl, + logo: , + }, + { + title: t`Claude`, + badge: t`Preset link`, + description: t`Open Claude with the Twenty connector name and MCP URL prefilled.`, + ctaLabel: t`Install`, + disabledTooltip: t`Claude install links require an HTTPS MCP URL.`, + href: isHttpsInstallLinkEnabled + ? buildClaudeInstallLink(mcpServerUrl) + : undefined, + isDisabled: !isHttpsInstallLinkEnabled, + logo: , + tooltipId: MCP_SETUP.tooltipIds.claudeInstallDisabled, + }, + { + title: t`Cursor`, + badge: t`Install link`, + description: t`Open Cursor's MCP installer with the Twenty remote server config.`, + ctaLabel: t`Install`, + href: buildCursorInstallLink(mcpServerUrl), + logo: , + }, + { + title: t`VS Code`, + badge: t`Install link`, + description: t`Install the remote MCP server into a VS Code profile.`, + ctaLabel: t`Install`, + href: buildVsCodeInstallLink(mcpServerUrl), + logo: , + }, + { + title: t`Goose`, + badge: t`Install link`, + description: t`Add Twenty as a Streamable HTTP extension in Goose.`, + ctaLabel: t`Install`, + href: buildGooseInstallLink(mcpServerUrl), + logo: , + }, + { + title: t`Replit Agent`, + badge: t`Install link`, + description: t`Open Replit with the Twenty remote server config.`, + ctaLabel: t`Install`, + disabledTooltip: t`Replit install links require an HTTPS MCP URL.`, + href: isHttpsInstallLinkEnabled + ? buildReplitInstallLink(mcpServerUrl) + : undefined, + isDisabled: !isHttpsInstallLinkEnabled, + logo: , + tooltipId: MCP_SETUP.tooltipIds.replitInstallDisabled, + }, + { + title: t`LM Studio`, + badge: t`Install link`, + description: t`Add the Twenty remote server with LM Studio's install link.`, + ctaLabel: t`Install`, + href: buildLmStudioInstallLink(mcpServerUrl), + logo: , + }, + ], + }, + { + title: t`Other manual clients`, + description: t`Follow client docs to connect your MCP manually.`, + cards: [ + { + title: t`Cline`, + badge: t`Manual`, + description: t`Configure the Twenty MCP server manually in Cline.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.cline, + logo: , + }, + { + title: t`Zed`, + badge: t`Manual`, + description: t`Add the Twenty MCP server as a remote context server in Zed.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.zed, + logo: , + }, + { + title: t`Windsurf / Cascade`, + badge: t`Manual`, + description: t`Configure the Twenty MCP server manually in Cascade.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.windsurf, + logo: , + }, + { + title: t`Raycast`, + badge: t`Settings`, + description: t`Add the Twenty MCP server from Raycast's MCP commands or settings.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.raycast, + logo: , + }, + { + title: t`Warp`, + badge: t`Settings`, + description: t`Configure the remote MCP server through Warp's agent settings.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.warp, + logo: , + }, + { + title: t`JetBrains AI / Junie`, + badge: t`Settings`, + description: t`Import or add the MCP configuration from JetBrains settings.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.jetbrains, + logo: , + }, + { + title: t`Gemini CLI`, + badge: t`CLI config`, + description: t`Add the Twenty MCP server in Gemini CLI settings.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.geminiCli, + logo: , + }, + { + title: t`Amazon Q Developer CLI`, + badge: t`CLI config`, + description: t`Configure the remote server in Amazon Q Developer's MCP settings.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.amazonQ, + logo: , + }, + { + title: t`LibreChat`, + badge: t`YAML config`, + description: t`Add the Twenty MCP server to your LibreChat configuration file.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.libreChat, + logo: , + }, + { + title: t`Augment Code`, + badge: t`Settings`, + description: t`Add the Twenty MCP server from Augment Code settings.`, + ctaLabel: t`Docs`, + href: MCP_SETUP.clientDocsUrls.augment, + logo: , + }, + ], + }, +]; diff --git a/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/mcpSetup.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/mcpSetup.ts new file mode 100644 index 0000000000..deb05d56d6 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/mcpSetup.ts @@ -0,0 +1,113 @@ +import { MCP_SETUP } from '@/settings/mcp-and-apis/constants/McpSetup'; + +export const buildMcpServerUrl = (serverBaseUrl: string) => + `${serverBaseUrl.replace(/\/+$/, '')}/mcp`; + +export const isHttpsUrl = (url: string) => { + try { + return new URL(url).protocol === 'https:'; + } catch { + return false; + } +}; + +export const buildMcpAuthorizationHeaders = () => ({ + [MCP_SETUP.authorizationHeader.key]: MCP_SETUP.authorizationHeader.value, +}); + +export const buildRemoteMcpServerConfig = (mcpServerUrl: string) => ({ + url: mcpServerUrl, + headers: buildMcpAuthorizationHeaders(), +}); + +export const buildMcpConfig = (mcpServerUrl: string) => + JSON.stringify( + { + mcpServers: { + [MCP_SETUP.server.name]: buildRemoteMcpServerConfig(mcpServerUrl), + }, + }, + null, + 2, + ); + +export const buildClaudeInstallLink = (mcpServerUrl: string) => { + const params = new URLSearchParams({ + modal: 'add-custom-connector', + connectorName: MCP_SETUP.server.displayName, + connectorUrl: mcpServerUrl, + }); + + return `https://claude.ai/customize/connectors?${params.toString()}`; +}; + +export const buildCursorInstallLink = (mcpServerUrl: string) => { + const config = btoa(JSON.stringify(buildRemoteMcpServerConfig(mcpServerUrl))); + + const params = new URLSearchParams({ + name: MCP_SETUP.server.name, + config, + }); + + return `https://cursor.com/en/install-mcp?${params.toString()}`; +}; + +export const buildVsCodeInstallLink = (mcpServerUrl: string) => + `vscode:mcp/install?${encodeURIComponent( + JSON.stringify({ + name: MCP_SETUP.server.name, + type: 'http', + url: mcpServerUrl, + headers: buildMcpAuthorizationHeaders(), + }), + )}`; + +export const buildGooseInstallLink = (mcpServerUrl: string) => { + const params = new URLSearchParams({ + url: mcpServerUrl, + type: 'streamable_http', + timeout: '300', + id: MCP_SETUP.server.name, + name: MCP_SETUP.server.displayName, + description: 'Access your Twenty workspace through MCP', + }); + + params.append( + 'header', + `${MCP_SETUP.authorizationHeader.key}=${MCP_SETUP.authorizationHeader.value}`, + ); + + return `goose://extension?${params.toString()}`; +}; + +export const buildReplitInstallLink = (mcpServerUrl: string) => { + const payload = btoa( + JSON.stringify({ + displayName: MCP_SETUP.server.displayName, + baseUrl: mcpServerUrl, + headers: [ + { + key: MCP_SETUP.authorizationHeader.key, + value: MCP_SETUP.authorizationHeader.value, + }, + ], + }), + ); + + const params = new URLSearchParams({ + mcp: payload, + }); + + return `https://replit.com/integrations?${params.toString()}`; +}; + +export const buildLmStudioInstallLink = (mcpServerUrl: string) => { + const config = btoa(JSON.stringify(buildRemoteMcpServerConfig(mcpServerUrl))); + + const params = new URLSearchParams({ + name: MCP_SETUP.server.name, + config, + }); + + return `lmstudio://add_mcp?${params.toString()}`; +}; diff --git a/packages/twenty-front/src/modules/settings/playground/utils/setupGraphiqlMonacoWorkers.ts b/packages/twenty-front/src/modules/settings/mcp-and-apis/utils/setupGraphiqlMonacoWorkers.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/playground/utils/setupGraphiqlMonacoWorkers.ts rename to packages/twenty-front/src/modules/settings/mcp-and-apis/utils/setupGraphiqlMonacoWorkers.ts diff --git a/packages/twenty-front/src/modules/settings/playground/components/SettingsMcpSetup.tsx b/packages/twenty-front/src/modules/settings/playground/components/SettingsMcpSetup.tsx deleted file mode 100644 index 35a62d37c0..0000000000 --- a/packages/twenty-front/src/modules/settings/playground/components/SettingsMcpSetup.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { styled } from '@linaria/react'; -import { useLingui } from '@lingui/react/macro'; -import { LightCopyIconButton } from '@/object-record/record-field/ui/components/LightCopyIconButton'; -import ModelContextProtocolLogo from '@/settings/playground/assets/model-context-protocol-logo.svg?react'; -import { H2Title } from 'twenty-ui/typography'; -import { CodeEditor, CoreEditorHeader } from 'twenty-ui/input'; -import { Section } from 'twenty-ui/layout'; -import { themeCssVariables } from 'twenty-ui/theme-constants'; -import { REACT_APP_SERVER_BASE_URL } from '~/config'; - -const StyledMcpEditorHeaderTitle = styled.div` - align-items: center; - display: flex; - gap: ${themeCssVariables.spacing[2]}; -`; - -const StyledMcpIcon = styled(ModelContextProtocolLogo)` - color: inherit; - flex-shrink: 0; - height: calc(${themeCssVariables.icon.size.md} * 1px); - width: calc(${themeCssVariables.icon.size.md} * 1px); -`; - -const buildMcpConfig = (serverUrl: string) => - `{ - "mcpServers": { - "twenty": { - "url": "${serverUrl}/mcp", - "headers": { - "Authorization": "Bearer " - } - } - } -}`; - -export const SettingsMcpSetup = () => { - const { t } = useLingui(); - const mcpConfig = buildMcpConfig(REACT_APP_SERVER_BASE_URL); - - return ( -
- - - - {t`MCP client configuration`} - , - ]} - rightNodes={[]} - /> - -
- ); -}; diff --git a/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts index ddd26339c5..ce14cea0c3 100644 --- a/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts +++ b/packages/twenty-front/src/modules/settings/roles/role-permissions/permission-flags/hooks/useSettingsRolePermissionFlagConfig.ts @@ -3,12 +3,12 @@ import { t } from '@lingui/core/macro'; import { useMemo } from 'react'; import { IconApps, - IconCode, IconCreditCard, IconHierarchy, IconKey, IconLayoutSidebarRightCollapse, IconLockOpen, + IconPlug, IconSettings, IconSettingsAutomation, IconShield, @@ -41,9 +41,9 @@ export const useSettingsRolePermissionFlagConfig = ({ const allPermissions: SettingsRolePermissionsSettingPermission[] = [ { key: PermissionFlagType.API_KEYS_AND_WEBHOOKS, - name: t`API Keys & Webhooks`, - description: t`Manage API keys and webhooks`, - Icon: IconCode, + name: t`MCP & APIs`, + description: t`Manage MCP, API keys, and webhooks`, + Icon: IconPlug, isRelevantForAgents: true, isRelevantForApiKeys: true, isRelevantForUsers: true, diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAiOverviewTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAiOverviewTab.tsx index f9c828dd6c..f7a1cf8912 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAiOverviewTab.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAiOverviewTab.tsx @@ -9,7 +9,12 @@ import { useMutation, useQuery } from '@apollo/client/react'; import { styled } from '@linaria/react'; import { t } from '@lingui/core/macro'; import { Fragment, useContext, useState } from 'react'; -import { IconMessage, IconRobot, IconSparkles, IconTool } from 'twenty-ui/icon'; +import { + IconMessage, + IconSparkle2, + IconSparkles, + IconTool, +} from 'twenty-ui/icon'; import { H2Title } from 'twenty-ui/typography'; import { Section } from 'twenty-ui/layout'; import { UndecoratedLink } from 'twenty-ui/navigation'; @@ -116,7 +121,7 @@ export const SettingsAiOverviewTab = () => { /> } + Icon={} title={t`Set up MCP`} /> diff --git a/packages/twenty-front/src/pages/settings/api-webhooks/SettingsApiWebhooks.tsx b/packages/twenty-front/src/pages/settings/api-webhooks/SettingsApiWebhooks.tsx index 8db5e44fb0..510fc78a5c 100644 --- a/packages/twenty-front/src/pages/settings/api-webhooks/SettingsApiWebhooks.tsx +++ b/packages/twenty-front/src/pages/settings/api-webhooks/SettingsApiWebhooks.tsx @@ -2,11 +2,13 @@ import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscove import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable'; import { SettingsWebhooksTable } from '@/settings/developers/components/SettingsWebhooksTable'; -import { PlaygroundSetupForm } from '@/settings/playground/components/PlaygroundSetupForm'; -import { SettingsMcpSetup } from '@/settings/playground/components/SettingsMcpSetup'; +import { PlaygroundSetupForm } from '@/settings/mcp-and-apis/components/PlaygroundSetupForm'; +import { SettingsMcpSetup } from '@/settings/mcp-and-apis/components/SettingsMcpSetup'; +import McpCoverDark from '@/settings/mcp-and-apis/assets/mcp-cover-dark.png'; +import McpCoverLight from '@/settings/mcp-and-apis/assets/mcp-cover-light.png'; import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout'; -import PlaygroundCoverDark from '@/settings/playground/assets/cover-dark.png'; -import PlaygroundCoverLight from '@/settings/playground/assets/cover-light.png'; +import PlaygroundCoverDark from '@/settings/mcp-and-apis/assets/cover-dark.png'; +import PlaygroundCoverLight from '@/settings/mcp-and-apis/assets/cover-light.png'; import { SettingsTabBar } from '@/settings/components/layout/SettingsTabBar'; import { useSettingsActiveTabId } from '@/settings/components/layout/useSettingsActiveTabId'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; @@ -16,9 +18,9 @@ import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { IconBrandGraphql, - IconCode, IconPlus, - IconRobot, + IconPlug, + IconSparkle2, IconWebhook, } from 'twenty-ui/icon'; import { H2Title } from 'twenty-ui/typography'; @@ -60,15 +62,15 @@ export const SettingsApiWebhooks = () => { const { t } = useLingui(); const tabs = [ - { - id: SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, - title: t`API`, - Icon: IconCode, - }, { id: SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.MCP, title: t`MCP`, - Icon: IconRobot, + Icon: IconSparkle2, + }, + { + id: SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + title: t`API`, + Icon: IconPlug, }, { id: SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.WEBHOOKS, @@ -81,11 +83,13 @@ export const SettingsApiWebhooks = () => { (useSettingsActiveTabId( SETTINGS_API_WEBHOOKS_TABS.COMPONENT_INSTANCE_ID, tabs.map((tab) => tab.id), - ) as TabKey) ?? SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API; + ) as TabKey) ?? SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.MCP; + + const isMcpTab = activeTab === SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.MCP; return ( { children: t`Workspace`, href: getSettingsPath(SettingsPath.General), }, - { children: t`APIs & Webhooks` }, + { children: t`MCP & APIs` }, ]} >
diff --git a/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsGraphQLPlayground.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsGraphQLPlayground.stories.tsx index be026dfde4..b7c81b8470 100644 --- a/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsGraphQLPlayground.stories.tsx +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsGraphQLPlayground.stories.tsx @@ -1,4 +1,4 @@ -import { playgroundApiKeyState } from '@/settings/playground/states/playgroundApiKeyState'; +import { playgroundApiKeyState } from '@/settings/mcp-and-apis/states/playgroundApiKeyState'; import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore'; import { type Meta, type StoryObj } from '@storybook/react-vite'; import { action } from 'storybook/actions'; diff --git a/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsRestPlayground.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsRestPlayground.stories.tsx index 137474c094..8341a14a11 100644 --- a/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsRestPlayground.stories.tsx +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/playground/SettingsRestPlayground.stories.tsx @@ -1,4 +1,4 @@ -import { playgroundApiKeyState } from '@/settings/playground/states/playgroundApiKeyState'; +import { playgroundApiKeyState } from '@/settings/mcp-and-apis/states/playgroundApiKeyState'; import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore'; import { type Meta, type StoryObj } from '@storybook/react-vite'; import { action } from 'storybook/actions'; diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx index 83103b6006..778c0f5f8f 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx +++ b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx @@ -36,6 +36,7 @@ import { RevokeApiKeyDocument, } from '~/generated-metadata/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { SETTINGS_API_WEBHOOKS_TABS } from '~/pages/settings/api-webhooks/constants/SettingsApiWebhooksTabs'; const StyledInfo = styled.span` color: ${themeCssVariables.font.color.light}; @@ -145,7 +146,13 @@ export const SettingsDevelopersApiKeyDetail = () => { }, }); if (redirect) { - navigate(SettingsPath.ApiWebhooks); + navigate( + SettingsPath.ApiWebhooks, + undefined, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + ); } } catch { enqueueErrorSnackBar({ message: t`Error deleting api key.` }); @@ -249,8 +256,13 @@ export const SettingsDevelopersApiKeyDetail = () => { href: getSettingsPath(SettingsPath.General), }, { - children: t`APIs & Webhooks`, - href: getSettingsPath(SettingsPath.ApiWebhooks), + children: t`MCP & APIs`, + href: getSettingsPath( + SettingsPath.ApiWebhooks, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + ), }, { children: apiKey.name || t`Unnamed API Key` }, ]} diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx index ed074f6aef..9496890b14 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx +++ b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx @@ -25,6 +25,7 @@ import { GetRolesDocument, } from '~/generated-metadata/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { SETTINGS_API_WEBHOOKS_TABS } from '~/pages/settings/api-webhooks/constants/SettingsApiWebhooksTabs'; export const SettingsDevelopersApiKeysNew = () => { const { t } = useLingui(); @@ -136,8 +137,13 @@ export const SettingsDevelopersApiKeysNew = () => { href: getSettingsPath(SettingsPath.General), }, { - children: t`APIs & Webhooks`, - href: getSettingsPath(SettingsPath.ApiWebhooks), + children: t`MCP & APIs`, + href: getSettingsPath( + SettingsPath.ApiWebhooks, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + ), }, { children: t`New Key` }, ]} @@ -145,7 +151,13 @@ export const SettingsDevelopersApiKeysNew = () => { { - navigateSettings(SettingsPath.ApiWebhooks); + navigateSettings( + SettingsPath.ApiWebhooks, + undefined, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + ); }} onSave={handleSave} /> diff --git a/packages/twenty-front/src/pages/settings/developers/playground/SettingsGraphQLPlayground.tsx b/packages/twenty-front/src/pages/settings/developers/playground/SettingsGraphQLPlayground.tsx index d8e05d060c..dc1e047409 100644 --- a/packages/twenty-front/src/pages/settings/developers/playground/SettingsGraphQLPlayground.tsx +++ b/packages/twenty-front/src/pages/settings/developers/playground/SettingsGraphQLPlayground.tsx @@ -1,5 +1,5 @@ -import { GraphQLPlayground } from '@/settings/playground/components/GraphQLPlayground'; -import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas'; +import { GraphQLPlayground } from '@/settings/mcp-and-apis/components/GraphQLPlayground'; +import { PlaygroundSchemas } from '@/settings/mcp-and-apis/types/PlaygroundSchemas'; import { SettingsPath } from 'twenty-shared/types'; import { FullScreenContainer } from '@/ui/layout/fullscreen/components/FullScreenContainer'; @@ -7,6 +7,7 @@ import { Trans } from '@lingui/react/macro'; import { useParams } from 'react-router-dom'; import { getSettingsPath } from 'twenty-shared/utils'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { SETTINGS_API_WEBHOOKS_TABS } from '~/pages/settings/api-webhooks/constants/SettingsApiWebhooksTabs'; export const SettingsGraphQLPlayground = () => { const navigateSettings = useNavigateSettings(); @@ -19,7 +20,13 @@ export const SettingsGraphQLPlayground = () => { : PlaygroundSchemas.CORE; const handleExitFullScreen = () => { - navigateSettings(SettingsPath.ApiWebhooks); + navigateSettings( + SettingsPath.ApiWebhooks, + undefined, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + ); }; const handleOnError = () => { @@ -35,8 +42,13 @@ export const SettingsGraphQLPlayground = () => { href: getSettingsPath(SettingsPath.General), }, { - children: APIs & Webhooks, - href: getSettingsPath(SettingsPath.ApiWebhooks), + children: MCP & APIs, + href: getSettingsPath( + SettingsPath.ApiWebhooks, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + ), }, { children: GraphQL API Playground }, ]} diff --git a/packages/twenty-front/src/pages/settings/developers/playground/SettingsRestPlayground.tsx b/packages/twenty-front/src/pages/settings/developers/playground/SettingsRestPlayground.tsx index f52c7695b0..81a9cca57b 100644 --- a/packages/twenty-front/src/pages/settings/developers/playground/SettingsRestPlayground.tsx +++ b/packages/twenty-front/src/pages/settings/developers/playground/SettingsRestPlayground.tsx @@ -1,5 +1,5 @@ -import { RestPlayground } from '@/settings/playground/components/RestPlayground'; -import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas'; +import { RestPlayground } from '@/settings/mcp-and-apis/components/RestPlayground'; +import { PlaygroundSchemas } from '@/settings/mcp-and-apis/types/PlaygroundSchemas'; import { FullScreenContainer } from '@/ui/layout/fullscreen/components/FullScreenContainer'; import { Trans } from '@lingui/react/macro'; import { useCallback } from 'react'; @@ -7,6 +7,7 @@ import { useParams } from 'react-router-dom'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; +import { SETTINGS_API_WEBHOOKS_TABS } from '~/pages/settings/api-webhooks/constants/SettingsApiWebhooksTabs'; export const SettingsRestPlayground = () => { const navigateSettings = useNavigateSettings(); @@ -15,11 +16,23 @@ export const SettingsRestPlayground = () => { }>(); const handleExitFullScreen = () => { - navigateSettings(SettingsPath.ApiWebhooks); + navigateSettings( + SettingsPath.ApiWebhooks, + undefined, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + ); }; const handleError = useCallback(() => { - navigateSettings(SettingsPath.ApiWebhooks); + navigateSettings( + SettingsPath.ApiWebhooks, + undefined, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + ); }, [navigateSettings]); return ( @@ -31,8 +44,13 @@ export const SettingsRestPlayground = () => { href: getSettingsPath(SettingsPath.General), }, { - children: APIs & Webhooks, - href: getSettingsPath(SettingsPath.ApiWebhooks), + children: MCP & APIs, + href: getSettingsPath( + SettingsPath.ApiWebhooks, + undefined, + undefined, + SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API, + ), }, { children: REST }, ]} diff --git a/packages/twenty-front/src/utils/title-utils.ts b/packages/twenty-front/src/utils/title-utils.ts index 14e614f1d4..7f014ed868 100644 --- a/packages/twenty-front/src/utils/title-utils.ts +++ b/packages/twenty-front/src/utils/title-utils.ts @@ -50,7 +50,7 @@ export const getPageTitleFromPath = (pathname: string): string => { case SettingsPathPrefixes.Objects: return t`Data model - Settings`; case SettingsPathPrefixes.ApiWebhooks: - return t`API Keys - Settings`; + return t`MCP & APIs - Settings`; case SettingsPathPrefixes.LogicFunctions: return t`Functions - Settings`; case SettingsPathPrefixes.Integration: diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant.ts index 8fb2aff1d0..94e62a00a0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/constants/standard-permission-flag-definitions.constant.ts @@ -25,9 +25,9 @@ const STANDARD_PERMISSION_FLAG_METADATA: Record< StandardPermissionFlagMetadata > = { [PermissionFlagType.API_KEYS_AND_WEBHOOKS]: { - label: 'API Keys & Webhooks', - description: 'Manage API keys and webhooks', - icon: 'IconCode', + label: 'MCP & APIs', + description: 'Manage MCP, API keys, and webhooks', + icon: 'IconPlug', }, [PermissionFlagType.WORKSPACE]: { label: 'Workspace', diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts index 5194ef4038..dbe8886c8b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/constants/standard-command-menu-item.constant.ts @@ -906,11 +906,11 @@ export const STANDARD_COMMAND_MENU_ITEMS = { }, goToSettingsApiWebhooks: { universalIdentifier: 'ed2c2fde-1e7a-4a42-ba63-221eaa7c9759', - label: 'Go to APIs & Webhooks Settings', - icon: 'IconApi', + label: 'Go to MCP & APIs Settings', + icon: 'IconPlug', isPinned: false, position: 58, - shortLabel: 'APIs & Webhooks', + shortLabel: 'MCP & APIs', availabilityType: CommandMenuItemAvailabilityType.GLOBAL, conditionalAvailabilityExpression: 'permissionFlags.API_KEYS_AND_WEBHOOKS', availabilityObjectMetadataUniversalIdentifier: null, @@ -922,7 +922,7 @@ export const STANDARD_COMMAND_MENU_ITEMS = { goToSettingsApplications: { universalIdentifier: '44db6d7a-79ac-485e-b3da-da8776bd7777', label: 'Go to Apps Settings', - icon: 'IconPlug', + icon: 'IconApps', isPinned: false, position: 59, shortLabel: 'Apps', diff --git a/packages/twenty-shared/src/types/SettingsPath.ts b/packages/twenty-shared/src/types/SettingsPath.ts index f88e4e4ab4..048a5edf5c 100644 --- a/packages/twenty-shared/src/types/SettingsPath.ts +++ b/packages/twenty-shared/src/types/SettingsPath.ts @@ -58,13 +58,13 @@ export enum SettingsPath { LogicFunctions = 'functions', NewLogicFunction = 'functions/new', LogicFunctionDetail = 'functions/:logicFunctionId', - ApiWebhooks = 'api-webhooks', + ApiWebhooks = 'mcp-apis', RestPlayground = 'playground/rest/:schema', GraphQLPlayground = 'playground/graphql/:schema', - NewApiKey = 'api-webhooks/apis/new', - ApiKeyDetail = 'api-webhooks/apis/:apiKeyId', - NewWebhook = 'api-webhooks/webhooks/new', - WebhookDetail = 'api-webhooks/webhooks/:webhookId', + NewApiKey = 'mcp-apis/apis/new', + ApiKeyDetail = 'mcp-apis/apis/:apiKeyId', + NewWebhook = 'mcp-apis/webhooks/new', + WebhookDetail = 'mcp-apis/webhooks/:webhookId', Integrations = 'integrations', Security = 'general#security', Logs = 'general#logs', diff --git a/packages/twenty-ui/src/icon/components/IconSparkle2.tsx b/packages/twenty-ui/src/icon/components/IconSparkle2.tsx new file mode 100644 index 0000000000..ce20f0f98c --- /dev/null +++ b/packages/twenty-ui/src/icon/components/IconSparkle2.tsx @@ -0,0 +1,32 @@ +import { type IconComponentProps } from '@ui/icon/types/IconComponent'; + +type IconSparkle2Props = Pick< + IconComponentProps, + 'aria-hidden' | 'className' | 'color' | 'size' | 'stroke' +>; + +export const IconSparkle2 = ({ + className, + color = 'currentColor', + size = 24, + stroke = 2, + 'aria-hidden': ariaHidden, +}: IconSparkle2Props) => ( + + + +); diff --git a/packages/twenty-ui/src/icon/index.ts b/packages/twenty-ui/src/icon/index.ts index 1aaef6f3e4..fe71a601ad 100644 --- a/packages/twenty-ui/src/icon/index.ts +++ b/packages/twenty-ui/src/icon/index.ts @@ -26,6 +26,7 @@ export { IconMicrosoftOutlook } from './components/IconMicrosoftOutlook'; export { IconModelClaude } from './components/IconModelClaude'; export { IconProviderOpenai } from './components/IconProviderOpenai'; export { IconRelationManyToOne } from './components/IconRelationManyToOne'; +export { IconSparkle2 } from './components/IconSparkle2'; export { IconTrashXOff } from './components/IconTrashXOff'; export { IconTwentyStar } from './components/IconTwentyStar'; export { IconTwentyStarFilled } from './components/IconTwentyStarFilled';