docusaurus-plugin-mcp-server 0.6.0 → 0.8.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.";
@@ -174,29 +281,7 @@ function formatSearchResults(results, baseUrl) {
174
281
  return lines.join("\n");
175
282
  }
176
283
 
177
- // 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
- }
284
+ // src/mcp/tools/docs-fetch.ts
200
285
  function formatPageContent(doc, baseUrl) {
201
286
  if (!doc) {
202
287
  return "Page not found. Please check the route path and try again.";
@@ -231,89 +316,6 @@ function formatPageContent(doc, baseUrl) {
231
316
  return lines.join("\n");
232
317
  }
233
318
 
234
- // src/processing/heading-extractor.ts
235
- function extractSection(markdown, headingId, headings) {
236
- const heading = headings.find((h) => h.id === headingId);
237
- if (!heading) {
238
- return null;
239
- }
240
- return markdown.slice(heading.startOffset, heading.endOffset).trim();
241
- }
242
-
243
- // 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
- function formatSectionContent(result, headingId, baseUrl) {
291
- if (!result.doc) {
292
- return "Page not found. Please check the route path and try again.";
293
- }
294
- if (!result.content) {
295
- const lines2 = [`Section "${headingId}" not found in this document.`, "", "Available sections:"];
296
- for (const heading of result.availableHeadings) {
297
- const indent = " ".repeat(heading.level - 1);
298
- lines2.push(`${indent}- ${heading.text} (id: ${heading.id})`);
299
- }
300
- return lines2.join("\n");
301
- }
302
- const lines = [];
303
- const fullUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}${result.doc.route}#${headingId}` : null;
304
- lines.push(`# ${result.headingText}`);
305
- if (fullUrl) {
306
- lines.push(`> From: ${result.doc.title} - ${fullUrl}`);
307
- } else {
308
- lines.push(`> From: ${result.doc.title} (${result.doc.route})`);
309
- }
310
- lines.push("");
311
- lines.push("---");
312
- lines.push("");
313
- lines.push(result.content);
314
- return lines.join("\n");
315
- }
316
-
317
319
  // src/mcp/server.ts
318
320
  function isFileConfig(config) {
319
321
  return "docsPath" in config && "indexPath" in config;
@@ -323,8 +325,7 @@ function isDataConfig(config) {
323
325
  }
324
326
  var McpDocsServer = class {
325
327
  config;
326
- docs = null;
327
- searchIndex = null;
328
+ searchProvider = null;
328
329
  mcpServer;
329
330
  initialized = false;
330
331
  constructor(config) {
@@ -357,75 +358,83 @@ var McpDocsServer = class {
357
358
  },
358
359
  async ({ query, limit }) => {
359
360
  await this.initialize();
360
- if (!this.docs || !this.searchIndex) {
361
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
361
362
  return {
362
363
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
363
364
  isError: true
364
365
  };
365
366
  }
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
- };
367
+ try {
368
+ const results = await this.searchProvider.search(query, { limit });
369
+ return {
370
+ content: [
371
+ { type: "text", text: formatSearchResults(results, this.config.baseUrl) }
372
+ ]
373
+ };
374
+ } catch (error) {
375
+ console.error("[MCP] Search error:", error);
376
+ return {
377
+ content: [{ type: "text", text: `Search error: ${String(error)}` }],
378
+ isError: true
379
+ };
380
+ }
372
381
  }
373
382
  );
374
383
  this.mcpServer.registerTool(
375
- "docs_get_page",
384
+ "docs_fetch",
376
385
  {
377
- description: "Retrieve the complete content of a documentation page as markdown. Use this when you need the full content of a specific page.",
386
+ description: "Fetch the complete content of a documentation page. Use this when you need the full content of a specific page.",
378
387
  inputSchema: {
379
388
  route: z.string().min(1).describe('The page route path (e.g., "/docs/getting-started" or "/api/reference")')
380
389
  }
381
390
  },
382
391
  async ({ route }) => {
383
392
  await this.initialize();
384
- if (!this.docs) {
393
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
385
394
  return {
386
395
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
387
396
  isError: true
388
397
  };
389
398
  }
390
- const doc = executeDocsGetPage({ route }, this.docs);
391
- return {
392
- content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
393
- };
394
- }
395
- );
396
- this.mcpServer.registerTool(
397
- "docs_get_section",
398
- {
399
- 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.",
400
- inputSchema: {
401
- route: z.string().min(1).describe("The page route path"),
402
- headingId: z.string().min(1).describe(
403
- 'The heading ID of the section to extract (e.g., "installation", "api-reference")'
404
- )
405
- }
406
- },
407
- async ({ route, headingId }) => {
408
- await this.initialize();
409
- if (!this.docs) {
399
+ try {
400
+ const doc = await this.getDocument(route);
410
401
  return {
411
- content: [{ type: "text", text: "Server not initialized. Please try again." }],
402
+ content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
403
+ };
404
+ } catch (error) {
405
+ console.error("[MCP] Get page error:", error);
406
+ return {
407
+ content: [{ type: "text", text: `Error getting page: ${String(error)}` }],
412
408
  isError: true
413
409
  };
414
410
  }
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
- };
424
411
  }
425
412
  );
426
413
  }
427
414
  /**
428
- * Load docs and search index
415
+ * Get a document by route using the search provider
416
+ */
417
+ async getDocument(route) {
418
+ if (!this.searchProvider) {
419
+ return null;
420
+ }
421
+ if (this.searchProvider.getDocument) {
422
+ return this.searchProvider.getDocument(route);
423
+ }
424
+ if (this.searchProvider instanceof FlexSearchProvider) {
425
+ const docs = this.searchProvider.getDocs();
426
+ if (!docs) return null;
427
+ if (docs[route]) {
428
+ return docs[route];
429
+ }
430
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
431
+ const withoutSlash = route.startsWith("/") ? route.slice(1) : route;
432
+ return docs[normalizedRoute] ?? docs[withoutSlash] ?? null;
433
+ }
434
+ return null;
435
+ }
436
+ /**
437
+ * Load docs and search index using the configured search provider
429
438
  *
430
439
  * For file-based config: reads from disk
431
440
  * For data config: uses pre-loaded data directly
@@ -435,24 +444,26 @@ var McpDocsServer = class {
435
444
  return;
436
445
  }
437
446
  try {
447
+ const searchSpecifier = this.config.search ?? "flexsearch";
448
+ this.searchProvider = await loadSearchProvider(searchSpecifier);
449
+ const providerContext = {
450
+ baseUrl: this.config.baseUrl ?? "",
451
+ serverName: this.config.name,
452
+ serverVersion: this.config.version ?? "1.0.0",
453
+ outputDir: ""
454
+ // Not relevant for runtime
455
+ };
456
+ const initData = {};
438
457
  if (isDataConfig(this.config)) {
439
- this.docs = this.config.docs;
440
- this.searchIndex = await importSearchIndex(this.config.searchIndexData);
458
+ initData.docs = this.config.docs;
459
+ initData.indexData = this.config.searchIndexData;
441
460
  } 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
- }
461
+ initData.docsPath = this.config.docsPath;
462
+ initData.indexPath = this.config.indexPath;
453
463
  } else {
454
464
  throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
455
465
  }
466
+ await this.searchProvider.initialize(providerContext, initData);
456
467
  this.initialized = true;
457
468
  } catch (error) {
458
469
  console.error("[MCP] Failed to initialize:", error);
@@ -513,12 +524,18 @@ var McpDocsServer = class {
513
524
  * Useful for health checks and debugging
514
525
  */
515
526
  async getStatus() {
527
+ let docCount = 0;
528
+ if (this.searchProvider instanceof FlexSearchProvider) {
529
+ const docs = this.searchProvider.getDocs();
530
+ docCount = docs ? Object.keys(docs).length : 0;
531
+ }
516
532
  return {
517
533
  name: this.config.name,
518
534
  version: this.config.version ?? "1.0.0",
519
535
  initialized: this.initialized,
520
- docCount: this.docs ? Object.keys(this.docs).length : 0,
521
- baseUrl: this.config.baseUrl
536
+ docCount,
537
+ baseUrl: this.config.baseUrl,
538
+ searchProvider: this.searchProvider?.name
522
539
  };
523
540
  }
524
541
  /**