diff --git a/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-discovery.controller.ts b/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-discovery.controller.ts index 14f32398cf..1783fbaa0f 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-discovery.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-oauth/controllers/oauth-discovery.controller.ts @@ -9,6 +9,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; import { cleanServerUrl } from 'src/utils/clean-server-url'; +import { getRequestBaseUrl } from 'src/utils/get-request-base-url.util'; import { TWENTY_CLI_APPLICATION_REGISTRATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-cli-application-registration.constant'; @Controller('.well-known') @@ -22,7 +23,7 @@ export class OAuthDiscoveryController { @Get('oauth-authorization-server') @UseGuards(PublicEndpointGuard, NoPermissionGuard) async getAuthorizationServerMetadata(@Req() request: Request) { - const issuer = this.getRequestBaseUrl(request); + const issuer = getRequestBaseUrl(request); // /authorize is served by the frontend; SERVER_URL (API-only) has no such // route, so we route the client to the default frontend base URL in that // case. All other hosts (app.twenty.com, workspace subdomains, custom @@ -73,7 +74,7 @@ export class OAuthDiscoveryController { @Get('oauth-protected-resource') @UseGuards(PublicEndpointGuard, NoPermissionGuard) getProtectedResourceMetadataRoot(@Req() request: Request) { - const base = this.getRequestBaseUrl(request); + const base = getRequestBaseUrl(request); return this.buildProtectedResourceMetadata(base, base); } @@ -81,7 +82,7 @@ export class OAuthDiscoveryController { @Get('oauth-protected-resource/mcp') @UseGuards(PublicEndpointGuard, NoPermissionGuard) getProtectedResourceMetadataMcp(@Req() request: Request) { - const base = this.getRequestBaseUrl(request); + const base = getRequestBaseUrl(request); return this.buildProtectedResourceMetadata(base, `${base}/mcp`); } @@ -95,10 +96,6 @@ export class OAuthDiscoveryController { }; } - private getRequestBaseUrl(request: Request): string { - return `${request.protocol}://${request.get('host')}`; - } - private isApiHost(request: Request): boolean { const serverUrl = this.twentyConfigService.get('SERVER_URL'); diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index 6429ba6f4b..b0bed69ba2 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -57,6 +57,7 @@ import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-cl import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service'; import { SearchModule } from 'src/engine/core-modules/search/search.module'; import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module'; +import { WellKnownModule } from 'src/engine/core-modules/well-known/well-known.module'; import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -98,6 +99,7 @@ import { FileModule } from './file/file.module'; FileModule, RowLevelPermissionModule, OpenApiModule, + WellKnownModule, ApplicationRegistrationModule, ApplicationOAuthModule, ApplicationModule, diff --git a/packages/twenty-server/src/engine/core-modules/well-known/controllers/__tests__/well-known.controller.spec.ts b/packages/twenty-server/src/engine/core-modules/well-known/controllers/__tests__/well-known.controller.spec.ts new file mode 100644 index 0000000000..679cd76088 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/well-known/controllers/__tests__/well-known.controller.spec.ts @@ -0,0 +1,81 @@ +import { Test, type TestingModule } from '@nestjs/testing'; + +import { type Request } from 'express'; + +import { WellKnownController } from 'src/engine/core-modules/well-known/controllers/well-known.controller'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; + +describe('WellKnownController', () => { + let controller: WellKnownController; + const configGet = jest.fn(); + + const buildMockRequest = (host: string, protocol = 'https') => + ({ + protocol, + get: (header: string) => + header.toLowerCase() === 'host' ? host : undefined, + }) as unknown as Request; + + beforeEach(async () => { + configGet.mockReset(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [WellKnownController], + providers: [ + { + provide: TwentyConfigService, + useValue: { get: configGet }, + }, + ], + }).compile(); + + controller = module.get(WellKnownController); + }); + + describe('getMcpServerCard', () => { + it('builds the card for the request host', () => { + configGet.mockReturnValue('1.2.3'); + + const card = controller.getMcpServerCard( + buildMockRequest('workspace.twenty.com'), + ); + + expect(card.remotes[0].url).toBe('https://workspace.twenty.com/mcp'); + expect(card.version).toBe('1.2.3'); + }); + + it('falls back to 0.0.0 when APP_VERSION is unset', () => { + configGet.mockReturnValue(undefined); + + const card = controller.getMcpServerCard( + buildMockRequest('workspace.twenty.com'), + ); + + expect(card.version).toBe('0.0.0'); + }); + }); + + describe('getApiCatalog', () => { + it('returns a parseable linkset anchored on the request host', () => { + const body = controller.getApiCatalog( + buildMockRequest('workspace.twenty.com'), + ); + + const parsed = JSON.parse(body); + + expect( + parsed.linkset.map((entry: { anchor: string }) => entry.anchor), + ).toContain('https://workspace.twenty.com/rest'); + }); + + it('respects the forwarded protocol', () => { + const body = controller.getApiCatalog( + buildMockRequest('localhost:3000', 'http'), + ); + + expect(JSON.parse(body).linkset[0].anchor).toBe( + 'http://localhost:3000/rest', + ); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/well-known/controllers/well-known.controller.ts b/packages/twenty-server/src/engine/core-modules/well-known/controllers/well-known.controller.ts new file mode 100644 index 0000000000..ad940fec8c --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/well-known/controllers/well-known.controller.ts @@ -0,0 +1,46 @@ +import { Controller, Get, Header, Req, UseGuards } from '@nestjs/common'; + +import { type Request } from 'express'; + +import { buildApiCatalog } from 'src/engine/core-modules/well-known/utils/build-api-catalog.util'; +import { buildMcpServerCard } from 'src/engine/core-modules/well-known/utils/build-mcp-server-card.util'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard'; +import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; +import { getRequestBaseUrl } from 'src/utils/get-request-base-url.util'; +import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch'; + +const DISCOVERY_CACHE_CONTROL = 'public, max-age=3600'; +const FALLBACK_SERVER_VERSION = '0.0.0'; + +@Controller('.well-known') +export class WellKnownController { + constructor(private readonly twentyConfigService: TwentyConfigService) {} + + @Get('mcp/server-card.json') + @UseGuards(PublicEndpointGuard, NoPermissionGuard) + @Header('Cache-Control', DISCOVERY_CACHE_CONTROL) + getMcpServerCard(@Req() request: Request) { + const version = + extractVersionMajorMinorPatch( + this.twentyConfigService.get('APP_VERSION'), + ) ?? FALLBACK_SERVER_VERSION; + + return buildMcpServerCard({ + baseUrl: getRequestBaseUrl(request), + version, + }); + } + + // Return a string so Nest keeps the explicit linkset+json Content-Type. + @Get('api-catalog') + @UseGuards(PublicEndpointGuard, NoPermissionGuard) + @Header( + 'Content-Type', + 'application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"', + ) + @Header('Cache-Control', DISCOVERY_CACHE_CONTROL) + getApiCatalog(@Req() request: Request): string { + return JSON.stringify(buildApiCatalog(getRequestBaseUrl(request)), null, 2); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/well-known/utils/__tests__/build-api-catalog.util.spec.ts b/packages/twenty-server/src/engine/core-modules/well-known/utils/__tests__/build-api-catalog.util.spec.ts new file mode 100644 index 0000000000..4ddb431400 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/well-known/utils/__tests__/build-api-catalog.util.spec.ts @@ -0,0 +1,61 @@ +import { buildApiCatalog } from 'src/engine/core-modules/well-known/utils/build-api-catalog.util'; + +describe('buildApiCatalog', () => { + const baseUrl = 'https://mycompany.twenty.com'; + + it('anchors each surface at its canonical URL on the given host', () => { + const catalog = buildApiCatalog(baseUrl); + + const anchors = catalog.linkset.map((entry) => entry.anchor); + + expect(anchors).toEqual([ + `${baseUrl}/rest`, + `${baseUrl}/rest/metadata`, + `${baseUrl}/graphql`, + `${baseUrl}/mcp`, + ]); + }); + + it('points the REST core surface at its live per-host OpenAPI + OAuth metadata', () => { + const catalog = buildApiCatalog(baseUrl); + + const restCore = catalog.linkset.find( + (entry) => entry.anchor === `${baseUrl}/rest`, + ); + + expect(restCore?.['service-desc']).toEqual([ + { href: `${baseUrl}/rest/open-api/core`, type: 'application/json' }, + ]); + expect(restCore?.['service-meta']).toEqual([ + { + href: `${baseUrl}/.well-known/oauth-protected-resource`, + type: 'application/json', + }, + ]); + }); + + it('references the MCP server card for the MCP surface', () => { + const catalog = buildApiCatalog(baseUrl); + + const mcp = catalog.linkset.find( + (entry) => entry.anchor === `${baseUrl}/mcp`, + ); + + expect(mcp?.['service-desc']).toEqual([ + { + href: `${baseUrl}/.well-known/mcp/server-card.json`, + type: 'application/json', + }, + ]); + }); + + it('gives every surface human documentation', () => { + const catalog = buildApiCatalog(baseUrl); + + for (const entry of catalog.linkset) { + expect(entry['service-doc']?.[0]?.href).toMatch( + /^https:\/\/docs\.twenty\.com\//, + ); + } + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/well-known/utils/__tests__/build-mcp-server-card.util.spec.ts b/packages/twenty-server/src/engine/core-modules/well-known/utils/__tests__/build-mcp-server-card.util.spec.ts new file mode 100644 index 0000000000..68321e81d7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/well-known/utils/__tests__/build-mcp-server-card.util.spec.ts @@ -0,0 +1,47 @@ +import { MCP_PROTOCOL_VERSION } from 'src/engine/api/mcp/constants/mcp-protocol-version.const'; +import { buildMcpServerCard } from 'src/engine/core-modules/well-known/utils/build-mcp-server-card.util'; + +describe('buildMcpServerCard', () => { + it('advertises the streamable-http endpoint on the given host', () => { + const card = buildMcpServerCard({ + baseUrl: 'https://mycompany.twenty.com', + version: '1.2.3', + }); + + expect(card.remotes).toHaveLength(1); + expect(card.remotes[0]).toMatchObject({ + type: 'streamable-http', + url: 'https://mycompany.twenty.com/mcp', + supportedProtocolVersions: [MCP_PROTOCOL_VERSION], + }); + }); + + it('carries the registry schema, stable identity and passed version', () => { + const card = buildMcpServerCard({ + baseUrl: 'https://api.twenty.com', + version: '0.42.0', + }); + + expect(card.$schema).toBe( + 'https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json', + ); + expect(card.name).toBe('com.twenty/twenty'); + expect(card.version).toBe('0.42.0'); + expect(card.repository.source).toBe('github'); + }); + + it('marks the Authorization header optional and secret (OAuth or API key)', () => { + const card = buildMcpServerCard({ + baseUrl: 'https://mycompany.twenty.com', + version: '1.0.0', + }); + + expect(card.remotes[0].headers).toEqual([ + expect.objectContaining({ + name: 'Authorization', + isRequired: false, + isSecret: true, + }), + ]); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/well-known/utils/build-api-catalog.util.ts b/packages/twenty-server/src/engine/core-modules/well-known/utils/build-api-catalog.util.ts new file mode 100644 index 0000000000..4725ce2d52 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/well-known/utils/build-api-catalog.util.ts @@ -0,0 +1,45 @@ +import { DOCUMENTATION_BASE_URL } from 'twenty-shared/constants'; + +const API_DOCS_URL = `${DOCUMENTATION_BASE_URL}/developers/extend/api`; +const MCP_DOCS_URL = `${DOCUMENTATION_BASE_URL}/user-guide/ai/capabilities/mcp`; + +// service-desc points at each host's live OpenAPI, which is generated per +// workspace and so includes that workspace's custom objects. +export const buildApiCatalog = (baseUrl: string) => ({ + linkset: [ + { + anchor: `${baseUrl}/rest`, + 'service-desc': [ + { href: `${baseUrl}/rest/open-api/core`, type: 'application/json' }, + ], + 'service-doc': [{ href: API_DOCS_URL, type: 'text/html' }], + 'service-meta': [ + { + href: `${baseUrl}/.well-known/oauth-protected-resource`, + type: 'application/json', + }, + ], + }, + { + anchor: `${baseUrl}/rest/metadata`, + 'service-desc': [ + { href: `${baseUrl}/rest/open-api/metadata`, type: 'application/json' }, + ], + 'service-doc': [{ href: API_DOCS_URL, type: 'text/html' }], + }, + { + anchor: `${baseUrl}/graphql`, + 'service-doc': [{ href: API_DOCS_URL, type: 'text/html' }], + }, + { + anchor: `${baseUrl}/mcp`, + 'service-desc': [ + { + href: `${baseUrl}/.well-known/mcp/server-card.json`, + type: 'application/json', + }, + ], + 'service-doc': [{ href: MCP_DOCS_URL, type: 'text/html' }], + }, + ], +}); diff --git a/packages/twenty-server/src/engine/core-modules/well-known/utils/build-mcp-server-card.util.ts b/packages/twenty-server/src/engine/core-modules/well-known/utils/build-mcp-server-card.util.ts new file mode 100644 index 0000000000..fe9375f5da --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/well-known/utils/build-mcp-server-card.util.ts @@ -0,0 +1,40 @@ +import { MCP_PROTOCOL_VERSION } from 'src/engine/api/mcp/constants/mcp-protocol-version.const'; + +type BuildMcpServerCardArgs = { + baseUrl: string; + version: string; +}; + +export const buildMcpServerCard = ({ + baseUrl, + version, +}: BuildMcpServerCardArgs) => ({ + $schema: + 'https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json', + name: 'com.twenty/twenty', + version, + title: 'Twenty CRM', + description: + 'Read and write your Twenty CRM data - companies, people, opportunities, tasks, notes and any custom objects - from AI assistants. Tools are discovered at runtime and scoped to the authenticated workspace.', + websiteUrl: 'https://twenty.com', + repository: { + url: 'https://github.com/twentyhq/twenty', + source: 'github', + }, + remotes: [ + { + type: 'streamable-http', + url: `${baseUrl}/mcp`, + supportedProtocolVersions: [MCP_PROTOCOL_VERSION], + headers: [ + { + name: 'Authorization', + description: + "Optional. Bearer for static API-key auth. Omit to use OAuth 2.1, auto-discovered from this host's /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server.", + isRequired: false, + isSecret: true, + }, + ], + }, + ], +}); diff --git a/packages/twenty-server/src/engine/core-modules/well-known/well-known.module.ts b/packages/twenty-server/src/engine/core-modules/well-known/well-known.module.ts new file mode 100644 index 0000000000..d0229e9998 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/well-known/well-known.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { WellKnownController } from 'src/engine/core-modules/well-known/controllers/well-known.controller'; +import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; + +@Module({ + imports: [TwentyConfigModule], + controllers: [WellKnownController], +}) +export class WellKnownModule {} diff --git a/packages/twenty-server/src/utils/get-request-base-url.util.ts b/packages/twenty-server/src/utils/get-request-base-url.util.ts new file mode 100644 index 0000000000..fe033a1158 --- /dev/null +++ b/packages/twenty-server/src/utils/get-request-base-url.util.ts @@ -0,0 +1,5 @@ +import { type Request } from 'express'; + +// Absolute origin the request arrived on (honors Express `trust proxy`). +export const getRequestBaseUrl = (request: Request): string => + `${request.protocol}://${request.get('host')}`; diff --git a/packages/twenty-website/public/.well-known/mcp/server-card.json b/packages/twenty-website/public/.well-known/mcp/server-card.json new file mode 100644 index 0000000000..a6599d9534 --- /dev/null +++ b/packages/twenty-website/public/.well-known/mcp/server-card.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json", + "name": "com.twenty/twenty", + "version": "0.2.1", + "title": "Twenty CRM", + "description": "Read and write your Twenty CRM data - companies, people, opportunities, tasks, notes and any custom objects - from AI assistants. Tools are discovered at runtime and scoped to the authenticated workspace.", + "websiteUrl": "https://twenty.com", + "repository": { + "url": "https://github.com/twentyhq/twenty", + "source": "github" + }, + "remotes": [ + { + "type": "streamable-http", + "url": "https://{your-workspace-url}/mcp", + "supportedProtocolVersions": ["2025-06-18"], + "headers": [ + { + "name": "Authorization", + "description": "Optional. Bearer for static API-key auth. Omit to use OAuth 2.1, which is auto-discovered from the workspace host's /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server.", + "isRequired": false, + "isSecret": true + } + ] + } + ] +} diff --git a/packages/twenty-website/public/llms.txt b/packages/twenty-website/public/llms.txt new file mode 100644 index 0000000000..7e16f651d9 --- /dev/null +++ b/packages/twenty-website/public/llms.txt @@ -0,0 +1,22 @@ +# Twenty + +> Twenty is an open-source CRM. AI assistants and agents can read and write CRM data through the Model Context Protocol (MCP) server or the REST/GraphQL API. + +Every endpoint is workspace-scoped: replace `{your-workspace-url}` with your workspace host (e.g. `mycompany.twenty.com` or a custom domain). The MCP server is at `https://{your-workspace-url}/mcp` (transport: streamable-http; auth: OAuth 2.1 or an API-key Bearer token). The REST base is `https://{your-workspace-url}/rest`, with per-workspace OpenAPI at `/rest/open-api/core` and `/rest/open-api/metadata` (send an `Authorization: Bearer ` header). GraphQL is at `/graphql` and `/metadata`. + +Twenty is multi-tenant, so there is no single global API host: each workspace has its own base URL, and the REST OpenAPI is generated per workspace, so it includes any custom objects and fields you have created. Prefer the MCP server for agents — it exposes typed tools with header-based auth and discovers your workspace's tools at runtime, so it does not depend on the per-workspace OpenAPI document. + +## Connect an AI assistant + +- [MCP server guide](https://docs.twenty.com/user-guide/ai/capabilities/mcp): connect Claude, Cursor, ChatGPT and others. +- [MCP server card](https://twenty.com/.well-known/mcp/server-card.json): machine-readable MCP metadata. + +## APIs + +- [API guide](https://docs.twenty.com/developers/extend/api): REST and GraphQL, generated from your workspace schema. +- [API catalog](https://twenty.com/.well-known/api-catalog): RFC 9727 index of the API surfaces. + +## Docs + +- [Developer documentation](https://docs.twenty.com/developers/introduction) +- [Source code](https://github.com/twentyhq/twenty) diff --git a/packages/twenty-website/src/app/.well-known/api-catalog/route.ts b/packages/twenty-website/src/app/.well-known/api-catalog/route.ts new file mode 100644 index 0000000000..5b63a83bf9 --- /dev/null +++ b/packages/twenty-website/src/app/.well-known/api-catalog/route.ts @@ -0,0 +1,65 @@ +import { SITE_URLS } from '@/platform/site-urls'; + +// Route handler (not a public/ file) so the RFC 9727 application/linkset+json +// content type survives the site's global nosniff header. +// +// Twenty is multi-tenant, so anchors use a `{your-workspace-url}` placeholder +// (a workspace host such as `mycompany.twenty.com` or a custom domain). + +const WORKSPACE = 'https://{your-workspace-url}'; + +const apiCatalog = { + linkset: [ + { + anchor: `${WORKSPACE}/rest`, + 'service-desc': [ + { href: `${WORKSPACE}/rest/open-api/core`, type: 'application/json' }, + ], + 'service-doc': [{ href: SITE_URLS.docsApi, type: 'text/html' }], + 'service-meta': [ + { + href: `${WORKSPACE}/.well-known/oauth-protected-resource`, + type: 'application/json', + }, + ], + }, + { + anchor: `${WORKSPACE}/rest/metadata`, + 'service-desc': [ + { + href: `${WORKSPACE}/rest/open-api/metadata`, + type: 'application/json', + }, + ], + 'service-doc': [{ href: SITE_URLS.docsApi, type: 'text/html' }], + }, + { + anchor: `${WORKSPACE}/graphql`, + 'service-doc': [{ href: SITE_URLS.docsApi, type: 'text/html' }], + }, + { + anchor: `${WORKSPACE}/mcp`, + 'service-desc': [ + { + href: 'https://twenty.com/.well-known/mcp/server-card.json', + type: 'application/json', + }, + ], + 'service-doc': [{ href: SITE_URLS.docsMcp, type: 'text/html' }], + }, + ], +}; + +export const dynamic = 'force-static'; + +export async function GET() { + return new Response(JSON.stringify(apiCatalog, null, 2), { + headers: { + 'Content-Type': + 'application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727"', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +} diff --git a/packages/twenty-website/src/platform/site-urls.ts b/packages/twenty-website/src/platform/site-urls.ts index 587f9551c5..eeba128d07 100644 --- a/packages/twenty-website/src/platform/site-urls.ts +++ b/packages/twenty-website/src/platform/site-urls.ts @@ -4,8 +4,10 @@ export const SITE_URLS: Record< | 'appWelcome' | 'calBooking' | 'discord' + | 'docsApi' | 'docsDevelopers' | 'docsGettingStarted' + | 'docsMcp' | 'docsUserGuide' | 'github' | 'linkedin' @@ -16,8 +18,10 @@ export const SITE_URLS: Record< appWelcome: 'https://app.twenty.com/welcome', calBooking: 'https://cal.com/forms/f7841033-0a20-4958-8c92-4e34ec128a81', discord: 'https://discord.gg/cx5n4Jzs57', + docsApi: 'https://docs.twenty.com/developers/extend/api', docsDevelopers: 'https://docs.twenty.com/developers/introduction', docsGettingStarted: 'https://docs.twenty.com/getting-started/introduction', + docsMcp: 'https://docs.twenty.com/user-guide/ai/capabilities/mcp', docsUserGuide: 'https://docs.twenty.com/user-guide/introduction', github: 'https://github.com/twentyhq/twenty', linkedin: 'https://www.linkedin.com/company/twenty',