mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Add "Available in MCP" badge for OpenAPI operations (#4350)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: '已复制',
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user