docusaurus-plugin-mcp-server 0.9.0 → 0.10.1

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