@samitouri / QOS-React-2 / commits / bb74190c26

[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"