docusaurus-plugin-mcp-server 0.10.2 → 0.12.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 CHANGED
@@ -1,5 +1,6 @@
1
- import path from 'path';
1
+ import FlexSearch from 'flexsearch';
2
2
  import fs3 from 'fs-extra';
3
+ import path from 'path';
3
4
  import pMap from 'p-map';
4
5
  import { unified } from 'unified';
5
6
  import rehypeParse from 'rehype-parse';
@@ -9,16 +10,327 @@ import { toHtml } from 'hast-util-to-html';
9
10
  import rehypeRemark from 'rehype-remark';
10
11
  import remarkStringify from 'remark-stringify';
11
12
  import remarkGfm from 'remark-gfm';
12
- import FlexSearch from 'flexsearch';
13
13
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
14
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
15
15
  import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
16
16
  import { z } from 'zod';
17
17
 
18
- // src/plugin/docusaurus-plugin.ts
18
+ var __defProp = Object.defineProperty;
19
+ var __getOwnPropNames = Object.getOwnPropertyNames;
20
+ var __esm = (fn, res) => function __init() {
21
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
22
+ };
23
+ var __export = (target, all) => {
24
+ for (var name in all)
25
+ __defProp(target, name, { get: all[name], enumerable: true });
26
+ };
27
+ function englishStemmer(word) {
28
+ if (word.length <= 3) return word;
29
+ 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");
30
+ }
31
+ function createSearchIndex() {
32
+ return new FlexSearch.Document({
33
+ // Use 'forward' tokenization to avoid OOM with large doc sets
34
+ // See: https://github.com/scalvert/docusaurus-plugin-mcp-server/issues/11
35
+ tokenize: "forward",
36
+ // Enable caching for faster repeated queries
37
+ cache: 100,
38
+ // Higher resolution = more granular ranking (1-9)
39
+ resolution: 9,
40
+ // Enable context for phrase/proximity matching
41
+ context: {
42
+ resolution: 2,
43
+ depth: 2,
44
+ bidirectional: true
45
+ },
46
+ // Apply stemming to normalize word forms
47
+ encode: (str) => {
48
+ const words = str.toLowerCase().split(/[\s\-_.,;:!?'"()[\]{}]+/);
49
+ return words.filter(Boolean).map(englishStemmer);
50
+ },
51
+ // Document schema
52
+ document: {
53
+ id: "id",
54
+ // Index these fields for searching
55
+ index: ["title", "content", "headings", "description"],
56
+ // Store these fields in results (for enriched queries)
57
+ store: ["title", "description"]
58
+ }
59
+ });
60
+ }
61
+ function addDocumentToIndex(index, doc, baseUrl) {
62
+ const id = baseUrl ? `${baseUrl.replace(/\/$/, "")}${doc.route}` : doc.route;
63
+ const indexable = {
64
+ id,
65
+ title: doc.title,
66
+ content: doc.markdown,
67
+ headings: doc.headings.map((h) => h.text).join(" "),
68
+ description: doc.description
69
+ };
70
+ index.add(indexable);
71
+ }
72
+ function buildSearchIndex(docs, baseUrl) {
73
+ const index = createSearchIndex();
74
+ for (const doc of docs) {
75
+ addDocumentToIndex(index, doc, baseUrl);
76
+ }
77
+ return index;
78
+ }
79
+ function querySearchIndex(index, docs, query, options = {}) {
80
+ const { limit = 16 } = options;
81
+ const rawResults = index.search(query, {
82
+ limit: limit * 3,
83
+ // Get extra results for better ranking after weighting
84
+ enrich: true
85
+ });
86
+ const docScores = /* @__PURE__ */ new Map();
87
+ for (const fieldResult of rawResults) {
88
+ const field = fieldResult.field;
89
+ const fieldWeight = FIELD_WEIGHTS[field] ?? 1;
90
+ const results2 = fieldResult.result;
91
+ for (let i = 0; i < results2.length; i++) {
92
+ const item = results2[i];
93
+ if (!item) continue;
94
+ const docId = typeof item === "string" ? item : item.id;
95
+ const positionScore = (results2.length - i) / results2.length;
96
+ const weightedScore = positionScore * fieldWeight;
97
+ const existingScore = docScores.get(docId) ?? 0;
98
+ docScores.set(docId, existingScore + weightedScore);
99
+ }
100
+ }
101
+ const results = [];
102
+ for (const [docId, score] of docScores) {
103
+ const doc = docs[docId];
104
+ if (!doc) continue;
105
+ results.push({
106
+ url: docId,
107
+ // docId is the full URL when indexed with baseUrl
108
+ route: doc.route,
109
+ title: doc.title,
110
+ score,
111
+ snippet: generateSnippet(doc.markdown, query),
112
+ matchingHeadings: findMatchingHeadings(doc, query)
113
+ });
114
+ }
115
+ results.sort((a, b) => b.score - a.score);
116
+ return results.slice(0, limit);
117
+ }
118
+ function generateSnippet(markdown, query) {
119
+ const maxLength = 200;
120
+ const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
121
+ if (queryTerms.length === 0) {
122
+ return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
123
+ }
124
+ const lowerMarkdown = markdown.toLowerCase();
125
+ let bestIndex = -1;
126
+ let bestTerm = "";
127
+ const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
128
+ for (const term of allTerms) {
129
+ const index = lowerMarkdown.indexOf(term);
130
+ if (index !== -1 && (bestIndex === -1 || index < bestIndex)) {
131
+ bestIndex = index;
132
+ bestTerm = term;
133
+ }
134
+ }
135
+ if (bestIndex === -1) {
136
+ return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
137
+ }
138
+ const snippetStart = Math.max(0, bestIndex - 50);
139
+ const snippetEnd = Math.min(markdown.length, bestIndex + bestTerm.length + 150);
140
+ let snippet = markdown.slice(snippetStart, snippetEnd);
141
+ snippet = snippet.replace(/^#{1,6}\s+/gm, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/```[a-z]*\n?/g, "").replace(/`([^`]+)`/g, "$1").replace(/\s+/g, " ").trim();
142
+ const prefix = snippetStart > 0 ? "..." : "";
143
+ const suffix = snippetEnd < markdown.length ? "..." : "";
144
+ return prefix + snippet + suffix;
145
+ }
146
+ function findMatchingHeadings(doc, query) {
147
+ const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
148
+ const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
149
+ const matching = [];
150
+ for (const heading of doc.headings) {
151
+ const headingLower = heading.text.toLowerCase();
152
+ const headingStemmed = headingLower.split(/\s+/).map(englishStemmer).join(" ");
153
+ if (allTerms.some(
154
+ (term) => headingLower.includes(term) || headingStemmed.includes(englishStemmer(term))
155
+ )) {
156
+ matching.push(heading.text);
157
+ }
158
+ }
159
+ return matching.slice(0, 3);
160
+ }
161
+ async function exportSearchIndex(index) {
162
+ const exportData = {};
163
+ await index.export((key, data) => {
164
+ exportData[key] = data;
165
+ });
166
+ return exportData;
167
+ }
168
+ async function importSearchIndex(data) {
169
+ const index = createSearchIndex();
170
+ for (const [key, value] of Object.entries(data)) {
171
+ await index.import(
172
+ key,
173
+ value
174
+ );
175
+ }
176
+ return index;
177
+ }
178
+ var FIELD_WEIGHTS;
179
+ var init_flexsearch_core = __esm({
180
+ "src/search/flexsearch-core.ts"() {
181
+ FIELD_WEIGHTS = {
182
+ title: 3,
183
+ headings: 2,
184
+ description: 1.5,
185
+ content: 1
186
+ };
187
+ }
188
+ });
189
+
190
+ // src/providers/indexers/flexsearch-indexer.ts
191
+ var flexsearch_indexer_exports = {};
192
+ __export(flexsearch_indexer_exports, {
193
+ FlexSearchIndexer: () => FlexSearchIndexer
194
+ });
195
+ var FlexSearchIndexer;
196
+ var init_flexsearch_indexer = __esm({
197
+ "src/providers/indexers/flexsearch-indexer.ts"() {
198
+ init_flexsearch_core();
199
+ FlexSearchIndexer = class {
200
+ name = "flexsearch";
201
+ baseUrl = "";
202
+ docsIndex = {};
203
+ exportedIndex = null;
204
+ docCount = 0;
205
+ /**
206
+ * FlexSearch indexer always runs by default.
207
+ * It respects the indexers configuration - if not included, it won't run.
208
+ */
209
+ shouldRun() {
210
+ return true;
211
+ }
212
+ async initialize(context) {
213
+ this.baseUrl = context.baseUrl.replace(/\/$/, "");
214
+ this.docsIndex = {};
215
+ this.exportedIndex = null;
216
+ this.docCount = 0;
217
+ }
218
+ async indexDocuments(docs) {
219
+ this.docCount = docs.length;
220
+ for (const doc of docs) {
221
+ const fullUrl = `${this.baseUrl}${doc.route}`;
222
+ this.docsIndex[fullUrl] = doc;
223
+ }
224
+ console.log("[FlexSearch] Building search index...");
225
+ const searchIndex = buildSearchIndex(docs, this.baseUrl);
226
+ this.exportedIndex = await exportSearchIndex(searchIndex);
227
+ console.log(`[FlexSearch] Indexed ${this.docCount} documents`);
228
+ }
229
+ async finalize() {
230
+ const artifacts = /* @__PURE__ */ new Map();
231
+ artifacts.set("docs.json", this.docsIndex);
232
+ artifacts.set("search-index.json", this.exportedIndex);
233
+ return artifacts;
234
+ }
235
+ async getManifestData() {
236
+ return {
237
+ searchEngine: "flexsearch"
238
+ };
239
+ }
240
+ };
241
+ }
242
+ });
243
+
244
+ // src/providers/search/flexsearch-provider.ts
245
+ var flexsearch_provider_exports = {};
246
+ __export(flexsearch_provider_exports, {
247
+ FlexSearchProvider: () => FlexSearchProvider
248
+ });
249
+ var FlexSearchProvider;
250
+ var init_flexsearch_provider = __esm({
251
+ "src/providers/search/flexsearch-provider.ts"() {
252
+ init_flexsearch_core();
253
+ FlexSearchProvider = class {
254
+ name = "flexsearch";
255
+ docs = null;
256
+ searchIndex = null;
257
+ ready = false;
258
+ async initialize(_context, initData) {
259
+ if (!initData) {
260
+ throw new Error("[FlexSearch] SearchProviderInitData required for FlexSearch provider");
261
+ }
262
+ if (initData.docs && initData.indexData) {
263
+ this.docs = initData.docs;
264
+ this.searchIndex = await importSearchIndex(initData.indexData);
265
+ this.ready = true;
266
+ return;
267
+ }
268
+ if (initData.docsPath && initData.indexPath) {
269
+ if (await fs3.pathExists(initData.docsPath)) {
270
+ this.docs = await fs3.readJson(initData.docsPath);
271
+ } else {
272
+ throw new Error(`[FlexSearch] Docs file not found: ${initData.docsPath}`);
273
+ }
274
+ if (await fs3.pathExists(initData.indexPath)) {
275
+ const indexData = await fs3.readJson(initData.indexPath);
276
+ this.searchIndex = await importSearchIndex(indexData);
277
+ } else {
278
+ throw new Error(`[FlexSearch] Search index not found: ${initData.indexPath}`);
279
+ }
280
+ this.ready = true;
281
+ return;
282
+ }
283
+ throw new Error(
284
+ "[FlexSearch] Invalid init data: must provide either file paths (docsPath, indexPath) or pre-loaded data (docs, indexData)"
285
+ );
286
+ }
287
+ isReady() {
288
+ return this.ready && this.docs !== null && this.searchIndex !== null;
289
+ }
290
+ async search(query, options) {
291
+ if (!this.isReady() || !this.docs || !this.searchIndex) {
292
+ throw new Error("[FlexSearch] Provider not initialized");
293
+ }
294
+ const limit = options?.limit ?? 16;
295
+ return querySearchIndex(this.searchIndex, this.docs, query, { limit });
296
+ }
297
+ async getDocument(url) {
298
+ if (!this.docs) {
299
+ throw new Error("[FlexSearch] Provider not initialized");
300
+ }
301
+ return this.docs[url] ?? null;
302
+ }
303
+ async healthCheck() {
304
+ if (!this.isReady()) {
305
+ return { healthy: false, message: "FlexSearch provider not initialized" };
306
+ }
307
+ const docCount = this.docs ? Object.keys(this.docs).length : 0;
308
+ return {
309
+ healthy: true,
310
+ message: `FlexSearch provider ready with ${docCount} documents`
311
+ };
312
+ }
313
+ getDocCount() {
314
+ return this.docs ? Object.keys(this.docs).length : 0;
315
+ }
316
+ /**
317
+ * Get all loaded documents (for compatibility with existing server code)
318
+ */
319
+ getDocs() {
320
+ return this.docs;
321
+ }
322
+ /**
323
+ * Get the FlexSearch index (for compatibility with existing server code)
324
+ */
325
+ getSearchIndex() {
326
+ return this.searchIndex;
327
+ }
328
+ };
329
+ }
330
+ });
19
331
 
20
332
  // src/types/index.ts
21
- var DEFAULT_OPTIONS = {
333
+ var DEFAULT_PLUGIN_OPTIONS = {
22
334
  outputDir: "mcp",
23
335
  contentSelectors: ["article", "main", ".main-wrapper", '[role="main"]'],
24
336
  excludeSelectors: [
@@ -280,292 +592,12 @@ function extractHeadingsFromMarkdown(markdown) {
280
592
  function generateHeadingId(text) {
281
593
  return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
282
594
  }
283
- function extractSection(markdown, headingId, headings) {
284
- const heading = headings.find((h) => h.id === headingId);
285
- if (!heading) {
286
- return null;
287
- }
288
- return markdown.slice(heading.startOffset, heading.endOffset).trim();
289
- }
290
- var FIELD_WEIGHTS = {
291
- title: 3,
292
- headings: 2,
293
- description: 1.5,
294
- content: 1
295
- };
296
- function englishStemmer(word) {
297
- if (word.length <= 3) return word;
298
- 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");
299
- }
300
- function createSearchIndex() {
301
- return new FlexSearch.Document({
302
- // Use 'full' tokenization for substring matching
303
- // This allows "auth" to match "authentication"
304
- tokenize: "full",
305
- // Enable caching for faster repeated queries
306
- cache: 100,
307
- // Higher resolution = more granular ranking (1-9)
308
- resolution: 9,
309
- // Enable context for phrase/proximity matching
310
- context: {
311
- resolution: 2,
312
- depth: 2,
313
- bidirectional: true
314
- },
315
- // Apply stemming to normalize word forms
316
- encode: (str) => {
317
- const words = str.toLowerCase().split(/[\s\-_.,;:!?'"()[\]{}]+/);
318
- return words.filter(Boolean).map(englishStemmer);
319
- },
320
- // Document schema
321
- document: {
322
- id: "id",
323
- // Index these fields for searching
324
- index: ["title", "content", "headings", "description"],
325
- // Store these fields in results (for enriched queries)
326
- store: ["title", "description"]
327
- }
328
- });
329
- }
330
- function addDocumentToIndex(index, doc, baseUrl) {
331
- const id = baseUrl ? `${baseUrl.replace(/\/$/, "")}${doc.route}` : doc.route;
332
- const indexable = {
333
- id,
334
- title: doc.title,
335
- content: doc.markdown,
336
- headings: doc.headings.map((h) => h.text).join(" "),
337
- description: doc.description
338
- };
339
- index.add(indexable);
340
- }
341
- function buildSearchIndex(docs, baseUrl) {
342
- const index = createSearchIndex();
343
- for (const doc of docs) {
344
- addDocumentToIndex(index, doc, baseUrl);
345
- }
346
- return index;
347
- }
348
- function searchIndex(index, docs, query, options = {}) {
349
- const { limit = 5 } = options;
350
- const rawResults = index.search(query, {
351
- limit: limit * 3,
352
- // Get extra results for better ranking after weighting
353
- enrich: true
354
- });
355
- const docScores = /* @__PURE__ */ new Map();
356
- for (const fieldResult of rawResults) {
357
- const field = fieldResult.field;
358
- const fieldWeight = FIELD_WEIGHTS[field] ?? 1;
359
- const results2 = fieldResult.result;
360
- for (let i = 0; i < results2.length; i++) {
361
- const item = results2[i];
362
- if (!item) continue;
363
- const docId = typeof item === "string" ? item : item.id;
364
- const positionScore = (results2.length - i) / results2.length;
365
- const weightedScore = positionScore * fieldWeight;
366
- const existingScore = docScores.get(docId) ?? 0;
367
- docScores.set(docId, existingScore + weightedScore);
368
- }
369
- }
370
- const results = [];
371
- for (const [docId, score] of docScores) {
372
- const doc = docs[docId];
373
- if (!doc) continue;
374
- results.push({
375
- url: docId,
376
- // docId is the full URL when indexed with baseUrl
377
- route: doc.route,
378
- title: doc.title,
379
- score,
380
- snippet: generateSnippet(doc.markdown, query),
381
- matchingHeadings: findMatchingHeadings(doc, query)
382
- });
383
- }
384
- results.sort((a, b) => b.score - a.score);
385
- return results.slice(0, limit);
386
- }
387
- function generateSnippet(markdown, query) {
388
- const maxLength = 200;
389
- const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
390
- if (queryTerms.length === 0) {
391
- return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
392
- }
393
- const lowerMarkdown = markdown.toLowerCase();
394
- let bestIndex = -1;
395
- let bestTerm = "";
396
- const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
397
- for (const term of allTerms) {
398
- const index = lowerMarkdown.indexOf(term);
399
- if (index !== -1 && (bestIndex === -1 || index < bestIndex)) {
400
- bestIndex = index;
401
- bestTerm = term;
402
- }
403
- }
404
- if (bestIndex === -1) {
405
- return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
406
- }
407
- const snippetStart = Math.max(0, bestIndex - 50);
408
- const snippetEnd = Math.min(markdown.length, bestIndex + bestTerm.length + 150);
409
- let snippet = markdown.slice(snippetStart, snippetEnd);
410
- snippet = snippet.replace(/^#{1,6}\s+/gm, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/```[a-z]*\n?/g, "").replace(/`([^`]+)`/g, "$1").replace(/\s+/g, " ").trim();
411
- const prefix = snippetStart > 0 ? "..." : "";
412
- const suffix = snippetEnd < markdown.length ? "..." : "";
413
- return prefix + snippet + suffix;
414
- }
415
- function findMatchingHeadings(doc, query) {
416
- const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
417
- const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
418
- const matching = [];
419
- for (const heading of doc.headings) {
420
- const headingLower = heading.text.toLowerCase();
421
- const headingStemmed = headingLower.split(/\s+/).map(englishStemmer).join(" ");
422
- if (allTerms.some(
423
- (term) => headingLower.includes(term) || headingStemmed.includes(englishStemmer(term))
424
- )) {
425
- matching.push(heading.text);
426
- }
427
- }
428
- return matching.slice(0, 3);
429
- }
430
- async function exportSearchIndex(index) {
431
- const exportData = {};
432
- await index.export((key, data) => {
433
- exportData[key] = data;
434
- });
435
- return exportData;
436
- }
437
- async function importSearchIndex(data) {
438
- const index = createSearchIndex();
439
- for (const [key, value] of Object.entries(data)) {
440
- await index.import(
441
- key,
442
- value
443
- );
444
- }
445
- return index;
446
- }
447
-
448
- // src/providers/indexers/flexsearch-indexer.ts
449
- var FlexSearchIndexer = class {
450
- name = "flexsearch";
451
- baseUrl = "";
452
- docsIndex = {};
453
- exportedIndex = null;
454
- docCount = 0;
455
- /**
456
- * FlexSearch indexer always runs by default.
457
- * It respects the indexers configuration - if not included, it won't run.
458
- */
459
- shouldRun() {
460
- return true;
461
- }
462
- async initialize(context) {
463
- this.baseUrl = context.baseUrl.replace(/\/$/, "");
464
- this.docsIndex = {};
465
- this.exportedIndex = null;
466
- this.docCount = 0;
467
- }
468
- async indexDocuments(docs) {
469
- this.docCount = docs.length;
470
- for (const doc of docs) {
471
- const fullUrl = `${this.baseUrl}${doc.route}`;
472
- this.docsIndex[fullUrl] = doc;
473
- }
474
- console.log("[FlexSearch] Building search index...");
475
- const searchIndex2 = buildSearchIndex(docs, this.baseUrl);
476
- this.exportedIndex = await exportSearchIndex(searchIndex2);
477
- console.log(`[FlexSearch] Indexed ${this.docCount} documents`);
478
- }
479
- async finalize() {
480
- const artifacts = /* @__PURE__ */ new Map();
481
- artifacts.set("docs.json", this.docsIndex);
482
- artifacts.set("search-index.json", this.exportedIndex);
483
- return artifacts;
484
- }
485
- async getManifestData() {
486
- return {
487
- searchEngine: "flexsearch"
488
- };
489
- }
490
- };
491
- var FlexSearchProvider = class {
492
- name = "flexsearch";
493
- docs = null;
494
- searchIndex = null;
495
- ready = false;
496
- async initialize(_context, initData) {
497
- if (!initData) {
498
- throw new Error("[FlexSearch] SearchProviderInitData required for FlexSearch provider");
499
- }
500
- if (initData.docs && initData.indexData) {
501
- this.docs = initData.docs;
502
- this.searchIndex = await importSearchIndex(initData.indexData);
503
- this.ready = true;
504
- return;
505
- }
506
- if (initData.docsPath && initData.indexPath) {
507
- if (await fs3.pathExists(initData.docsPath)) {
508
- this.docs = await fs3.readJson(initData.docsPath);
509
- } else {
510
- throw new Error(`[FlexSearch] Docs file not found: ${initData.docsPath}`);
511
- }
512
- if (await fs3.pathExists(initData.indexPath)) {
513
- const indexData = await fs3.readJson(initData.indexPath);
514
- this.searchIndex = await importSearchIndex(indexData);
515
- } else {
516
- throw new Error(`[FlexSearch] Search index not found: ${initData.indexPath}`);
517
- }
518
- this.ready = true;
519
- return;
520
- }
521
- throw new Error(
522
- "[FlexSearch] Invalid init data: must provide either file paths (docsPath, indexPath) or pre-loaded data (docs, indexData)"
523
- );
524
- }
525
- isReady() {
526
- return this.ready && this.docs !== null && this.searchIndex !== null;
527
- }
528
- async search(query, options) {
529
- if (!this.isReady() || !this.docs || !this.searchIndex) {
530
- throw new Error("[FlexSearch] Provider not initialized");
531
- }
532
- const limit = options?.limit ?? 5;
533
- return searchIndex(this.searchIndex, this.docs, query, { limit });
534
- }
535
- async getDocument(url) {
536
- if (!this.docs) {
537
- throw new Error("[FlexSearch] Provider not initialized");
538
- }
539
- return this.docs[url] ?? null;
540
- }
541
- async healthCheck() {
542
- if (!this.isReady()) {
543
- return { healthy: false, message: "FlexSearch provider not initialized" };
544
- }
545
- const docCount = this.docs ? Object.keys(this.docs).length : 0;
546
- return {
547
- healthy: true,
548
- message: `FlexSearch provider ready with ${docCount} documents`
549
- };
550
- }
551
- /**
552
- * Get all loaded documents (for compatibility with existing server code)
553
- */
554
- getDocs() {
555
- return this.docs;
556
- }
557
- /**
558
- * Get the FlexSearch index (for compatibility with existing server code)
559
- */
560
- getSearchIndex() {
561
- return this.searchIndex;
562
- }
563
- };
564
595
 
565
596
  // src/providers/loader.ts
566
597
  async function loadIndexer(specifier) {
567
598
  if (specifier === "flexsearch") {
568
- return new FlexSearchIndexer();
599
+ const { FlexSearchIndexer: FlexSearchIndexer2 } = await Promise.resolve().then(() => (init_flexsearch_indexer(), flexsearch_indexer_exports));
600
+ return new FlexSearchIndexer2();
569
601
  }
570
602
  try {
571
603
  const module = await import(specifier);
@@ -587,14 +619,17 @@ async function loadIndexer(specifier) {
587
619
  );
588
620
  } catch (error) {
589
621
  if (error instanceof Error && error.message.includes("Cannot find module")) {
590
- throw new Error(`Indexer module not found: "${specifier}". Check the path or package name.`);
622
+ throw new Error(`Indexer module not found: "${specifier}". Check the path or package name.`, {
623
+ cause: error
624
+ });
591
625
  }
592
626
  throw error;
593
627
  }
594
628
  }
595
629
  async function loadSearchProvider(specifier) {
596
630
  if (specifier === "flexsearch") {
597
- return new FlexSearchProvider();
631
+ const { FlexSearchProvider: FlexSearchProvider2 } = await Promise.resolve().then(() => (init_flexsearch_provider(), flexsearch_provider_exports));
632
+ return new FlexSearchProvider2();
598
633
  }
599
634
  try {
600
635
  const module = await import(specifier);
@@ -617,7 +652,8 @@ async function loadSearchProvider(specifier) {
617
652
  } catch (error) {
618
653
  if (error instanceof Error && error.message.includes("Cannot find module")) {
619
654
  throw new Error(
620
- `Search provider module not found: "${specifier}". Check the path or package name.`
655
+ `Search provider module not found: "${specifier}". Check the path or package name.`,
656
+ { cause: error }
621
657
  );
622
658
  }
623
659
  throw error;
@@ -641,10 +677,10 @@ function isSearchProvider(obj) {
641
677
  // src/plugin/docusaurus-plugin.ts
642
678
  function resolveOptions(options) {
643
679
  return {
644
- ...DEFAULT_OPTIONS,
680
+ ...DEFAULT_PLUGIN_OPTIONS,
645
681
  ...options,
646
682
  server: {
647
- ...DEFAULT_OPTIONS.server,
683
+ ...DEFAULT_PLUGIN_OPTIONS.server,
648
684
  ...options.server
649
685
  }
650
686
  };
@@ -769,9 +805,12 @@ function mcpServerPlugin(context, options) {
769
805
  }
770
806
  };
771
807
  }
808
+
809
+ // src/mcp/tools/docs-search.ts
810
+ init_flexsearch_core();
772
811
  var docsSearchInputSchema = {
773
812
  query: z.string().min(1).describe("The search query string"),
774
- limit: z.number().int().min(1).max(20).optional().default(5).describe("Maximum number of results to return (1-20, default: 5)")
813
+ limit: z.number().int().min(1).max(20).optional().default(16).describe("Maximum number of results to return (1-20, default: 16)")
775
814
  };
776
815
  var docsSearchTool = {
777
816
  name: "docs_search",
@@ -846,14 +885,22 @@ function isDataConfig(config) {
846
885
  var McpDocsServer = class {
847
886
  config;
848
887
  searchProvider = null;
849
- mcpServer;
850
888
  initialized = false;
889
+ initPromise = null;
890
+ initError = null;
851
891
  constructor(config) {
852
892
  this.config = config;
853
- this.mcpServer = new McpServer(
893
+ }
894
+ /**
895
+ * Create a fresh McpServer instance with tools registered.
896
+ * Each request gets its own server to avoid concurrency issues
897
+ * with the SDK's transport reassignment.
898
+ */
899
+ createMcpServer() {
900
+ const server = new McpServer(
854
901
  {
855
- name: config.name,
856
- version: config.version ?? "1.0.0"
902
+ name: this.config.name,
903
+ version: this.config.version ?? "1.0.0"
857
904
  },
858
905
  {
859
906
  capabilities: {
@@ -861,20 +908,20 @@ var McpDocsServer = class {
861
908
  }
862
909
  }
863
910
  );
864
- this.registerTools();
911
+ this.registerTools(server);
912
+ return server;
865
913
  }
866
914
  /**
867
915
  * Register all MCP tools using definitions from tool files
868
916
  */
869
- registerTools() {
870
- this.mcpServer.registerTool(
917
+ registerTools(server) {
918
+ server.registerTool(
871
919
  docsSearchTool.name,
872
920
  {
873
921
  description: docsSearchTool.description,
874
922
  inputSchema: docsSearchTool.inputSchema
875
923
  },
876
924
  async ({ query, limit }) => {
877
- await this.initialize();
878
925
  if (!this.searchProvider || !this.searchProvider.isReady()) {
879
926
  return {
880
927
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
@@ -889,20 +936,24 @@ var McpDocsServer = class {
889
936
  } catch (error) {
890
937
  console.error("[MCP] Search error:", error);
891
938
  return {
892
- content: [{ type: "text", text: `Search error: ${String(error)}` }],
939
+ content: [
940
+ {
941
+ type: "text",
942
+ text: "An error occurred while searching. Please try again."
943
+ }
944
+ ],
893
945
  isError: true
894
946
  };
895
947
  }
896
948
  }
897
949
  );
898
- this.mcpServer.registerTool(
950
+ server.registerTool(
899
951
  docsFetchTool.name,
900
952
  {
901
953
  description: docsFetchTool.description,
902
954
  inputSchema: docsFetchTool.inputSchema
903
955
  },
904
956
  async ({ url }) => {
905
- await this.initialize();
906
957
  if (!this.searchProvider || !this.searchProvider.isReady()) {
907
958
  return {
908
959
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
@@ -917,7 +968,12 @@ var McpDocsServer = class {
917
968
  } catch (error) {
918
969
  console.error("[MCP] Fetch error:", error);
919
970
  return {
920
- content: [{ type: "text", text: `Error fetching page: ${String(error)}` }],
971
+ content: [
972
+ {
973
+ type: "text",
974
+ text: "An error occurred while fetching the page. Please try again."
975
+ }
976
+ ],
921
977
  isError: true
922
978
  };
923
979
  }
@@ -941,43 +997,54 @@ var McpDocsServer = class {
941
997
  *
942
998
  * For file-based config: reads from disk
943
999
  * For data config: uses pre-loaded data directly
1000
+ *
1001
+ * Uses promise-based locking to prevent concurrent initialization.
1002
+ * Caches initialization errors so repeated calls fail fast.
944
1003
  */
945
1004
  async initialize() {
1005
+ if (this.initError) {
1006
+ throw this.initError;
1007
+ }
946
1008
  if (this.initialized) {
947
1009
  return;
948
1010
  }
949
- try {
950
- const searchSpecifier = this.config.search ?? "flexsearch";
951
- this.searchProvider = await loadSearchProvider(searchSpecifier);
952
- const providerContext = {
953
- baseUrl: this.config.baseUrl ?? "",
954
- serverName: this.config.name,
955
- serverVersion: this.config.version ?? "1.0.0",
956
- outputDir: ""
957
- // Not relevant for runtime
958
- };
959
- const initData = {};
960
- if (isDataConfig(this.config)) {
961
- initData.docs = this.config.docs;
962
- initData.indexData = this.config.searchIndexData;
963
- } else if (isFileConfig(this.config)) {
964
- initData.docsPath = this.config.docsPath;
965
- initData.indexPath = this.config.indexPath;
966
- } else {
967
- throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
968
- }
969
- await this.searchProvider.initialize(providerContext, initData);
970
- this.initialized = true;
971
- } catch (error) {
972
- console.error("[MCP] Failed to initialize:", error);
973
- throw error;
1011
+ if (!this.initPromise) {
1012
+ this.initPromise = this._doInitialize().catch((error) => {
1013
+ this.initError = error instanceof Error ? error : new Error(String(error));
1014
+ this.initPromise = null;
1015
+ throw this.initError;
1016
+ });
1017
+ }
1018
+ return this.initPromise;
1019
+ }
1020
+ async _doInitialize() {
1021
+ const searchSpecifier = this.config.search ?? "flexsearch";
1022
+ this.searchProvider = await loadSearchProvider(searchSpecifier);
1023
+ const providerContext = {
1024
+ baseUrl: this.config.baseUrl ?? "",
1025
+ serverName: this.config.name,
1026
+ serverVersion: this.config.version ?? "1.0.0",
1027
+ outputDir: ""
1028
+ // Not relevant for runtime
1029
+ };
1030
+ const initData = {};
1031
+ if (isDataConfig(this.config)) {
1032
+ initData.docs = this.config.docs;
1033
+ initData.indexData = this.config.searchIndexData;
1034
+ } else if (isFileConfig(this.config)) {
1035
+ initData.docsPath = this.config.docsPath;
1036
+ initData.indexPath = this.config.indexPath;
1037
+ } else {
1038
+ throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
974
1039
  }
1040
+ await this.searchProvider.initialize(providerContext, initData);
1041
+ this.initialized = true;
975
1042
  }
976
1043
  /**
977
1044
  * Handle an HTTP request using the MCP SDK's transport
978
1045
  *
979
1046
  * This method is designed for serverless environments (Vercel, Netlify).
980
- * It creates a stateless transport instance and processes the request.
1047
+ * Creates a fresh McpServer per request to avoid concurrency issues.
981
1048
  *
982
1049
  * @param req - Node.js IncomingMessage or compatible request object
983
1050
  * @param res - Node.js ServerResponse or compatible response object
@@ -985,13 +1052,14 @@ var McpDocsServer = class {
985
1052
  */
986
1053
  async handleHttpRequest(req, res, parsedBody) {
987
1054
  await this.initialize();
1055
+ const server = this.createMcpServer();
988
1056
  const transport = new StreamableHTTPServerTransport({
989
1057
  sessionIdGenerator: void 0,
990
1058
  // Stateless mode - no session tracking
991
1059
  enableJsonResponse: true
992
1060
  // Return JSON instead of SSE streams
993
1061
  });
994
- await this.mcpServer.connect(transport);
1062
+ await server.connect(transport);
995
1063
  try {
996
1064
  await transport.handleRequest(req, res, parsedBody);
997
1065
  } finally {
@@ -1003,18 +1071,20 @@ var McpDocsServer = class {
1003
1071
  *
1004
1072
  * This method is designed for Web Standard environments that use
1005
1073
  * the Fetch API Request/Response pattern.
1074
+ * Creates a fresh McpServer per request to avoid concurrency issues.
1006
1075
  *
1007
1076
  * @param request - Web Standard Request object
1008
1077
  * @returns Web Standard Response object
1009
1078
  */
1010
1079
  async handleWebRequest(request) {
1011
1080
  await this.initialize();
1081
+ const server = this.createMcpServer();
1012
1082
  const transport = new WebStandardStreamableHTTPServerTransport({
1013
1083
  sessionIdGenerator: void 0,
1014
1084
  // Stateless mode
1015
1085
  enableJsonResponse: true
1016
1086
  });
1017
- await this.mcpServer.connect(transport);
1087
+ await server.connect(transport);
1018
1088
  try {
1019
1089
  return await transport.handleRequest(request);
1020
1090
  } finally {
@@ -1028,9 +1098,8 @@ var McpDocsServer = class {
1028
1098
  */
1029
1099
  async getStatus() {
1030
1100
  let docCount = 0;
1031
- if (this.searchProvider instanceof FlexSearchProvider) {
1032
- const docs = this.searchProvider.getDocs();
1033
- docCount = docs ? Object.keys(docs).length : 0;
1101
+ if (this.searchProvider?.getDocCount) {
1102
+ docCount = this.searchProvider.getDocCount();
1034
1103
  }
1035
1104
  return {
1036
1105
  name: this.config.name,
@@ -1041,16 +1110,8 @@ var McpDocsServer = class {
1041
1110
  searchProvider: this.searchProvider?.name
1042
1111
  };
1043
1112
  }
1044
- /**
1045
- * Get the underlying McpServer instance
1046
- *
1047
- * Useful for advanced use cases like custom transports
1048
- */
1049
- getMcpServer() {
1050
- return this.mcpServer;
1051
- }
1052
1113
  };
1053
1114
 
1054
- export { DEFAULT_OPTIONS, FlexSearchIndexer, FlexSearchProvider, McpDocsServer, buildSearchIndex, collectRoutes, mcpServerPlugin as default, discoverHtmlFiles, docsFetchTool, docsSearchTool, exportSearchIndex, extractContent, extractHeadingsFromMarkdown, extractSection, htmlToMarkdown, importSearchIndex, loadIndexer, loadSearchProvider, mcpServerPlugin, parseHtml, parseHtmlFile, searchIndex };
1115
+ export { DEFAULT_PLUGIN_OPTIONS, McpDocsServer, mcpServerPlugin as default, docsFetchTool, docsSearchTool, loadIndexer, loadSearchProvider, mcpServerPlugin };
1055
1116
  //# sourceMappingURL=index.js.map
1056
1117
  //# sourceMappingURL=index.js.map