From 2c4d40ad97c47ff81b4150c043b60719cb4e69bc Mon Sep 17 00:00:00 2001
From: Peter White <1788320+peterwhite@users.noreply.github.com>
Date: Fri, 11 Sep 2026 08:30:36 +0200
Subject: [PATCH] Expose the site MCP tools through WebMCP in published docs
(#4604)
---
.changeset/webmcp-published-docs.md | 5 ++
.../src/components/SiteLayout/SiteLayout.tsx | 6 +-
.../gitbook/src/components/WebMCP/WebMCP.tsx | 81 +++++++++++++++++++
3 files changed, 91 insertions(+), 1 deletion(-)
create mode 100644 .changeset/webmcp-published-docs.md
create mode 100644 packages/gitbook/src/components/WebMCP/WebMCP.tsx
diff --git a/.changeset/webmcp-published-docs.md b/.changeset/webmcp-published-docs.md
new file mode 100644
index 000000000..57454328c
--- /dev/null
+++ b/.changeset/webmcp-published-docs.md
@@ -0,0 +1,5 @@
+---
+"gitbook": patch
+---
+
+Expose the site's MCP tools to browser agents through WebMCP when the MCP page action is enabled.
diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
index 939543b6e..156373fa0 100644
--- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
+++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
@@ -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: {
+ {customization.pageActions.items.includes(CustomizationPageActionType.Mcp) ? (
+
+ ) : null}
{scripts.map(({ script }) =>
isDeferrableScript(script) ? (
diff --git a/packages/gitbook/src/components/WebMCP/WebMCP.tsx b/packages/gitbook/src/components/WebMCP/WebMCP.tsx
new file mode 100644
index 000000000..e9cae97ab
--- /dev/null
+++ b/packages/gitbook/src/components/WebMCP/WebMCP.tsx
@@ -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;
+ },
+ options?: { signal?: AbortSignal }
+ ) => Promise;
+};
+
+/**
+ * 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 },
+ 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;
+}