docusaurus-plugin-mcp-server 0.11.0 → 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.
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import FlexSearch from 'flexsearch';
2
3
  import fs2 from 'fs-extra';
3
4
  import path from 'path';
4
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
6
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
6
7
  import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
7
- import FlexSearch from 'flexsearch';
8
8
  import { z } from 'zod';
9
9
 
10
- var FIELD_WEIGHTS = {
11
- title: 3,
12
- headings: 2,
13
- description: 1.5,
14
- content: 1
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __esm = (fn, res) => function __init() {
13
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
14
+ };
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
15
18
  };
16
19
  function englishStemmer(word) {
17
20
  if (word.length <= 3) return word;
@@ -47,7 +50,7 @@ function createSearchIndex() {
47
50
  }
48
51
  });
49
52
  }
50
- function searchIndex(index, docs, query, options = {}) {
53
+ function querySearchIndex(index, docs, query, options = {}) {
51
54
  const { limit = 16 } = options;
52
55
  const rawResults = index.search(query, {
53
56
  limit: limit * 3,
@@ -139,84 +142,111 @@ async function importSearchIndex(data) {
139
142
  }
140
143
  return index;
141
144
  }
142
- var FlexSearchProvider = class {
143
- name = "flexsearch";
144
- docs = null;
145
- searchIndex = null;
146
- ready = false;
147
- async initialize(_context, initData) {
148
- if (!initData) {
149
- throw new Error("[FlexSearch] SearchProviderInitData required for FlexSearch provider");
150
- }
151
- if (initData.docs && initData.indexData) {
152
- this.docs = initData.docs;
153
- this.searchIndex = await importSearchIndex(initData.indexData);
154
- this.ready = true;
155
- return;
156
- }
157
- if (initData.docsPath && initData.indexPath) {
158
- if (await fs2.pathExists(initData.docsPath)) {
159
- this.docs = await fs2.readJson(initData.docsPath);
160
- } else {
161
- throw new Error(`[FlexSearch] Docs file not found: ${initData.docsPath}`);
145
+ var FIELD_WEIGHTS;
146
+ var init_flexsearch_core = __esm({
147
+ "src/search/flexsearch-core.ts"() {
148
+ FIELD_WEIGHTS = {
149
+ title: 3,
150
+ headings: 2,
151
+ description: 1.5,
152
+ content: 1
153
+ };
154
+ }
155
+ });
156
+
157
+ // src/providers/search/flexsearch-provider.ts
158
+ var flexsearch_provider_exports = {};
159
+ __export(flexsearch_provider_exports, {
160
+ FlexSearchProvider: () => FlexSearchProvider
161
+ });
162
+ var FlexSearchProvider;
163
+ var init_flexsearch_provider = __esm({
164
+ "src/providers/search/flexsearch-provider.ts"() {
165
+ init_flexsearch_core();
166
+ FlexSearchProvider = class {
167
+ name = "flexsearch";
168
+ docs = null;
169
+ searchIndex = null;
170
+ ready = false;
171
+ async initialize(_context, initData) {
172
+ if (!initData) {
173
+ throw new Error("[FlexSearch] SearchProviderInitData required for FlexSearch provider");
174
+ }
175
+ if (initData.docs && initData.indexData) {
176
+ this.docs = initData.docs;
177
+ this.searchIndex = await importSearchIndex(initData.indexData);
178
+ this.ready = true;
179
+ return;
180
+ }
181
+ if (initData.docsPath && initData.indexPath) {
182
+ if (await fs2.pathExists(initData.docsPath)) {
183
+ this.docs = await fs2.readJson(initData.docsPath);
184
+ } else {
185
+ throw new Error(`[FlexSearch] Docs file not found: ${initData.docsPath}`);
186
+ }
187
+ if (await fs2.pathExists(initData.indexPath)) {
188
+ const indexData = await fs2.readJson(initData.indexPath);
189
+ this.searchIndex = await importSearchIndex(indexData);
190
+ } else {
191
+ throw new Error(`[FlexSearch] Search index not found: ${initData.indexPath}`);
192
+ }
193
+ this.ready = true;
194
+ return;
195
+ }
196
+ throw new Error(
197
+ "[FlexSearch] Invalid init data: must provide either file paths (docsPath, indexPath) or pre-loaded data (docs, indexData)"
198
+ );
162
199
  }
163
- if (await fs2.pathExists(initData.indexPath)) {
164
- const indexData = await fs2.readJson(initData.indexPath);
165
- this.searchIndex = await importSearchIndex(indexData);
166
- } else {
167
- throw new Error(`[FlexSearch] Search index not found: ${initData.indexPath}`);
200
+ isReady() {
201
+ return this.ready && this.docs !== null && this.searchIndex !== null;
202
+ }
203
+ async search(query, options) {
204
+ if (!this.isReady() || !this.docs || !this.searchIndex) {
205
+ throw new Error("[FlexSearch] Provider not initialized");
206
+ }
207
+ const limit = options?.limit ?? 16;
208
+ return querySearchIndex(this.searchIndex, this.docs, query, { limit });
209
+ }
210
+ async getDocument(url) {
211
+ if (!this.docs) {
212
+ throw new Error("[FlexSearch] Provider not initialized");
213
+ }
214
+ return this.docs[url] ?? null;
215
+ }
216
+ async healthCheck() {
217
+ if (!this.isReady()) {
218
+ return { healthy: false, message: "FlexSearch provider not initialized" };
219
+ }
220
+ const docCount = this.docs ? Object.keys(this.docs).length : 0;
221
+ return {
222
+ healthy: true,
223
+ message: `FlexSearch provider ready with ${docCount} documents`
224
+ };
225
+ }
226
+ getDocCount() {
227
+ return this.docs ? Object.keys(this.docs).length : 0;
228
+ }
229
+ /**
230
+ * Get all loaded documents (for compatibility with existing server code)
231
+ */
232
+ getDocs() {
233
+ return this.docs;
234
+ }
235
+ /**
236
+ * Get the FlexSearch index (for compatibility with existing server code)
237
+ */
238
+ getSearchIndex() {
239
+ return this.searchIndex;
168
240
  }
169
- this.ready = true;
170
- return;
171
- }
172
- throw new Error(
173
- "[FlexSearch] Invalid init data: must provide either file paths (docsPath, indexPath) or pre-loaded data (docs, indexData)"
174
- );
175
- }
176
- isReady() {
177
- return this.ready && this.docs !== null && this.searchIndex !== null;
178
- }
179
- async search(query, options) {
180
- if (!this.isReady() || !this.docs || !this.searchIndex) {
181
- throw new Error("[FlexSearch] Provider not initialized");
182
- }
183
- const limit = options?.limit ?? 16;
184
- return searchIndex(this.searchIndex, this.docs, query, { limit });
185
- }
186
- async getDocument(url) {
187
- if (!this.docs) {
188
- throw new Error("[FlexSearch] Provider not initialized");
189
- }
190
- return this.docs[url] ?? null;
191
- }
192
- async healthCheck() {
193
- if (!this.isReady()) {
194
- return { healthy: false, message: "FlexSearch provider not initialized" };
195
- }
196
- const docCount = this.docs ? Object.keys(this.docs).length : 0;
197
- return {
198
- healthy: true,
199
- message: `FlexSearch provider ready with ${docCount} documents`
200
241
  };
201
242
  }
202
- /**
203
- * Get all loaded documents (for compatibility with existing server code)
204
- */
205
- getDocs() {
206
- return this.docs;
207
- }
208
- /**
209
- * Get the FlexSearch index (for compatibility with existing server code)
210
- */
211
- getSearchIndex() {
212
- return this.searchIndex;
213
- }
214
- };
243
+ });
215
244
 
216
245
  // src/providers/loader.ts
217
246
  async function loadSearchProvider(specifier) {
218
247
  if (specifier === "flexsearch") {
219
- return new FlexSearchProvider();
248
+ const { FlexSearchProvider: FlexSearchProvider2 } = await Promise.resolve().then(() => (init_flexsearch_provider(), flexsearch_provider_exports));
249
+ return new FlexSearchProvider2();
220
250
  }
221
251
  try {
222
252
  const module = await import(specifier);
@@ -239,7 +269,8 @@ async function loadSearchProvider(specifier) {
239
269
  } catch (error) {
240
270
  if (error instanceof Error && error.message.includes("Cannot find module")) {
241
271
  throw new Error(
242
- `Search provider module not found: "${specifier}". Check the path or package name.`
272
+ `Search provider module not found: "${specifier}". Check the path or package name.`,
273
+ { cause: error }
243
274
  );
244
275
  }
245
276
  throw error;
@@ -252,6 +283,9 @@ function isSearchProvider(obj) {
252
283
  const provider = obj;
253
284
  return typeof provider.name === "string" && typeof provider.initialize === "function" && typeof provider.isReady === "function" && typeof provider.search === "function";
254
285
  }
286
+
287
+ // src/mcp/tools/docs-search.ts
288
+ init_flexsearch_core();
255
289
  var docsSearchInputSchema = {
256
290
  query: z.string().min(1).describe("The search query string"),
257
291
  limit: z.number().int().min(1).max(20).optional().default(16).describe("Maximum number of results to return (1-20, default: 16)")
@@ -329,14 +363,22 @@ function isDataConfig(config) {
329
363
  var McpDocsServer = class {
330
364
  config;
331
365
  searchProvider = null;
332
- mcpServer;
333
366
  initialized = false;
367
+ initPromise = null;
368
+ initError = null;
334
369
  constructor(config) {
335
370
  this.config = config;
336
- this.mcpServer = new McpServer(
371
+ }
372
+ /**
373
+ * Create a fresh McpServer instance with tools registered.
374
+ * Each request gets its own server to avoid concurrency issues
375
+ * with the SDK's transport reassignment.
376
+ */
377
+ createMcpServer() {
378
+ const server = new McpServer(
337
379
  {
338
- name: config.name,
339
- version: config.version ?? "1.0.0"
380
+ name: this.config.name,
381
+ version: this.config.version ?? "1.0.0"
340
382
  },
341
383
  {
342
384
  capabilities: {
@@ -344,20 +386,20 @@ var McpDocsServer = class {
344
386
  }
345
387
  }
346
388
  );
347
- this.registerTools();
389
+ this.registerTools(server);
390
+ return server;
348
391
  }
349
392
  /**
350
393
  * Register all MCP tools using definitions from tool files
351
394
  */
352
- registerTools() {
353
- this.mcpServer.registerTool(
395
+ registerTools(server) {
396
+ server.registerTool(
354
397
  docsSearchTool.name,
355
398
  {
356
399
  description: docsSearchTool.description,
357
400
  inputSchema: docsSearchTool.inputSchema
358
401
  },
359
402
  async ({ query, limit }) => {
360
- await this.initialize();
361
403
  if (!this.searchProvider || !this.searchProvider.isReady()) {
362
404
  return {
363
405
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
@@ -372,20 +414,24 @@ var McpDocsServer = class {
372
414
  } catch (error) {
373
415
  console.error("[MCP] Search error:", error);
374
416
  return {
375
- content: [{ type: "text", text: `Search error: ${String(error)}` }],
417
+ content: [
418
+ {
419
+ type: "text",
420
+ text: "An error occurred while searching. Please try again."
421
+ }
422
+ ],
376
423
  isError: true
377
424
  };
378
425
  }
379
426
  }
380
427
  );
381
- this.mcpServer.registerTool(
428
+ server.registerTool(
382
429
  docsFetchTool.name,
383
430
  {
384
431
  description: docsFetchTool.description,
385
432
  inputSchema: docsFetchTool.inputSchema
386
433
  },
387
434
  async ({ url }) => {
388
- await this.initialize();
389
435
  if (!this.searchProvider || !this.searchProvider.isReady()) {
390
436
  return {
391
437
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
@@ -400,7 +446,12 @@ var McpDocsServer = class {
400
446
  } catch (error) {
401
447
  console.error("[MCP] Fetch error:", error);
402
448
  return {
403
- content: [{ type: "text", text: `Error fetching page: ${String(error)}` }],
449
+ content: [
450
+ {
451
+ type: "text",
452
+ text: "An error occurred while fetching the page. Please try again."
453
+ }
454
+ ],
404
455
  isError: true
405
456
  };
406
457
  }
@@ -424,43 +475,54 @@ var McpDocsServer = class {
424
475
  *
425
476
  * For file-based config: reads from disk
426
477
  * For data config: uses pre-loaded data directly
478
+ *
479
+ * Uses promise-based locking to prevent concurrent initialization.
480
+ * Caches initialization errors so repeated calls fail fast.
427
481
  */
428
482
  async initialize() {
483
+ if (this.initError) {
484
+ throw this.initError;
485
+ }
429
486
  if (this.initialized) {
430
487
  return;
431
488
  }
432
- try {
433
- const searchSpecifier = this.config.search ?? "flexsearch";
434
- this.searchProvider = await loadSearchProvider(searchSpecifier);
435
- const providerContext = {
436
- baseUrl: this.config.baseUrl ?? "",
437
- serverName: this.config.name,
438
- serverVersion: this.config.version ?? "1.0.0",
439
- outputDir: ""
440
- // Not relevant for runtime
441
- };
442
- const initData = {};
443
- if (isDataConfig(this.config)) {
444
- initData.docs = this.config.docs;
445
- initData.indexData = this.config.searchIndexData;
446
- } else if (isFileConfig(this.config)) {
447
- initData.docsPath = this.config.docsPath;
448
- initData.indexPath = this.config.indexPath;
449
- } else {
450
- throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
451
- }
452
- await this.searchProvider.initialize(providerContext, initData);
453
- this.initialized = true;
454
- } catch (error) {
455
- console.error("[MCP] Failed to initialize:", error);
456
- throw error;
489
+ if (!this.initPromise) {
490
+ this.initPromise = this._doInitialize().catch((error) => {
491
+ this.initError = error instanceof Error ? error : new Error(String(error));
492
+ this.initPromise = null;
493
+ throw this.initError;
494
+ });
457
495
  }
496
+ return this.initPromise;
497
+ }
498
+ async _doInitialize() {
499
+ const searchSpecifier = this.config.search ?? "flexsearch";
500
+ this.searchProvider = await loadSearchProvider(searchSpecifier);
501
+ const providerContext = {
502
+ baseUrl: this.config.baseUrl ?? "",
503
+ serverName: this.config.name,
504
+ serverVersion: this.config.version ?? "1.0.0",
505
+ outputDir: ""
506
+ // Not relevant for runtime
507
+ };
508
+ const initData = {};
509
+ if (isDataConfig(this.config)) {
510
+ initData.docs = this.config.docs;
511
+ initData.indexData = this.config.searchIndexData;
512
+ } else if (isFileConfig(this.config)) {
513
+ initData.docsPath = this.config.docsPath;
514
+ initData.indexPath = this.config.indexPath;
515
+ } else {
516
+ throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
517
+ }
518
+ await this.searchProvider.initialize(providerContext, initData);
519
+ this.initialized = true;
458
520
  }
459
521
  /**
460
522
  * Handle an HTTP request using the MCP SDK's transport
461
523
  *
462
524
  * This method is designed for serverless environments (Vercel, Netlify).
463
- * It creates a stateless transport instance and processes the request.
525
+ * Creates a fresh McpServer per request to avoid concurrency issues.
464
526
  *
465
527
  * @param req - Node.js IncomingMessage or compatible request object
466
528
  * @param res - Node.js ServerResponse or compatible response object
@@ -468,13 +530,14 @@ var McpDocsServer = class {
468
530
  */
469
531
  async handleHttpRequest(req, res, parsedBody) {
470
532
  await this.initialize();
533
+ const server = this.createMcpServer();
471
534
  const transport = new StreamableHTTPServerTransport({
472
535
  sessionIdGenerator: void 0,
473
536
  // Stateless mode - no session tracking
474
537
  enableJsonResponse: true
475
538
  // Return JSON instead of SSE streams
476
539
  });
477
- await this.mcpServer.connect(transport);
540
+ await server.connect(transport);
478
541
  try {
479
542
  await transport.handleRequest(req, res, parsedBody);
480
543
  } finally {
@@ -486,18 +549,20 @@ var McpDocsServer = class {
486
549
  *
487
550
  * This method is designed for Web Standard environments that use
488
551
  * the Fetch API Request/Response pattern.
552
+ * Creates a fresh McpServer per request to avoid concurrency issues.
489
553
  *
490
554
  * @param request - Web Standard Request object
491
555
  * @returns Web Standard Response object
492
556
  */
493
557
  async handleWebRequest(request) {
494
558
  await this.initialize();
559
+ const server = this.createMcpServer();
495
560
  const transport = new WebStandardStreamableHTTPServerTransport({
496
561
  sessionIdGenerator: void 0,
497
562
  // Stateless mode
498
563
  enableJsonResponse: true
499
564
  });
500
- await this.mcpServer.connect(transport);
565
+ await server.connect(transport);
501
566
  try {
502
567
  return await transport.handleRequest(request);
503
568
  } finally {
@@ -511,9 +576,8 @@ var McpDocsServer = class {
511
576
  */
512
577
  async getStatus() {
513
578
  let docCount = 0;
514
- if (this.searchProvider instanceof FlexSearchProvider) {
515
- const docs = this.searchProvider.getDocs();
516
- docCount = docs ? Object.keys(docs).length : 0;
579
+ if (this.searchProvider?.getDocCount) {
580
+ docCount = this.searchProvider.getDocCount();
517
581
  }
518
582
  return {
519
583
  name: this.config.name,
@@ -524,14 +588,6 @@ var McpDocsServer = class {
524
588
  searchProvider: this.searchProvider?.name
525
589
  };
526
590
  }
527
- /**
528
- * Get the underlying McpServer instance
529
- *
530
- * Useful for advanced use cases like custom transports
531
- */
532
- getMcpServer() {
533
- return this.mcpServer;
534
- }
535
591
  };
536
592
 
537
593
  // src/cli/verify.ts