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/adapters-entry.d.mts +1 -1
- package/dist/adapters-entry.d.ts +1 -1
- package/dist/adapters-entry.js +195 -177
- package/dist/adapters-entry.js.map +1 -1
- package/dist/adapters-entry.mjs +194 -176
- package/dist/adapters-entry.mjs.map +1 -1
- package/dist/cli/verify.js +192 -175
- package/dist/cli/verify.js.map +1 -1
- package/dist/cli/verify.mjs +192 -175
- package/dist/cli/verify.mjs.map +1 -1
- package/dist/{index-CzA4FjeE.d.mts → index-4g0ZZK3z.d.mts} +33 -0
- package/dist/{index-CzA4FjeE.d.ts → index-4g0ZZK3z.d.ts} +33 -0
- package/dist/index.d.mts +289 -31
- package/dist/index.d.ts +289 -31
- package/dist/index.js +328 -213
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +323 -211
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var
|
|
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
|
|
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 =
|
|
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 =
|
|
76
|
-
let routePath = "/" +
|
|
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
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
793
|
+
serverVersion: resolvedOptions.server.version,
|
|
794
|
+
outputDir: mcpOutputDir
|
|
600
795
|
};
|
|
601
|
-
const
|
|
796
|
+
const indexerSpecs = resolvedOptions.indexers ?? ["flexsearch"];
|
|
602
797
|
await fs3__default.default.ensureDir(mcpOutputDir);
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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-
|
|
671
|
-
var
|
|
672
|
-
name: "
|
|
673
|
-
description: "
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
"
|
|
995
|
+
"docs_fetch",
|
|
892
996
|
{
|
|
893
|
-
description: "
|
|
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.
|
|
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
|
-
|
|
907
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
|
|
956
|
-
|
|
1069
|
+
initData.docs = this.config.docs;
|
|
1070
|
+
initData.indexData = this.config.searchIndexData;
|
|
957
1071
|
} else if (isFileConfig(this.config)) {
|
|
958
|
-
|
|
959
|
-
|
|
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
|
|
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.
|
|
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;
|