docusaurus-plugin-mcp-server 0.6.0 → 0.7.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.
@@ -137,19 +137,126 @@ async function importSearchIndex(data) {
137
137
  }
138
138
  return index;
139
139
  }
140
+ var FlexSearchProvider = class {
141
+ name = "flexsearch";
142
+ docs = null;
143
+ searchIndex = null;
144
+ ready = false;
145
+ async initialize(_context, initData) {
146
+ if (!initData) {
147
+ throw new Error("[FlexSearch] SearchProviderInitData required for FlexSearch provider");
148
+ }
149
+ if (initData.docs && initData.indexData) {
150
+ this.docs = initData.docs;
151
+ this.searchIndex = await importSearchIndex(initData.indexData);
152
+ this.ready = true;
153
+ return;
154
+ }
155
+ if (initData.docsPath && initData.indexPath) {
156
+ if (await fs2.pathExists(initData.docsPath)) {
157
+ this.docs = await fs2.readJson(initData.docsPath);
158
+ } else {
159
+ throw new Error(`[FlexSearch] Docs file not found: ${initData.docsPath}`);
160
+ }
161
+ if (await fs2.pathExists(initData.indexPath)) {
162
+ const indexData = await fs2.readJson(initData.indexPath);
163
+ this.searchIndex = await importSearchIndex(indexData);
164
+ } else {
165
+ throw new Error(`[FlexSearch] Search index not found: ${initData.indexPath}`);
166
+ }
167
+ this.ready = true;
168
+ return;
169
+ }
170
+ throw new Error(
171
+ "[FlexSearch] Invalid init data: must provide either file paths (docsPath, indexPath) or pre-loaded data (docs, indexData)"
172
+ );
173
+ }
174
+ isReady() {
175
+ return this.ready && this.docs !== null && this.searchIndex !== null;
176
+ }
177
+ async search(query, options) {
178
+ if (!this.isReady() || !this.docs || !this.searchIndex) {
179
+ throw new Error("[FlexSearch] Provider not initialized");
180
+ }
181
+ const limit = options?.limit ?? 5;
182
+ return searchIndex(this.searchIndex, this.docs, query, { limit });
183
+ }
184
+ async getDocument(route) {
185
+ if (!this.docs) {
186
+ throw new Error("[FlexSearch] Provider not initialized");
187
+ }
188
+ if (this.docs[route]) {
189
+ return this.docs[route];
190
+ }
191
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
192
+ const withoutSlash = route.startsWith("/") ? route.slice(1) : route;
193
+ return this.docs[normalizedRoute] ?? this.docs[withoutSlash] ?? null;
194
+ }
195
+ async healthCheck() {
196
+ if (!this.isReady()) {
197
+ return { healthy: false, message: "FlexSearch provider not initialized" };
198
+ }
199
+ const docCount = this.docs ? Object.keys(this.docs).length : 0;
200
+ return {
201
+ healthy: true,
202
+ message: `FlexSearch provider ready with ${docCount} documents`
203
+ };
204
+ }
205
+ /**
206
+ * Get all loaded documents (for compatibility with existing server code)
207
+ */
208
+ getDocs() {
209
+ return this.docs;
210
+ }
211
+ /**
212
+ * Get the FlexSearch index (for compatibility with existing server code)
213
+ */
214
+ getSearchIndex() {
215
+ return this.searchIndex;
216
+ }
217
+ };
140
218
 
