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.
@@ -1,9 +1,10 @@
1
- import fs from 'fs-extra';
2
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
2
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
3
  import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
5
4
  import { z } from 'zod';
6
5
  import FlexSearch from 'flexsearch';
6
+ import fs from 'fs-extra';
7
+ import 'http';
7
8
 
8
9
  // src/mcp/server.ts
9
10
  var FIELD_WEIGHTS = {
@@ -136,19 +137,126 @@ async function importSearchIndex(data) {
136
137
  }
137
138
  return index;
138
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 fs.pathExists(initData.docsPath)) {
157
+ this.docs = await fs.readJson(initData.docsPath);
158
+ } else {
159
+ throw new Error(`[FlexSearch] Docs file not found: ${initData.docsPath}`);
160
+ }
161
+ if (await fs.pathExists(initData.indexPath)) {
162
+ const indexData = await fs.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
+ };
218
+
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";
257
+ }
139
258
 
140
259
  // src/mcp/tools/docs-search.ts
141
- function executeDocsSearch(params, index, docs) {
142
- const { query, limit = 5 } = params;
143
- if (!query || typeof query !== "string" || query.trim().length === 0) {
144
- throw new Error("Query parameter is required and must be a non-empty string");
145
- }
146
- const effectiveLimit = Math.min(Math.max(1, limit), 20);
147
- const results = searchIndex(index, docs, query.trim(), {
148
- limit: effectiveLimit
149
- });
150
- return results;
151
- }
152
260
  function formatSearchResults(results, baseUrl) {
153
261
  if (results.length === 0) {
154
262
  return "No matching documents found.";
@@ -174,28 +282,6 @@ function formatSearchResults(results, baseUrl) {
174
282
  }
175
283
 
176
284
  // src/mcp/tools/docs-get-page.ts
177
- function executeDocsGetPage(params, docs) {
178
- const { route } = params;
179
- if (!route || typeof route !== "string") {
180
- throw new Error("Route parameter is required and must be a string");
181
- }
182
- let normalizedRoute = route.trim();
183
- if (!normalizedRoute.startsWith("/")) {
184
- normalizedRoute = "/" + normalizedRoute;
185
- }
186
- if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
187
- normalizedRoute = normalizedRoute.slice(0, -1);
188
- }
189
- const doc = docs[normalizedRoute];
190
- if (!doc) {
191
- const altRoute = normalizedRoute.slice(1);
192
- if (docs[altRoute]) {
193
- return docs[altRoute] ?? null;
194
- }
195
- return null;
196
- }
197
- return doc;
198
- }
199
285
  function formatPageContent(doc, baseUrl) {
200
286
  if (!doc) {
201
287
  return "Page not found. Please check the route path and try again.";
@@ -240,52 +326,6 @@ function extractSection(markdown, headingId, headings) {
240
326
  }
241
327
 
242
328
  // src/mcp/tools/docs-get-section.ts
243
- function executeDocsGetSection(params, docs) {
244
- const { route, headingId } = params;
245
- if (!route || typeof route !== "string") {
246
- throw new Error("Route parameter is required and must be a string");
247
- }
248
- if (!headingId || typeof headingId !== "string") {
249
- throw new Error("HeadingId parameter is required and must be a string");
250
- }
251
- let normalizedRoute = route.trim();
252
- if (!normalizedRoute.startsWith("/")) {
253
- normalizedRoute = "/" + normalizedRoute;
254
- }
255
- if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
256
- normalizedRoute = normalizedRoute.slice(0, -1);
257
- }
258
- const doc = docs[normalizedRoute];
259
- if (!doc) {
260
- return {
261
- content: null,
262
- doc: null,
263
- headingText: null,
264
- availableHeadings: []
265
- };
266
- }
267
- const availableHeadings = doc.headings.map((h) => ({
268
- id: h.id,
269
- text: h.text,
270
- level: h.level
271
- }));
272
- const heading = doc.headings.find((h) => h.id === headingId.trim());
273
- if (!heading) {
274
- return {
275
- content: null,
276
- doc,
277
- headingText: null,
278
- availableHeadings
279
- };
280
- }
281
- const content = extractSection(doc.markdown, headingId.trim(), doc.headings);
282
- return {
283
- content,
284
- doc,
285
- headingText: heading.text,
286
- availableHeadings
287
- };
288
- }
289
329
  function formatSectionContent(result, headingId, baseUrl) {
290
330
  if (!result.doc) {
291
331
  return "Page not found. Please check the route path and try again.";
@@ -322,8 +362,7 @@ function isDataConfig(config) {
322
362
  }
323
363
  var McpDocsServer = class {
324
364
  config;
325
- docs = null;
326
- searchIndex = null;
365
+ searchProvider = null;
327
366
  mcpServer;
328
367
  initialized = false;
329
368
  constructor(config) {
@@ -356,18 +395,26 @@ var McpDocsServer = class {
356
395
  },
357
396
  async ({ query, limit }) => {
358
397
  await this.initialize();
359
- if (!this.docs || !this.searchIndex) {
398
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
360
399
  return {
361
400
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
362
401
  isError: true
363
402
  };
364
403
  }
365
- const results = executeDocsSearch({ query, limit }, this.searchIndex, this.docs);
366
- return {
367
- content: [
368
- { type: "text", text: formatSearchResults(results, this.config.baseUrl) }
369
- ]
370
- };
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
+ }
371
418
  }
372
419
  );
373
420
  this.mcpServer.registerTool(
@@ -380,16 +427,24 @@ var McpDocsServer = class {
380
427
  },
381
428
  async ({ route }) => {
382
429
  await this.initialize();
383
- if (!this.docs) {
430
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
384
431
  return {
385
432
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
386
433
  isError: true
387
434
  };
388
435
  }
389
- const doc = executeDocsGetPage({ route }, this.docs);
390
- return {
391
- content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
392
- };
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
+ }
393
448
  }
394
449
  );
395
450
  this.mcpServer.registerTool(
@@ -405,26 +460,95 @@ var McpDocsServer = class {
405
460
  },
406
461
  async ({ route, headingId }) => {
407
462
  await this.initialize();
408
- if (!this.docs) {
463
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
409
464
  return {
410
465
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
411
466
  isError: true
412
467
  };
413
468
  }
414
- const result = executeDocsGetSection({ route, headingId }, this.docs);
415
- return {
416
- content: [
417
- {
418
- type: "text",
419
- text: formatSectionContent(result, headingId, this.config.baseUrl)
420
- }
421
- ]
422
- };
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
+ }
423
525
  }
424
526
  );
425
527
  }
426
528
  /**
427
- * 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
428
552
  *
429
553
  * For file-based config: reads from disk
430
554
  * For data config: uses pre-loaded data directly
@@ -434,24 +558,26 @@ var McpDocsServer = class {
434
558
  return;
435
559
  }
436
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 = {};
437
571
  if (isDataConfig(this.config)) {
438
- this.docs = this.config.docs;
439
- this.searchIndex = await importSearchIndex(this.config.searchIndexData);
572
+ initData.docs = this.config.docs;
573
+ initData.indexData = this.config.searchIndexData;
440
574
  } else if (isFileConfig(this.config)) {
441
- if (await fs.pathExists(this.config.docsPath)) {
442
- this.docs = await fs.readJson(this.config.docsPath);
443
- } else {
444
- throw new Error(`Docs file not found: ${this.config.docsPath}`);
445
- }
446
- if (await fs.pathExists(this.config.indexPath)) {
447
- const indexData = await fs.readJson(this.config.indexPath);
448
- this.searchIndex = await importSearchIndex(indexData);
449
- } else {
450
- throw new Error(`Search index not found: ${this.config.indexPath}`);
451
- }
575
+ initData.docsPath = this.config.docsPath;
576
+ initData.indexPath = this.config.indexPath;
452
577
  } else {
453
578
  throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
454
579
  }
580
+ await this.searchProvider.initialize(providerContext, initData);
455
581
  this.initialized = true;
456
582
  } catch (error) {
457
583
  console.error("[MCP] Failed to initialize:", error);
@@ -512,12 +638,18 @@ var McpDocsServer = class {
512
638
  * Useful for health checks and debugging
513
639
  */
514
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
+ }
515
646
  return {
516
647
  name: this.config.name,
517
648
  version: this.config.version ?? "1.0.0",
518
649
  initialized: this.initialized,
519
- docCount: this.docs ? Object.keys(this.docs).length : 0,
520
- baseUrl: this.config.baseUrl
650
+ docCount,
651
+ baseUrl: this.config.baseUrl,
652
+ searchProvider: this.searchProvider?.name
521
653
  };
522
654
  }
523
655
  /**