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.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var path2 = require('path');
5
+ var path = require('path');
6
6
  var fs3 = require('fs-extra');
7
7
  var pMap = require('p-map');
8
8
  var unified = require('unified');
@@ -20,7 +20,7 @@ var zod = require('zod');
20
20
 
21
21
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
22
22
 
23
- var path2__default = /*#__PURE__*/_interopDefault(path2);
23
+ var path__default = /*#__PURE__*/_interopDefault(path);
24
24
  var fs3__default = /*#__PURE__*/_interopDefault(fs3);
25
25
  var pMap__default = /*#__PURE__*/_interopDefault(pMap);
26
26
  var rehypeParse__default = /*#__PURE__*/_interopDefault(rehypeParse);
@@ -49,7 +49,10 @@ var DEFAULT_OPTIONS = {
49
49
  name: "docs-mcp-server",
50
50
  version: "1.0.0"
51
51
  },
52
- excludeRoutes: ["/404*", "/search*"]
52
+ excludeRoutes: ["/404*", "/search*"],
53
+ indexers: void 0,
54
+ // Default: ['flexsearch'] applied at runtime
55
+ search: "flexsearch"
53
56
  };
54
57
  function filterRoutes(routes, excludePatterns) {
55
58
  return routes.filter((route) => {
@@ -65,15 +68,15 @@ async function discoverHtmlFiles(outDir) {
65
68
  async function scanDirectory(dir) {
66
69
  const entries = await fs3__default.default.readdir(dir, { withFileTypes: true });
67
70
  for (const entry of entries) {
68
- const fullPath = path2__default.default.join(dir, entry.name);
71
+ const fullPath = path__default.default.join(dir, entry.name);
69
72
  if (entry.isDirectory()) {
70
73
  if (["assets", "img", "static"].includes(entry.name)) {
71
74
  continue;
72
75
  }
73
76
  await scanDirectory(fullPath);
74
77
  } else if (entry.name === "index.html") {
75
- const relativePath = path2__default.default.relative(outDir, fullPath);
76
- let routePath = "/" + path2__default.default.dirname(relativePath).replace(/\\/g, "/");
78
+ const relativePath = path__default.default.relative(outDir, fullPath);
79
+ let routePath = "/" + path__default.default.dirname(relativePath).replace(/\\/g, "/");
77
80
  if (routePath === "/.") {
78
81
  routePath = "/";
79
82
  }
@@ -504,6 +507,201 @@ async function importSearchIndex(data) {
504
507
  return index;
505
508
  }
506
509
 
510
+ // src/providers/indexers/flexsearch-indexer.ts
511
+ var FlexSearchIndexer = class {
512
+ name = "flexsearch";
513
+ docsIndex = {};
514
+ exportedIndex = null;
515
+ docCount = 0;
516
+ /**
517
+ * FlexSearch indexer always runs by default.
518
+ * It respects the indexers configuration - if not included, it won't run.
519
+ */
520
+ shouldRun() {
521
+ return true;
522
+ }
523
+ async initialize(_context) {
524
+ this.docsIndex = {};
525
+ this.exportedIndex = null;
526
+ this.docCount = 0;
527
+ }
528
+ async indexDocuments(docs) {
529
+ this.docCount = docs.length;
530
+ for (const doc of docs) {
531
+ this.docsIndex[doc.route] = doc;
532
+ }
533
+ console.log("[FlexSearch] Building search index...");
534
+ const searchIndex2 = buildSearchIndex(docs);
535
+ this.exportedIndex = await exportSearchIndex(searchIndex2);
536
+ console.log(`[FlexSearch] Indexed ${this.docCount} documents`);
537
+ }
538
+ async finalize() {
539
+ const artifacts = /* @__PURE__ */ new Map();
540
+ artifacts.set("docs.json", this.docsIndex);
541
+ artifacts.set("search-index.json", this.exportedIndex);
542
+ return artifacts;
543
+ }
544
+ async getManifestData() {
545
+ return {
546
+ searchEngine: "flexsearch"
547
+ };
548
+ }
549
+ };
550
+ var FlexSearchProvider = class {
551
+ name = "flexsearch";
552
+ docs = null;
553
+ searchIndex = null;
554
+ ready = false;
555
+ async initialize(_context, initData) {
556
+ if (!initData) {
557
+ throw new Error("[FlexSearch] SearchProviderInitData required for FlexSearch provider");
558
+ }
559
+ if (initData.docs && initData.indexData) {
560
+ this.docs = initData.docs;
561
+ this.searchIndex = await importSearchIndex(initData.indexData);
562
+ this.ready = true;
563
+ return;
564
+ }
565
+ if (initData.docsPath && initData.indexPath) {
566
+ if (await fs3__default.default.pathExists(initData.docsPath)) {
567
+ this.docs = await fs3__default.default.readJson(initData.docsPath);
568
+ } else {
569
+ throw new Error(`[FlexSearch] Docs file not found: ${initData.docsPath}`);
570
+ }
571
+ if (await fs3__default.default.pathExists(initData.indexPath)) {
572
+ const indexData = await fs3__default.default.readJson(initData.indexPath);
573
+ this.searchIndex = await importSearchIndex(indexData);
574
+ } else {
575
+ throw new Error(`[FlexSearch] Search index not found: ${initData.indexPath}`);
576
+ }
577
+ this.ready = true;
578
+ return;
579
+ }
580
+ throw new Error(
581
+ "[FlexSearch] Invalid init data: must provide either file paths (docsPath, indexPath) or pre-loaded data (docs, indexData)"
582
+ );
583
+ }
584
+ isReady() {
585
+ return this.ready && this.docs !== null && this.searchIndex !== null;
586
+ }
587
+ async search(query, options) {
588
+ if (!this.isReady() || !this.docs || !this.searchIndex) {
589
+ throw new Error("[FlexSearch] Provider not initialized");
590
+ }
591
+ const limit = options?.limit ?? 5;
592
+ return searchIndex(this.searchIndex, this.docs, query, { limit });
593
+ }
594
+ async getDocument(route) {
595
+ if (!this.docs) {
596
+ throw new Error("[FlexSearch] Provider not initialized");
597
+ }
598
+ if (this.docs[route]) {
599
+ return this.docs[route];
600
+ }
601
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
602
+ const withoutSlash = route.startsWith("/") ? route.slice(1) : route;
603
+ return this.docs[normalizedRoute] ?? this.docs[withoutSlash] ?? null;
604
+ }
605
+ async healthCheck() {
606
+ if (!this.isReady()) {
607
+ return { healthy: false, message: "FlexSearch provider not initialized" };
608
+ }
609
+ const docCount = this.docs ? Object.keys(this.docs).length : 0;
610
+ return {
611
+ healthy: true,
612
+ message: `FlexSearch provider ready with ${docCount} documents`
613
+ };
614
+ }
615
+ /**
616
+ * Get all loaded documents (for compatibility with existing server code)
617
+ */
618
+ getDocs() {
619
+ return this.docs;
620
+ }
621
+ /**
622
+ * Get the FlexSearch index (for compatibility with existing server code)
623
+ */
624
+ getSearchIndex() {
625
+ return this.searchIndex;
626
+ }
627
+ };
628
+
629
+ // src/providers/loader.ts
630
+ async function loadIndexer(specifier) {
631
+ if (specifier === "flexsearch") {
632
+ return new FlexSearchIndexer();
633
+ }
634
+ try {
635
+ const module = await import(specifier);
636
+ const IndexerClass = module.default;
637
+ if (typeof IndexerClass === "function") {
638
+ const instance = new IndexerClass();
639
+ if (!isContentIndexer(instance)) {
640
+ throw new Error(
641
+ `Invalid indexer module "${specifier}": does not implement ContentIndexer interface`
642
+ );
643
+ }
644
+ return instance;
645
+ }
646
+ if (isContentIndexer(IndexerClass)) {
647
+ return IndexerClass;
648
+ }
649
+ throw new Error(
650
+ `Invalid indexer module "${specifier}": must export a default class or ContentIndexer instance`
651
+ );
652
+ } catch (error) {
653
+ if (error instanceof Error && error.message.includes("Cannot find module")) {
654
+ throw new Error(`Indexer module not found: "${specifier}". Check the path or package name.`);
655
+ }
656
+ throw error;
657
+ }
658
+ }
659
+ async function loadSearchProvider(specifier) {
660
+ if (specifier === "flexsearch") {
661
+ return new FlexSearchProvider();
662
+ }
663
+ try {
664
+ const module = await import(specifier);
665
+ const ProviderClass = module.default;
666
+ if (typeof ProviderClass === "function") {
667
+ const instance = new ProviderClass();
668
+ if (!isSearchProvider(instance)) {
669
+ throw new Error(
670
+ `Invalid search provider module "${specifier}": does not implement SearchProvider interface`
671
+ );
672
+ }
673
+ return instance;
674
+ }
675
+ if (isSearchProvider(ProviderClass)) {
676
+ return ProviderClass;
677
+ }
678
+ throw new Error(
679
+ `Invalid search provider module "${specifier}": must export a default class or SearchProvider instance`
680
+ );
681
+ } catch (error) {
682
+ if (error instanceof Error && error.message.includes("Cannot find module")) {
683
+ throw new Error(
684
+ `Search provider module not found: "${specifier}". Check the path or package name.`
685
+ );
686
+ }
687
+ throw error;
688
+ }
689
+ }
690
+ function isContentIndexer(obj) {
691
+ if (!obj || typeof obj !== "object") {
692
+ return false;
693
+ }
694
+ const indexer = obj;
695
+ return typeof indexer.name === "string" && typeof indexer.initialize === "function" && typeof indexer.indexDocuments === "function" && typeof indexer.finalize === "function";
696
+ }
697
+ function isSearchProvider(obj) {
698
+ if (!obj || typeof obj !== "object") {
699
+ return false;
700
+ }
701
+ const provider = obj;
702
+ return typeof provider.name === "string" && typeof provider.initialize === "function" && typeof provider.isReady === "function" && typeof provider.search === "function";
703
+ }
704
+
507
705
  // src/plugin/docusaurus-plugin.ts
508
706
  function resolveOptions(options) {
509
707
  return {
@@ -560,6 +758,10 @@ function mcpServerPlugin(context, options) {
560
758
  async postBuild({ outDir }) {
561
759
  console.log("[MCP] Starting MCP artifact generation...");
562
760
  const startTime = Date.now();
761
+ if (resolvedOptions.indexers === false) {
762
+ console.log("[MCP] Indexing disabled, skipping artifact generation");
763
+ return;
764
+ }
563
765
  const routes = await collectRoutes(outDir, resolvedOptions.excludeRoutes);
564
766
  console.log(`[MCP] Found ${routes.length} routes to process`);
565
767
  if (routes.length === 0) {
@@ -584,27 +786,47 @@ function mcpServerPlugin(context, options) {
584
786
  console.warn("[MCP] No valid documents to index");
585
787
  return;
586
788
  }
587
- const docsIndex = {};
588
- for (const doc of validDocs) {
589
- docsIndex[doc.route] = doc;
590
- }
591
- console.log("[MCP] Building search index...");
592
- const searchIndex2 = buildSearchIndex(validDocs);
593
- const exportedIndex = await exportSearchIndex(searchIndex2);
594
- const manifest = {
595
- version: resolvedOptions.server.version,
596
- buildTime: (/* @__PURE__ */ new Date()).toISOString(),
597
- docCount: validDocs.length,
789
+ const mcpOutputDir = path__default.default.join(outDir, resolvedOptions.outputDir);
790
+ const providerContext = {
791
+ baseUrl: context.siteConfig.url,
598
792
  serverName: resolvedOptions.server.name,
599
- baseUrl: context.siteConfig.url
793
+ serverVersion: resolvedOptions.server.version,
794
+ outputDir: mcpOutputDir
600
795
  };
601
- const mcpOutputDir = path2__default.default.join(outDir, resolvedOptions.outputDir);
796
+ const indexerSpecs = resolvedOptions.indexers ?? ["flexsearch"];
602
797
  await fs3__default.default.ensureDir(mcpOutputDir);
603
- await Promise.all([
604
- fs3__default.default.writeJson(path2__default.default.join(mcpOutputDir, "docs.json"), docsIndex, { spaces: 0 }),
605
- fs3__default.default.writeJson(path2__default.default.join(mcpOutputDir, "search-index.json"), exportedIndex, { spaces: 0 }),
606
- fs3__default.default.writeJson(path2__default.default.join(mcpOutputDir, "manifest.json"), manifest, { spaces: 2 })
607
- ]);
798
+ const indexerNames = [];
799
+ for (const indexerSpec of indexerSpecs) {
800
+ try {
801
+ const indexer = await loadIndexer(indexerSpec);
802
+ if (indexer.shouldRun && !indexer.shouldRun()) {
803
+ console.log(`[MCP] Skipping indexer: ${indexer.name}`);
804
+ continue;
805
+ }
806
+ console.log(`[MCP] Running indexer: ${indexer.name}`);
807
+ await indexer.initialize(providerContext);
808
+ await indexer.indexDocuments(validDocs);
809
+ const artifacts = await indexer.finalize();
810
+ for (const [filename, content] of artifacts) {
811
+ await fs3__default.default.writeJson(path__default.default.join(mcpOutputDir, filename), content, { spaces: 0 });
812
+ }
813
+ indexerNames.push(indexer.name);
814
+ } catch (error) {
815
+ console.error(`[MCP] Error running indexer "${indexerSpec}":`, error);
816
+ throw error;
817
+ }
818
+ }
819
+ if (indexerNames.length > 0) {
820
+ const manifest = {
821
+ version: resolvedOptions.server.version,
822
+ buildTime: (/* @__PURE__ */ new Date()).toISOString(),
823
+ docCount: validDocs.length,
824
+ serverName: resolvedOptions.server.name,
825
+ baseUrl: context.siteConfig.url,
826
+ indexers: indexerNames
827
+ };
828
+ await fs3__default.default.writeJson(path__default.default.join(mcpOutputDir, "manifest.json"), manifest, { spaces: 2 });
829
+ }
608
830
  const elapsed = Date.now() - startTime;
609
831
  console.log(`[MCP] Artifacts written to ${mcpOutputDir}`);
610
832
  console.log(`[MCP] Generation complete in ${elapsed}ms`);
@@ -632,17 +854,6 @@ var docsSearchTool = {
632
854
  required: ["query"]
633
855
  }
634
856
  };
635
- function executeDocsSearch(params, index, docs) {
636
- const { query, limit = 5 } = params;
637
- if (!query || typeof query !== "string" || query.trim().length === 0) {
638
- throw new Error("Query parameter is required and must be a non-empty string");
639
- }
640
- const effectiveLimit = Math.min(Math.max(1, limit), 20);
641
- const results = searchIndex(index, docs, query.trim(), {
642
- limit: effectiveLimit
643
- });
644
- return results;
645
- }
646
857
  function formatSearchResults(results, baseUrl) {
647
858
  if (results.length === 0) {
648
859
  return "No matching documents found.";
@@ -667,10 +878,10 @@ function formatSearchResults(results, baseUrl) {
667
878
  return lines.join("\n");
668
879
  }
669
880
 
670
- // src/mcp/tools/docs-get-page.ts
671
- var docsGetPageTool = {
672
- name: "docs_get_page",
673
- description: "Retrieve the full content of a documentation page as markdown. Use this after searching to get complete page content.",
881
+ // src/mcp/tools/docs-fetch.ts
882
+ var docsFetchTool = {
883
+ name: "docs_fetch",
884
+ description: "Fetch the complete content of a documentation page. Use this after searching to get full page content.",
674
885
  inputSchema: {
675
886
  type: "object",
676
887
  properties: {
@@ -682,28 +893,6 @@ var docsGetPageTool = {
682
893
  required: ["route"]
683
894
  }
684
895
  };
685
- function executeDocsGetPage(params, docs) {
686
- const { route } = params;
687
- if (!route || typeof route !== "string") {
688
- throw new Error("Route parameter is required and must be a string");
689
- }
690
- let normalizedRoute = route.trim();
691
- if (!normalizedRoute.startsWith("/")) {
692
- normalizedRoute = "/" + normalizedRoute;
693
- }
694
- if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
695
- normalizedRoute = normalizedRoute.slice(0, -1);
696
- }
697
- const doc = docs[normalizedRoute];
698
- if (!doc) {
699
- const altRoute = normalizedRoute.slice(1);
700
- if (docs[altRoute]) {
701
- return docs[altRoute] ?? null;
702
- }
703
- return null;
704
- }
705
- return doc;
706
- }
707
896
  function formatPageContent(doc, baseUrl) {
708
897
  if (!doc) {
709
898
  return "Page not found. Please check the route path and try again.";
@@ -738,98 +927,6 @@ function formatPageContent(doc, baseUrl) {
738
927
  return lines.join("\n");
739
928
  }
740
929
 
741
- // src/mcp/tools/docs-get-section.ts
742
- var docsGetSectionTool = {
743
- name: "docs_get_section",
744
- description: "Retrieve a specific section of a documentation page by heading ID. Use this to get focused content from a larger page.",
745
- inputSchema: {
746
- type: "object",
747
- properties: {
748
- route: {
749
- type: "string",
750
- description: "The route path of the page (e.g., /docs/getting-started)"
751
- },
752
- headingId: {
753
- type: "string",
754
- description: "The ID of the heading to retrieve (e.g., authentication)"
755
- }
756
- },
757
- required: ["route", "headingId"]
758
- }
759
- };
760
- function executeDocsGetSection(params, docs) {
761
- const { route, headingId } = params;
762
- if (!route || typeof route !== "string") {
763
- throw new Error("Route parameter is required and must be a string");
764
- }
765
- if (!headingId || typeof headingId !== "string") {
766
- throw new Error("HeadingId parameter is required and must be a string");
767
- }
768
- let normalizedRoute = route.trim();
769
- if (!normalizedRoute.startsWith("/")) {
770
- normalizedRoute = "/" + normalizedRoute;
771
- }
772
- if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
773
- normalizedRoute = normalizedRoute.slice(0, -1);
774
- }
775
- const doc = docs[normalizedRoute];
776
- if (!doc) {
777
- return {
778
- content: null,
779
- doc: null,
780
- headingText: null,
781
- availableHeadings: []
782
- };
783
- }
784
- const availableHeadings = doc.headings.map((h) => ({
785
- id: h.id,
786
- text: h.text,
787
- level: h.level
788
- }));
789
- const heading = doc.headings.find((h) => h.id === headingId.trim());
790
- if (!heading) {
791
- return {
792
- content: null,
793
- doc,
794
- headingText: null,
795
- availableHeadings
796
- };
797
- }
798
- const content = extractSection(doc.markdown, headingId.trim(), doc.headings);
799
- return {
800
- content,
801
- doc,
802
- headingText: heading.text,
803
- availableHeadings
804
- };
805
- }
806
- function formatSectionContent(result, headingId, baseUrl) {
807
- if (!result.doc) {
808
- return "Page not found. Please check the route path and try again.";
809
- }
810
- if (!result.content) {
811
- const lines2 = [`Section "${headingId}" not found in this document.`, "", "Available sections:"];
812
- for (const heading of result.availableHeadings) {
813
- const indent = " ".repeat(heading.level - 1);
814
- lines2.push(`${indent}- ${heading.text} (id: ${heading.id})`);
815
- }
816
- return lines2.join("\n");
817
- }
818
- const lines = [];
819
- const fullUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}${result.doc.route}#${headingId}` : null;
820
- lines.push(`# ${result.headingText}`);
821
- if (fullUrl) {
822
- lines.push(`> From: ${result.doc.title} - ${fullUrl}`);
823
- } else {
824
- lines.push(`> From: ${result.doc.title} (${result.doc.route})`);
825
- }
826
- lines.push("");
827
- lines.push("---");
828
- lines.push("");
829
- lines.push(result.content);
830
- return lines.join("\n");
831
- }
832
-
833
930
  // src/mcp/server.ts
834
931
  function isFileConfig(config) {
835
932
  return "docsPath" in config && "indexPath" in config;
@@ -839,8 +936,7 @@ function isDataConfig(config) {
839
936
  }
840
937
  var McpDocsServer = class {
841
938
  config;
842
- docs = null;
843
- searchIndex = null;
939
+ searchProvider = null;
844
940
  mcpServer;
845
941
  initialized = false;
846
942
  constructor(config) {
@@ -873,75 +969,83 @@ var McpDocsServer = class {
873
969
  },
874
970
  async ({ query, limit }) => {
875
971
  await this.initialize();
876
- if (!this.docs || !this.searchIndex) {
972
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
877
973
  return {
878
974
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
879
975
  isError: true
880
976
  };
881
977
  }
882
- const results = executeDocsSearch({ query, limit }, this.searchIndex, this.docs);
883
- return {
884
- content: [
885
- { type: "text", text: formatSearchResults(results, this.config.baseUrl) }
886
- ]
887
- };
978
+ try {
979
+ const results = await this.searchProvider.search(query, { limit });
980
+ return {
981
+ content: [
982
+ { type: "text", text: formatSearchResults(results, this.config.baseUrl) }
983
+ ]
984
+ };
985
+ } catch (error) {
986
+ console.error("[MCP] Search error:", error);
987
+ return {
988
+ content: [{ type: "text", text: `Search error: ${String(error)}` }],
989
+ isError: true
990
+ };
991
+ }
888
992
  }
889
993
  );
890
994
  this.mcpServer.registerTool(
891
- "docs_get_page",
995
+ "docs_fetch",
892
996
  {
893
- description: "Retrieve the complete content of a documentation page as markdown. Use this when you need the full content of a specific page.",
997
+ description: "Fetch the complete content of a documentation page. Use this when you need the full content of a specific page.",
894
998
  inputSchema: {
895
999
  route: zod.z.string().min(1).describe('The page route path (e.g., "/docs/getting-started" or "/api/reference")')
896
1000
  }
897
1001
  },
898
1002
  async ({ route }) => {
899
1003
  await this.initialize();
900
- if (!this.docs) {
1004
+ if (!this.searchProvider || !this.searchProvider.isReady()) {
901
1005
  return {
902
1006
  content: [{ type: "text", text: "Server not initialized. Please try again." }],
903
1007
  isError: true
904
1008
  };
905
1009
  }
906
- const doc = executeDocsGetPage({ route }, this.docs);
907
- return {
908
- content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
909
- };
910
- }
911
- );
912
- this.mcpServer.registerTool(
913
- "docs_get_section",
914
- {
915
- 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.",
916
- inputSchema: {
917
- route: zod.z.string().min(1).describe("The page route path"),
918
- headingId: zod.z.string().min(1).describe(
919
- 'The heading ID of the section to extract (e.g., "installation", "api-reference")'
920
- )
921
- }
922
- },
923
- async ({ route, headingId }) => {
924
- await this.initialize();
925
- if (!this.docs) {
1010
+ try {
1011
+ const doc = await this.getDocument(route);
926
1012
  return {
927
- content: [{ type: "text", text: "Server not initialized. Please try again." }],
1013
+ content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
1014
+ };
1015
+ } catch (error) {
1016
+ console.error("[MCP] Get page error:", error);
1017
+ return {
1018
+ content: [{ type: "text", text: `Error getting page: ${String(error)}` }],
928
1019
  isError: true
929
1020
  };
930
1021
  }
931
- const result = executeDocsGetSection({ route, headingId }, this.docs);
932
- return {
933
- content: [
934
- {
935
- type: "text",
936
- text: formatSectionContent(result, headingId, this.config.baseUrl)
937
- }
938
- ]
939
- };
940
1022
  }
941
1023
  );
942
1024
  }
943
1025
  /**
944
- * Load docs and search index
1026
+ * Get a document by route using the search provider
1027
+ */
1028
+ async getDocument(route) {
1029
+ if (!this.searchProvider) {
1030
+ return null;
1031
+ }
1032
+ if (this.searchProvider.getDocument) {
1033
+ return this.searchProvider.getDocument(route);
1034
+ }
1035
+ if (this.searchProvider instanceof FlexSearchProvider) {
1036
+ const docs = this.searchProvider.getDocs();
1037
+ if (!docs) return null;
1038
+ if (docs[route]) {
1039
+ return docs[route];
1040
+ }
1041
+ const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
1042
+ const withoutSlash = route.startsWith("/") ? route.slice(1) : route;
1043
+ return docs[normalizedRoute] ?? docs[withoutSlash] ?? null;
1044
+ }
1045
+ return null;
1046
+ }
1047
+ /**
1048
+ * Load docs and search index using the configured search provider
945
1049
  *
946
1050
  * For file-based config: reads from disk
947
1051
  * For data config: uses pre-loaded data directly
@@ -951,24 +1055,26 @@ var McpDocsServer = class {
951
1055
  return;
952
1056
  }
953
1057
  try {
1058
+ const searchSpecifier = this.config.search ?? "flexsearch";
1059
+ this.searchProvider = await loadSearchProvider(searchSpecifier);
1060
+ const providerContext = {
1061
+ baseUrl: this.config.baseUrl ?? "",
1062
+ serverName: this.config.name,
1063
+ serverVersion: this.config.version ?? "1.0.0",
1064
+ outputDir: ""
1065
+ // Not relevant for runtime
1066
+ };
1067
+ const initData = {};
954
1068
  if (isDataConfig(this.config)) {
955
- this.docs = this.config.docs;
956
- this.searchIndex = await importSearchIndex(this.config.searchIndexData);
1069
+ initData.docs = this.config.docs;
1070
+ initData.indexData = this.config.searchIndexData;
957
1071
  } else if (isFileConfig(this.config)) {
958
- if (await fs3__default.default.pathExists(this.config.docsPath)) {
959
- this.docs = await fs3__default.default.readJson(this.config.docsPath);
960
- } else {
961
- throw new Error(`Docs file not found: ${this.config.docsPath}`);
962
- }
963
- if (await fs3__default.default.pathExists(this.config.indexPath)) {
964
- const indexData = await fs3__default.default.readJson(this.config.indexPath);
965
- this.searchIndex = await importSearchIndex(indexData);
966
- } else {
967
- throw new Error(`Search index not found: ${this.config.indexPath}`);
968
- }
1072
+ initData.docsPath = this.config.docsPath;
1073
+ initData.indexPath = this.config.indexPath;
969
1074
  } else {
970
1075
  throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
971
1076
  }
1077
+ await this.searchProvider.initialize(providerContext, initData);
972
1078
  this.initialized = true;
973
1079
  } catch (error) {
974
1080
  console.error("[MCP] Failed to initialize:", error);
@@ -1029,12 +1135,18 @@ var McpDocsServer = class {
1029
1135
  * Useful for health checks and debugging
1030
1136
  */
1031
1137
  async getStatus() {
1138
+ let docCount = 0;
1139
+ if (this.searchProvider instanceof FlexSearchProvider) {
1140
+ const docs = this.searchProvider.getDocs();
1141
+ docCount = docs ? Object.keys(docs).length : 0;
1142
+ }
1032
1143
  return {
1033
1144
  name: this.config.name,
1034
1145
  version: this.config.version ?? "1.0.0",
1035
1146
  initialized: this.initialized,
1036
- docCount: this.docs ? Object.keys(this.docs).length : 0,
1037
- baseUrl: this.config.baseUrl
1147
+ docCount,
1148
+ baseUrl: this.config.baseUrl,
1149
+ searchProvider: this.searchProvider?.name
1038
1150
  };
1039
1151
  }
1040
1152
  /**
@@ -1048,13 +1160,14 @@ var McpDocsServer = class {
1048
1160
  };
1049
1161
 
1050
1162
  exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
1163
+ exports.FlexSearchIndexer = FlexSearchIndexer;
1164
+ exports.FlexSearchProvider = FlexSearchProvider;
1051
1165
  exports.McpDocsServer = McpDocsServer;
1052
1166
  exports.buildSearchIndex = buildSearchIndex;
1053
1167
  exports.collectRoutes = collectRoutes;
1054
1168
  exports.default = mcpServerPlugin;
1055
1169
  exports.discoverHtmlFiles = discoverHtmlFiles;
1056
- exports.docsGetPageTool = docsGetPageTool;
1057
- exports.docsGetSectionTool = docsGetSectionTool;
1170
+ exports.docsFetchTool = docsFetchTool;
1058
1171
  exports.docsSearchTool = docsSearchTool;
1059
1172
  exports.exportSearchIndex = exportSearchIndex;
1060
1173
  exports.extractContent = extractContent;
@@ -1062,6 +1175,8 @@ exports.extractHeadingsFromMarkdown = extractHeadingsFromMarkdown;
1062
1175
  exports.extractSection = extractSection;
1063
1176
  exports.htmlToMarkdown = htmlToMarkdown;
1064
1177
  exports.importSearchIndex = importSearchIndex;
1178
+ exports.loadIndexer = loadIndexer;
1179
+ exports.loadSearchProvider = loadSearchProvider;
1065
1180
  exports.mcpServerPlugin = mcpServerPlugin;
1066
1181
  exports.parseHtml = parseHtml;
1067
1182
  exports.parseHtmlFile = parseHtmlFile;