Add "Available in MCP" badge for OpenAPI operations (#4350)

This commit is contained in:
Nolann B.
2026-07-01 17:38:04 +02:00
committed by GitHub
parent cb2dfa76af
commit 7e55cd5e4c
19 changed files with 136 additions and 5 deletions
+7
View File
@@ -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.
@@ -51,6 +51,7 @@ export function getOpenAPIContext(args: {
copy: <Icon icon="copy" />,
check: <Icon icon="check" />,
lock: <Icon icon="lock" />,
mcp: <Icon icon="mcp" />,
},
renderCodeBlock: (codeProps) => (
<PlainCodeBlock
@@ -38,11 +38,29 @@
@apply pt-0;
}
/* In headless mode the tags row sits directly above the path; match the operation-level rhythm. */
.openapi-summary-tags + .openapi-path {
@apply mt-3;
}
.openapi-deprecated,
.openapi-stability {
.openapi-stability,
.openapi-mcp {
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded-md straight-corners:rounded-none circular-corners:rounded-lg text-sm leading-[calc(max(1.20em,1.25rem))] before:content-none! after:!content-none;
}
.openapi-mcp {
@apply inline-flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-900/6 ring-emerald-500/5;
}
button.openapi-mcp {
@apply cursor-pointer transition-colors hover:bg-emerald-100 dark:hover:bg-emerald-900/12;
}
.openapi-mcp .gb-icon {
@apply size-3;
}
.openapi-stability-alpha {
@apply text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-900/6 ring-amber-500/5;
}
+18
View File
@@ -37,6 +37,12 @@ export interface OpenAPICustomSpecProperties {
* Description in HTML format.
*/
'x-gitbook-description-html'?: string;
/**
* Default URL of the MCP server exposing the operations in this spec.
* Can be overridden at the path or operation level via the same extension.
*/
'x-gitbook-mcp-url'?: string;
}
/**
@@ -100,6 +106,18 @@ export interface OpenAPICustomOperationProperties {
* @enum 'experimental' | 'alpha' | 'beta'
*/
'x-stability'?: OpenAPIStability;
/**
* If `true`, indicates this operation is available as a tool in an MCP server.
*/
'x-gitbook-mcp'?: boolean;
/**
* URL of the MCP server exposing this operation. When set — here, or inherited from
* the path or root level (operation > path > root) — the "Available in MCP" badge
* becomes a button that copies this URL to the clipboard.
*/
'x-gitbook-mcp-url'?: string;
}
/**
@@ -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 <div className="openapi-mcp">{content}</div>;
}
return (
<OpenAPICopyButton
value={url}
context={getOpenAPIClientContext(context)}
className="openapi-mcp"
label={tString(context.translation, 'copy_url')}
>
{content}
</OpenAPICopyButton>
);
}
@@ -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']) && (
<div className="openapi-summary-tags">
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
{operation['x-stability'] && (
<OpenAPIStability stability={operation['x-stability']} />
)}
{operation['x-gitbook-mcp'] && (
<OpenAPIMcpBadge url={operation['x-gitbook-mcp-url']} context={context} />
)}
</div>
)}
{!context.headless && title
+1
View File
@@ -16,6 +16,7 @@ export interface OpenAPIClientContext {
copy: React.ReactNode;
check: React.ReactNode;
lock: React.ReactNode;
mcp: React.ReactNode;
};
/**
@@ -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.
*/
@@ -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':
@@ -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',
@@ -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',
@@ -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',
@@ -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é',
@@ -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: 'コピー済み',
@@ -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',
@@ -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',
@@ -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',
@@ -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: '已复制',
+12
View File
@@ -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.
*/