[mcp] Convert docs resource to tool (#33009)
Seems to work better as a tool. Also it now returns plaintext instead of markdown.
lauren committed
Apr 24, 2025 at 14:57 UTC
bb74190c2635990ee6dd754c596a5c409b50ccda
4 files changed
+127
-84
compiler/packages/react-mcp-server/package.json
+2
-2
@@ -19,12 +19,12 @@
19
"@modelcontextprotocol/sdk": "^1.9.0",
20
"algoliasearch": "^5.23.3",
21
"cheerio": "^1.0.0",
22
+ "html-to-text": "^9.0.5",
23
"prettier": "^3.3.3",
23
- "turndown": "^7.2.0",
24
"zod": "^3.23.8"
25
},
26
"devDependencies": {
27
- "@types/turndown": "^5.0.5"
27
+ "@types/html-to-text": "^9.0.4"
28
},
29
"license": "MIT",
30
"repository": {
compiler/packages/react-mcp-server/src/index.ts
+31
-63
@@ -5,10 +5,7 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import {
9
- McpServer,
10
- ResourceTemplate,
11
-} from '@modelcontextprotocol/sdk/server/mcp.js';
8
+import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
9
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
10
import {z} from 'zod';
11
import {compile, type PrintedCompilerPipelineValue} from './compiler';
@@ -20,82 +17,55 @@ import {
17
SourceLocation,
18
} from 'babel-plugin-react-compiler/src';
19
import * as cheerio from 'cheerio';
23
-import TurndownService from 'turndown';
20
import {queryAlgolia} from './utils/algolia';
21
import assertExhaustive from './utils/assertExhaustive';
22
+import {convert} from 'html-to-text';
23
27
-const turndownService = new TurndownService();
24
const server = new McpServer({
25
name: 'React',
26
version: '0.0.0',
27
});
28
33
-function slugify(heading: string): string {
34
- return heading
35
- .split(' ')
36
- .map(w => w.toLowerCase())
37
- .join('-');
38
-}
39
-
40
-// TODO: how to verify this works?
41
-server.resource(
42
- 'docs',
43
- new ResourceTemplate('docs://{message}', {list: undefined}),
44
- async (_uri, {message}) => {
45
- const hits = await queryAlgolia(message);
46
- const deduped = new Map();
47
- for (const hit of hits) {
48
- // drop hashes to dedupe properly
49
- const u = new URL(hit.url);
50
- if (deduped.has(u.pathname)) {
51
- continue;
29
+server.tool(
30
+ 'query-react-dev-docs',
31
+ 'Search/look up official docs from react.dev',
32
+ {
33
+ query: z.string(),
34
+ },
35
+ async ({query}) => {
36
+ try {
37
+ const pages = await queryAlgolia(query);
38
+ if (pages.length === 0) {
39
+ return {
40
+ content: [{type: 'text' as const, text: `No results`}],
41
+ };
42
}
53
- deduped.set(u.pathname, hit);
54
- }
55
- const pages: Array<string | null> = await Promise.all(
56
- Array.from(deduped.values()).map(hit => {
57
- return fetch(hit.url, {
58
- headers: {
59
- 'User-Agent':
60
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
61
- },
62
- }).then(res => {
63
- if (res.ok === true) {
64
- return res.text();
65
- } else {
66
- console.error(
67
- `Could not fetch docs: ${res.status} ${res.statusText}`,
68
- );
69
- return null;
70
- }
71
- });
72
- }),
73
- );
74
-
75
- const resultsMarkdown = pages
76
- .filter(html => html !== null)
77
- .map(html => {
43
+ const content = pages.map(html => {
44
const $ = cheerio.load(html);
79
- const title = encodeURIComponent(slugify($('h1').text()));
45
// react.dev should always have at least one <article> with the main content
46
const article = $('article').html();
47
if (article != null) {
48
return {
84
- uri: `docs://${title}`,
85
- text: turndownService.turndown(article),
49
+ type: 'text' as const,
50
+ text: convert(article),
51
};
52
} else {
53
return {
89
- uri: `docs://${title}`,
90
- // Fallback to converting the whole page to markdown
91
- text: turndownService.turndown($.html()),
54
+ type: 'text' as const,
55
+ // Fallback to converting the whole page to text.
56
+ text: convert($.html()),
57
};
58
}
59
});
95
-
96
- return {
97
- contents: resultsMarkdown,
98
- };
60
+ return {
61
+ content,
62
+ };
63
+ } catch (err) {
64
+ return {
65
+ isError: true,
66
+ content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
67
+ };
68
+ }
69
},
70
);
71
@@ -341,10 +311,8 @@ Design for a good user experience - Provide clear, minimal, and non-blocking UI
311
312
Server Components - Shift data-heavy logic to the server whenever possible. Break up the more static parts of the app into server components. Break up data fetching into server components. Only client components (denoted by the 'use client' top level directive) need interactivity. By rendering parts of your UI on the server, you reduce the client-side JavaScript needed and avoid sending unnecessary data over the wire. Use Server Components to prefetch and pre-render data, allowing faster initial loads and smaller bundle sizes. This also helps manage or eliminate certain waterfalls by resolving data on the server before streaming the HTML (and partial React tree) to the client.
313
344
-## Available Resources
345
-- 'docs': Look up documentation from docs://{query}. Returns markdown as a string.
346
-
314
## Available Tools
315
+- 'docs': Look up documentation from react.dev. Returns text as a string.
316
- 'compile': Run the user's code through React Compiler. Returns optimized JS/TS code with potential diagnostics.
317
318
## Process
compiler/packages/react-mcp-server/src/utils/algolia.ts
+30
-2
@@ -44,7 +44,7 @@ export function printHierarchy(
44
45
export async function queryAlgolia(
46
message: string | Array<string>,
47
-): Promise<Hit<DocSearchHit>[]> {
47
+): Promise<Array<string>> {
48
const {results} = await ALGOLIA_CLIENT.search<DocSearchHit>({
49
requests: [
50
{
@@ -87,5 +87,33 @@ export async function queryAlgolia(
87
});
88
const firstResult = results[0] as SearchResponse<DocSearchHit>;
89
const {hits} = firstResult;
90
- return hits;
90
+ const deduped = new Map();
91
+ for (const hit of hits) {
92
+ // drop hashes to dedupe properly
93
+ const u = new URL(hit.url);
94
+ if (deduped.has(u.pathname)) {
95
+ continue;
96
+ }
97
+ deduped.set(u.pathname, hit);
98
+ }
99
+ const pages: Array<string | null> = await Promise.all(
100
+ Array.from(deduped.values()).map(hit => {
101
+ return fetch(hit.url, {
102
+ headers: {
103
+ 'User-Agent':
104
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
105
+ },
106
+ }).then(res => {
107
+ if (res.ok === true) {
108
+ return res.text();
109
+ } else {
110
+ console.error(
111
+ `Could not fetch docs: ${res.status} ${res.statusText}`,
112
+ );
113
+ return null;
114
+ }
115
+ });
116
+ }),
117
+ );
118
+ return pages.filter(page => page !== null);
119
}
compiler/yarn.lock
+64
-17
@@ -2909,11 +2909,6 @@
2909
"@jridgewell/resolve-uri" "^3.1.0"
2910
"@jridgewell/sourcemap-codec" "^1.4.14"
2911
2912
-"@mixmark-io/domino@^2.2.0":
2913
- version "2.2.0"
2914
- resolved "https://registry.yarnpkg.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3"
2915
- integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==
2916
-
2912
"@modelcontextprotocol/sdk@^1.9.0":
2913
version "1.9.0"
2914
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz#1bf7a4843870b81da26983b8e69bf398d87055f1"
@@ -3061,6 +3056,14 @@
3056
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz#1973871850856ae72bc678aeb066ab952330e923"
3057
integrity sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==
3058
3059
+"@selderee/plugin-htmlparser2@^0.11.0":
3060
+ version "0.11.0"
3061
+ resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517"
3062
+ integrity sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==
3063
+ dependencies:
3064
+ domhandler "^5.0.3"
3065
+ selderee "^0.11.0"
3066
+
3067
"@sideway/address@^4.1.5":
3068
version "4.1.5"
3069
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5"
@@ -3257,6 +3260,11 @@
3260
dependencies:
3261
"@types/node" "*"
3262
3263
+"@types/html-to-text@^9.0.4":
3264
+ version "9.0.4"
3265
+ resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-9.0.4.tgz#4a83dd8ae8bfa91457d0b1ffc26f4d0537eff58c"
3266
+ integrity sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==
3267
+
3268
"@types/invariant@^2.2.35":
3269
version "2.2.35"
3270
resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be"
@@ -3397,11 +3405,6 @@
3405
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
3406
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
3407
3400
-"@types/turndown@^5.0.5":
3401
- version "5.0.5"
3402
- resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.5.tgz#614de24fc9ace4d8c0d9483ba81dc8c1976dd26f"
3403
- integrity sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==
3404
-
3408
"@types/vscode@^1.96.0":
3409
version "1.96.0"
3410
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.96.0.tgz#3181004bf25d71677ae4aacdd7605a3fd7edf08e"
@@ -4802,6 +4805,11 @@ deepmerge@^4.2.2:
4805
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
4806
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
4807
4808
+deepmerge@^4.3.1:
4809
+ version "4.3.1"
4810
+ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
4811
+ integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
4812
+
4813
defaults@^1.0.3:
4814
version "1.0.4"
4815
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
@@ -6046,6 +6054,27 @@ html-escaper@^2.0.0:
6054
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
6055
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
6056
6057
+html-to-text@^9.0.5:
6058
+ version "9.0.5"
6059
+ resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-9.0.5.tgz#6149a0f618ae7a0db8085dca9bbf96d32bb8368d"
6060
+ integrity sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==
6061
+ dependencies:
6062
+ "@selderee/plugin-htmlparser2" "^0.11.0"
6063
+ deepmerge "^4.3.1"
6064
+ dom-serializer "^2.0.0"
6065
+ htmlparser2 "^8.0.2"
6066
+ selderee "^0.11.0"
6067
+
6068
+htmlparser2@^8.0.2:
6069
+ version "8.0.2"
6070
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
6071
+ integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
6072
+ dependencies:
6073
+ domelementtype "^2.3.0"
6074
+ domhandler "^5.0.3"
6075
+ domutils "^3.0.1"
6076
+ entities "^4.4.0"
6077
+
6078
htmlparser2@^9.1.0:
6079
version "9.1.0"
6080
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
@@ -7682,6 +7711,11 @@ kuler@^2.0.0:
7711
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
7712
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
7713
7714
+leac@^0.6.0:
7715
+ version "0.6.0"
7716
+ resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
7717
+ integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
7718
+
7719
leven@2.1.0:
7720
version "2.1.0"
7721
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
@@ -8406,6 +8440,14 @@ parse5@^7.1.2:
8440
dependencies:
8441
entities "^4.4.0"
8442
8443
+parseley@^0.12.0:
8444
+ version "0.12.1"
8445
+ resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef"
8446
+ integrity sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==
8447
+ dependencies:
8448
+ leac "^0.6.0"
8449
+ peberminta "^0.9.0"
8450
+
8451
parseurl@^1.3.3:
8452
version "1.3.3"
8453
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -8470,6 +8512,11 @@ path-type@^4.0.0:
8512
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
8513
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
8514
8515
+peberminta@^0.9.0:
8516
+ version "0.9.0"
8517
+ resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352"
8518
+ integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==
8519
+
8520
picocolors@^1.0.0:
8521
version "1.0.0"
8522
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@@ -9013,6 +9060,13 @@ scheduler@0.0.0-experimental-4beb1fd8-20241118:
9060
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-4beb1fd8-20241118.tgz#3143baa23dfb4daed6a9d0bfd44a8050a0cdab93"
9061
integrity sha512-b7GQktevD5BPcS+R5qY5se5oX4b8AHQyebWswGZBdLCmElIwR3Q+RO5EgsLOA4t5D3/TDjLm58CQG16uEB5rMA==
9062
9063
+selderee@^0.11.0:
9064
+ version "0.11.0"
9065
+ resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.11.0.tgz#6af0c7983e073ad3e35787ffe20cefd9daf0ec8a"
9066
+ integrity sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==
9067
+ dependencies:
9068
+ parseley "^0.12.0"
9069
+
9070
semver@7.x, semver@^7.3.5:
9071
version "7.3.7"
9072
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
@@ -9633,13 +9687,6 @@ tsup@^8.4.0:
9687
tinyglobby "^0.2.11"
9688
tree-kill "^1.2.2"
9689
9636
-turndown@^7.2.0:
9637
- version "7.2.0"
9638
- resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.2.0.tgz#67d614fe8371fb511079a93345abfd156c0ffcf4"
9639
- integrity sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==
9640
- dependencies:
9641
- "@mixmark-io/domino" "^2.2.0"
9642
-
9690
type-check@^0.4.0, type-check@~0.4.0:
9691
version "0.4.0"
9692
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"