From 7e55cd5e4c0533860f2cf0c9b4140dd6f3f6ebda Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:38:04 +0200 Subject: [PATCH] Add "Available in MCP" badge for OpenAPI operations (#4350) --- .changeset/mcp-available-badge.md | 7 +++++ .../DocumentView/OpenAPI/context.tsx | 1 + .../components/DocumentView/OpenAPI/style.css | 20 ++++++++++++- packages/openapi-parser/src/types.ts | 18 ++++++++++++ .../src/common/OpenAPIMcpBadge.tsx | 29 +++++++++++++++++++ .../src/common/OpenAPISummary.tsx | 6 +++- packages/react-openapi/src/context.ts | 1 + .../src/resolveOpenAPIOperation.ts | 20 +++++++++++-- .../src/resolveOpenAPIWebhook.ts | 9 +++++- packages/react-openapi/src/translations/de.ts | 2 ++ packages/react-openapi/src/translations/en.ts | 2 ++ packages/react-openapi/src/translations/es.ts | 2 ++ packages/react-openapi/src/translations/fr.ts | 2 ++ packages/react-openapi/src/translations/ja.ts | 2 ++ packages/react-openapi/src/translations/nl.ts | 2 ++ packages/react-openapi/src/translations/no.ts | 2 ++ .../react-openapi/src/translations/pt-br.ts | 2 ++ packages/react-openapi/src/translations/zh.ts | 2 ++ packages/react-openapi/src/utils.ts | 12 ++++++++ 19 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 .changeset/mcp-available-badge.md create mode 100644 packages/react-openapi/src/common/OpenAPIMcpBadge.tsx diff --git a/.changeset/mcp-available-badge.md b/.changeset/mcp-available-badge.md new file mode 100644 index 000000000..67a7b53bd --- /dev/null +++ b/.changeset/mcp-available-badge.md @@ -0,0 +1,7 @@ +--- +"@gitbook/openapi-parser": patch +"@gitbook/react-openapi": patch +"gitbook": patch +--- + +Add an "Available in MCP" badge on OpenAPI operations marked with `x-gitbook-mcp: true`. When `x-gitbook-mcp-url` is set (on the operation, path, or root — most specific wins), the badge becomes a button that copies the MCP server URL to the clipboard. diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx index ba44e5d31..110eef2dd 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/context.tsx @@ -51,6 +51,7 @@ export function getOpenAPIContext(args: { copy: , check: , lock: , + mcp: , }, renderCodeBlock: (codeProps) => ( path > root) — the "Available in MCP" badge + * becomes a button that copies this URL to the clipboard. + */ + 'x-gitbook-mcp-url'?: string; } /** diff --git a/packages/react-openapi/src/common/OpenAPIMcpBadge.tsx b/packages/react-openapi/src/common/OpenAPIMcpBadge.tsx new file mode 100644 index 000000000..914f22e9e --- /dev/null +++ b/packages/react-openapi/src/common/OpenAPIMcpBadge.tsx @@ -0,0 +1,29 @@ +import { OpenAPICopyButton } from '../OpenAPICopyButton'; +import { type OpenAPIContext, getOpenAPIClientContext } from '../context'; +import { t, tString } from '../translate'; + +export function OpenAPIMcpBadge(props: { url?: string; context: OpenAPIContext }) { + const { url, context } = props; + + const content = ( + <> + {context.icons.mcp} + {t(context.translation, 'available_in_mcp')} + + ); + + if (!url) { + return
{content}
; + } + + return ( + + {content} + + ); +} diff --git a/packages/react-openapi/src/common/OpenAPISummary.tsx b/packages/react-openapi/src/common/OpenAPISummary.tsx index 78e8b289d..f5b214c39 100644 --- a/packages/react-openapi/src/common/OpenAPISummary.tsx +++ b/packages/react-openapi/src/common/OpenAPISummary.tsx @@ -1,6 +1,7 @@ import { OpenAPIPath } from '../OpenAPIPath'; import type { OpenAPIContext } from '../context'; import type { OpenAPIOperationData, OpenAPIWebhookData } from '../types'; +import { OpenAPIMcpBadge } from './OpenAPIMcpBadge'; import { OpenAPIStability } from './OpenAPIStability'; export function OpenAPISummary(props: { @@ -27,12 +28,15 @@ export function OpenAPISummary(props: { className="openapi-summary" id={!context.headless && operation.summary ? undefined : context.id} > - {(operation.deprecated || operation['x-stability']) && ( + {(operation.deprecated || operation['x-stability'] || operation['x-gitbook-mcp']) && (
{operation.deprecated &&
Deprecated
} {operation['x-stability'] && ( )} + {operation['x-gitbook-mcp'] && ( + + )}
)} {!context.headless && title diff --git a/packages/react-openapi/src/context.ts b/packages/react-openapi/src/context.ts index 5933f6b46..cfb07eadc 100644 --- a/packages/react-openapi/src/context.ts +++ b/packages/react-openapi/src/context.ts @@ -16,6 +16,7 @@ export interface OpenAPIClientContext { copy: React.ReactNode; check: React.ReactNode; lock: React.ReactNode; + mcp: React.ReactNode; }; /** diff --git a/packages/react-openapi/src/resolveOpenAPIOperation.ts b/packages/react-openapi/src/resolveOpenAPIOperation.ts index ebb7ab5f9..5c4ba647b 100644 --- a/packages/react-openapi/src/resolveOpenAPIOperation.ts +++ b/packages/react-openapi/src/resolveOpenAPIOperation.ts @@ -6,7 +6,7 @@ import type { } from '@gitbook/openapi-parser'; import { dereferenceFilesystem } from './dereference'; import type { OpenAPIOperationData, OpenAPISecurityScope } from './types'; -import { checkIsReference } from './utils'; +import { checkIsReference, readMcpUrl } from './utils'; export { fromJSON, toJSON } from 'flatted'; @@ -71,7 +71,11 @@ export async function resolveOpenAPIOperation( return { servers, - operation: { ...operation, security }, + operation: { + ...operation, + security, + 'x-gitbook-mcp-url': getMcpUrl(schema, path, operation), + }, method, path, securities: Array.from(securitiesMap.entries()), @@ -148,6 +152,18 @@ function getServers( return 'servers' in schema ? (schema.servers ?? []) : []; } +/** + * Resolve the MCP server URL for an operation, following the same precedence as servers: + * operation > path > root. + */ +function getMcpUrl( + schema: OpenAPIV3.Document | OpenAPIV3_1.Document, + path: string, + operation: OpenAPIV3.OperationObject +): string | undefined { + return readMcpUrl(operation) ?? readMcpUrl(getPathObject(schema, path)) ?? readMcpUrl(schema); +} + /** * Get an operation by its path and method. */ diff --git a/packages/react-openapi/src/resolveOpenAPIWebhook.ts b/packages/react-openapi/src/resolveOpenAPIWebhook.ts index ed799ac9a..0825b45a2 100644 --- a/packages/react-openapi/src/resolveOpenAPIWebhook.ts +++ b/packages/react-openapi/src/resolveOpenAPIWebhook.ts @@ -6,6 +6,7 @@ import type { } from '@gitbook/openapi-parser'; import { dereferenceFilesystem } from './dereference'; import type { OpenAPIWebhookData } from './types'; +import { readMcpUrl } from './utils'; export { fromJSON, toJSON } from 'flatted'; @@ -40,7 +41,13 @@ export async function resolveOpenAPIWebhook( return { servers, - operation, + operation: { + ...operation, + 'x-gitbook-mcp-url': + readMcpUrl(operation) ?? + readMcpUrl(getPathObject(schema, name)) ?? + readMcpUrl(schema), + }, method, name, 'x-expandAllResponses': diff --git a/packages/react-openapi/src/translations/de.ts b/packages/react-openapi/src/translations/de.ts index 043d06640..a4238288d 100644 --- a/packages/react-openapi/src/translations/de.ts +++ b/packages/react-openapi/src/translations/de.ts @@ -5,6 +5,8 @@ export const de = { stability_experimental: 'Experimentell', stability_alpha: 'Alpha', stability_beta: 'Beta', + available_in_mcp: 'Im MCP verfügbar', + copy_url: 'URL kopieren', discriminator: 'Diskriminator', copy_to_clipboard: 'In die Zwischenablage kopieren', copied: 'Kopiert', diff --git a/packages/react-openapi/src/translations/en.ts b/packages/react-openapi/src/translations/en.ts index c6079979e..e50d7cdaa 100644 --- a/packages/react-openapi/src/translations/en.ts +++ b/packages/react-openapi/src/translations/en.ts @@ -5,6 +5,8 @@ export const en = { stability_experimental: 'Experimental', stability_alpha: 'Alpha', stability_beta: 'Beta', + available_in_mcp: 'Available in MCP', + copy_url: 'Copy URL', discriminator: 'Discriminator', copy_to_clipboard: 'Copy to clipboard', copied: 'Copied', diff --git a/packages/react-openapi/src/translations/es.ts b/packages/react-openapi/src/translations/es.ts index 6f572b1f6..21f61d010 100644 --- a/packages/react-openapi/src/translations/es.ts +++ b/packages/react-openapi/src/translations/es.ts @@ -5,6 +5,8 @@ export const es = { stability_experimental: 'Experimental', stability_alpha: 'Alfa', stability_beta: 'Beta', + available_in_mcp: 'Disponible en el MCP', + copy_url: 'Copiar URL', discriminator: 'Discriminador', copy_to_clipboard: 'Copiar al portapapeles', copied: 'Copiado', diff --git a/packages/react-openapi/src/translations/fr.ts b/packages/react-openapi/src/translations/fr.ts index f7971d9b0..aa2078905 100644 --- a/packages/react-openapi/src/translations/fr.ts +++ b/packages/react-openapi/src/translations/fr.ts @@ -5,6 +5,8 @@ export const fr = { stability_experimental: 'Expérimental', stability_alpha: 'Alpha', stability_beta: 'Bêta', + available_in_mcp: 'Disponible dans le MCP', + copy_url: "Copier l'URL", discriminator: 'Discriminateur', copy_to_clipboard: 'Copier dans le presse-papiers', copied: 'Copié', diff --git a/packages/react-openapi/src/translations/ja.ts b/packages/react-openapi/src/translations/ja.ts index e71b71dac..4806663e6 100644 --- a/packages/react-openapi/src/translations/ja.ts +++ b/packages/react-openapi/src/translations/ja.ts @@ -5,6 +5,8 @@ export const ja = { stability_experimental: '実験的', stability_alpha: 'アルファ', stability_beta: 'ベータ', + available_in_mcp: 'MCPで利用可能', + copy_url: 'URLをコピー', discriminator: 'ディスクリミネーター', copy_to_clipboard: 'クリップボードにコピー', copied: 'コピー済み', diff --git a/packages/react-openapi/src/translations/nl.ts b/packages/react-openapi/src/translations/nl.ts index 748be822d..3c58e1313 100644 --- a/packages/react-openapi/src/translations/nl.ts +++ b/packages/react-openapi/src/translations/nl.ts @@ -5,6 +5,8 @@ export const nl = { stability_experimental: 'Experimenteel', stability_alpha: 'Alfa', stability_beta: 'Bèta', + available_in_mcp: 'Beschikbaar in MCP', + copy_url: 'URL kopiëren', discriminator: 'Discriminator', copy_to_clipboard: 'Kopiëren naar klembord', copied: 'Gekopieerd', diff --git a/packages/react-openapi/src/translations/no.ts b/packages/react-openapi/src/translations/no.ts index f5b580b1b..ed6c096e2 100644 --- a/packages/react-openapi/src/translations/no.ts +++ b/packages/react-openapi/src/translations/no.ts @@ -5,6 +5,8 @@ export const no = { stability_experimental: 'Eksperimentell', stability_alpha: 'Alfa', stability_beta: 'Beta', + available_in_mcp: 'Tilgjengelig i MCP', + copy_url: 'Kopier URL', discriminator: 'Diskriminator', copy_to_clipboard: 'Kopier til utklippstavle', copied: 'Kopiert', diff --git a/packages/react-openapi/src/translations/pt-br.ts b/packages/react-openapi/src/translations/pt-br.ts index a727d9673..58cc8bf37 100644 --- a/packages/react-openapi/src/translations/pt-br.ts +++ b/packages/react-openapi/src/translations/pt-br.ts @@ -5,6 +5,8 @@ export const pt_br = { stability_experimental: 'Experimental', stability_alpha: 'Alfa', stability_beta: 'Beta', + available_in_mcp: 'Disponível no MCP', + copy_url: 'Copiar URL', discriminator: 'Discriminador', copy_to_clipboard: 'Copiar para a área de transferência', copied: 'Copiado', diff --git a/packages/react-openapi/src/translations/zh.ts b/packages/react-openapi/src/translations/zh.ts index 9a34ccb55..4fb1bc88b 100644 --- a/packages/react-openapi/src/translations/zh.ts +++ b/packages/react-openapi/src/translations/zh.ts @@ -5,6 +5,8 @@ export const zh = { stability_experimental: '实验性', stability_alpha: 'Alpha', stability_beta: 'Beta', + available_in_mcp: '可在 MCP 中使用', + copy_url: '复制 URL', discriminator: '判别器', copy_to_clipboard: '复制到剪贴板', copied: '已复制', diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts index f0523766b..1c6ef6a88 100644 --- a/packages/react-openapi/src/utils.ts +++ b/packages/react-openapi/src/utils.ts @@ -16,6 +16,18 @@ export function createStateKey(key: string, scope?: string) { return scope ? `${scope}_${key}` : key; } +/** + * Read the `x-gitbook-mcp-url` extension from any spec object, treating an empty string as unset + * so it falls through to the next level when resolving the operation > path > root cascade. + */ +export function readMcpUrl(obj: unknown): string | undefined { + if (obj && typeof obj === 'object' && 'x-gitbook-mcp-url' in obj) { + const value = obj['x-gitbook-mcp-url']; + return typeof value === 'string' && value ? value : undefined; + } + return undefined; +} + /** * Check if an object has a description. Either at the root level or in items. */