141
- // src/mcp/tools/docs-search.ts
142
- function executeDocsSearch(params, index, docs) {
143
- const { query, limit = 5 } = params;
144
- if (!query || typeof query !== "string" || query.trim().length === 0) {
145
- throw new Error("Query parameter is required and must be a non-empty string");
146
- }
147
- const effectiveLimit = Math.min(Math.max(1, limit), 20);
148
- const results = searchIndex(index, docs, query.trim(), {
149
- limit: effectiveLimit
150
- });
151
- return results;
219
+ // src/providers/loader.ts
220
+ async function loadSearchProvider(specifier) {
221
+ if (specifier === "flexsearch") {
222
+ return new FlexSearchProvider();
223
+ }
224
+ try {
225
+ const module = await import(specifier);
226
+ const ProviderClass = module.default;
227
+ if (typeof ProviderClass === "function") {
228
+ const instance = new ProviderClass();
229
+ if (!isSearchProvider(instance)) {
230
+ throw new Error(
231
+ `Invalid search provider module "${specifier}": does not implement SearchProvider interface`
232
+ );
233
+ }
234
+ return instance;
235
+ }
236
+ if (isSearchProvider(ProviderClass)) {
237
+ return ProviderClass;
238
+ }
239
+ throw new Error(
240
+ `Invalid search provider module "${specifier}": must export a default class or SearchProvider instance`
241
+ );
242
+ } catch (error) {
243
+ if (error instanceof Error && error.message.includes("Cannot find module")) {
244
+ throw new Error(
245
+ `Search provider module not found: "${specifier}". Check the path or package name.`
246
+ );
247
+ }
248
+ throw error;
249
+ }
250
+ }
251
+ function isSearchProvider(obj) {
252
+ if (!obj || typeof obj !== "object") {
253
+ return false;
254
+ }
255
+ const provider = obj;
256
+ return typeof provider.name === "string" && typeof provider.initialize === "function" && typeof provider.isReady === "function" && typeof provider.search === "function";
152
257
  }
258
+
259
+ // src/mcp/tools/docs-search.ts
153
260
  function formatSearchResults(results, baseUrl) {
154
261
  if (results.length === 0) {
155
262
  return "No matching documents found.";
@@ -175,28 +282,6 @@ function formatSearchResults(results, baseUrl) {
175
282
  }
176
283
 
177
284
  // src/mcp/tools/docs-get-page.ts
178
- function executeDocsGetPage(params, docs) {
179
- const { route } = params;
180
- if (!route || typeof route !== "string") {
181
- throw new Error("Route parameter is required and must be a string");
182
- }
183
- let normalizedRoute = route.trim();
184
- if (!normalizedRoute.startsWith("/")) {
185
- normalizedRoute = "/" + normalizedRoute;
186
- }
187
- if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
188
- normalizedRoute = normalizedRoute.slice(0, -1);
189
- }
190
- const doc = docs[normalizedRoute];
191
- if (!doc) {
192
- const altRoute = normalizedRoute.slice(1);
193
- if (docs[altRoute]) {
194
- return docs[altRoute] ?? null;
195
- }
196
- return null;
197
- }
198
- return doc;
199
- }
200
285
  function formatPageContent(doc, baseUrl) {
201
286
  if (!doc) {
202
287
  return "Page not found. Please check the route path and try again.";
@@ -241,52 +326,6 @@ function extractSection(markdown, headingId, headings) {
241
326
  }
242
327
 
243
328
  // src/mcp/tools/docs-get-section.ts
244
- function executeDocsGetSection(params, docs) {
245
- const { route, headingId } = params;
246
- if (!route || typeof route !== "string") {
247
- throw new Error("Route parameter is required and must be a string");
248
- }
249
- if (!headingId || typeof headingId !== "string") {
250
- throw new Error("HeadingId parameter is required and must be a string");
251
- }
252
- let normalizedRoute = route.trim();
253
- if (!normalizedRoute.startsWith("/")) {
254
- normalizedRoute = "/" + normalizedRoute;
255
- }
256
- if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
257
- normalizedRoute = normalizedRoute.slice(0, -1);
258
- }
259
- const doc = docs[normalizedRoute];
260
- if (!doc) {
261
- return {
262
- content: null,
263
- doc: null,
264
- headingText: null,
265
- availableHeadings: []
266
- };
267
- }
268
- const availableHeadings = doc.headings.map((h) => ({
269
- id: h.id,
270
- text: h.text,
271
- level: h.level
272
- }));
273
- const heading = doc.headings.find((h) => h.id === headingId.trim());
274
- if (!heading) {
275
- return {
276
- content: null,
277
- doc,
278
- headingText: null,
279
- availableHeadings
280
- };
281
- }
282
- const content = extractSection(doc.markdown, headingId.trim(), doc.headings);
283
- return {
284
- content,
285
- doc,
286
- headingText: heading.text,
287
- availableHeadings
288
- };
289
- }
290
329
  function formatSectionContent(result, headingId, baseUrl) {
291
330
  if (!result.doc) {
292
331
  return "Page not found. Please check the route path and try again.";
@@ -323,8 +362,7 @@ function isDataConfig(config) {
323
362
  }
324
363
  var McpDocsServer = class {
325
364
  config;
326
- docs = null;
327
- searchIndex = null;
365
+ searchProvider = null;
328
366
  mcpServer;
329
367
  initialized = false;
330
368
  constructor(config) {
@@ -357,18 +395,26 @@ var McpDocsServer = class {
357
395
  },
358
396
  async ({ query, limit }) => {
359
397
  await this.initialize();
360
- if (!this.docs || !this.searchIndex) {
398
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
361
399
  return {
362
400
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
363
401
  isError: true
364
402
  };
365
403
  }
366
- const results = executeDocsSearch({ query, limit }, this.searchIndex, this.docs);
367
- return {
368
- content: [
369
- { type: "text", text: formatSearchResults(results, this.config.baseUrl) }
370
- ]
371
- };
404
+ try {
405
+ const results = await this.searchProvider.search(query, { limit });
406
+ return {
407
+ content: [
408
+ { type: "text", text: formatSearchResults(results, this.config.baseUrl) }
409
+ ]
410
+ };
411
+ } catch (error) {
412
+ console.error("[MCP] Search error:", error);
413
+ return {
414
+ content: [{ type: "text", text: `Search error: ${String(error)}` }],
415
+ isError: true
416
+ };
417
+ }
372
418
  }
373
419
  );
374
420
  this.mcpServer.registerTool(
@@ -381,16 +427,24 @@ var McpDocsServer = class {
381
427
  },
382
428
  async ({ route }) => {
383
429
  await this.initialize();
384
- if (!this.docs) {
430
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
385
431
  return {
386
432
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
387
433
  isError: true
388
434
  };
389
435
  }
390
- const doc = executeDocsGetPage({ route }, this.docs);
391
- return {
392
- content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
393
- };
436
+ try {
437
+ const doc = await this.getDocument(route);
438
+ return {
439
+ content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
440
+ };
441
+ } catch (error) {
442
+ console.error("[MCP] Get page error:", error);
443
+ return {
444
+ content: [{ type: "text", text: `Error getting page: ${String(error)}` }],
445
+ isError: true
446
+ };
447
+ }
394
448
  }
395
449
  );
396
450
  this.mcpServer.registerTool(
@@ -406,26 +460,95 @@ var McpDocsServer = class {
406
460
  },
407
461
  async ({ route, headingId }) => {
408
462
  await this.initialize();
409
- if (!this.docs) {
463
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
410
464
  return {
411
465
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
412
466
  isError: true
413
467
  };
414
468
  }
415
- const result = executeDocsGetSection({ route, headingId }, this.docs);
416
- return {
417
- content: [
418
- {
419
- type: "text",
420
- text: formatSectionContent(result, headingId, this.config.baseUrl)
421
- }
422
- ]
423
- };
469
+ try {
470
+ const doc = await this.getDocument(route);
471
+ if (!doc) {
472
+ return {
473
+ content: [
474
+ {
475
+ type: "text",
476
+ text: formatSectionContent(
477
+ { content: null, doc: null, headingText: null, availableHeadings: [] },
478
+ headingId,
479
+ this.config.baseUrl
480
+ )
481
+ }
482
+ ]
483
+ };
484
+ }
485
+ const availableHeadings = doc.headings.map((h) => ({
486
+ id: h.id,
487
+ text: h.text,
488
+ level: h.level
489
+ }));
490
+ const heading = doc.headings.find((h) => h.id === headingId.trim());
491
+ if (!heading) {
492
+ return {
493
+ content: [
494
+ {
495
+ type: "text",
496
+ text: formatSectionContent(
497
+ { content: null, doc, headingText: null, availableHeadings },
498
+ headingId,
499
+ this.config.baseUrl
500
+ )
501
+ }
502
+ ]
503
+ };
504
+ }
505
+ const sectionContent = extractSection(doc.markdown, headingId.trim(), doc.headings);
506
+ return {
507
+ content: [
508
+ {
509
+ type: "text",
510
+ text: formatSectionContent(
511
+ { content: sectionContent, doc, headingText: heading.text, availableHeadings },
512
+ headingId,
513
+ this.config.baseUrl
514
+ )
515
+ }
516
+ ]
517
+ };
518
+ } catch (error) {
519
+ console.error("[MCP] Get section error:", error);
520
+ return {
521
+ content: [{ type: "text", text: `Error getting section: ${String(error)}` }],
522
+ isError: true
523
+ };
524
+ }
424
525
  }
425
526
  );
426
527
  }
427
528
  /**
428
- * Load docs and search index
529
+ * Get a document by route using the search provider
530
+ */
531
+ async getDocument(route) {
532
+ if (!this.searchProvider) {
533
+ return null;
534
+ }
535
+ if (this.searchProvider.getDocument) {
536
+ return this.searchProvider.getDocument(route);
537
+ }
538
+ if (this.searchProvider instanceof FlexSearchProvider) {
539
+ const docs = this.searchProvider.getDocs();
540
+ if (!docs) return null;
541
+ if (docs[route]) {
542
+ return docs[route];
543
+ }
544
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
545
+ const withoutSlash = route.startsWith("/") ? route.slice(1) : route;
546
+ return docs[normalizedRoute] ?? docs[withoutSlash] ?? null;
547
+ }
548
+ return null;
549
+ }
550
+ /**
551
+ * Load docs and search index using the configured search provider
429
552
  *
430
553
  * For file-based config: reads from disk
431
554
  * For data config: uses pre-loaded data directly
@@ -435,24 +558,26 @@ var McpDocsServer = class {
435
558
  return;
436
559
  }
437
560
  try {
561
+ const searchSpecifier = this.config.search ?? "flexsearch";
562
+ this.searchProvider = await loadSearchProvider(searchSpecifier);
563
+ const providerContext = {
564
+ baseUrl: this.config.baseUrl ?? "",
565
+ serverName: this.config.name,
566
+ serverVersion: this.config.version ?? "1.0.0",
567
+ outputDir: ""
568
+ // Not relevant for runtime
569
+ };
570
+ const initData = {};
438
571
  if (isDataConfig(this.config)) {
439
- this.docs = this.config.docs;
440
- this.searchIndex = await importSearchIndex(this.config.searchIndexData);
572
+ initData.docs = this.config.docs;
573
+ initData.indexData = this.config.searchIndexData;
441
574
  } else if (isFileConfig(this.config)) {
442
- if (await fs2.pathExists(this.config.docsPath)) {
443
- this.docs = await fs2.readJson(this.config.docsPath);
444
- } else {
445
- throw new Error(`Docs file not found: ${this.config.docsPath}`);
446
- }
447
- if (await fs2.pathExists(this.config.indexPath)) {
448
- const indexData = await fs2.readJson(this.config.indexPath);
449
- this.searchIndex = await importSearchIndex(indexData);
450
- } else {
451
- throw new Error(`Search index not found: ${this.config.indexPath}`);
452
- }
575
+ initData.docsPath = this.config.docsPath;
576
+ initData.indexPath = this.config.indexPath;
453
577
  } else {
454
578
  throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
455
579
  }
580
+ await this.searchProvider.initialize(providerContext, initData);
456
581
  this.initialized = true;
457
582
  } catch (error) {
458
583
  console.error("[MCP] Failed to initialize:", error);
@@ -513,12 +638,18 @@ var McpDocsServer = class {
513
638
  * Useful for health checks and debugging
514
639
  */
515
640
  async getStatus() {
641
+ let docCount = 0;
642
+ if (this.searchProvider instanceof FlexSearchProvider) {
643
+ const docs = this.searchProvider.getDocs();
644
+ docCount = docs ? Object.keys(docs).length : 0;
645
+ }
516
646
  return {
517
647
  name: this.config.name,
518
648
  version: this.config.version ?? "1.0.0",
519
649
  initialized: this.initialized,
520
- docCount: this.docs ? Object.keys(this.docs).length : 0,
521
- baseUrl: this.config.baseUrl
650
+ docCount,
651
+ baseUrl: this.config.baseUrl,
652
+ searchProvider: this.searchProvider?.name
522
653
  };
523
654
  }
524
655
  /**