Add MCP setup screen (#22468)

## Summary

- Add a first-tab MCP setup experience under MCP & APIs with quick
install cards, manual configuration, client logos, and HTTPS gating for
Claude install links.
- Rename API/Webhooks settings surfaces to MCP & APIs and update related
icons, permissions, breadcrumbs, and command menu entries.
- Add the Tabler sparkle-2 icon wrapper and MCP setup visual assets.

## Screenshots

| Before | After |
| --- | --- |
| ![Before: APIs & Webhooks MCP
tab](https://gist.githubusercontent.com/Bonapara/f8a97d31fbc3cab2771d18cbacd53d4c/raw/5838d123bc3aae3df0d37507b3e69135bec86444/before-mcp-settings.png)
| <img alt="image"
src="https://github.com/user-attachments/assets/a6ae2ae6-322b-4370-b9b6-0a3d73ff7fa7"
/> |

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22468?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas des Francs
2026-07-04 16:38:59 +02:00
committed by GitHub
parent cfe0fc7ce6
commit 1a60d4eaa3
66 changed files with 1304 additions and 178 deletions
@@ -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<Params<string>>,
) => {
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 (
<Navigate to={`${pathname}${location.search}${location.hash}`} replace />
);
}
const SettingsAccountsConfiguration = lazy(() =>
import('~/pages/settings/accounts/SettingsAccountsConfiguration').then(
(module) => ({
@@ -832,6 +888,30 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
/>
}
>
<Route
path={LEGACY_API_WEBHOOKS_SETTINGS_PATHS.ApiWebhooks}
element={<LegacySettingsPathRedirect to={SettingsPath.ApiWebhooks} />}
/>
<Route
path={LEGACY_API_WEBHOOKS_SETTINGS_PATHS.NewApiKey}
element={<LegacySettingsPathRedirect to={SettingsPath.NewApiKey} />}
/>
<Route
path={LEGACY_API_WEBHOOKS_SETTINGS_PATHS.ApiKeyDetail}
element={
<LegacySettingsPathRedirect to={SettingsPath.ApiKeyDetail} />
}
/>
<Route
path={LEGACY_API_WEBHOOKS_SETTINGS_PATHS.NewWebhook}
element={<LegacySettingsPathRedirect to={SettingsPath.NewWebhook} />}
/>
<Route
path={LEGACY_API_WEBHOOKS_SETTINGS_PATHS.WebhookDetail}
element={
<LegacySettingsPathRedirect to={SettingsPath.WebhookDetail} />
}
/>
<Route
path={SettingsPath.ApiWebhooks}
element={<SettingsApiWebhooks />}
@@ -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', () => {
@@ -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',
@@ -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}
/>
</StyledTitleInputContainer>
);
@@ -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 = ({
<SaveAndCancelButtons
isSaveDisabled={!canSave}
isCancelDisabled={formConfig.formState.isSubmitting}
onCancel={() => navigate(SettingsPath.ApiWebhooks)}
onCancel={() =>
navigate(
SettingsPath.ApiWebhooks,
undefined,
undefined,
undefined,
SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.WEBHOOKS,
)
}
onSave={formConfig.handleSubmit(handleSave)}
/>
}
@@ -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,
@@ -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],
},
{

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon-Architecture/64/Arch_Amazon-Q_64</title>
<g id="Icon-Architecture/64/Arch_Amazon-Q_64" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icon-Architecture-BG/64/Machine-Learning" fill="#01A88D">
<rect id="Rectangle" x="0" y="0" width="80" height="80"></rect>
</g>
<g id="Icon-Service/64/Amazon-Q_64" transform="translate(16.000000, 12.350000)" fill="#FFFFFF">
<path d="M27,25.767 L27,29.532 L24,31.032 L21,29.532 L21,25.767 L24,24.26 L27,25.767 Z M43.968,41.715 L25.968,51.889 C24.755,52.576 23.245,52.576 22.032,51.889 L4.032,41.715 C2.778,41.007 2,39.672 2,38.233 L2,16.96 C2,15.574 2.736,14.265 3.92,13.543 L21.921,2.587 C22.562,2.197 23.281,2.001 24,2.001 C24.719,2.001 25.438,2.197 26.08,2.587 L44.08,13.543 C45.264,14.265 46,15.574 46,16.96 L46,38.233 C46,38.481 45.969,38.725 45.924,38.964 L29,29.562 L29,25.15 C29,24.772 28.787,24.427 28.449,24.257 L24.449,22.247 C24.166,22.106 23.834,22.106 23.551,22.247 L19.551,24.257 C19.213,24.427 19,24.772 19,25.15 L19,30.15 C19,30.529 19.214,30.875 19.553,31.045 L23.553,33.045 C23.693,33.115 23.847,33.15 24,33.15 C24.153,33.15 24.307,33.115 24.447,33.045 L27.976,31.281 L45.075,40.78 C44.769,41.15 44.399,41.472 43.968,41.715 L43.968,41.715 Z M45.119,11.835 L27.12,0.879 C25.194,-0.293 22.805,-0.293 20.881,0.879 L2.881,11.835 C1.104,12.917 0,14.881 0,16.96 L0,38.233 C0,40.392 1.168,42.393 3.048,43.456 L21.048,53.63 C21.959,54.145 22.979,54.402 24,54.402 C25.021,54.402 26.042,54.145 26.952,53.63 L44.952,43.456 C46.832,42.393 48,40.392 48,38.233 L48,16.96 C48,14.881 46.896,12.917 45.119,11.835 L45.119,11.835 Z" id="Fill-1"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none">
<style>
path { fill: #000000; }
@media (prefers-color-scheme: dark) {
path { fill: #ffffff; }
}
</style>
<path d="M78.844 464.762c-8.453 0-15.573-1.451-21.359-4.339-5.77-2.888-10.144-7.289-13.076-13.095-2.932-5.807-4.436-12.912-4.436-21.255v-86.028c0-10.605-2.125-18.321-6.329-23.135-4.234-4.798-11.742-7.334-22.507-7.579-3.35 0-6.034-1.253-8.066-3.804C1.008 303.005 0 300.087 0 296.832c0-3.53 1.008-6.448 3.071-8.725 2.048-2.277 4.762-3.53 8.066-3.774 10.765-.26 18.273-2.781 22.507-7.579 4.235-4.798 6.329-12.392 6.329-22.752v-86.028c0-12.637 3.35-22.249 10.005-28.804 6.654-6.555 16.287-9.856 28.866-9.856H181.5c3.862 0 7.042 1.146 9.617 3.408 2.559 2.277 3.862 5.195 3.862 8.694 0 3.301-1.086 6.128-3.257 8.542-2.172 2.414-5.057 3.622-8.671 3.622H87.732c-5.413 0-9.508 1.39-12.316 4.171-2.823 2.781-4.234 7.075-4.234 12.912v86.425c0 7.579-1.551 14.455-4.623 20.644-3.07 6.204-7.181 11.063-12.316 14.623-5.134 3.53-11.137 5.302-18.07 5.302v-1.528c6.933 0 12.936 1.773 18.07 5.303 5.135 3.529 9.245 8.404 12.316 14.623 3.072 6.188 4.623 13.064 4.623 20.643v86.808c0 5.837 1.411 10.115 4.234 12.911 2.823 2.812 6.934 4.172 12.316 4.172h95.318c3.583 0 6.468 1.207 8.671 3.606 2.202 2.414 3.257 5.257 3.257 8.542s-1.272 6.097-3.862 8.511c-2.575 2.414-5.771 3.606-9.617 3.606H78.844v-.092ZM330.501 464.768c-3.862 0-7.042-1.207-9.617-3.606-2.575-2.414-3.863-5.256-3.863-8.511 0-3.255 1.086-6.128 3.258-8.542 2.171-2.414 5.057-3.606 8.671-3.606h95.317c5.414 0 9.509-1.36 12.316-4.171 2.823-2.781 4.235-7.075 4.235-12.912v-86.808c0-7.579 1.551-14.455 4.622-20.643 3.071-6.204 7.182-11.063 12.316-14.623 5.134-3.53 11.137-5.303 18.071-5.303v1.528c-6.934 0-12.937-1.772-18.071-5.302-5.134-3.53-9.245-8.404-12.316-14.623-3.071-6.189-4.622-13.065-4.622-20.644v-86.425c0-5.807-1.412-10.1-4.235-12.912-2.823-2.781-6.933-4.171-12.316-4.171H328.95c-3.583 0-6.469-1.208-8.671-3.622-2.172-2.384-3.258-5.241-3.258-8.542 0-3.529 1.272-6.417 3.863-8.694 2.559-2.277 5.755-3.407 9.617-3.407h102.654c12.58 0 22.181 3.3 28.867 9.855 6.685 6.556 10.005 16.167 10.005 28.804v86.028c0 10.36 2.125 17.969 6.328 22.752 4.235 4.798 11.742 7.334 22.507 7.579 3.351.244 6.034 1.497 8.066 3.774 2.063 2.277 3.071 5.195 3.071 8.725 0 3.301-1.008 6.189-3.071 8.695-2.032 2.521-4.762 3.804-8.066 3.804-10.765.245-18.257 2.781-22.507 7.579-4.234 4.798-6.328 12.5-6.328 23.135v86.028c0 8.358-1.474 15.418-4.437 21.255-2.962 5.837-7.305 10.176-13.076 13.095-5.785 2.888-12.905 4.339-21.359 4.339H330.501v.092Z"/>
<path d="M356.885 329.738c18.691 0 33.846-14.929 33.846-33.342 0-18.412-15.155-33.341-33.846-33.341-18.691 0-33.846 14.929-33.846 33.341 0 18.413 15.155 33.342 33.846 33.342ZM167.305 329.738c18.691 0 33.846-14.929 33.846-33.342 0-18.412-15.155-33.341-33.846-33.341-18.691 0-33.846 14.929-33.846 33.341 0 18.413 15.155 33.342 33.846 33.342ZM244.477 32.846l-2.59 68.135c0 3.82-3.661 5.73-10.983 5.73-7.321 0-10.982-1.91-10.982-5.73-.651-16.976-1.178-30.148-1.613-39.484-.217-9.55-.434-16.35-.651-20.384-.217-4.034-.326-6.479-.326-7.32v-1.268c0-4.874 4.529-7.319 13.572-7.319 9.044 0 13.573 2.552 13.573 7.64Zm54.941 0-2.59 68.135c0 3.82-3.661 5.73-10.982 5.73-7.322 0-10.982-1.91-10.982-5.73-.652-16.976-1.179-30.148-1.613-39.484-.218-9.55-.435-16.35-.652-20.384-.217-4.034-.326-6.479-.326-7.32v-1.268c0-4.874 4.53-7.319 13.573-7.319s13.572 2.552 13.572 7.64Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1 @@
<svg fill="#18181B" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Cline</title><path d="m23.365 13.556-1.442-2.895V8.994c0-2.764-2.218-5.002-4.954-5.002h-2.464c.178-.367.276-.779.276-1.213A2.77 2.77 0 0 0 12.018 0a2.77 2.77 0 0 0-2.763 2.779c0 .434.098.846.276 1.213H7.067c-2.736 0-4.954 2.238-4.954 5.002v1.667L.64 13.549c-.149.29-.149.636 0 .927l1.472 2.855v1.667C2.113 21.762 4.33 24 7.067 24h9.902c2.736 0 4.954-2.238 4.954-5.002V17.33l1.44-2.865c.143-.286.143-.622.002-.91m-12.854 2.36a2.27 2.27 0 0 1-2.261 2.273 2.27 2.27 0 0 1-2.261-2.273v-4.042A2.27 2.27 0 0 1 8.249 9.6a2.267 2.267 0 0 1 2.262 2.274zm7.285 0a2.27 2.27 0 0 1-2.26 2.273 2.27 2.27 0 0 1-2.262-2.273v-4.042A2.267 2.267 0 0 1 15.535 9.6a2.267 2.267 0 0 1 2.261 2.274z"/></svg>

After

Width:  |  Height:  |  Size: 776 B

@@ -0,0 +1 @@
<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Cursor</title><path d="M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23"/></svg>

After

Width:  |  Height:  |  Size: 469 B

@@ -0,0 +1,12 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Gemini CLI</title>
<path d="M0 4.391A4.391 4.391 0 0 1 4.391 0h15.217A4.391 4.391 0 0 1 24 4.391v15.217A4.391 4.391 0 0 1 19.608 24H4.391A4.391 4.391 0 0 1 0 19.608V4.391z" fill="url(#gemini-cli-gradient)" />
<path clip-rule="evenodd" d="M19.74 1.444a2.816 2.816 0 0 1 2.816 2.816v15.48a2.816 2.816 0 0 1-2.816 2.816H4.26a2.816 2.816 0 0 1-2.816-2.816V4.26A2.816 2.816 0 0 1 4.26 1.444h15.48zM7.236 8.564l7.752 3.728-7.752 3.727v2.802l9.557-4.596v-3.866L7.236 5.763v2.801z" fill="#1E1E2E" fill-rule="evenodd" />
<defs>
<linearGradient gradientUnits="userSpaceOnUse" id="gemini-cli-gradient" x1="24" x2="0" y1="6.587" y2="16.494">
<stop stop-color="#EE4D5D" />
<stop offset=".328" stop-color="#B381DD" />
<stop offset=".476" stop-color="#207CFE" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 885 B

@@ -0,0 +1,4 @@
<svg fill="#000000" fill-rule="evenodd" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Goose</title>
<path d="M21.595 23.61c1.167-.254 2.405-.944 2.405-.944l-2.167-1.784a12.124 12.124 0 0 1-2.695-3.131 12.127 12.127 0 0 0-3.97-4.049l-.794-.462a1.115 1.115 0 0 1-.488-.815.844.844 0 0 1 .154-.575c.413-.582 2.548-3.115 2.94-3.44.503-.416 1.065-.762 1.586-1.159.074-.056.148-.112.221-.17.003-.002.007-.004.009-.007.167-.131.325-.272.45-.438.453-.524.563-.988.59-1.193-.061-.197-.244-.639-.753-1.148.319.02.705.272 1.056.569.235-.376.481-.773.727-1.171.165-.266-.08-.465-.086-.471h-.001V3.22c-.007-.007-.206-.25-.471-.086-.567.35-1.134.702-1.639 1.021 0 0-.597-.012-1.305.599a2.464 2.464 0 0 0-.438.45l-.007.009c-.058.072-.114.147-.17.221-.397.521-.743 1.083-1.16 1.587-.323.391-2.857 2.526-3.44 2.94a.842.842 0 0 1-.574.153 1.115 1.115 0 0 1-.815-.488l-.462-.794a12.123 12.123 0 0 0-4.049-3.97 12.133 12.133 0 0 1-3.13-2.695L1.332 0S.643 1.238.39 2.405c.352.428 1.27 1.49 2.34 2.302C1.58 4.167.73 3.75.06 3.4c-.103.765-.063 1.92.043 2.816.726.317 1.961.806 3.219 1.066-1.006.236-2.11.278-2.961.262.15.554.358 1.119.64 1.688.119.263.25.52.39.77.452.125 2.222.383 3.164.171l-2.51.897a27.776 27.776 0 0 0 2.544 2.726c2.031-1.092 2.494-1.241 4.018-2.238-2.467 2.008-3.108 2.828-3.8 3.67l-.483.678c-.25.351-.469.725-.65 1.117-.61 1.31-1.47 4.1-1.47 4.1-.154.486.202.842.674.674 0 0 2.79-.861 4.1-1.47.392-.182.766-.4 1.118-.65l.677-.483c.227-.187.453-.37.701-.586 0 0 1.705 2.02 3.458 3.349l.896-2.511c-.211.942.046 2.712.17 3.163.252.142.509.272.772.392.569.28 1.134.49 1.688.64-.016-.853.026-1.956.261-2.962.26 1.258.75 2.493 1.067 3.219.895.106 2.051.146 2.816.043a73.87 73.87 0 0 1-1.308-2.67c.811 1.07 1.874 1.988 2.302 2.34h-.001z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>JetBrains</title><path d="M2.345 23.997A2.347 2.347 0 0 1 0 21.652V10.988C0 9.665.535 8.37 1.473 7.433l5.965-5.961A5.01 5.01 0 0 1 10.989 0h10.666A2.347 2.347 0 0 1 24 2.345v10.664a5.056 5.056 0 0 1-1.473 3.554l-5.965 5.965A5.017 5.017 0 0 1 13.007 24v-.003H2.345Zm8.969-6.854H5.486v1.371h5.828v-1.371ZM3.963 6.514h13.523v13.519l4.257-4.257a3.936 3.936 0 0 0 1.146-2.767V2.345c0-.678-.552-1.234-1.234-1.234H10.989a3.897 3.897 0 0 0-2.767 1.145L3.963 6.514Zm-.192.192L2.256 8.22a3.944 3.944 0 0 0-1.145 2.768v10.664c0 .678.552 1.234 1.234 1.234h10.666a3.9 3.9 0 0 0 2.767-1.146l1.512-1.511H3.771V6.706Z"/></svg>

After

Width:  |  Height:  |  Size: 703 B

@@ -0,0 +1,32 @@
<svg width="512" height="512" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="linearGradient22708">
<stop stop-color="#21facf" offset="0"/>
<stop stop-color="#0970ef" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient6949" x1="68.454" x2="198.59" y1="246.73" y2="96.35" gradientTransform="translate(-5.754,-56.594)" gradientUnits="userSpaceOnUse">
<stop stop-color="#72004e" offset="0"/>
<stop stop-color="#0015b1" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient22718" x1="56.735" x2="155.2" y1="246.96" y2="58.575" gradientUnits="userSpaceOnUse">
<stop stop-color="#4f00da" offset="0"/>
<stop stop-color="#e5311b" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient23463" x1="68.454" x2="198.59" y1="246.73" y2="96.35" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient22708"/>
<linearGradient id="linearGradient903" x1="54.478" x2="192.1" y1="247.56" y2="9.8095" gradientTransform="matrix(.87923 0 0 .87923 -9.551 48.787)" gradientUnits="userSpaceOnUse">
<stop stop-color="#dc180d" offset="0"/>
<stop stop-color="#f96e20" offset=".5"/>
<stop stop-color="#f4ce41" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient918" x1="39.468" x2="154.99" y1="204.22" y2="124.47" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient22708"/>
</defs>
<g transform="matrix(2.473 0 0 2.473 -4.8978 -4.8812)">
<path transform="translate(-5.5496,-57.412)" d="m148.16 59.393c-7.7098 9.3985-19.951 42.888-20.696 49.204-0.16994 4.6737 1.3731 14.231 0.67182 15.805-0.71909 1.6134-5.117-9.4461-7.2151-6.3266-12.219 18.168-10.7 17.731-15.582 31.378-1.8357 5.1315-0.42447 21.99-1.5666 23.773-1.273 1.9866-3.962-12.31-6.8063-9.236-11.603 12.54-16.279 20.379-22.336 30.607-3.3589 5.6725-2.1817 23.33-3.506 24.674-1.3023 1.3215-3.8566-18.326-7.6437-14.309-8.5193 9.038-14.054 13.441-18.946 19.252-5.1981 6.1739-0.78251 17.584-5.0672 35.383l0.1448 0.22073c77.447-50.308 101.52-127.16 107.61-181.19-0.68051 63.93-29.41 142.78-105.33 184.65l0.1127 0.17141c20.241-2.181 22.307 10.458 44.562-4.2837 55.792-48.277 81.856-124.29 61.593-199.78z" display="none" fill="url(#linearGradient903)"/>
<path transform="translate(-5.5498,-57.412)" d="m148.16 59.393c-7.7098 9.3985-19.951 42.888-20.696 49.204-0.16994 4.6737 1.3731 14.231 0.67182 15.805-0.71909 1.6134-5.117-9.4461-7.2151-6.3266-12.219 18.168-10.7 17.731-15.582 31.378-1.8357 5.1315-0.42447 21.99-1.5666 23.773-1.273 1.9866-3.962-12.31-6.8063-9.236-11.603 12.54-16.279 20.379-22.336 30.607-3.3589 5.6725-2.1817 23.33-3.506 24.674-1.3023 1.3215-3.8566-18.326-7.6437-14.309-8.5193 9.038-14.054 13.441-18.946 19.252-5.1981 6.1739-0.78251 17.584-5.0672 35.383l0.1448 0.22073c77.447-50.308 101.52-127.16 107.61-181.19-0.68051 63.93-29.41 142.78-105.33 184.65l0.1127 0.17141c20.241-2.181 22.307 10.458 44.562-4.2837 55.792-48.277 81.856-124.29 61.593-199.78z" fill="url(#linearGradient918)"/>
<g transform="translate(0 2.0218e-5)">
<path transform="translate(-5.7543,-56.594)" d="m111.25 81.024c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" display="none" fill="url(#linearGradient22718)"/>
<path transform="translate(-5.754,-56.594)" d="m111.25 81.024c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" display="none" fill="url(#linearGradient23463)"/>
<path d="m105.5 24.43c-48.394-1.5e-5 -87.625 39.231-87.625 87.625 0.0174 20.443 7.1818 40.236 20.253 55.954 0.2523-0.42224 0.53629-0.82423 0.85783-1.2061 4.892-5.8104 10.427-10.214 18.946-19.252 3.7871-4.0176 6.3412 15.63 7.6435 14.309 1.3243-1.3439 0.1473-19.001 3.5062-24.674 6.0563-10.228 10.733-18.067 22.336-30.607 2.8443-3.0741 5.5333 11.223 6.8063 9.2361 1.1421-1.7823-0.26941-18.641 1.5663-23.773 4.8819-13.647 3.3631-13.21 15.582-31.378 2.098-3.1195 6.496 7.9402 7.2151 6.3268 0.70126-1.5734-0.84173-11.131-0.67179-15.805 0.37161-3.1498 3.6036-13.059 7.7055-23.367-7.8432-2.2472-15.962-3.3881-24.12-3.3895zm43.142 11.356c5.5662 61.595-18.426 120.7-62.796 161.65 6.446 1.4857 13.04 2.2367 19.655 2.2386 48.394 1e-5 87.625-39.231 87.625-87.625-3.1e-4 -31.581-16.995-60.719-44.484-76.268z" fill="url(#linearGradient6949)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -0,0 +1 @@
<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>LM Studio</title><path d="M5.6 0A5.6 5.6 0 0 0 0 5.6v12.8A5.6 5.6 0 0 0 5.6 24h12.8a5.6 5.6 0 0 0 5.6-5.6V5.6A5.6 5.6 0 0 0 18.4 0zm0 2h12.8A3.6 3.6 0 0 1 22 5.6v12.8a3.6 3.6 0 0 1-3.6 3.6H5.6A3.6 3.6 0 0 1 2 18.4V5.6A3.6 3.6 0 0 1 5.6 2m-.4 2.8a1.2 1.2 0 0 0 0 2.4h10.4a1.2 1.2 0 0 0 0-2.4zm3.2 4a1.2 1.2 0 0 0 0 2.4h10.4a1.2 1.2 0 0 0 0-2.4zm-3.2 4a1.2 1.2 0 0 0 0 2.4h10.4a1.2 1.2 0 0 0 0-2.4zm3.2 4a1.2 1.2 0 0 0 0 2.4h10.4a1.2 1.2 0 0 0 0-2.4z"/></svg>

After

Width:  |  Height:  |  Size: 550 B

@@ -0,0 +1 @@
<svg fill="#FF6363" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Raycast</title><path d="M6.004 15.492v2.504L0 11.992l1.258-1.249Zm2.504 2.504H6.004L12.008 24l1.253-1.253zm14.24-4.747L24 11.997 12.003 0 10.75 1.251 15.491 6h-2.865L9.317 2.692 8.065 3.944l2.06 2.06H8.691v9.31H18v-1.432l2.06 2.06 1.252-1.252-3.312-3.32V8.506ZM6.63 5.372 5.38 6.625l1.342 1.343 1.251-1.253Zm10.655 10.655-1.247 1.251 1.342 1.343 1.253-1.251zM3.944 8.059 2.692 9.31l3.312 3.314v-2.506zm9.936 9.937h-2.504l3.314 3.312 1.25-1.252z"/></svg>

After

Width:  |  Height:  |  Size: 546 B

@@ -0,0 +1 @@
<svg fill="#F26207" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Replit</title><path d="M2 1.5A1.5 1.5 0 0 1 3.5 0h7A1.5 1.5 0 0 1 12 1.5V8H3.5A1.5 1.5 0 0 1 2 6.5ZM12 8h8.5A1.5 1.5 0 0 1 22 9.5v5a1.5 1.5 0 0 1-1.5 1.5H12ZM2 17.5A1.5 1.5 0 0 1 3.5 16H12v6.5a1.5 1.5 0 0 1-1.5 1.5h-7A1.5 1.5 0 0 1 2 22.5Z"/></svg>

After

Width:  |  Height:  |  Size: 341 B

@@ -0,0 +1,41 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.9119 99.3171C72.4869 99.9307 74.2828 99.8914 75.8725 99.1264L96.4608 89.2197C98.6242 88.1787 100 85.9892 100 83.5872V16.4133C100 14.0113 98.6243 11.8218 96.4609 10.7808L75.8725 0.873756C73.7862 -0.130129 71.3446 0.11576 69.5135 1.44695C69.252 1.63711 69.0028 1.84943 68.769 2.08341L29.3551 38.0415L12.1872 25.0096C10.589 23.7965 8.35363 23.8959 6.86933 25.2461L1.36303 30.2549C-0.452552 31.9064 -0.454633 34.7627 1.35853 36.417L16.2471 50.0001L1.35853 63.5832C-0.454633 65.2374 -0.452552 68.0938 1.36303 69.7453L6.86933 74.7541C8.35363 76.1043 10.589 76.2037 12.1872 74.9905L29.3551 61.9587L68.769 97.9167C69.3925 98.5406 70.1246 99.0104 70.9119 99.3171ZM75.0152 27.2989L45.1091 50.0001L75.0152 72.7012V27.2989Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path d="M96.4614 10.7962L75.8569 0.875542C73.4719 -0.272773 70.6217 0.211611 68.75 2.08333L1.29858 63.5832C-0.515693 65.2373 -0.513607 68.0937 1.30308 69.7452L6.81272 74.754C8.29793 76.1042 10.5347 76.2036 12.1338 74.9905L93.3609 13.3699C96.086 11.3026 100 13.2462 100 16.6667V16.4275C100 14.0265 98.6246 11.8378 96.4614 10.7962Z" fill="#0065A9"/>
<g filter="url(#filter0_d)">
<path d="M96.4614 89.2038L75.8569 99.1245C73.4719 100.273 70.6217 99.7884 68.75 97.9167L1.29858 36.4169C-0.515693 34.7627 -0.513607 31.9063 1.30308 30.2548L6.81272 25.246C8.29793 23.8958 10.5347 23.7964 12.1338 25.0095L93.3609 86.6301C96.086 88.6974 100 86.7538 100 83.3334V83.5726C100 85.9735 98.6246 88.1622 96.4614 89.2038Z" fill="#007ACC"/>
</g>
<g filter="url(#filter1_d)">
<path d="M75.8578 99.1263C73.4721 100.274 70.6219 99.7885 68.75 97.9166C71.0564 100.223 75 98.5895 75 95.3278V4.67213C75 1.41039 71.0564 -0.223106 68.75 2.08329C70.6219 0.211402 73.4721 -0.273666 75.8578 0.873633L96.4587 10.7807C98.6234 11.8217 100 14.0112 100 16.4132V83.5871C100 85.9891 98.6234 88.1786 96.4586 89.2196L75.8578 99.1263Z" fill="#1F9CF0"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.25">
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.8511 99.3171C72.4261 99.9306 74.2221 99.8913 75.8117 99.1264L96.4 89.2197C98.5634 88.1787 99.9392 85.9892 99.9392 83.5871V16.4133C99.9392 14.0112 98.5635 11.8217 96.4001 10.7807L75.8117 0.873695C73.7255 -0.13019 71.2838 0.115699 69.4527 1.44688C69.1912 1.63705 68.942 1.84937 68.7082 2.08335L29.2943 38.0414L12.1264 25.0096C10.5283 23.7964 8.29285 23.8959 6.80855 25.246L1.30225 30.2548C-0.513334 31.9064 -0.515415 34.7627 1.29775 36.4169L16.1863 50L1.29775 63.5832C-0.515415 65.2374 -0.513334 68.0937 1.30225 69.7452L6.80855 74.754C8.29285 76.1042 10.5283 76.2036 12.1264 74.9905L29.2943 61.9586L68.7082 97.9167C69.3317 98.5405 70.0638 99.0104 70.8511 99.3171ZM74.9544 27.2989L45.0483 50L74.9544 72.7012V27.2989Z" fill="url(#paint0_linear)"/>
</g>
</g>
<defs>
<filter id="filter0_d" x="-8.39411" y="15.8291" width="116.727" height="92.2456" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter1_d" x="60.4167" y="-8.07558" width="47.9167" height="116.151" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="4.16667"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="overlay" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<linearGradient id="paint0_linear" x1="49.9392" y1="0.257812" x2="49.9392" y2="99.7423" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

@@ -0,0 +1 @@
<svg fill="#01A4FF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Warp</title><path d="M12.035 2.723h9.253A2.712 2.712 0 0 1 24 5.435v10.529a2.712 2.712 0 0 1-2.712 2.713H8.047Zm-1.681 2.6L6.766 19.677h5.598l-.399 1.6H2.712A2.712 2.712 0 0 1 0 18.565V8.036a2.712 2.712 0 0 1 2.712-2.712Z"/></svg>

After

Width:  |  Height:  |  Size: 323 B

@@ -0,0 +1 @@
<svg fill="#0B100F" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Windsurf</title><path d="M23.55 5.067c-1.2038-.002-2.1806.973-2.1806 2.1765v4.8676c0 .972-.8035 1.7594-1.7597 1.7594-.568 0-1.1352-.286-1.4718-.7659l-4.9713-7.1003c-.4125-.5896-1.0837-.941-1.8103-.941-1.1334 0-2.1533.9635-2.1533 2.153v4.8957c0 .972-.7969 1.7594-1.7596 1.7594-.57 0-1.1363-.286-1.4728-.7658L.4076 5.1598C.2822 4.9798 0 5.0688 0 5.2882v4.2452c0 .2147.0656.4228.1884.599l5.4748 7.8183c.3234.462.8006.8052 1.3509.9298 1.3771.313 2.6446-.747 2.6446-2.0977v-4.893c0-.972.7875-1.7593 1.7596-1.7593h.003a1.798 1.798 0 0 1 1.4718.7658l4.9723 7.0994c.4135.5905 1.05.941 1.8093.941 1.1587 0 2.1515-.9645 2.1515-2.153v-4.8948c0-.972.7875-1.7594 1.7596-1.7594h.194a.22.22 0 0 0 .2204-.2202v-4.622a.22.22 0 0 0-.2203-.2203Z"/></svg>

After

Width:  |  Height:  |  Size: 828 B

@@ -0,0 +1 @@
<svg fill="#084CCF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Zed Industries</title><path d="M2.25 1.5a.75.75 0 0 0-.75.75v16.5H0V2.25A2.25 2.25 0 0 1 2.25 0h20.095c1.002 0 1.504 1.212.795 1.92L10.764 14.298h3.486V12.75h1.5v1.922a1.125 1.125 0 0 1-1.125 1.125H9.264l-2.578 2.578h11.689V9h1.5v9.375a1.5 1.5 0 0 1-1.5 1.5H5.185L2.562 22.5H21.75a.75.75 0 0 0 .75-.75V5.25H24v16.5A2.25 2.25 0 0 1 21.75 24H1.655C.653 24 .151 22.788.86 22.08L13.19 9.75H9.75v1.5h-1.5V9.375A1.125 1.125 0 0 1 9.375 8.25h5.314l2.625-2.625H5.625V15h-1.5V5.625a1.5 1.5 0 0 1 1.5-1.5h13.19L21.438 1.5z"/></svg>

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

@@ -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';
@@ -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 (
<StyledLogoImage
src={src}
alt=""
isInverted={invertInDarkMode && colorScheme === 'dark'}
/>
);
};
export const McpClaudeLogo = () => {
const theme = useTheme();
return <IconModelClaude size={theme.icon.size.xl} />;
};
export const McpOpenAiLogo = () => {
const theme = useTheme();
return <IconProviderOpenai size={theme.icon.size.xl} />;
};
@@ -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';
@@ -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';
@@ -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';
@@ -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 (
<StyledMcpSetupContainer>
{categories.map((category) => (
<Fragment key={category.title}>
<Section>
<H2Title
title={category.title}
description={category.description}
/>
<StyledCardsGrid>
{category.cards.map((card) => (
<SettingsMcpSetupCard key={card.title} card={card} />
))}
</StyledCardsGrid>
</Section>
{category.showManualConfigurationAfter && (
<Section>
<H2Title
title={t`Manual configuration`}
description={t`Access your workspace data from your favorite MCP client like Claude, Codex or Cursor.`}
/>
<CoreEditorHeader
leftNodes={[
<StyledMcpEditorHeaderTitle key="mcp-editor-header-title">
<StyledMcpIcon aria-hidden />
<span>{t`MCP client configuration`}</span>
</StyledMcpEditorHeaderTitle>,
]}
rightNodes={[
<LightCopyIconButton
key="mcp-config-copy-button"
copyText={mcpConfig}
/>,
]}
/>
<CodeEditor
value={mcpConfig}
language="json"
variant="with-header"
contentPadding="comfortable"
autoHeight
options={{
readOnly: true,
domReadOnly: true,
lineNumbers: 'off',
lineNumbersMinChars: 0,
folding: false,
glyphMargin: false,
scrollBeyondLastLine: false,
renderLineHighlight: 'none',
wordWrap: 'on',
}}
/>
</Section>
)}
</Fragment>
))}
</StyledMcpSetupContainer>
);
};
@@ -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 (
<>
<StyledInstallAction
as="span"
aria-disabled="true"
data-tooltip-id={card.tooltipId}
>
<IconExternalLink size={theme.icon.size.sm} />
{card.ctaLabel}
</StyledInstallAction>
{isDefined(card.disabledTooltip) && isDefined(card.tooltipId) && (
<AppTooltip
anchorSelect={`[data-tooltip-id='${card.tooltipId}']`}
content={card.disabledTooltip}
delay={TooltipDelay.shortDelay}
noArrow
place="bottom"
positionStrategy="fixed"
/>
)}
</>
);
}
if (!isDefined(card.href)) {
return null;
}
return (
<StyledInstallAction
href={card.href}
rel="noopener noreferrer"
target="_blank"
>
<IconExternalLink size={theme.icon.size.sm} />
{card.ctaLabel}
</StyledInstallAction>
);
};
type SettingsMcpSetupCardProps = {
card: McpSetupCard;
};
export const SettingsMcpSetupCard = ({ card }: SettingsMcpSetupCardProps) => (
<Card rounded>
<StyledCardContent>
<StyledLogo aria-hidden>{card.logo}</StyledLogo>
<StyledBody>
<StyledHeader>
<StyledTitle>{card.title}</StyledTitle>
<Pill label={card.badge} />
<StyledActionSlot>
<SettingsMcpSetupCardAction card={card} />
</StyledActionSlot>
</StyledHeader>
<StyledDescription>{card.description}</StyledDescription>
</StyledBody>
</StyledCardContent>
</Card>
);
@@ -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';
@@ -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';
@@ -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';
@@ -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<typeof SettingsMcpSetup> = {
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<typeof SettingsMcpSetup>;
export const Default: Story = {};
@@ -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 <YOUR_API_KEY>',
},
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;
@@ -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 = [
@@ -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';
@@ -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;
};
@@ -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 <YOUR_API_KEY>',
},
},
},
});
});
});
describe('buildRemoteMcpServerConfig', () => {
it('builds the common remote server config', () => {
expect(buildRemoteMcpServerConfig(mcpServerUrl)).toEqual({
url: mcpServerUrl,
headers: {
Authorization: 'Bearer <YOUR_API_KEY>',
},
});
});
});
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 <YOUR_API_KEY>',
},
});
});
});
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 <YOUR_API_KEY>');
});
});
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 <YOUR_API_KEY>',
},
],
});
});
});
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),
);
});
});
@@ -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: <McpOpenAiLogo />,
},
{
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: <McpClaudeLogo />,
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: <McpClientLogo src={CursorLogo} invertInDarkMode />,
},
{
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: <McpClientLogo src={VsCodeLogo} />,
},
{
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: <McpClientLogo src={GooseLogo} invertInDarkMode />,
},
{
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: <McpClientLogo src={ReplitLogo} />,
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: <McpClientLogo src={LmStudioLogo} invertInDarkMode />,
},
],
},
{
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: <McpClientLogo src={ClineLogo} invertInDarkMode />,
},
{
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: <McpClientLogo src={ZedLogo} />,
},
{
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: <McpClientLogo src={WindsurfLogo} invertInDarkMode />,
},
{
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: <McpClientLogo src={RaycastLogo} />,
},
{
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: <McpClientLogo src={WarpLogo} />,
},
{
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: <McpClientLogo src={JetBrainsLogo} invertInDarkMode />,
},
{
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: <McpClientLogo src={GeminiCliLogo} />,
},
{
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: <McpClientLogo src={AmazonQLogo} />,
},
{
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: <McpClientLogo src={LibreChatLogo} />,
},
{
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: <McpClientLogo src={AugmentCodeLogo} invertInDarkMode />,
},
],
},
];
@@ -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()}`;
};
@@ -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 <YOUR_API_KEY>"
}
}
}
}`;
export const SettingsMcpSetup = () => {
const { t } = useLingui();
const mcpConfig = buildMcpConfig(REACT_APP_SERVER_BASE_URL);
return (
<Section>
<H2Title
title={t`Connect your AI assistant`}
description={t`Paste this configuration into MCP-aware clients like Claude, Codex, or Cursor.`}
/>
<CoreEditorHeader
leftNodes={[
<StyledMcpEditorHeaderTitle>
<StyledMcpIcon aria-hidden focusable={false} />
<span>{t`MCP client configuration`}</span>
</StyledMcpEditorHeaderTitle>,
]}
rightNodes={[<LightCopyIconButton copyText={mcpConfig} />]}
/>
<CodeEditor
value={mcpConfig}
language="json"
variant="with-header"
contentPadding="comfortable"
autoHeight
options={{
readOnly: true,
domReadOnly: true,
lineNumbers: 'off',
lineNumbersMinChars: 0,
folding: false,
glyphMargin: false,
scrollBeyondLastLine: false,
renderLineHighlight: 'none',
wordWrap: 'on',
}}
/>
</Section>
);
};
@@ -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,
@@ -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 = () => {
/>
<UndecoratedLink to={MCP_DEEP_LINK}>
<SettingsCard
Icon={<IconRobot size={theme.icon.size.md} />}
Icon={<IconSparkle2 size={theme.icon.size.md} />}
title={t`Set up MCP`}
/>
</UndecoratedLink>
@@ -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 (
<SettingsPageLayout
title={t`APIs & Webhooks`}
title={t`MCP & APIs`}
secondaryBar={
<SettingsTabBar
tabs={tabs}
@@ -97,29 +101,33 @@ export const SettingsApiWebhooks = () => {
children: t`Workspace`,
href: getSettingsPath(SettingsPath.General),
},
{ children: t`APIs & Webhooks` },
{ children: t`MCP & APIs` },
]}
>
<SettingsPageContainer>
<Section>
<SettingsDiscoveryHeroCard
lightSrc={PlaygroundCoverLight}
darkSrc={PlaygroundCoverDark}
lightSrc={isMcpTab ? McpCoverLight : PlaygroundCoverLight}
darkSrc={isMcpTab ? McpCoverDark : PlaygroundCoverDark}
instanceIdPrefix={SETTINGS_API_HERO_INSTANCE_ID_PREFIX}
tabs={[
{
id: 'rest',
title: t`REST`,
Icon: IconCode,
vimeoId: '928786722',
},
{
id: 'graphql',
title: t`GraphQL`,
Icon: IconBrandGraphql,
vimeoId: '928786722',
},
]}
tabs={
isMcpTab
? []
: [
{
id: 'rest',
title: t`REST`,
Icon: IconPlug,
vimeoId: '928786722',
},
{
id: 'graphql',
title: t`GraphQL`,
Icon: IconBrandGraphql,
vimeoId: '928786722',
},
]
}
playButtonAriaLabel={t`Watch API demo`}
/>
</Section>
@@ -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';
@@ -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';
@@ -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` },
]}
@@ -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 = () => {
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => {
navigateSettings(SettingsPath.ApiWebhooks);
navigateSettings(
SettingsPath.ApiWebhooks,
undefined,
undefined,
undefined,
SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API,
);
}}
onSave={handleSave}
/>
@@ -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: <Trans>APIs & Webhooks</Trans>,
href: getSettingsPath(SettingsPath.ApiWebhooks),
children: <Trans>MCP & APIs</Trans>,
href: getSettingsPath(
SettingsPath.ApiWebhooks,
undefined,
undefined,
SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API,
),
},
{ children: <Trans>GraphQL API Playground</Trans> },
]}
@@ -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: <Trans>APIs & Webhooks</Trans>,
href: getSettingsPath(SettingsPath.ApiWebhooks),
children: <Trans>MCP & APIs</Trans>,
href: getSettingsPath(
SettingsPath.ApiWebhooks,
undefined,
undefined,
SETTINGS_API_WEBHOOKS_TABS.TABS_IDS.API,
),
},
{ children: <Trans>REST</Trans> },
]}
@@ -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:
@@ -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',
@@ -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',
@@ -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',
@@ -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) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth={stroke}
strokeLinecap="round"
strokeLinejoin="round"
className={['tabler-icon', 'tabler-icon-sparkle-2', className]
.filter(Boolean)
.join(' ')}
aria-hidden={ariaHidden}
>
<path d="M12 3c.375 0 .711 .231 .846 .581l1.65 4.29a2.85 2.85 0 0 0 1.632 1.633l4.291 1.65a.906 .906 0 0 1 0 1.692l-4.29 1.65a2.84 2.84 0 0 0 -1.633 1.632l-1.65 4.291a.906 .906 0 0 1 -1.692 0l-1.65 -4.29a2.84 2.84 0 0 0 -1.632 -1.633l-4.291 -1.65a.906 .906 0 0 1 0 -1.692l4.29 -1.65a2.84 2.84 0 0 0 1.633 -1.632l1.65 -4.291a.91 .91 0 0 1 .846 -.581" />
</svg>
);
+1
View File
@@ -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';