Skip to content

Commit 262afa3

Browse files
authored
Implement an MCP server on each docs site (#3641)
1 parent 7375d3c commit 262afa3

File tree

30 files changed

+494
-501
lines changed

30 files changed

+494
-501
lines changed

.changeset/fifty-news-act.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": minor
3+
---
4+
5+
Expose a MCP server for the docs site under /~gitbook/mcp

.changeset/wild-camels-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@gitbook/icons": patch
3+
---
4+
5+
Fix types for custom icons and update list of icons

.github/workflows/deploy-preview.yaml

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,9 @@ jobs:
182182
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
183183
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
184184
ARGOS_BUILD_NAME: 'customers-v2'
185-
pagespeed-testing-v2:
185+
browserless-testing-v2-vercel:
186186
runs-on: ubuntu-latest
187-
name: PageSpeed Testing v1
187+
name: Browserless Testing v2 (Vercel)
188188
needs: deploy-v2-vercel
189189
steps:
190190
- name: Checkout
@@ -195,8 +195,26 @@ jobs:
195195
run: bun install --frozen-lockfile
196196
env:
197197
PUPPETEER_SKIP_DOWNLOAD: 1
198-
- name: Run pagespeed tests
199-
run: bun ./packages/gitbook/tests/pagespeed-testing.ts
198+
- name: Run tests
199+
run: cd ./packages/gitbook && bun e2e-browserless
200200
env:
201201
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
202-
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
202+
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
203+
# browserless-testing-v2-cloudflare:
204+
# runs-on: ubuntu-latest
205+
# name: Browserless Testing v2 (Cloudflare)
206+
# needs: deploy-v2-cloudflare
207+
# steps:
208+
# - name: Checkout
209+
# uses: actions/checkout@v4
210+
# - name: Setup Bun
211+
# uses: ./.github/composite/setup-bun
212+
# - name: Install dependencies
213+
# run: bun install --frozen-lockfile
214+
# env:
215+
# PUPPETEER_SKIP_DOWNLOAD: 1
216+
# - name: Run tests
217+
# run: cd ./packages/gitbook && bun e2e-browserless
218+
# env:
219+
# BASE_URL: ${{needs.deploy-v2-cloudflare.outputs.deployment-url}}
220+
# SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/

bun.lock

Lines changed: 79 additions & 312 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"workspaces": {
3535
"packages": ["packages/*"],
3636
"catalog": {
37-
"@gitbook/api": "^0.140.0",
37+
"@gitbook/api": "^0.141.0",
3838
"bidc": "^0.0.2"
3939
}
4040
},

packages/gitbook/e2e/util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ export function getCustomizationURL(partial: DeepPartial<SiteCustomizationSettin
336336
pageActions: {
337337
externalAI: true,
338338
markdown: true,
339+
mcp: true,
339340
},
340341
trademark: {
341342
enabled: true,

packages/gitbook/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@
7171
"url-join": "^5.0.0",
7272
"usehooks-ts": "^3.1.0",
7373
"warn-once": "^0.1.1",
74-
"zustand": "^5.0.3"
74+
"zustand": "^5.0.3",
75+
"mcp-handler": "^1.0.2",
76+
"@modelcontextprotocol/sdk": "^1.17.5",
77+
"zod": "^3"
7578
},
7679
"devDependencies": {
7780
"@argos-ci/playwright": "^5.0.9",
@@ -84,15 +87,13 @@
8487
"@types/node": "^20",
8588
"@types/object-hash": "^3.0.6",
8689
"@types/parse-cache-control": "^1.0.4",
87-
"@types/psi": "^4.1.6",
8890
"@types/react": "18.3.13",
8991
"@types/react-dom": "18.3.1",
9092
"@types/rison": "^0.0.9",
9193
"deepmerge": "^4.3.1",
9294
"env-cmd": "^10.1.0",
9395
"jsonwebtoken": "^9.0.2",
9496
"postcss": "^8",
95-
"psi": "^4.1.0",
9697
"stylelint": "^16.16.0",
9798
"tailwindcss": "^4.1.11",
9899
"ts-essentials": "^10.0.1",
@@ -113,6 +114,7 @@
113114
"e2e": "playwright test e2e/internal.spec.ts e2e/pdf.spec.ts --project=chromium",
114115
"e2e-customers": "playwright test e2e/customers.spec.ts --project=chromium",
115116
"unit": "bun test {src,packages} --preload ./tests/preload-bun.ts",
117+
"e2e-browserless": "bun test ./tests/",
116118
"typecheck": "tsc --noEmit"
117119
},
118120
"browserslist": [
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { type RouteLayoutParams, getStaticSiteContext } from '@/app/utils';
2+
import { throwIfDataError } from '@/lib/data';
3+
import { joinPathWithBaseURL } from '@/lib/paths';
4+
import { findSiteSpaceBy } from '@/lib/sites';
5+
import { createMcpHandler } from 'mcp-handler';
6+
import type { NextRequest } from 'next/server';
7+
import { z } from 'zod';
8+
9+
async function handler(request: NextRequest, { params }: { params: Promise<RouteLayoutParams> }) {
10+
const { context } = await getStaticSiteContext(await params);
11+
const { dataFetcher, linker, site } = context;
12+
13+
const mcpHandler = createMcpHandler(
14+
(server) => {
15+
server.tool(
16+
'searchDocumentation',
17+
`Search across the documentation to find relevant information, code examples, API references, and guides. Use this tool when you need to answer questions about ${site.title}, find specific documentation, understand how features work, or locate implementation details. The search returns contextual content with titles and direct links to the documentation pages.`,
18+
{
19+
query: z.string(),
20+
},
21+
async ({ query }) => {
22+
const results = await throwIfDataError(
23+
dataFetcher.searchSiteContent({
24+
organizationId: context.organizationId,
25+
siteId: site.id,
26+
query,
27+
scope: { mode: 'all' },
28+
})
29+
);
30+
31+
return {
32+
content: results.flatMap((spaceResult) => {
33+
const found = findSiteSpaceBy(
34+
context.structure,
35+
(siteSpace) => siteSpace.space.id === spaceResult.id
36+
);
37+
const spaceURL = found?.siteSpace.urls.published;
38+
if (!spaceURL) {
39+
return [];
40+
}
41+
42+
return spaceResult.pages.map((pageResult) => {
43+
const pageURL = linker.toAbsoluteURL(
44+
linker.toLinkForContent(
45+
joinPathWithBaseURL(spaceURL, pageResult.path)
46+
)
47+
);
48+
49+
const body = pageResult.sections
50+
?.map((section) => section.body)
51+
.join('\n');
52+
53+
return {
54+
type: 'text',
55+
text: [
56+
`Title: ${pageResult.title}`,
57+
`Link: ${pageURL}`,
58+
body ? `Content: ${body}` : '',
59+
]
60+
.filter(Boolean)
61+
.join('\n'),
62+
};
63+
});
64+
}),
65+
};
66+
}
67+
);
68+
},
69+
{
70+
// Optional server options
71+
},
72+
{
73+
basePath: context.linker.toPathInSite('~gitbook/'),
74+
streamableHttpEndpoint: '/mcp',
75+
maxDuration: 60,
76+
verboseLogs: true,
77+
disableSse: true,
78+
}
79+
);
80+
81+
return mcpHandler(request);
82+
}
83+
84+
export { handler as GET, handler as POST };

0 commit comments

Comments
 (0)