docusaurus-plugin-mcp-server 0.7.0 → 0.9.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.
@@ -3,10 +3,10 @@
3
3
  var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
4
4
  var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
5
5
  var webStandardStreamableHttp_js = require('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js');
6
- var zod = require('zod');
7
6
  var FlexSearch = require('flexsearch');
8
7
  var fs = require('fs-extra');
9
- require('http');
8
+ var zod = require('zod');
9
+ var http = require('http');
10
10
 
11
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
12
 
@@ -81,6 +81,8 @@ function searchIndex(index, docs, query, options = {}) {
81
81
  const doc = docs[docId];
82
82
  if (!doc) continue;
83
83
  results.push({
84
+ url: docId,
85
+ // docId is the full URL when indexed with baseUrl
84
86
  route: doc.route,
85
87
  title: doc.title,
86
88
  score,
@@ -188,16 +190,11 @@ var FlexSearchProvider = class {
188
190
  const limit = options?.limit ?? 5;
189
191
  return searchIndex(this.searchIndex, this.docs, query, { limit });
190
192
  }
191
- async getDocument(route) {
193
+ async getDocument(url) {
192
194
  if (!this.docs) {
193
195
  throw new Error("[FlexSearch] Provider not initialized");
194
196
  }
195
- if (this.docs[route]) {
196
- return this.docs[route];
197
- }
198
- const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
199
- const withoutSlash = route.startsWith("/") ? route.slice(1) : route;
200
- return this.docs[normalizedRoute] ?? this.docs[withoutSlash] ?? null;
197
+ return this.docs[url] ?? null;
201
198
  }
202
199
  async healthCheck() {
203
200
  if (!this.isReady()) {
@@ -262,9 +259,16 @@ function isSearchProvider(obj) {
262
259
  const provider = obj;
263
260
  return typeof provider.name === "string" && typeof provider.initialize === "function" && typeof provider.isReady === "function" && typeof provider.search === "function";
264
261
  }
265
-
266
- // src/mcp/tools/docs-search.ts
267
- function formatSearchResults(results, baseUrl) {
262
+ var docsSearchInputSchema = {
263
+ query: zod.z.string().min(1).describe("The search query string"),
264
+ limit: zod.z.number().int().min(1).max(20).optional().default(5).describe("Maximum number of results to return (1-20, default: 5)")
265
+ };
266
+ var docsSearchTool = {
267
+ name: "docs_search",
268
+ description: "Search the documentation for relevant pages. Returns matching documents with URLs, snippets, and relevance scores. Use this to find information across all documentation.",
269
+ inputSchema: docsSearchInputSchema
270
+ };
271
+ function formatSearchResults(results) {
268
272
  if (results.length === 0) {
269
273
  return "No matching documents found.";
270
274
  }
@@ -274,24 +278,29 @@ function formatSearchResults(results, baseUrl) {
274
278
  const result = results[i];
275
279
  if (!result) continue;
276
280
  lines.push(`${i + 1}. **${result.title}**`);
277
- if (baseUrl) {
278
- const fullUrl = `${baseUrl.replace(/\/$/, "")}${result.route}`;
279
- lines.push(` URL: ${fullUrl}`);
280
- }
281
- lines.push(` Route: ${result.route}`);
281
+ lines.push(` URL: ${result.url}`);
282
282
  if (result.matchingHeadings && result.matchingHeadings.length > 0) {
283
283
  lines.push(` Matching sections: ${result.matchingHeadings.join(", ")}`);
284
284
  }
285
285
  lines.push(` ${result.snippet}`);
286
286
  lines.push("");
287
287
  }
288
+ lines.push("Use docs_fetch with the URL to retrieve the full page content.");
288
289
  return lines.join("\n");
289
290
  }
290
-
291
- // src/mcp/tools/docs-get-page.ts
292
- function formatPageContent(doc, baseUrl) {
291
+ var docsFetchInputSchema = {
292
+ url: zod.z.string().url().describe(
293
+ 'The full URL of the page to fetch (e.g., "https://docs.example.com/docs/getting-started")'
294
+ )
295
+ };
296
+ var docsFetchTool = {
297
+ name: "docs_fetch",
298
+ description: "Fetch the complete content of a documentation page. Use this after searching to get the full markdown content of a specific page.",
299
+ inputSchema: docsFetchInputSchema
300
+ };
301
+ function formatPageContent(doc) {
293
302
  if (!doc) {
294
- return "Page not found. Please check the route path and try again.";
303
+ return "Page not found. Please check the URL and try again.";
295
304
  }
296
305
  const lines = [];
297
306
  lines.push(`# ${doc.title}`);
@@ -300,12 +309,6 @@ function formatPageContent(doc, baseUrl) {
300
309
  lines.push(`> ${doc.description}`);
301
310
  lines.push("");
302
311
  }
303
- if (baseUrl) {
304
- const fullUrl = `${baseUrl.replace(/\/$/, "")}${doc.route}`;
305
- lines.push(`**URL:** ${fullUrl}`);
306
- }
307
- lines.push(`**Route:** ${doc.route}`);
308
- lines.push("");
309
312
  if (doc.headings.length > 0) {
310
313
  lines.push("## Contents");
311
314
  lines.push("");
@@ -323,43 +326,6 @@ function formatPageContent(doc, baseUrl) {
323
326
  return lines.join("\n");
324
327
  }
325
328
 
326
- // src/processing/heading-extractor.ts
327
- function extractSection(markdown, headingId, headings) {
328
- const heading = headings.find((h) => h.id === headingId);
329
- if (!heading) {
330
- return null;
331
- }
332
- return markdown.slice(heading.startOffset, heading.endOffset).trim();
333
- }
334
-
335
- // src/mcp/tools/docs-get-section.ts
336
- function formatSectionContent(result, headingId, baseUrl) {
337
- if (!result.doc) {
338
- return "Page not found. Please check the route path and try again.";
339
- }
340
- if (!result.content) {
341
- const lines2 = [`Section "${headingId}" not found in this document.`, "", "Available sections:"];
342
- for (const heading of result.availableHeadings) {
343
- const indent = " ".repeat(heading.level - 1);
344
- lines2.push(`${indent}- ${heading.text} (id: ${heading.id})`);
345
- }
346
- return lines2.join("\n");
347
- }
348
- const lines = [];
349
- const fullUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}${result.doc.route}#${headingId}` : null;
350
- lines.push(`# ${result.headingText}`);
351
- if (fullUrl) {
352
- lines.push(`> From: ${result.doc.title} - ${fullUrl}`);
353
- } else {
354
- lines.push(`> From: ${result.doc.title} (${result.doc.route})`);
355
- }
356
- lines.push("");
357
- lines.push("---");
358
- lines.push("");
359
- lines.push(result.content);
360
- return lines.join("\n");
361
- }
362
-
363
329
  // src/mcp/server.ts
364
330
  function isFileConfig(config) {
365
331
  return "docsPath" in config && "indexPath" in config;
@@ -388,17 +354,14 @@ var McpDocsServer = class {
388
354
  this.registerTools();
389
355
  }
390
356
  /**
391
- * Register all MCP tools using the SDK's registerTool API
357
+ * Register all MCP tools using definitions from tool files
392
358
  */
393
359
  registerTools() {
394
360
  this.mcpServer.registerTool(
395
- "docs_search",
361
+ docsSearchTool.name,
396
362
  {
397
- description: "Search the documentation for relevant pages. Returns matching documents with snippets and relevance scores. Use this to find information across all documentation.",
398
- inputSchema: {
399
- query: zod.z.string().min(1).describe("The search query string"),
400
- limit: zod.z.number().int().min(1).max(20).optional().default(5).describe("Maximum number of results to return (1-20, default: 5)")
401
- }
363
+ description: docsSearchTool.description,
364
+ inputSchema: docsSearchTool.inputSchema
402
365
  },
403
366
  async ({ query, limit }) => {
404
367
  await this.initialize();
@@ -411,9 +374,7 @@ var McpDocsServer = class {
411
374
  try {
412
375
  const results = await this.searchProvider.search(query, { limit });
413
376
  return {
414
- content: [
415
- { type: "text", text: formatSearchResults(results, this.config.baseUrl) }
416
- ]
377
+ content: [{ type: "text", text: formatSearchResults(results) }]
417
378
  };
418
379
  } catch (error) {
419
380
  console.error("[MCP] Search error:", error);
@@ -425,47 +386,12 @@ var McpDocsServer = class {
425
386
  }
426
387
  );
427
388
  this.mcpServer.registerTool(
428
- "docs_get_page",
429
- {
430
- description: "Retrieve the complete content of a documentation page as markdown. Use this when you need the full content of a specific page.",
431
- inputSchema: {
432
- route: zod.z.string().min(1).describe('The page route path (e.g., "/docs/getting-started" or "/api/reference")')
433
- }
434
- },
435
- async ({ route }) => {
436
- await this.initialize();
437
- if (!this.searchProvider || !this.searchProvider.isReady()) {
438
- return {
439
- content: [{ type: "text", text: "Server not initialized. Please try again." }],
440
- isError: true
441
- };
442
- }
443
- try {
444
- const doc = await this.getDocument(route);
445
- return {
446
- content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
447
- };
448
- } catch (error) {
449
- console.error("[MCP] Get page error:", error);
450
- return {
451
- content: [{ type: "text", text: `Error getting page: ${String(error)}` }],
452
- isError: true
453
- };
454
- }
455
- }
456
- );
457
- this.mcpServer.registerTool(
458
- "docs_get_section",
389
+ docsFetchTool.name,
459
390
  {
460
- 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.",
461
- inputSchema: {
462
- route: zod.z.string().min(1).describe("The page route path"),
463
- headingId: zod.z.string().min(1).describe(
464
- 'The heading ID of the section to extract (e.g., "installation", "api-reference")'
465
- )
466
- }
391
+ description: docsFetchTool.description,
392
+ inputSchema: docsFetchTool.inputSchema
467
393
  },
468
- async ({ route, headingId }) => {
394
+ async ({ url }) => {
469
395
  await this.initialize();
470
396
  if (!this.searchProvider || !this.searchProvider.isReady()) {
471
397
  return {
@@ -474,58 +400,14 @@ var McpDocsServer = class {
474
400
  };
475
401
  }
476
402
  try {
477
- const doc = await this.getDocument(route);
478
- if (!doc) {
479
- return {
480
- content: [
481
- {
482
- type: "text",
483
- text: formatSectionContent(
484
- { content: null, doc: null, headingText: null, availableHeadings: [] },
485
- headingId,
486
- this.config.baseUrl
487
- )
488
- }
489
- ]
490
- };
491
- }
492
- const availableHeadings = doc.headings.map((h) => ({
493
- id: h.id,
494
- text: h.text,
495
- level: h.level
496
- }));
497
- const heading = doc.headings.find((h) => h.id === headingId.trim());
498
- if (!heading) {
499
- return {
500
- content: [
501
- {
502
- type: "text",
503
- text: formatSectionContent(
504
- { content: null, doc, headingText: null, availableHeadings },
505
- headingId,
506
- this.config.baseUrl
507
- )
508
- }
509
- ]
510
- };
511
- }
512
- const sectionContent = extractSection(doc.markdown, headingId.trim(), doc.headings);
403
+ const doc = await this.getDocument(url);
513
404
  return {
514
- content: [
515
- {
516
- type: "text",
517
- text: formatSectionContent(
518
- { content: sectionContent, doc, headingText: heading.text, availableHeadings },
519
- headingId,
520
- this.config.baseUrl
521
- )
522
- }
523
- ]
405
+ content: [{ type: "text", text: formatPageContent(doc) }]
524
406
  };
525
407
  } catch (error) {
526
- console.error("[MCP] Get section error:", error);
408
+ console.error("[MCP] Fetch error:", error);
527
409
  return {
528
- content: [{ type: "text", text: `Error getting section: ${String(error)}` }],
410
+ content: [{ type: "text", text: `Error fetching page: ${String(error)}` }],
529
411
  isError: true
530
412
  };
531
413
  }
@@ -533,24 +415,14 @@ var McpDocsServer = class {
533
415
  );
534
416
  }
535
417
  /**
536
- * Get a document by route using the search provider
418
+ * Get a document by URL using the search provider
537
419
  */
538
- async getDocument(route) {
420
+ async getDocument(url) {
539
421
  if (!this.searchProvider) {
540
422
  return null;
541
423
  }
542
424
  if (this.searchProvider.getDocument) {
543
- return this.searchProvider.getDocument(route);
544
- }
545
- if (this.searchProvider instanceof FlexSearchProvider) {
546
- const docs = this.searchProvider.getDocs();
547
- if (!docs) return null;
548
- if (docs[route]) {
549
- return docs[route];
550
- }
551
- const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
552
- const withoutSlash = route.startsWith("/") ? route.slice(1) : route;
553
- return docs[normalizedRoute] ?? docs[withoutSlash] ?? null;
425
+ return this.searchProvider.getDocument(url);
554
426
  }
555
427
  return null;
556
428
  }
@@ -669,6 +541,16 @@ var McpDocsServer = class {
669
541
  }
670
542
  };
671
543
 
544
+ // src/adapters/cors.ts
545
+ var CORS_HEADERS = {
546
+ "Access-Control-Allow-Origin": "*",
547
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
548
+ "Access-Control-Allow-Headers": "Content-Type"
549
+ };
550
+ function getCorsHeaders() {
551
+ return { ...CORS_HEADERS };
552
+ }
553
+
672
554
  // src/adapters/vercel.ts
673
555
  function createVercelHandler(config) {
674
556
  let server = null;
@@ -679,12 +561,24 @@ function createVercelHandler(config) {
679
561
  return server;
680
562
  }
681
563
  return async function handler(req, res) {
564
+ const corsHeaders = getCorsHeaders();
565
+ if (req.method === "OPTIONS") {
566
+ res.writeHead(204, corsHeaders);
567
+ res.end();
568
+ return;
569
+ }
682
570
  if (req.method === "GET") {
571
+ Object.entries(corsHeaders).forEach(([key, value]) => {
572
+ res.setHeader(key, value);
573
+ });
683
574
  const mcpServer = getServer();
684
575
  const status = await mcpServer.getStatus();
685
576
  return res.status(200).json(status);
686
577
  }
687
578
  if (req.method !== "POST") {
579
+ Object.entries(corsHeaders).forEach(([key, value]) => {
580
+ res.setHeader(key, value);
581
+ });
688
582
  return res.status(405).json({
689
583
  jsonrpc: "2.0",
690
584
  id: null,
@@ -695,6 +589,9 @@ function createVercelHandler(config) {
695
589
  });
696
590
  }
697
591
  try {
592
+ Object.entries(corsHeaders).forEach(([key, value]) => {
593
+ res.setHeader(key, value);
594
+ });
698
595
  const mcpServer = getServer();
699
596
  await mcpServer.handleHttpRequest(req, res, req.body);
700
597
  } catch (error) {
@@ -731,8 +628,8 @@ function eventToRequest(event) {
731
628
  body: event.httpMethod !== "GET" && event.httpMethod !== "HEAD" ? body : void 0
732
629
  });
733
630
  }
734
- async function responseToNetlify(response) {
735
- const headers = {};
631
+ async function responseToNetlify(response, additionalHeaders) {
632
+ const headers = { ...additionalHeaders };
736
633
  response.headers.forEach((value, key) => {
737
634
  headers[key] = value;
738
635
  });
@@ -752,9 +649,17 @@ function createNetlifyHandler(config) {
752
649
  return server;
753
650
  }
754
651
  return async function handler(event, _context) {
652
+ const corsHeaders = getCorsHeaders();
755
653
  const headers = {
756
- "Content-Type": "application/json"
654
+ "Content-Type": "application/json",
655
+ ...corsHeaders
757
656
  };
657
+ if (event.httpMethod === "OPTIONS") {
658
+ return {
659
+ statusCode: 204,
660
+ headers: corsHeaders
661
+ };
662
+ }
758
663
  if (event.httpMethod === "GET") {
759
664
  const mcpServer = getServer();
760
665
  const status = await mcpServer.getStatus();
@@ -782,7 +687,7 @@ function createNetlifyHandler(config) {
782
687
  const mcpServer = getServer();
783
688
  const request = eventToRequest(event);
784
689
  const response = await mcpServer.handleWebRequest(request);
785
- return await responseToNetlify(response);
690
+ return await responseToNetlify(response, corsHeaders);
786
691
  } catch (error) {
787
692
  const errorMessage = error instanceof Error ? error.message : String(error);
788
693
  console.error("MCP Server Error:", error);
@@ -819,20 +724,16 @@ function createCloudflareHandler(config) {
819
724
  return server;
820
725
  }
821
726
  return async function fetch(request) {
822
- const headers = {
823
- "Access-Control-Allow-Origin": "*",
824
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
825
- "Access-Control-Allow-Headers": "Content-Type"
826
- };
727
+ const corsHeaders = getCorsHeaders();
827
728
  if (request.method === "OPTIONS") {
828
- return new Response(null, { status: 204, headers });
729
+ return new Response(null, { status: 204, headers: corsHeaders });
829
730
  }
830
731
  if (request.method === "GET") {
831
732
  const mcpServer = getServer();
832
733
  const status = await mcpServer.getStatus();
833
734
  return new Response(JSON.stringify(status), {
834
735
  status: 200,
835
- headers: { ...headers, "Content-Type": "application/json" }
736
+ headers: { ...corsHeaders, "Content-Type": "application/json" }
836
737
  });
837
738
  }
838
739
  if (request.method !== "POST") {
@@ -847,7 +748,7 @@ function createCloudflareHandler(config) {
847
748
  }),
848
749
  {
849
750
  status: 405,
850
- headers: { ...headers, "Content-Type": "application/json" }
751
+ headers: { ...corsHeaders, "Content-Type": "application/json" }
851
752
  }
852
753
  );
853
754
  }
@@ -855,7 +756,7 @@ function createCloudflareHandler(config) {
855
756
  const mcpServer = getServer();
856
757
  const response = await mcpServer.handleWebRequest(request);
857
758
  const newHeaders = new Headers(response.headers);
858
- Object.entries(headers).forEach(([key, value]) => {
759
+ Object.entries(corsHeaders).forEach(([key, value]) => {
859
760
  newHeaders.set(key, value);
860
761
  });
861
762
  return new Response(response.body, {
@@ -877,7 +778,7 @@ function createCloudflareHandler(config) {
877
778
  }),
878
779
  {
879
780
  status: 500,
880
- headers: { ...headers, "Content-Type": "application/json" }
781
+ headers: { ...corsHeaders, "Content-Type": "application/json" }
881
782
  }
882
783
  );
883
784
  }
@@ -1041,9 +942,102 @@ compatibility_date = "2024-01-01"
1041
942
  }
1042
943
  ];
1043
944
  }
945
+ function createNodeHandler(options) {
946
+ const { corsOrigin = "*", ...config } = options;
947
+ let server = null;
948
+ function getServer() {
949
+ if (!server) {
950
+ server = new McpDocsServer(config);
951
+ }
952
+ return server;
953
+ }
954
+ function setCorsHeaders(res) {
955
+ if (corsOrigin !== false) {
956
+ res.setHeader("Access-Control-Allow-Origin", corsOrigin);
957
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
958
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
959
+ }
960
+ }
961
+ return async function handler(req, res) {
962
+ setCorsHeaders(res);
963
+ if (req.method === "OPTIONS") {
964
+ res.writeHead(204);
965
+ res.end();
966
+ return;
967
+ }
968
+ if (req.method === "GET") {
969
+ try {
970
+ const mcpServer = getServer();
971
+ const status = await mcpServer.getStatus();
972
+ res.writeHead(200, { "Content-Type": "application/json" });
973
+ res.end(JSON.stringify(status, null, 2));
974
+ } catch (error) {
975
+ const message = error instanceof Error ? error.message : String(error);
976
+ res.writeHead(500, { "Content-Type": "application/json" });
977
+ res.end(JSON.stringify({ error: message }));
978
+ }
979
+ return;
980
+ }
981
+ if (req.method !== "POST") {
982
+ res.writeHead(405, { "Content-Type": "application/json" });
983
+ res.end(
984
+ JSON.stringify({
985
+ jsonrpc: "2.0",
986
+ id: null,
987
+ error: {
988
+ code: -32600,
989
+ message: "Method not allowed. Use POST for MCP requests, GET for status."
990
+ }
991
+ })
992
+ );
993
+ return;
994
+ }
995
+ try {
996
+ const body = await parseRequestBody(req);
997
+ const mcpServer = getServer();
998
+ await mcpServer.handleHttpRequest(req, res, body);
999
+ } catch (error) {
1000
+ const message = error instanceof Error ? error.message : String(error);
1001
+ console.error("[MCP] Request error:", error);
1002
+ res.writeHead(500, { "Content-Type": "application/json" });
1003
+ res.end(
1004
+ JSON.stringify({
1005
+ jsonrpc: "2.0",
1006
+ id: null,
1007
+ error: {
1008
+ code: -32603,
1009
+ message: `Internal server error: ${message}`
1010
+ }
1011
+ })
1012
+ );
1013
+ }
1014
+ };
1015
+ }
1016
+ function createNodeServer(options) {
1017
+ const handler = createNodeHandler(options);
1018
+ return http.createServer(handler);
1019
+ }
1020
+ async function parseRequestBody(req) {
1021
+ return new Promise((resolve, reject) => {
1022
+ let body = "";
1023
+ req.on("data", (chunk) => {
1024
+ body += chunk;
1025
+ });
1026
+ req.on("end", () => {
1027
+ try {
1028
+ resolve(body ? JSON.parse(body) : void 0);
1029
+ } catch {
1030
+ reject(new Error("Invalid JSON in request body"));
1031
+ }
1032
+ });
1033
+ req.on("error", reject);
1034
+ });
1035
+ }
1044
1036
 
1045
1037
  exports.createCloudflareHandler = createCloudflareHandler;
1046
1038
  exports.createNetlifyHandler = createNetlifyHandler;
1039
+ exports.createNodeHandler = createNodeHandler;
1040
+ exports.createNodeServer = createNodeServer;
1047
1041
  exports.createVercelHandler = createVercelHandler;
1048
1042
  exports.generateAdapterFiles = generateAdapterFiles;
1049
1043
  //# sourceMappingURL=adapters-entry.js.map