Skip to content

Commit f99ae15

Browse files
committed
[mcp] Convert docs resource to tool
Seems to work better as a tool. Also it now returns plaintext instead of markdown.
1 parent 5010364 commit f99ae15

File tree

4 files changed

+127
-84
lines changed

4 files changed

+127
-84
lines changed

compiler/packages/react-mcp-server/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
"@modelcontextprotocol/sdk": "^1.9.0",
2020
"algoliasearch": "^5.23.3",
2121
"cheerio": "^1.0.0",
22+
"html-to-text": "^9.0.5",
2223
"prettier": "^3.3.3",
23-
"turndown": "^7.2.0",
2424
"zod": "^3.23.8"
2525
},
2626
"devDependencies": {
27-
"@types/turndown": "^5.0.5"
27+
"@types/html-to-text": "^9.0.4"
2828
},
2929
"license": "MIT",
3030
"repository": {

compiler/packages/react-mcp-server/src/index.ts

+31-63
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {
9-
McpServer,
10-
ResourceTemplate,
11-
} from '@modelcontextprotocol/sdk/server/mcp.js';
8+
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
129
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
1310
import {z} from 'zod';
1411
import {compile, type PrintedCompilerPipelineValue} from './compiler';
@@ -20,82 +17,55 @@ import {
2017
SourceLocation,
2118
} from 'babel-plugin-react-compiler/src';
2219
import * as cheerio from 'cheerio';
23-
import TurndownService from 'turndown';
2420
import {queryAlgolia} from './utils/algolia';
2521
import assertExhaustive from './utils/assertExhaustive';
22+
import {convert} from 'html-to-text';
2623

27-
const turndownService = new TurndownService();
2824
const server = new McpServer({
2925
name: 'React',
3026
version: '0.0.0',
3127
});
3228

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+
};
5242
}
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 => {
7844
const $ = cheerio.load(html);
79-
const title = encodeURIComponent(slugify($('h1').text()));
8045
// react.dev should always have at least one <article> with the main content
8146
const article = $('article').html();
8247
if (article != null) {
8348
return {
84-
uri: `docs://${title}`,
85-
text: turndownService.turndown(article),
49+
type: 'text' as const,
50+
text: convert(article),
8651
};
8752
} else {
8853
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()),
9257
};
9358
}
9459
});
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+
}
9969
},
10070
);
10171

@@ -341,10 +311,8 @@ Design for a good user experience - Provide clear, minimal, and non-blocking UI
341311
342312
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.
343313
344-
## Available Resources
345-
- 'docs': Look up documentation from docs://{query}. Returns markdown as a string.
346-
347314
## Available Tools
315+
- 'docs': Look up documentation from react.dev. Returns text as a string.
348316
- 'compile': Run the user's code through React Compiler. Returns optimized JS/TS code with potential diagnostics.
349317
350318
## Process

compiler/packages/react-mcp-server/src/utils/algolia.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function printHierarchy(
4444

4545
export async function queryAlgolia(
4646
message: string | Array<string>,
47-
): Promise<Hit<DocSearchHit>[]> {
47+
): Promise<Array<string>> {
4848
const {results} = await ALGOLIA_CLIENT.search<DocSearchHit>({
4949
requests: [
5050
{
@@ -87,5 +87,33 @@ export async function queryAlgolia(
8787
});
8888
const firstResult = results[0] as SearchResponse<DocSearchHit>;
8989
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);
91119
}

compiler/yarn.lock

+64-17
Original file line numberDiff line numberDiff line change
@@ -2909,11 +2909,6 @@
29092909
"@jridgewell/resolve-uri" "^3.1.0"
29102910
"@jridgewell/sourcemap-codec" "^1.4.14"
29112911

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-
29172912
"@modelcontextprotocol/sdk@^1.9.0":
29182913
version "1.9.0"
29192914
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz#1bf7a4843870b81da26983b8e69bf398d87055f1"
@@ -3061,6 +3056,14 @@
30613056
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz#1973871850856ae72bc678aeb066ab952330e923"
30623057
integrity sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==
30633058

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+
30643067
"@sideway/address@^4.1.5":
30653068
version "4.1.5"
30663069
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5"
@@ -3257,6 +3260,11 @@
32573260
dependencies:
32583261
"@types/node" "*"
32593262

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+
32603268
"@types/invariant@^2.2.35":
32613269
version "2.2.35"
32623270
resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.35.tgz#cd3ebf581a6557452735688d8daba6cf0bd5a3be"
@@ -3397,11 +3405,6 @@
33973405
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
33983406
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
33993407

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-
34053408
"@types/vscode@^1.96.0":
34063409
version "1.96.0"
34073410
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.96.0.tgz#3181004bf25d71677ae4aacdd7605a3fd7edf08e"
@@ -4802,6 +4805,11 @@ deepmerge@^4.2.2:
48024805
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
48034806
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
48044807

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+
48054813
defaults@^1.0.3:
48064814
version "1.0.4"
48074815
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
@@ -6046,6 +6054,27 @@ html-escaper@^2.0.0:
60466054
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
60476055
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
60486056

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+
60496078
htmlparser2@^9.1.0:
60506079
version "9.1.0"
60516080
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23"
@@ -7682,6 +7711,11 @@ kuler@^2.0.0:
76827711
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
76837712
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
76847713

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+
76857719
76867720
version "2.1.0"
76877721
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
@@ -8406,6 +8440,14 @@ parse5@^7.1.2:
84068440
dependencies:
84078441
entities "^4.4.0"
84088442

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+
84098451
parseurl@^1.3.3:
84108452
version "1.3.3"
84118453
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -8470,6 +8512,11 @@ path-type@^4.0.0:
84708512
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
84718513
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
84728514

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+
84738520
picocolors@^1.0.0:
84748521
version "1.0.0"
84758522
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@@ -9013,6 +9060,13 @@ [email protected]:
90139060
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-4beb1fd8-20241118.tgz#3143baa23dfb4daed6a9d0bfd44a8050a0cdab93"
90149061
integrity sha512-b7GQktevD5BPcS+R5qY5se5oX4b8AHQyebWswGZBdLCmElIwR3Q+RO5EgsLOA4t5D3/TDjLm58CQG16uEB5rMA==
90159062

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+
90169070
[email protected], semver@^7.3.5:
90179071
version "7.3.7"
90189072
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
@@ -9633,13 +9687,6 @@ tsup@^8.4.0:
96339687
tinyglobby "^0.2.11"
96349688
tree-kill "^1.2.2"
96359689

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-
96439690
type-check@^0.4.0, type-check@~0.4.0:
96449691
version "0.4.0"
96459692
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"

0 commit comments

Comments
 (0)