Expose the site MCP tools through WebMCP in published docs (#4604)

This commit is contained in:
Peter White
2026-09-11 08:30:36 +02:00
committed by GitHub
parent 688515efcf
commit 2c4d40ad97
3 changed files with 91 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Expose the site's MCP tools to browser agents through WebMCP when the MCP page action is enabled.
@@ -3,7 +3,7 @@ import Script from 'next/script';
import React from 'react';
import * as ReactDOM from 'react-dom';
import { CustomizationDefaultThemeMode } from '@gitbook/api';
import { CustomizationDefaultThemeMode, CustomizationPageActionType } from '@gitbook/api';
import { AIContextProvider } from '../AI';
import { RocketLoaderDetector } from './RocketLoaderDetector';
@@ -12,6 +12,7 @@ import { AdminToolbar } from '@/components/AdminToolbar';
import { CookiesToast } from '@/components/Cookies';
import { LoadIntegrations } from '@/components/Integrations';
import { SpaceLayout } from '@/components/SpaceLayout';
import { WebMCP } from '@/components/WebMCP/WebMCP';
import type { VisitorAuthClaims } from '@/lib/adaptive';
import { buildVersion } from '@/lib/build';
import type { GitBookSiteContext } from '@/lib/context';
@@ -117,6 +118,9 @@ export async function SiteLayout(props: {
</AIContextProvider>
<LoadIntegrations />
{customization.pageActions.items.includes(CustomizationPageActionType.Mcp) ? (
<WebMCP mcpURL={context.linker.toPathInSite('~gitbook/mcp')} />
) : null}
{scripts.map(({ script }) =>
isDeferrableScript(script) ? (
<Script key={script} src={script} strategy="lazyOnload" />
@@ -0,0 +1,81 @@
'use client';
import * as React from 'react';
// Subset of the WebMCP `ModelContext` interface (https://webmachinelearning.github.io/webmcp/).
type ModelContext = {
registerTool: (
tool: {
name: string;
description: string;
inputSchema?: object;
execute: (input: object, options?: { signal?: AbortSignal }) => Promise<unknown>;
},
options?: { signal?: AbortSignal }
) => Promise<void>;
};
/**
* Expose the site's MCP tools (`~gitbook/mcp`) to browser agents through WebMCP, so anything
* added to the server is automatically available to them. Renders nothing.
*/
export function WebMCP(props: { mcpURL: string }) {
const { mcpURL } = props;
React.useEffect(() => {
const modelContext = (document as { modelContext?: ModelContext }).modelContext;
if (!modelContext) {
return;
}
// Aborting unregisters the tools and discards a load still in flight.
const controller = new AbortController();
const { signal } = controller;
(async () => {
// The MCP SDK is imported lazily: only agentic browsers pay for it.
const [{ Client }, { StreamableHTTPClientTransport }] = await Promise.all([
import('@modelcontextprotocol/sdk/client/index.js'),
import('@modelcontextprotocol/sdk/client/streamableHttp.js'),
]);
// Tagged so WebMCP calls are distinguishable in insights (the request URL is tracked).
const url = new URL(mcpURL, window.location.href);
url.searchParams.set('client', 'webmcp');
const client = new Client({ name: 'gitbook-webmcp', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(url));
const { tools } = await client.listTools();
if (signal.aborted) {
return;
}
for (const tool of tools) {
// Answer synthesis takes 20-30s and browser agents abort tool calls around 30s.
if (tool.name === 'askQuestion') {
continue;
}
await modelContext.registerTool(
{
name: tool.name,
description: tool.description ?? tool.name,
inputSchema: tool.inputSchema,
// The MCP result (`content` blocks, plus `isError` on failure) is passed through.
execute: (input, options) =>
client.callTool(
{ name: tool.name, arguments: input as Record<string, unknown> },
undefined,
{ signal: options?.signal }
),
},
{ signal }
);
}
})().catch((error) => {
// oxlint-disable-next-line no-console
console.warn('WebMCP: could not expose the site MCP tools', error);
});
return () => controller.abort();
}, [mcpURL]);
return null;
}