docusaurus-plugin-mcp-server 0.5.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.
@@ -0,0 +1,918 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs-extra');
4
+ var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
5
+ var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
6
+ var webStandardStreamableHttp_js = require('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js');
7
+ var zod = require('zod');
8
+ var FlexSearch = require('flexsearch');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
13
+ var FlexSearch__default = /*#__PURE__*/_interopDefault(FlexSearch);
14
+
15
+ // src/mcp/server.ts
16
+ var FIELD_WEIGHTS = {
17
+ title: 3,
18
+ headings: 2,
19
+ description: 1.5,
20
+ content: 1
21
+ };
22
+ function englishStemmer(word) {
23
+ if (word.length <= 3) return word;
24
+ return word.replace(/ing$/, "").replace(/tion$/, "t").replace(/sion$/, "s").replace(/([^aeiou])ed$/, "$1").replace(/([^aeiou])es$/, "$1").replace(/ly$/, "").replace(/ment$/, "").replace(/ness$/, "").replace(/ies$/, "y").replace(/([^s])s$/, "$1");
25
+ }
26
+ function createSearchIndex() {
27
+ return new FlexSearch__default.default.Document({
28
+ // Use 'full' tokenization for substring matching
29
+ // This allows "auth" to match "authentication"
30
+ tokenize: "full",
31
+ // Enable caching for faster repeated queries
32
+ cache: 100,
33
+ // Higher resolution = more granular ranking (1-9)
34
+ resolution: 9,
35
+ // Enable context for phrase/proximity matching
36
+ context: {
37
+ resolution: 2,
38
+ depth: 2,
39
+ bidirectional: true
40
+ },
41
+ // Apply stemming to normalize word forms
42
+ encode: (str) => {
43
+ const words = str.toLowerCase().split(/[\s\-_.,;:!?'"()[\]{}]+/);
44
+ return words.filter(Boolean).map(englishStemmer);
45
+ },
46
+ // Document schema
47
+ document: {
48
+ id: "id",
49
+ // Index these fields for searching
50
+ index: ["title", "content", "headings", "description"],
51
+ // Store these fields in results (for enriched queries)
52
+ store: ["title", "description"]
53
+ }
54
+ });
55
+ }
56
+ function searchIndex(index, docs, query, options = {}) {
57
+ const { limit = 5 } = options;
58
+ const rawResults = index.search(query, {
59
+ limit: limit * 3,
60
+ // Get extra results for better ranking after weighting
61
+ enrich: true
62
+ });
63
+ const docScores = /* @__PURE__ */ new Map();
64
+ for (const fieldResult of rawResults) {
65
+ const field = fieldResult.field;
66
+ const fieldWeight = FIELD_WEIGHTS[field] ?? 1;
67
+ const results2 = fieldResult.result;
68
+ for (let i = 0; i < results2.length; i++) {
69
+ const item = results2[i];
70
+ if (!item) continue;
71
+ const docId = typeof item === "string" ? item : item.id;
72
+ const positionScore = (results2.length - i) / results2.length;
73
+ const weightedScore = positionScore * fieldWeight;
74
+ const existingScore = docScores.get(docId) ?? 0;
75
+ docScores.set(docId, existingScore + weightedScore);
76
+ }
77
+ }
78
+ const results = [];
79
+ for (const [docId, score] of docScores) {
80
+ const doc = docs[docId];
81
+ if (!doc) continue;
82
+ results.push({
83
+ route: doc.route,
84
+ title: doc.title,
85
+ score,
86
+ snippet: generateSnippet(doc.markdown, query),
87
+ matchingHeadings: findMatchingHeadings(doc, query)
88
+ });
89
+ }
90
+ results.sort((a, b) => b.score - a.score);
91
+ return results.slice(0, limit);
92
+ }
93
+ function generateSnippet(markdown, query) {
94
+ const maxLength = 200;
95
+ const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
96
+ if (queryTerms.length === 0) {
97
+ return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
98
+ }
99
+ const lowerMarkdown = markdown.toLowerCase();
100
+ let bestIndex = -1;
101
+ let bestTerm = "";
102
+ const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
103
+ for (const term of allTerms) {
104
+ const index = lowerMarkdown.indexOf(term);
105
+ if (index !== -1 && (bestIndex === -1 || index < bestIndex)) {
106
+ bestIndex = index;
107
+ bestTerm = term;
108
+ }
109
+ }
110
+ if (bestIndex === -1) {
111
+ return markdown.slice(0, maxLength) + (markdown.length > maxLength ? "..." : "");
112
+ }
113
+ const snippetStart = Math.max(0, bestIndex - 50);
114
+ const snippetEnd = Math.min(markdown.length, bestIndex + bestTerm.length + 150);
115
+ let snippet = markdown.slice(snippetStart, snippetEnd);
116
+ snippet = snippet.replace(/^#{1,6}\s+/gm, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/```[a-z]*\n?/g, "").replace(/`([^`]+)`/g, "$1").replace(/\s+/g, " ").trim();
117
+ const prefix = snippetStart > 0 ? "..." : "";
118
+ const suffix = snippetEnd < markdown.length ? "..." : "";
119
+ return prefix + snippet + suffix;
120
+ }
121
+ function findMatchingHeadings(doc, query) {
122
+ const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
123
+ const allTerms = [...queryTerms, ...queryTerms.map(englishStemmer)];
124
+ const matching = [];
125
+ for (const heading of doc.headings) {
126
+ const headingLower = heading.text.toLowerCase();
127
+ const headingStemmed = headingLower.split(/\s+/).map(englishStemmer).join(" ");
128
+ if (allTerms.some(
129
+ (term) => headingLower.includes(term) || headingStemmed.includes(englishStemmer(term))
130
+ )) {
131
+ matching.push(heading.text);
132
+ }
133
+ }
134
+ return matching.slice(0, 3);
135
+ }
136
+ async function importSearchIndex(data) {
137
+ const index = createSearchIndex();
138
+ for (const [key, value] of Object.entries(data)) {
139
+ await index.import(
140
+ key,
141
+ value
142
+ );
143
+ }
144
+ return index;
145
+ }
146
+
147
+ // src/mcp/tools/docs-search.ts
148
+ function executeDocsSearch(params, index, docs) {
149
+ const { query, limit = 5 } = params;
150
+ if (!query || typeof query !== "string" || query.trim().length === 0) {
151
+ throw new Error("Query parameter is required and must be a non-empty string");
152
+ }
153
+ const effectiveLimit = Math.min(Math.max(1, limit), 20);
154
+ const results = searchIndex(index, docs, query.trim(), {
155
+ limit: effectiveLimit
156
+ });
157
+ return results;
158
+ }
159
+ function formatSearchResults(results, baseUrl) {
160
+ if (results.length === 0) {
161
+ return "No matching documents found.";
162
+ }
163
+ const lines = [`Found ${results.length} result(s):
164
+ `];
165
+ for (let i = 0; i < results.length; i++) {
166
+ const result = results[i];
167
+ if (!result) continue;
168
+ lines.push(`${i + 1}. **${result.title}**`);
169
+ if (baseUrl) {
170
+ const fullUrl = `${baseUrl.replace(/\/$/, "")}${result.route}`;
171
+ lines.push(` URL: ${fullUrl}`);
172
+ }
173
+ lines.push(` Route: ${result.route}`);
174
+ if (result.matchingHeadings && result.matchingHeadings.length > 0) {
175
+ lines.push(` Matching sections: ${result.matchingHeadings.join(", ")}`);
176
+ }
177
+ lines.push(` ${result.snippet}`);
178
+ lines.push("");
179
+ }
180
+ return lines.join("\n");
181
+ }
182
+
183
+ // src/mcp/tools/docs-get-page.ts
184
+ function executeDocsGetPage(params, docs) {
185
+ const { route } = params;
186
+ if (!route || typeof route !== "string") {
187
+ throw new Error("Route parameter is required and must be a string");
188
+ }
189
+ let normalizedRoute = route.trim();
190
+ if (!normalizedRoute.startsWith("/")) {
191
+ normalizedRoute = "/" + normalizedRoute;
192
+ }
193
+ if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
194
+ normalizedRoute = normalizedRoute.slice(0, -1);
195
+ }
196
+ const doc = docs[normalizedRoute];
197
+ if (!doc) {
198
+ const altRoute = normalizedRoute.slice(1);
199
+ if (docs[altRoute]) {
200
+ return docs[altRoute] ?? null;
201
+ }
202
+ return null;
203
+ }
204
+ return doc;
205
+ }
206
+ function formatPageContent(doc, baseUrl) {
207
+ if (!doc) {
208
+ return "Page not found. Please check the route path and try again.";
209
+ }
210
+ const lines = [];
211
+ lines.push(`# ${doc.title}`);
212
+ lines.push("");
213
+ if (doc.description) {
214
+ lines.push(`> ${doc.description}`);
215
+ lines.push("");
216
+ }
217
+ if (baseUrl) {
218
+ const fullUrl = `${baseUrl.replace(/\/$/, "")}${doc.route}`;
219
+ lines.push(`**URL:** ${fullUrl}`);
220
+ }
221
+ lines.push(`**Route:** ${doc.route}`);
222
+ lines.push("");
223
+ if (doc.headings.length > 0) {
224
+ lines.push("## Contents");
225
+ lines.push("");
226
+ for (const heading of doc.headings) {
227
+ if (heading.level <= 3) {
228
+ const indent = " ".repeat(heading.level - 1);
229
+ lines.push(`${indent}- [${heading.text}](#${heading.id})`);
230
+ }
231
+ }
232
+ lines.push("");
233
+ lines.push("---");
234
+ lines.push("");
235
+ }
236
+ lines.push(doc.markdown);
237
+ return lines.join("\n");
238
+ }
239
+
240
+ // src/processing/heading-extractor.ts
241
+ function extractSection(markdown, headingId, headings) {
242
+ const heading = headings.find((h) => h.id === headingId);
243
+ if (!heading) {
244
+ return null;
245
+ }
246
+ return markdown.slice(heading.startOffset, heading.endOffset).trim();
247
+ }
248
+
249
+ // src/mcp/tools/docs-get-section.ts
250
+ function executeDocsGetSection(params, docs) {
251
+ const { route, headingId } = params;
252
+ if (!route || typeof route !== "string") {
253
+ throw new Error("Route parameter is required and must be a string");
254
+ }
255
+ if (!headingId || typeof headingId !== "string") {
256
+ throw new Error("HeadingId parameter is required and must be a string");
257
+ }
258
+ let normalizedRoute = route.trim();
259
+ if (!normalizedRoute.startsWith("/")) {
260
+ normalizedRoute = "/" + normalizedRoute;
261
+ }
262
+ if (normalizedRoute.length > 1 && normalizedRoute.endsWith("/")) {
263
+ normalizedRoute = normalizedRoute.slice(0, -1);
264
+ }
265
+ const doc = docs[normalizedRoute];
266
+ if (!doc) {
267
+ return {
268
+ content: null,
269
+ doc: null,
270
+ headingText: null,
271
+ availableHeadings: []
272
+ };
273
+ }
274
+ const availableHeadings = doc.headings.map((h) => ({
275
+ id: h.id,
276
+ text: h.text,
277
+ level: h.level
278
+ }));
279
+ const heading = doc.headings.find((h) => h.id === headingId.trim());
280
+ if (!heading) {
281
+ return {
282
+ content: null,
283
+ doc,
284
+ headingText: null,
285
+ availableHeadings
286
+ };
287
+ }
288
+ const content = extractSection(doc.markdown, headingId.trim(), doc.headings);
289
+ return {
290
+ content,
291
+ doc,
292
+ headingText: heading.text,
293
+ availableHeadings
294
+ };
295
+ }
296
+ function formatSectionContent(result, headingId, baseUrl) {
297
+ if (!result.doc) {
298
+ return "Page not found. Please check the route path and try again.";
299
+ }
300
+ if (!result.content) {
301
+ const lines2 = [`Section "${headingId}" not found in this document.`, "", "Available sections:"];
302
+ for (const heading of result.availableHeadings) {
303
+ const indent = " ".repeat(heading.level - 1);
304
+ lines2.push(`${indent}- ${heading.text} (id: ${heading.id})`);
305
+ }
306
+ return lines2.join("\n");
307
+ }
308
+ const lines = [];
309
+ const fullUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}${result.doc.route}#${headingId}` : null;
310
+ lines.push(`# ${result.headingText}`);
311
+ if (fullUrl) {
312
+ lines.push(`> From: ${result.doc.title} - ${fullUrl}`);
313
+ } else {
314
+ lines.push(`> From: ${result.doc.title} (${result.doc.route})`);
315
+ }
316
+ lines.push("");
317
+ lines.push("---");
318
+ lines.push("");
319
+ lines.push(result.content);
320
+ return lines.join("\n");
321
+ }
322
+
323
+ // src/mcp/server.ts
324
+ function isFileConfig(config) {
325
+ return "docsPath" in config && "indexPath" in config;
326
+ }
327
+ function isDataConfig(config) {
328
+ return "docs" in config && "searchIndexData" in config;
329
+ }
330
+ var McpDocsServer = class {
331
+ config;
332
+ docs = null;
333
+ searchIndex = null;
334
+ mcpServer;
335
+ initialized = false;
336
+ constructor(config) {
337
+ this.config = config;
338
+ this.mcpServer = new mcp_js.McpServer(
339
+ {
340
+ name: config.name,
341
+ version: config.version ?? "1.0.0"
342
+ },
343
+ {
344
+ capabilities: {
345
+ tools: {}
346
+ }
347
+ }
348
+ );
349
+ this.registerTools();
350
+ }
351
+ /**
352
+ * Register all MCP tools using the SDK's registerTool API
353
+ */
354
+ registerTools() {
355
+ this.mcpServer.registerTool(
356
+ "docs_search",
357
+ {
358
+ description: "Search the documentation for relevant pages. Returns matching documents with snippets and relevance scores. Use this to find information across all documentation.",
359
+ inputSchema: {
360
+ query: zod.z.string().min(1).describe("The search query string"),
361
+ limit: zod.z.number().int().min(1).max(20).optional().default(5).describe("Maximum number of results to return (1-20, default: 5)")
362
+ }
363
+ },
364
+ async ({ query, limit }) => {
365
+ await this.initialize();
366
+ if (!this.docs || !this.searchIndex) {
367
+ return {
368
+ content: [{ type: "text", text: "Server not initialized. Please try again." }],
369
+ isError: true
370
+ };
371
+ }
372
+ const results = executeDocsSearch({ query, limit }, this.searchIndex, this.docs);
373
+ return {
374
+ content: [
375
+ { type: "text", text: formatSearchResults(results, this.config.baseUrl) }
376
+ ]
377
+ };
378
+ }
379
+ );
380
+ this.mcpServer.registerTool(
381
+ "docs_get_page",
382
+ {
383
+ description: "Retrieve the complete content of a documentation page as markdown. Use this when you need the full content of a specific page.",
384
+ inputSchema: {
385
+ route: zod.z.string().min(1).describe('The page route path (e.g., "/docs/getting-started" or "/api/reference")')
386
+ }
387
+ },
388
+ async ({ route }) => {
389
+ await this.initialize();
390
+ if (!this.docs) {
391
+ return {
392
+ content: [{ type: "text", text: "Server not initialized. Please try again." }],
393
+ isError: true
394
+ };
395
+ }
396
+ const doc = executeDocsGetPage({ route }, this.docs);
397
+ return {
398
+ content: [{ type: "text", text: formatPageContent(doc, this.config.baseUrl) }]
399
+ };
400
+ }
401
+ );
402
+ this.mcpServer.registerTool(
403
+ "docs_get_section",
404
+ {
405
+ 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.",
406
+ inputSchema: {
407
+ route: zod.z.string().min(1).describe("The page route path"),
408
+ headingId: zod.z.string().min(1).describe(
409
+ 'The heading ID of the section to extract (e.g., "installation", "api-reference")'
410
+ )
411
+ }
412
+ },
413
+ async ({ route, headingId }) => {
414
+ await this.initialize();
415
+ if (!this.docs) {
416
+ return {
417
+ content: [{ type: "text", text: "Server not initialized. Please try again." }],
418
+ isError: true
419
+ };
420
+ }
421
+ const result = executeDocsGetSection({ route, headingId }, this.docs);
422
+ return {
423
+ content: [
424
+ {
425
+ type: "text",
426
+ text: formatSectionContent(result, headingId, this.config.baseUrl)
427
+ }
428
+ ]
429
+ };
430
+ }
431
+ );
432
+ }
433
+ /**
434
+ * Load docs and search index
435
+ *
436
+ * For file-based config: reads from disk
437
+ * For data config: uses pre-loaded data directly
438
+ */
439
+ async initialize() {
440
+ if (this.initialized) {
441
+ return;
442
+ }
443
+ try {
444
+ if (isDataConfig(this.config)) {
445
+ this.docs = this.config.docs;
446
+ this.searchIndex = await importSearchIndex(this.config.searchIndexData);
447
+ } else if (isFileConfig(this.config)) {
448
+ if (await fs__default.default.pathExists(this.config.docsPath)) {
449
+ this.docs = await fs__default.default.readJson(this.config.docsPath);
450
+ } else {
451
+ throw new Error(`Docs file not found: ${this.config.docsPath}`);
452
+ }
453
+ if (await fs__default.default.pathExists(this.config.indexPath)) {
454
+ const indexData = await fs__default.default.readJson(this.config.indexPath);
455
+ this.searchIndex = await importSearchIndex(indexData);
456
+ } else {
457
+ throw new Error(`Search index not found: ${this.config.indexPath}`);
458
+ }
459
+ } else {
460
+ throw new Error("Invalid server config: must provide either file paths or pre-loaded data");
461
+ }
462
+ this.initialized = true;
463
+ } catch (error) {
464
+ console.error("[MCP] Failed to initialize:", error);
465
+ throw error;
466
+ }
467
+ }
468
+ /**
469
+ * Handle an HTTP request using the MCP SDK's transport
470
+ *
471
+ * This method is designed for serverless environments (Vercel, Netlify).
472
+ * It creates a stateless transport instance and processes the request.
473
+ *
474
+ * @param req - Node.js IncomingMessage or compatible request object
475
+ * @param res - Node.js ServerResponse or compatible response object
476
+ * @param parsedBody - Optional pre-parsed request body
477
+ */
478
+ async handleHttpRequest(req, res, parsedBody) {
479
+ await this.initialize();
480
+ const transport = new streamableHttp_js.StreamableHTTPServerTransport({
481
+ sessionIdGenerator: void 0,
482
+ // Stateless mode - no session tracking
483
+ enableJsonResponse: true
484
+ // Return JSON instead of SSE streams
485
+ });
486
+ await this.mcpServer.connect(transport);
487
+ try {
488
+ await transport.handleRequest(req, res, parsedBody);
489
+ } finally {
490
+ await transport.close();
491
+ }
492
+ }
493
+ /**
494
+ * Handle a Web Standard Request (Cloudflare Workers, Deno, Bun)
495
+ *
496
+ * This method is designed for Web Standard environments that use
497
+ * the Fetch API Request/Response pattern.
498
+ *
499
+ * @param request - Web Standard Request object
500
+ * @returns Web Standard Response object
501
+ */
502
+ async handleWebRequest(request) {
503
+ await this.initialize();
504
+ const transport = new webStandardStreamableHttp_js.WebStandardStreamableHTTPServerTransport({
505
+ sessionIdGenerator: void 0,
506
+ // Stateless mode
507
+ enableJsonResponse: true
508
+ });
509
+ await this.mcpServer.connect(transport);
510
+ try {
511
+ return await transport.handleRequest(request);
512
+ } finally {
513
+ await transport.close();
514
+ }
515
+ }
516
+ /**
517
+ * Get server status information
518
+ *
519
+ * Useful for health checks and debugging
520
+ */
521
+ async getStatus() {
522
+ return {
523
+ name: this.config.name,
524
+ version: this.config.version ?? "1.0.0",
525
+ initialized: this.initialized,
526
+ docCount: this.docs ? Object.keys(this.docs).length : 0,
527
+ baseUrl: this.config.baseUrl
528
+ };
529
+ }
530
+ /**
531
+ * Get the underlying McpServer instance
532
+ *
533
+ * Useful for advanced use cases like custom transports
534
+ */
535
+ getMcpServer() {
536
+ return this.mcpServer;
537
+ }
538
+ };
539
+
540
+ // src/adapters/vercel.ts
541
+ function createVercelHandler(config) {
542
+ let server = null;
543
+ function getServer() {
544
+ if (!server) {
545
+ server = new McpDocsServer(config);
546
+ }
547
+ return server;
548
+ }
549
+ return async function handler(req, res) {
550
+ if (req.method === "GET") {
551
+ const mcpServer = getServer();
552
+ const status = await mcpServer.getStatus();
553
+ return res.status(200).json(status);
554
+ }
555
+ if (req.method !== "POST") {
556
+ return res.status(405).json({
557
+ jsonrpc: "2.0",
558
+ id: null,
559
+ error: {
560
+ code: -32600,
561
+ message: "Method not allowed. Use POST for MCP requests, GET for status."
562
+ }
563
+ });
564
+ }
565
+ try {
566
+ const mcpServer = getServer();
567
+ await mcpServer.handleHttpRequest(req, res, req.body);
568
+ } catch (error) {
569
+ const errorMessage = error instanceof Error ? error.message : String(error);
570
+ console.error("MCP Server Error:", error);
571
+ return res.status(500).json({
572
+ jsonrpc: "2.0",
573
+ id: null,
574
+ error: {
575
+ code: -32603,
576
+ message: `Internal server error: ${errorMessage}`
577
+ }
578
+ });
579
+ }
580
+ };
581
+ }
582
+
583
+ // src/adapters/netlify.ts
584
+ function eventToRequest(event) {
585
+ const url = event.rawUrl || `https://localhost${event.path || "/"}`;
586
+ const headers = new Headers();
587
+ for (const [key, value] of Object.entries(event.headers)) {
588
+ if (value) {
589
+ headers.set(key, value);
590
+ }
591
+ }
592
+ let body = event.body;
593
+ if (body && event.isBase64Encoded) {
594
+ body = Buffer.from(body, "base64").toString("utf-8");
595
+ }
596
+ return new Request(url, {
597
+ method: event.httpMethod,
598
+ headers,
599
+ body: event.httpMethod !== "GET" && event.httpMethod !== "HEAD" ? body : void 0
600
+ });
601
+ }
602
+ async function responseToNetlify(response) {
603
+ const headers = {};
604
+ response.headers.forEach((value, key) => {
605
+ headers[key] = value;
606
+ });
607
+ const body = await response.text();
608
+ return {
609
+ statusCode: response.status,
610
+ headers,
611
+ body: body || void 0
612
+ };
613
+ }
614
+ function createNetlifyHandler(config) {
615
+ let server = null;
616
+ function getServer() {
617
+ if (!server) {
618
+ server = new McpDocsServer(config);
619
+ }
620
+ return server;
621
+ }
622
+ return async function handler(event, _context) {
623
+ const headers = {
624
+ "Content-Type": "application/json"
625
+ };
626
+ if (event.httpMethod === "GET") {
627
+ const mcpServer = getServer();
628
+ const status = await mcpServer.getStatus();
629
+ return {
630
+ statusCode: 200,
631
+ headers,
632
+ body: JSON.stringify(status)
633
+ };
634
+ }
635
+ if (event.httpMethod !== "POST") {
636
+ return {
637
+ statusCode: 405,
638
+ headers,
639
+ body: JSON.stringify({
640
+ jsonrpc: "2.0",
641
+ id: null,
642
+ error: {
643
+ code: -32600,
644
+ message: "Method not allowed. Use POST for MCP requests, GET for status."
645
+ }
646
+ })
647
+ };
648
+ }
649
+ try {
650
+ const mcpServer = getServer();
651
+ const request = eventToRequest(event);
652
+ const response = await mcpServer.handleWebRequest(request);
653
+ return await responseToNetlify(response);
654
+ } catch (error) {
655
+ const errorMessage = error instanceof Error ? error.message : String(error);
656
+ console.error("MCP Server Error:", error);
657
+ return {
658
+ statusCode: 500,
659
+ headers,
660
+ body: JSON.stringify({
661
+ jsonrpc: "2.0",
662
+ id: null,
663
+ error: {
664
+ code: -32603,
665
+ message: `Internal server error: ${errorMessage}`
666
+ }
667
+ })
668
+ };
669
+ }
670
+ };
671
+ }
672
+
673
+ // src/adapters/cloudflare.ts
674
+ function createCloudflareHandler(config) {
675
+ let server = null;
676
+ const serverConfig = {
677
+ docs: config.docs,
678
+ searchIndexData: config.searchIndexData,
679
+ name: config.name,
680
+ version: config.version,
681
+ baseUrl: config.baseUrl
682
+ };
683
+ function getServer() {
684
+ if (!server) {
685
+ server = new McpDocsServer(serverConfig);
686
+ }
687
+ return server;
688
+ }
689
+ return async function fetch(request) {
690
+ const headers = {
691
+ "Access-Control-Allow-Origin": "*",
692
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
693
+ "Access-Control-Allow-Headers": "Content-Type"
694
+ };
695
+ if (request.method === "OPTIONS") {
696
+ return new Response(null, { status: 204, headers });
697
+ }
698
+ if (request.method === "GET") {
699
+ const mcpServer = getServer();
700
+ const status = await mcpServer.getStatus();
701
+ return new Response(JSON.stringify(status), {
702
+ status: 200,
703
+ headers: { ...headers, "Content-Type": "application/json" }
704
+ });
705
+ }
706
+ if (request.method !== "POST") {
707
+ return new Response(
708
+ JSON.stringify({
709
+ jsonrpc: "2.0",
710
+ id: null,
711
+ error: {
712
+ code: -32600,
713
+ message: "Method not allowed. Use POST for MCP requests, GET for status."
714
+ }
715
+ }),
716
+ {
717
+ status: 405,
718
+ headers: { ...headers, "Content-Type": "application/json" }
719
+ }
720
+ );
721
+ }
722
+ try {
723
+ const mcpServer = getServer();
724
+ const response = await mcpServer.handleWebRequest(request);
725
+ const newHeaders = new Headers(response.headers);
726
+ Object.entries(headers).forEach(([key, value]) => {
727
+ newHeaders.set(key, value);
728
+ });
729
+ return new Response(response.body, {
730
+ status: response.status,
731
+ statusText: response.statusText,
732
+ headers: newHeaders
733
+ });
734
+ } catch (error) {
735
+ const errorMessage = error instanceof Error ? error.message : String(error);
736
+ console.error("MCP Server Error:", error);
737
+ return new Response(
738
+ JSON.stringify({
739
+ jsonrpc: "2.0",
740
+ id: null,
741
+ error: {
742
+ code: -32603,
743
+ message: `Internal server error: ${errorMessage}`
744
+ }
745
+ }),
746
+ {
747
+ status: 500,
748
+ headers: { ...headers, "Content-Type": "application/json" }
749
+ }
750
+ );
751
+ }
752
+ };
753
+ }
754
+
755
+ // src/adapters/generator.ts
756
+ function generateAdapterFiles(options) {
757
+ const { platform, name, baseUrl } = options;
758
+ switch (platform) {
759
+ case "vercel":
760
+ return generateVercelFiles(name, baseUrl);
761
+ case "netlify":
762
+ return generateNetlifyFiles(name, baseUrl);
763
+ case "cloudflare":
764
+ return generateCloudflareFiles(name, baseUrl);
765
+ default:
766
+ throw new Error(`Unknown platform: ${platform}`);
767
+ }
768
+ }
769
+ function generateVercelFiles(name, baseUrl) {
770
+ return [
771
+ {
772
+ path: "api/mcp.js",
773
+ description: "Vercel serverless function for MCP server",
774
+ content: `/**
775
+ * Vercel API route for MCP server
776
+ * Deploy to Vercel and this will be available at /api/mcp
777
+ */
778
+
779
+ const { createVercelHandler } = require('docusaurus-plugin-mcp-server/adapters');
780
+ const path = require('path');
781
+
782
+ const projectRoot = path.join(__dirname, '..');
783
+
784
+ module.exports = createVercelHandler({
785
+ docsPath: path.join(projectRoot, 'build/mcp/docs.json'),
786
+ indexPath: path.join(projectRoot, 'build/mcp/search-index.json'),
787
+ name: '${name}',
788
+ version: '1.0.0',
789
+ baseUrl: '${baseUrl}',
790
+ });
791
+ `
792
+ },
793
+ {
794
+ path: "vercel.json",
795
+ description: "Vercel configuration (merge with existing if present)",
796
+ content: `{
797
+ "functions": {
798
+ "api/mcp.js": {
799
+ "includeFiles": "build/mcp/**"
800
+ }
801
+ },
802
+ "rewrites": [
803
+ {
804
+ "source": "/mcp",
805
+ "destination": "/api/mcp"
806
+ }
807
+ ]
808
+ }
809
+ `
810
+ }
811
+ ];
812
+ }
813
+ function generateNetlifyFiles(name, baseUrl) {
814
+ return [
815
+ {
816
+ path: "netlify/functions/mcp.js",
817
+ description: "Netlify serverless function for MCP server",
818
+ content: `/**
819
+ * Netlify function for MCP server
820
+ * Deploy to Netlify and this will be available at /.netlify/functions/mcp
821
+ */
822
+
823
+ const { createNetlifyHandler } = require('docusaurus-plugin-mcp-server/adapters');
824
+ const path = require('path');
825
+
826
+ const projectRoot = path.join(__dirname, '../..');
827
+
828
+ exports.handler = createNetlifyHandler({
829
+ docsPath: path.join(projectRoot, 'build/mcp/docs.json'),
830
+ indexPath: path.join(projectRoot, 'build/mcp/search-index.json'),
831
+ name: '${name}',
832
+ version: '1.0.0',
833
+ baseUrl: '${baseUrl}',
834
+ });
835
+ `
836
+ },
837
+ {
838
+ path: "netlify.toml",
839
+ description: "Netlify configuration (merge with existing if present)",
840
+ content: `[build]
841
+ publish = "build"
842
+ command = "npm run build"
843
+
844
+ [functions]
845
+ directory = "netlify/functions"
846
+ included_files = ["build/mcp/**"]
847
+
848
+ [[redirects]]
849
+ from = "/mcp"
850
+ to = "/.netlify/functions/mcp"
851
+ status = 200
852
+ `
853
+ }
854
+ ];
855
+ }
856
+ function generateCloudflareFiles(name, baseUrl) {
857
+ return [
858
+ {
859
+ path: "workers/mcp.js",
860
+ description: "Cloudflare Worker for MCP server",
861
+ content: `/**
862
+ * Cloudflare Worker for MCP server
863
+ *
864
+ * Note: This requires bundling docs.json and search-index.json with the worker,
865
+ * or using Cloudflare KV/R2 for storage.
866
+ *
867
+ * For bundling, use wrangler with custom build configuration.
868
+ */
869
+
870
+ import { createCloudflareHandler } from 'docusaurus-plugin-mcp-server/adapters';
871
+
872
+ // Option 1: Import bundled data (requires bundler configuration)
873
+ // import docs from '../build/mcp/docs.json';
874
+ // import searchIndex from '../build/mcp/search-index.json';
875
+
876
+ // Option 2: Use KV bindings (requires KV namespace configuration)
877
+ // const docs = await env.MCP_KV.get('docs', { type: 'json' });
878
+ // const searchIndex = await env.MCP_KV.get('search-index', { type: 'json' });
879
+
880
+ export default {
881
+ fetch: createCloudflareHandler({
882
+ name: '${name}',
883
+ version: '1.0.0',
884
+ baseUrl: '${baseUrl}',
885
+ // docsPath and indexPath are used for file-based loading
886
+ // For Workers, you'll need to configure data loading differently
887
+ docsPath: './mcp/docs.json',
888
+ indexPath: './mcp/search-index.json',
889
+ }),
890
+ };
891
+ `
892
+ },
893
+ {
894
+ path: "wrangler.toml",
895
+ description: "Cloudflare Wrangler configuration",
896
+ content: `name = "${name}-mcp"
897
+ main = "workers/mcp.js"
898
+ compatibility_date = "2024-01-01"
899
+
900
+ # Uncomment to use KV for storing docs
901
+ # [[kv_namespaces]]
902
+ # binding = "MCP_KV"
903
+ # id = "your-kv-namespace-id"
904
+
905
+ # Static assets (the Docusaurus build)
906
+ # [site]
907
+ # bucket = "./build"
908
+ `
909
+ }
910
+ ];
911
+ }
912
+
913
+ exports.createCloudflareHandler = createCloudflareHandler;
914
+ exports.createNetlifyHandler = createNetlifyHandler;
915
+ exports.createVercelHandler = createVercelHandler;
916
+ exports.generateAdapterFiles = generateAdapterFiles;
917
+ //# sourceMappingURL=adapters-entry.js.map
918
+ //# sourceMappingURL=adapters-entry.js.map