docusaurus-plugin-mcp-server 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1070 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var path2 = require('path');
6
+ var fs3 = require('fs-extra');
7
+ var pMap = require('p-map');
8
+ var unified = require('unified');
9
+ var rehypeParse = require('rehype-parse');
10
+ var hastUtilSelect = require('hast-util-select');
11
+ var hastUtilToString = require('hast-util-to-string');
12
+ var rehypeRemark = require('rehype-remark');
13
+ var remarkStringify = require('remark-stringify');
14
+ var remarkGfm = require('remark-gfm');
15
+ var FlexSearch = require('flexsearch');
16
+ var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
17
+ var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
18
+ var webStandardStreamableHttp_js = require('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js');
19
+ var zod = require('zod');
20
+
21
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
22
+
23
+ var path2__default = /*#__PURE__*/_interopDefault(path2);
24
+ var fs3__default = /*#__PURE__*/_interopDefault(fs3);
25
+ var pMap__default = /*#__PURE__*/_interopDefault(pMap);
26
+ var rehypeParse__default = /*#__PURE__*/_interopDefault(rehypeParse);
27
+ var rehypeRemark__default = /*#__PURE__*/_interopDefault(rehypeRemark);
28
+ var remarkStringify__default = /*#__PURE__*/_interopDefault(remarkStringify);
29
+ var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
30
+ var FlexSearch__default = /*#__PURE__*/_interopDefault(FlexSearch);
31
+
32
+ // src/plugin/docusaurus-plugin.ts
33
+
34
+ // src/types/index.ts
35
+ var DEFAULT_OPTIONS = {
36
+ outputDir: "mcp",
37
+ contentSelectors: ["article", "main", ".main-wrapper", '[role="main"]'],
38
+ excludeSelectors: [
39
+ "nav",
40
+ "header",
41
+ "footer",
42
+ "aside",
43
+ '[role="navigation"]',
44
+ '[role="banner"]',
45
+ '[role="contentinfo"]'
46
+ ],
47
+ minContentLength: 50,
48
+ server: {
49
+ name: "docs-mcp-server",
50
+ version: "1.0.0"
51
+ },
52
+ excludeRoutes: ["/404*", "/search*"]
53
+ };
54
+ function filterRoutes(routes, excludePatterns) {
55
+ return routes.filter((route) => {
56
+ return !excludePatterns.some((pattern) => {
57
+ const regexPattern = pattern.replace(/\*/g, ".*").replace(/\?/g, ".");
58
+ const regex = new RegExp(`^${regexPattern}$`);
59
+ return regex.test(route.path);
60
+ });
61
+ });
62
+ }
63
+ async function discoverHtmlFiles(outDir) {
64
+ const routes = [];
65
+ async function scanDirectory(dir) {
66
+ const entries = await fs3__default.default.readdir(dir, { withFileTypes: true });
67
+ for (const entry of entries) {
68
+ const fullPath = path2__default.default.join(dir, entry.name);
69
+ if (entry.isDirectory()) {
70
+ if (["assets", "img", "static"].includes(entry.name)) {
71
+ continue;
72
+ }
73
+ await scanDirectory(fullPath);
74
+ } else if (entry.name === "index.html") {
75
+ const relativePath = path2__default.default.relative(outDir, fullPath);
76
+ let routePath = "/" + path2__default.default.dirname(relativePath).replace(/\\/g, "/");
77
+ if (routePath === "/.") {
78
+ routePath = "/";
79
+ }
80
+ routes.push({
81
+ path: routePath,
82
+ htmlPath: fullPath
83
+ });
84
+ }
85
+ }
86
+ }
87
+ await scanDirectory(outDir);
88
+ return routes;
89
+ }
90
+ async function collectRoutes(outDir, excludePatterns) {
91
+ const allRoutes = await discoverHtmlFiles(outDir);
92
+ const filteredRoutes = filterRoutes(allRoutes, excludePatterns);
93
+ const uniqueRoutes = /* @__PURE__ */ new Map();
94
+ for (const route of filteredRoutes) {
95
+ if (!uniqueRoutes.has(route.path)) {
96
+ uniqueRoutes.set(route.path, route);
97
+ }
98
+ }
99
+ return Array.from(uniqueRoutes.values());
100
+ }
101
+ function parseHtml(html) {
102
+ const processor = unified.unified().use(rehypeParse__default.default);
103
+ return processor.parse(html);
104
+ }
105
+ async function parseHtmlFile(filePath) {
106
+ const html = await fs3__default.default.readFile(filePath, "utf-8");
107
+ return parseHtml(html);
108
+ }
109
+ function extractTitle(tree) {
110
+ const h1Element = hastUtilSelect.select("h1", tree);
111
+ if (h1Element) {
112
+ return hastUtilToString.toString(h1Element).trim();
113
+ }
114
+ const titleElement = hastUtilSelect.select("title", tree);
115
+ if (titleElement) {
116
+ return hastUtilToString.toString(titleElement).trim();
117
+ }
118
+ return "Untitled";
119
+ }
120
+ function extractDescription(tree) {
121
+ const metaDescription = hastUtilSelect.select('meta[name="description"]', tree);
122
+ if (metaDescription && metaDescription.properties?.content) {
123
+ return String(metaDescription.properties.content);
124
+ }
125
+ const ogDescription = hastUtilSelect.select('meta[property="og:description"]', tree);
126
+ if (ogDescription && ogDescription.properties?.content) {
127
+ return String(ogDescription.properties.content);
128
+ }
129
+ return "";
130
+ }
131
+ function findContentElement(tree, selectors) {
132
+ for (const selector of selectors) {
133
+ const element = hastUtilSelect.select(selector, tree);
134
+ if (element) {
135
+ const text = hastUtilToString.toString(element).trim();
136
+ if (text.length > 50) {
137
+ return element;
138
+ }
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ var ALWAYS_EXCLUDED = ["script", "style", "noscript"];
144
+ function cleanContentElement(element, excludeSelectors) {
145
+ const allSelectors = [...ALWAYS_EXCLUDED, ...excludeSelectors];
146
+ const cloned = JSON.parse(JSON.stringify(element));
147
+ function removeUnwanted(node) {
148
+ if (!node.children) return;
149
+ node.children = node.children.filter((child) => {
150
+ if (child.type !== "element") return true;
151
+ const childElement = child;
152
+ for (const selector of allSelectors) {
153
+ if (selector.startsWith(".")) {
154
+ const className = selector.slice(1);
155
+ const classes = childElement.properties?.className;
156
+ if (Array.isArray(classes) && classes.includes(className)) {
157
+ return false;
158
+ }
159
+ if (typeof classes === "string" && classes.includes(className)) {
160
+ return false;
161
+ }
162
+ } else if (selector.startsWith("[")) {
163
+ const match = selector.match(/\[([^=]+)="([^"]+)"\]/);
164
+ if (match) {
165
+ const [, attr, value] = match;
166
+ if (attr && childElement.properties?.[attr] === value) {
167
+ return false;
168
+ }
169
+ }
170
+ } else {
171
+ if (childElement.tagName === selector) {
172
+ return false;
173
+ }
174
+ }
175
+ }
176
+ removeUnwanted(childElement);
177
+ return true;
178
+ });
179
+ }
180
+ removeUnwanted(cloned);
181
+ return cloned;
182
+ }
183
+ async function extractContent(filePath, options) {
184
+ const tree = await parseHtmlFile(filePath);
185
+ const title = extractTitle(tree);
186
+ const description = extractDescription(tree);
187
+ let contentElement = findContentElement(tree, options.contentSelectors);
188
+ if (!contentElement) {
189
+ const body = hastUtilSelect.select("body", tree);
190
+ if (body) {
191
+ contentElement = body;
192
+ }
193
+ }
194
+ let contentHtml = "";
195
+ if (contentElement) {
196
+ const cleanedElement = cleanContentElement(contentElement, options.excludeSelectors);
197
+ contentHtml = serializeElement(cleanedElement);
198
+ }
199
+ return {
200
+ title,
201
+ description,
202
+ contentHtml
203
+ };
204
+ }
205
+ function serializeElement(element) {
206
+ const voidElements = /* @__PURE__ */ new Set([
207
+ "area",
208
+ "base",
209
+ "br",
210
+ "col",
211
+ "embed",
212
+ "hr",
213
+ "img",
214
+ "input",
215
+ "link",
216
+ "meta",
217
+ "param",
218
+ "source",
219
+ "track",
220
+ "wbr"
221
+ ]);
222
+ function serialize(node) {
223
+ if (!node || typeof node !== "object") return "";
224
+ const n = node;
225
+ if (n.type === "text") {
226
+ return n.value ?? "";
227
+ }
228
+ if (n.type === "element" && n.tagName) {
229
+ const tagName = n.tagName;
230
+ const props = n.properties ?? {};
231
+ const attrs = [];
232
+ for (const [key, value] of Object.entries(props)) {
233
+ if (key === "className" && Array.isArray(value)) {
234
+ attrs.push(`class="${value.join(" ")}"`);
235
+ } else if (typeof value === "boolean") {
236
+ if (value) attrs.push(key);
237
+ } else if (value !== void 0 && value !== null) {
238
+ attrs.push(`${key}="${String(value)}"`);
239
+ }
240
+ }
241
+ const attrStr = attrs.length > 0 ? " " + attrs.join(" ") : "";
242
+ if (voidElements.has(tagName)) {
243
+ return `<${tagName}${attrStr} />`;
244
+ }
245
+ const children = n.children ?? [];
246
+ const childrenHtml = children.map(serialize).join("");
247
+ return `<${tagName}${attrStr}>${childrenHtml}</${tagName}>`;
248
+ }
249
+ if (n.type === "root" && n.children) {
250
+ return n.children.map(serialize).join("");
251
+ }
252
+ return "";
253
+ }
254
+ return serialize(element);
255
+ }
256
+ async function htmlToMarkdown(html) {
257
+ if (!html || html.trim().length === 0) {
258
+ return "";
259
+ }
260
+ try {
261
+ const processor = unified.unified().use(rehypeParse__default.default, { fragment: true }).use(rehypeRemark__default.default).use(remarkGfm__default.default).use(remarkStringify__default.default, {
262
+ bullet: "-",
263
+ fences: true
264
+ });
265
+ const result = await processor.process(html);
266
+ let markdown = String(result);
267
+ markdown = cleanMarkdown(markdown);
268
+ return markdown;
269
+ } catch (error) {
270
+ console.error("Error converting HTML to Markdown:", error);
271
+ return extractTextFallback(html);
272
+ }
273
+ }
274
+ function cleanMarkdown(markdown) {
275
+ return markdown.replace(/\n{3,}/g, "\n\n").split("\n").map((line) => line.trimEnd()).join("\n").trim() + "\n";
276
+ }
277
+ function extractTextFallback(html) {
278
+ let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
279
+ text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
280
+ text = text.replace(/<br\s*\/?>/gi, "\n");
281
+ text = text.replace(/<\/p>/gi, "\n\n");
282
+ text = text.replace(/<\/h[1-6]>/gi, "\n\n");
283
+ text = text.replace(/<\/li>/gi, "\n");
284
+ text = text.replace(/<\/div>/gi, "\n");
285
+ text = text.replace(/<[^>]+>/g, "");
286
+ text = text.replace(/&nbsp;/g, " ");
287
+ text = text.replace(/&amp;/g, "&");
288
+ text = text.replace(/&lt;/g, "<");
289
+ text = text.replace(/&gt;/g, ">");
290
+ text = text.replace(/&quot;/g, '"');
291
+ text = text.replace(/&#39;/g, "'");
292
+ text = text.replace(/[ \t]+/g, " ");
293
+ text = text.replace(/\n{3,}/g, "\n\n");
294
+ return text.trim();
295
+ }
296
+
297
+ // src/processing/heading-extractor.ts
298
+ function extractHeadingsFromMarkdown(markdown) {
299
+ const headings = [];
300
+ const lines = markdown.split("\n");
301
+ let currentOffset = 0;
302
+ for (let i = 0; i < lines.length; i++) {
303
+ const line = lines[i] ?? "";
304
+ const headingMatch = line.match(/^(#{1,6})\s+(.+?)(?:\s+\{#([^}]+)\})?$/);
305
+ if (headingMatch) {
306
+ const hashes = headingMatch[1] ?? "";
307
+ const level = hashes.length;
308
+ let text = headingMatch[2] ?? "";
309
+ let id = headingMatch[3] ?? "";
310
+ if (!id) {
311
+ id = generateHeadingId(text);
312
+ }
313
+ text = text.replace(/\*\*([^*]+)\*\*/g, "$1");
314
+ text = text.replace(/_([^_]+)_/g, "$1");
315
+ text = text.replace(/`([^`]+)`/g, "$1");
316
+ headings.push({
317
+ level,
318
+ text: text.trim(),
319
+ id,
320
+ startOffset: currentOffset,
321
+ endOffset: -1
322
+ // Will be calculated below
323
+ });
324
+ }
325
+ currentOffset += line.length + 1;
326
+ }
327
+ for (let i = 0; i < headings.length; i++) {
328
+ const current = headings[i];
329
+ if (!current) continue;
330
+ let endOffset = markdown.length;
331
+ for (let j = i + 1; j < headings.length; j++) {
332
+ const next = headings[j];
333
+ if (next && next.level <= current.level) {
334
+ endOffset = next.startOffset;
335
+ break;
336
+ }
337
+ }
338
+ current.endOffset = endOffset;
339
+ }
340
+ return headings;
341
+ }
342
+ function generateHeadingId(text) {
343
+ return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
344
+ }
345
+ function extractSection(markdown, headingId, headings) {
346
+ const heading = headings.find((h) => h.id === headingId);
347
+ if (!heading) {
348
+ return null;
349
+ }
350
+ return markdown.slice(heading.startOffset, heading.endOffset).trim();
351
+ }
352
+ var FIELD_WEIGHTS = {
353
+ title: 3,
354
+ headings: 2,
355
+ description: 1.5,
356
+ content: 1
357
+ };
358
+ function englishStemmer(word) {
359
+ if (word.length <= 3) return word;
360
+ return word.replace(/ing$/, "").replace(/tion$/, "t").replace(/sion$/, "s").replace(/([^aeiou])ed$/, "$1").replace(/([^aeiou])es$/, "$1").replace(/ly$/, "").replace(/ment$/, "").replace(/ness$/, "").replace(/ies$/, "y").replace(/([^s])s$/, "$1");
361
+ }
362
+ function createSearchIndex() {
363
+ return new FlexSearch__default.default.Document({
364
+ // Use 'full' tokenization for substring matching
365
+ // This allows "auth" to match "authentication"
366
+ tokenize: "full",
367
+ // Enable caching for faster repeated queries
368
+ cache: 100,
369
+ // Higher resolution = more granular ranking (1-9)
370
+ resolution: 9,
371
+ // Enable context for phrase/proximity matching
372
+ context: {
373
+ resolution: 2,
374
+ depth: 2,
375
+ bidirectional: true
376
+ },
377
+ // Apply stemming to normalize word forms
378
+ encode: (str) => {
379
+ const words = str.toLowerCase().split(/[\s\-_.,;:!?'"()[\]{}]+/);
380
+ return words.filter(Boolean).map(englishStemmer);
381
+ },
382
+ // Document schema
383
+ document: {
384
+ id: "id",
385
+ // Index these fields for searching
386
+ index: ["title", "content", "headings", "description"],
387
+ // Store these fields in results (for enriched queries)
388
+ store: ["title", "description"]
389
+ }
390
+ });
391
+ }
392
+ function addDocumentToIndex(index, doc) {
393
+ const indexable = {
394
+ id: doc.route,
395
+ title: doc.title,
396
+ content: doc.markdown,
397
+ headings: doc.headings.map((h) => h.text).join(" "),
398
+ description: doc.description
399
+ };
400
+ index.add(indexable);
401
+ }
402
+ function buildSearchIndex(docs) {
403
+ const index = createSearchIndex();
404
+ for (const doc of docs) {
405
+ addDocumentToIndex(index, doc);
406
+ }
407
+ return index;
408
+ }
409
+ function searchIndex(index, docs, query, options = {}) {
410
+ const { limit = 5 } = options;
411
+ const rawResults = index.search(query, {
412
+ limit: limit * 3,
413
+ // Get extra results for better ranking after weighting
414
+ enrich: true
415
+ });
416
+ const docScores = /* @__PURE__ */ new Map();
417
+ for (const fieldResult of rawResults) {
418
+ const field = fieldResult.field;
419
+ const fieldWeight = FIELD_WEIGHTS[field] ?? 1;
420
+ const results2 = fieldResult.result;
421
+ for (let i = 0; i < results2.length; i++) {
422
+ const item = results2[i];
423
+ if (!item) continue;
424
+ const docId = typeof item === "string" ? item : item.id;
425
+ const positionScore = (results2.length - i) / results2.length;
426
+ const weightedScore = positionScore * fieldWeight;
427
+ const existingScore = docScores.get(docId) ?? 0;
428
+ docScores.set(docId, existingScore + weightedScore);
429
+ }
430
+ }
431
+ const results = [];
432
+ for (const [docId, score] of docScores) {
433
+ const doc = docs[docId];
434
+ if (!doc) continue;
435
+ results.push({
436
+ route: doc.route,
437
+ title: doc.title,
438
+ score,
439
+ snippet: generateSnippet(doc.markdown, query),
440
+ matchingHeadings: findMatchingHeadings(doc, query)
441
+ });
442
+ }
443
+ results.sort((a, b) => b.score - a.score);
444
+ return results.slice(0, limit);
445
+ }
446
+ function generateSnippet(markdown, query) {
447
+ const maxLength = 200;
448
+ const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
449
+ if (queryTerms.length === 0) {
450
+ return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
451
+ }
452
+ const lowerMarkdown = markdown.toLowerCase();
453
+ let bestIndex = -1;
454
+ let bestTerm = "";
455
+ const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
456
+ for (const term of allTerms) {
457
+ const index = lowerMarkdown.indexOf(term);
458
+ if (index !== -1 && (bestIndex === -1 || index < bestIndex)) {
459
+ bestIndex = index;
460
+ bestTerm = term;
461
+ }
462
+ }
463
+ if (bestIndex === -1) {
464
+ return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
465
+ }
466
+ const snippetStart = Math.max(0, bestIndex - 50);
467
+ const snippetEnd = Math.min(markdown.length, bestIndex + bestTerm.length + 150);
468
+ let snippet = markdown.slice(snippetStart, snippetEnd);
469
+ snippet = snippet.replace(/^#{1,6}\s+/gm, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/```[a-z]*\n?/g, "").replace(/`([^`]+)`/g, "$1").replace(/\s+/g, " ").trim();
470
+ const prefix = snippetStart > 0 ? "..." : "";
471
+ const suffix = snippetEnd < markdown.length ? "..." : "";
472
+ return prefix + snippet + suffix;
473
+ }
474
+ function findMatchingHeadings(doc, query) {
475
+ const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
476
+ const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
477
+ const matching = [];
478
+ for (const heading of doc.headings) {
479
+ const headingLower = heading.text.toLowerCase();
480
+ const headingStemmed = headingLower.split(/\s+/).map(englishStemmer).join(" ");
481
+ if (allTerms.some(
482
+ (term) => headingLower.includes(term) || headingStemmed.includes(englishStemmer(term))
483
+ )) {
484
+ matching.push(heading.text);
485
+ }
486
+ }
487
+ return matching.slice(0, 3);
488
+ }
489
+ async function exportSearchIndex(index) {
490
+ const exportData = {};
491
+ await index.export((key, data) => {
492
+ exportData[key] = data;
493
+ });
494
+ return exportData;
495
+ }
496
+ async function importSearchIndex(data) {
497
+ const index = createSearchIndex();
498
+ for (const [key, value] of Object.entries(data)) {
499
+ await index.import(
500
+ key,
501
+ value
502
+ );
503
+ }
504
+ return index;
505
+ }
506
+
507
+ // src/plugin/docusaurus-plugin.ts
508
+ function resolveOptions(options) {
509
+ return {
510
+ ...DEFAULT_OPTIONS,
511
+ ...options,
512
+ server: {
513
+ ...DEFAULT_OPTIONS.server,
514
+ ...options.server
515
+ }
516
+ };
517
+ }
518
+ async function processHtmlFile(htmlPath, route, options) {
519
+ try {
520
+ const extractOptions = {
521
+ contentSelectors: options.contentSelectors,
522
+ excludeSelectors: options.excludeSelectors
523
+ };
524
+ const extracted = await extractContent(htmlPath, extractOptions);
525
+ if (!extracted.contentHtml) {
526
+ console.warn(`[MCP] No content found in ${htmlPath}`);
527
+ return null;
528
+ }
529
+ const markdown = await htmlToMarkdown(extracted.contentHtml);
530
+ if (!markdown || markdown.trim().length < options.minContentLength) {
531
+ console.warn(`[MCP] Insufficient content in ${htmlPath}`);
532
+ return null;
533
+ }
534
+ const headings = extractHeadingsFromMarkdown(markdown);
535
+ return {
536
+ route,
537
+ title: extracted.title,
538
+ description: extracted.description,
539
+ markdown,
540
+ headings
541
+ };
542
+ } catch (error) {
543
+ console.error(`[MCP] Error processing ${htmlPath}:`, error);
544
+ return null;
545
+ }
546
+ }
547
+ function mcpServerPlugin(context, options) {
548
+ const resolvedOptions = resolveOptions(options);
549
+ return {
550
+ name: "docusaurus-plugin-mcp-server",
551
+ // Expose configuration to theme components via globalData
552
+ async contentLoaded({ actions }) {
553
+ const { setGlobalData } = actions;
554
+ const serverUrl = `${context.siteConfig.url}/${resolvedOptions.outputDir}`;
555
+ setGlobalData({
556
+ serverUrl,
557
+ serverName: resolvedOptions.server.name
558
+ });
559
+ },
560
+ async postBuild({ outDir }) {
561
+ console.log("[MCP] Starting MCP artifact generation...");
562
+ const startTime = Date.now();
563
+ const routes = await collectRoutes(outDir, resolvedOptions.excludeRoutes);
564
+ console.log(`[MCP] Found ${routes.length} routes to process`);
565
+ if (routes.length === 0) {
566
+ console.warn("[MCP] No routes found to process");
567
+ return;
568
+ }
569
+ const processOptions = {
570
+ contentSelectors: resolvedOptions.contentSelectors,
571
+ excludeSelectors: resolvedOptions.excludeSelectors,
572
+ minContentLength: resolvedOptions.minContentLength
573
+ };
574
+ const processedDocs = await pMap__default.default(
575
+ routes,
576
+ async (route) => {
577
+ return processHtmlFile(route.htmlPath, route.path, processOptions);
578
+ },
579
+ { concurrency: 10 }
580
+ );
581
+ const validDocs = processedDocs.filter((doc) => doc !== null);
582
+ console.log(`[MCP] Successfully processed ${validDocs.length} documents`);
583
+ if (validDocs.length === 0) {
584
+ console.warn("[MCP] No valid documents to index");
585
+ return;
586
+ }
587
+ const docsIndex = {};
588
+ for (const doc of validDocs) {
589
+ docsIndex[doc.route] = doc;
590
+ }
591
+ console.log("[MCP] Building search index...");
592
+ const searchIndex2 = buildSearchIndex(validDocs);
593
+ const exportedIndex = await exportSearchIndex(searchIndex2);
594
+ const manifest = {
595
+ version: resolvedOptions.server.version,
596
+ buildTime: (/* @__PURE__ */ new Date()).toISOString(),
597
+ docCount: validDocs.length,
598
+ serverName: resolvedOptions.server.name,
599
+ baseUrl: context.siteConfig.url
600
+ };
601
+ const mcpOutputDir = path2__default.default.join(outDir, resolvedOptions.outputDir);
602
+ await fs3__default.default.ensureDir(mcpOutputDir);
603
+ await Promise.all([
604
+ fs3__default.default.writeJson(path2__default.default.join(mcpOutputDir, "docs.json"), docsIndex, { spaces: 0 }),
605
+ fs3__default.default.writeJson(path2__default.default.join(mcpOutputDir, "search-index.json"), exportedIndex, { spaces: 0 }),
606
+ fs3__default.default.writeJson(path2__default.default.join(mcpOutputDir, "manifest.json"), manifest, { spaces: 2 })
607
+ ]);
608
+ const elapsed = Date.now() - startTime;
609
+ console.log(`[MCP] Artifacts written to ${mcpOutputDir}`);
610
+ console.log(`[MCP] Generation complete in ${elapsed}ms`);
611
+ }
612
+ };
613
+ }
614
+
615
+ // src/mcp/tools/docs-search.ts
616
+ var docsSearchTool = {
617
+ name: "docs_search",
618
+ description: "Search across developer documentation. Returns ranked results with snippets and matching headings.",
619
+ inputSchema: {
620
+ type: "object",
621
+ properties: {
622
+ query: {
623
+ type: "string",
624
+ description: "Search query string"
625
+ },
626
+ limit: {
627
+ type: "number",
628
+ description: "Maximum number of results to return (default: 5, max: 20)",
629
+ default: 5
630
+ }
631
+ },
632
+ required: ["query"]
633
+ }
634
+ };
635
+ function executeDocsSearch(params, index, docs) {
636
+ const { query, limit = 5 } = params;
637
+ if (!query || typeof query !== "string" || query.trim().length === 0) {
638
+ throw new Error("Query parameter is required and must be a non-empty string");
639
+ }
640
+ const effectiveLimit = Math.min(Math.max(1, limit), 20);
641
+ const results = searchIndex(index, docs, query.trim(), {
642
+ limit: effectiveLimit
643
+ });
644
+ return results;
645
+ }
646
+ function formatSearchResults(results, baseUrl) {
647
+ if (results.length === 0) {
648
+ return "No matching documents found.";
649
+ }
650
+ const lines = [`Found ${results.length} result(s):
651
+ `];
652
+ for (let i = 0; i < results.length; i++) {
653
+ const result = results[i];
654
+ if (!result) continue;
655
+ lines.push(`${i + 1}. **${result.title}**`);
656
+ if (baseUrl) {
657
+ const fullUrl = `${baseUrl.replace(/\/$/, "")}${result.route}`;
658
+ lines.push(` URL: ${fullUrl}`);
659
+ }
660
+ lines.push(` Route: ${result.route}`);
661
+ if (result.matchingHeadings && result.matchingHeadings.length > 0) {
662
+ lines.push(` Matching sections: ${result.matchingHeadings.join(", ")}`);
663
+ }
664
+ lines.push(` ${result.snippet}`);
665
+ lines.push("");
666
+ }
667
+ return lines.join("\n");
668
+ }
669
+
670
+ // src/mcp/tools/docs-get-page.ts
671
+ var docsGetPageTool = {
672
+ name: "docs_get_page",
673
+ description: "Retrieve the full content of a documentation page as markdown. Use this after searching to get complete page content.",
674
+ inputSchema: {
675
+ type: "object",
676
+ properties: {
677
+ route: {
678
+ type: "string",
679
+ description: "The route path of the page (e.g., /docs/getting-started)"
680
+ }
681
+ },
682
+ required: ["route"]
683
+ }
684
+ };
685
+ function executeDocsGetPage(params, docs) {
686
+ const { route } = params;
687
+ if (!route || typeof route !== "string") {
688
+ throw new Error("Route parameter is required and must be a string");
689
+ }
690
+ let normalizedRoute = route.trim();
691
+ if (!normalizedRoute.startsWith("/")) {
692
+ normalizedRoute = "/" + normalizedRoute;
693
+ }
694
+ if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
695
+ normalizedRoute = normalizedRoute.slice(0, -1);
696
+ }
697
+ const doc = docs[normalizedRoute];
698
+ if (!doc) {
699
+ const altRoute = normalizedRoute.slice(1);
700
+ if (docs[altRoute]) {
701
+ return docs[altRoute] ?? null;
702
+ }
703
+ return null;
704
+ }
705
+ return doc;
706
+ }
707
+ function formatPageContent(doc, baseUrl) {
708
+ if (!doc) {
709
+ return "Page not found. Please check the route path and try again.";
710
+ }
711
+ const lines = [];
712
+ lines.push(`# ${doc.title}`);
713
+ lines.push("");
714
+ if (doc.description) {
715
+ lines.push(`> ${doc.description}`);
716
+ lines.push("");
717
+ }
718
+ if (baseUrl) {
719
+ const fullUrl = `${baseUrl.replace(/\/$/, "")}${doc.route}`;
720
+ lines.push(`**URL:** ${fullUrl}`);
721
+ }
722
+ lines.push(`**Route:** ${doc.route}`);
723
+ lines.push("");
724
+ if (doc.headings.length > 0) {
725
+ lines.push("## Contents");
726
+ lines.push("");
727
+ for (const heading of doc.headings) {
728
+ if (heading.level <= 3) {
729
+ const indent = " ".repeat(heading.level - 1);
730
+ lines.push(`${indent}- [${heading.text}](#${heading.id})`);
731
+ }
732
+ }
733
+ lines.push("");
734
+ lines.push("---");
735
+ lines.push("");
736
+ }
737
+ lines.push(doc.markdown);
738
+ return lines.join("\n");
739
+ }
740
+
741
+ // src/mcp/tools/docs-get-section.ts
742
+ var docsGetSectionTool = {
743
+ name: "docs_get_section",
744
+ description: "Retrieve a specific section of a documentation page by heading ID. Use this to get focused content from a larger page.",
745
+ inputSchema: {
746
+ type: "object",
747
+ properties: {
748
+ route: {
749
+ type: "string",
750
+ description: "The route path of the page (e.g., /docs/getting-started)"
751
+ },
752
+ headingId: {
753
+ type: "string",
754
+ description: "The ID of the heading to retrieve (e.g., authentication)"
755
+ }
756
+ },
757
+ required: ["route", "headingId"]
758
+ }
759
+ };
760
+ function executeDocsGetSection(params, docs) {
761
+ const { route, headingId } = params;
762
+ if (!route || typeof route !== "string") {
763
+ throw new Error("Route parameter is required and must be a string");
764
+ }
765
+ if (!headingId || typeof headingId !== "string") {
766
+ throw new Error("HeadingId parameter is required and must be a string");
767
+ }
768
+ let normalizedRoute = route.trim();
769
+ if (!normalizedRoute.startsWith("/")) {
770
+ normalizedRoute = "/" + normalizedRoute;
771
+ }
772
+ if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
773
+ normalizedRoute = normalizedRoute.slice(0, -1);
774
+ }
775
+ const doc = docs[normalizedRoute];
776
+ if (!doc) {
777
+ return {
778
+ content: null,
779
+ doc: null,
780
+ headingText: null,
781
+ availableHeadings: []
782
+ };
783
+ }
784
+ const availableHeadings = doc.headings.map((h) => ({
785
+ id: h.id,
786
+ text: h.text,
787
+ level: h.level
788
+ }));
789
+ const heading = doc.headings.find((h) => h.id === headingId.trim());
790
+ if (!heading) {
791
+ return {
792
+ content: null,
793
+ doc,
794
+ headingText: null,
795
+ availableHeadings
796
+ };
797
+ }
798
+ const content = extractSection(doc.markdown, headingId.trim(), doc.headings);
799
+ return {
800
+ content,
801
+ doc,
802
+ headingText: heading.text,
803
+ availableHeadings
804
+ };
805
+ }
806
+ function formatSectionContent(result, headingId, baseUrl) {
807
+ if (!result.doc) {
808
+ return "Page not found. Please check the route path and try again.";
809
+ }
810
+ if (!result.content) {
811
+ const lines2 = [`Section "${headingId}" not found in this document.`, "", "Available sections:"];
812
+ for (const heading of result.availableHeadings) {
813
+ const indent = " ".repeat(heading.level - 1);
814
+ lines2.push(`${indent}- ${heading.text} (id: ${heading.id})`);
815
+ }
816
+ return lines2.join("\n");
817
+ }
818
+ const lines = [];
819
+ const fullUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}${result.doc.route}#${headingId}` : null;
820
+ lines.push(`# ${result.headingText}`);
821
+ if (fullUrl) {
822
+ lines.push(`> From: ${result.doc.title} - ${fullUrl}`);
823
+ } else {
824
+ lines.push(`> From: ${result.doc.title} (${result.doc.route})`);
825
+ }
826
+ lines.push("");
827
+ lines.push("---");
828
+ lines.push("");
829
+ lines.push(result.content);
830
+ return lines.join("\n");
831
+ }
832
+
833
+ // src/mcp/server.ts
834
+ function isFileConfig(config) {
835
+ return "docsPath" in config && "indexPath" in config;
836
+ }
837
+ function isDataConfig(config) {
838
+ return "docs" in config && "searchIndexData" in config;
839
+ }
840
+ var McpDocsServer = class {
841
+ config;
842
+ docs = null;
843
+ searchIndex = null;
844
+ mcpServer;
845
+ initialized = false;
846
+ constructor(config) {
847
+ this.config = config;
848
+ this.mcpServer = new mcp_js.McpServer(
849
+ {
850
+ name: config.name,
851
+ version: config.version ?? "1.0.0"
852
+ },
853
+ {
854
+ capabilities: {
855
+ tools: {}
856
+ }
857
+ }
858
+ );
859
+ this.registerTools();
860
+ }
861
+ /**
862
+ * Register all MCP tools using the SDK's registerTool API
863
+ */
864
+ registerTools() {
865
+ this.mcpServer.registerTool(
866
+ "docs_search",
867
+ {
868
+ description: "Search the documentation for relevant pages. Returns matching documents with snippets and relevance scores. Use this to find information across all documentation.",
869
+ inputSchema: {
870
+ query: zod.z.string().min(1).describe("The search query string"),
871
+ limit: zod.z.number().int().min(1).max(20).optional().default(5).describe("Maximum number of results to return (1-20, default: 5)")
872
+ }
873
+ },
874
+ async ({ query, limit }) => {
875
+ await this.initialize();
876
+ if (!this.docs || !this.searchIndex) {
877
+ return {
878
+ content: [{ type: "text", text: "Server not initialized. Please try again." }],
879
+ isError: true
880
+ };
881
+ }
882
+ const results = executeDocsSearch({ query, limit }, this.searchIndex, this.docs);
883
+ return {
884
+ content: [
885
+ { type: "text", text: formatSearchResults(results, this.config.baseUrl) }
886
+ ]
887
+ };
888
+ }
889
+ );
890
+ this.mcpServer.registerTool(
891
+ "docs_get_page",
892
+ {
893
+ description: "Retrieve the complete content of a documentation page as markdown. Use this when you need the full content of a specific page.",
894
+ inputSchema: {
895
+ route: zod.z.string().min(1).describe('The page route path (e.g., "/docs/getting-started" or "/api/reference")')
896
+ }
897
+ },
898
+ async ({ route }) => {
899
+ await this.initialize();
900
+ if (!this.docs) {
901
+ return {
902
+ content: [{ type: "text", text: "Server not initialized. Please try again." }],
903
+ isError: true
904
+ };
905
+ }
906
+ const doc = executeDocsGetPage({ route }, this.docs);
907
+ return {
908
+ content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
909
+ };
910
+ }
911
+ );
912
+ this.mcpServer.registerTool(
913
+ "docs_get_section",
914
+ {
915
+ description: "Retrieve a specific section from a documentation page by its heading ID. Use this when you need only a portion of a page rather than the entire content.",
916
+ inputSchema: {
917
+ route: zod.z.string().min(1).describe("The page route path"),
918
+ headingId: zod.z.string().min(1).describe(
919
+ 'The heading ID of the section to extract (e.g., "installation", "api-reference")'
920
+ )
921
+ }
922
+ },
923
+ async ({ route, headingId }) => {
924
+ await this.initialize();
925
+ if (!this.docs) {
926
+ return {
927
+ content: [{ type: "text", text: "Server not initialized. Please try again." }],
928
+ isError: true
929
+ };
930
+ }
931
+ const result = executeDocsGetSection({ route, headingId }, this.docs);
932
+ return {
933
+ content: [
934
+ {
935
+ type: "text",
936
+ text: formatSectionContent(result, headingId, this.config.baseUrl)
937
+ }
938
+ ]
939
+ };
940
+ }
941
+ );
942
+ }
943
+ /**
944
+ * Load docs and search index
945
+ *
946
+ * For file-based config: reads from disk
947
+ * For data config: uses pre-loaded data directly
948
+ */
949
+ async initialize() {
950
+ if (this.initialized) {
951
+ return;
952
+ }
953
+ try {
954
+ if (isDataConfig(this.config)) {
955
+ this.docs = this.config.docs;
956
+ this.searchIndex = await importSearchIndex(this.config.searchIndexData);
957
+ } else if (isFileConfig(this.config)) {
958
+ if (await fs3__default.default.pathExists(this.config.docsPath)) {
959
+ this.docs = await fs3__default.default.readJson(this.config.docsPath);
960
+ } else {
961
+ throw new Error(`Docs file not found: ${this.config.docsPath}`);
962
+ }
963
+ if (await fs3__default.default.pathExists(this.config.indexPath)) {
964
+ const indexData = await fs3__default.default.readJson(this.config.indexPath);
965
+ this.searchIndex = await importSearchIndex(indexData);
966
+ } else {
967
+ throw new Error(`Search index not found: ${this.config.indexPath}`);
968
+ }
969
+ } else {
970
+ throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
971
+ }
972
+ this.initialized = true;
973
+ } catch (error) {
974
+ console.error("[MCP] Failed to initialize:", error);
975
+ throw error;
976
+ }
977
+ }
978
+ /**
979
+ * Handle an HTTP request using the MCP SDK's transport
980
+ *
981
+ * This method is designed for serverless environments (Vercel, Netlify).
982
+ * It creates a stateless transport instance and processes the request.
983
+ *
984
+ * @param req - Node.js IncomingMessage or compatible request object
985
+ * @param res - Node.js ServerResponse or compatible response object
986
+ * @param parsedBody - Optional pre-parsed request body
987
+ */
988
+ async handleHttpRequest(req, res, parsedBody) {
989
+ await this.initialize();
990
+ const transport = new streamableHttp_js.StreamableHTTPServerTransport({
991
+ sessionIdGenerator: void 0,
992
+ // Stateless mode - no session tracking
993
+ enableJsonResponse: true
994
+ // Return JSON instead of SSE streams
995
+ });
996
+ await this.mcpServer.connect(transport);
997
+ try {
998
+ await transport.handleRequest(req, res, parsedBody);
999
+ } finally {
1000
+ await transport.close();
1001
+ }
1002
+ }
1003
+ /**
1004
+ * Handle a Web Standard Request (Cloudflare Workers, Deno, Bun)
1005
+ *
1006
+ * This method is designed for Web Standard environments that use
1007
+ * the Fetch API Request/Response pattern.
1008
+ *
1009
+ * @param request - Web Standard Request object
1010
+ * @returns Web Standard Response object
1011
+ */
1012
+ async handleWebRequest(request) {
1013
+ await this.initialize();
1014
+ const transport = new webStandardStreamableHttp_js.WebStandardStreamableHTTPServerTransport({
1015
+ sessionIdGenerator: void 0,
1016
+ // Stateless mode
1017
+ enableJsonResponse: true
1018
+ });
1019
+ await this.mcpServer.connect(transport);
1020
+ try {
1021
+ return await transport.handleRequest(request);
1022
+ } finally {
1023
+ await transport.close();
1024
+ }
1025
+ }
1026
+ /**
1027
+ * Get server status information
1028
+ *
1029
+ * Useful for health checks and debugging
1030
+ */
1031
+ async getStatus() {
1032
+ return {
1033
+ name: this.config.name,
1034
+ version: this.config.version ?? "1.0.0",
1035
+ initialized: this.initialized,
1036
+ docCount: this.docs ? Object.keys(this.docs).length : 0,
1037
+ baseUrl: this.config.baseUrl
1038
+ };
1039
+ }
1040
+ /**
1041
+ * Get the underlying McpServer instance
1042
+ *
1043
+ * Useful for advanced use cases like custom transports
1044
+ */
1045
+ getMcpServer() {
1046
+ return this.mcpServer;
1047
+ }
1048
+ };
1049
+
1050
+ exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
1051
+ exports.McpDocsServer = McpDocsServer;
1052
+ exports.buildSearchIndex = buildSearchIndex;
1053
+ exports.collectRoutes = collectRoutes;
1054
+ exports.default = mcpServerPlugin;
1055
+ exports.discoverHtmlFiles = discoverHtmlFiles;
1056
+ exports.docsGetPageTool = docsGetPageTool;
1057
+ exports.docsGetSectionTool = docsGetSectionTool;
1058
+ exports.docsSearchTool = docsSearchTool;
1059
+ exports.exportSearchIndex = exportSearchIndex;
1060
+ exports.extractContent = extractContent;
1061
+ exports.extractHeadingsFromMarkdown = extractHeadingsFromMarkdown;
1062
+ exports.extractSection = extractSection;
1063
+ exports.htmlToMarkdown = htmlToMarkdown;
1064
+ exports.importSearchIndex = importSearchIndex;
1065
+ exports.mcpServerPlugin = mcpServerPlugin;
1066
+ exports.parseHtml = parseHtml;
1067
+ exports.parseHtmlFile = parseHtmlFile;
1068
+ exports.searchIndex = searchIndex;
1069
+ //# sourceMappingURL=index.js.map
1070
+ //# sourceMappingURL=index.js.map