Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions src/adapters/cloudflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export interface CloudflareAdapterConfig {
version?: string;
/** Base URL for constructing full page URLs */
baseUrl?: string;
/** CORS origin to allow. Defaults to '*' (all origins). */
corsOrigin?: string;
}

/**
Expand All @@ -52,13 +54,14 @@ export interface CloudflareAdapterConfig {
*/
export function createCloudflareHandler(config: CloudflareAdapterConfig) {
let server: McpDocsServer | null = null;
const { corsOrigin, ...rest } = config;

const serverConfig: McpServerDataConfig = {
docs: config.docs,
searchIndexData: config.searchIndexData,
name: config.name,
version: config.version,
baseUrl: config.baseUrl,
docs: rest.docs,
searchIndexData: rest.searchIndexData,
name: rest.name,
version: rest.version,
baseUrl: rest.baseUrl,
};

function getServer(): McpDocsServer {
Expand All @@ -69,7 +72,7 @@ export function createCloudflareHandler(config: CloudflareAdapterConfig) {
}

return async function fetch(request: Request): Promise<Response> {
const corsHeaders = getCorsHeaders();
const corsHeaders = getCorsHeaders(corsOrigin);

// Handle CORS preflight
if (request.method === 'OPTIONS') {
Expand Down Expand Up @@ -121,15 +124,14 @@ export function createCloudflareHandler(config: CloudflareAdapterConfig) {
headers: newHeaders,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('MCP Server Error:', error);
return new Response(
JSON.stringify({
jsonrpc: '2.0',
id: null,
error: {
code: -32603,
message: `Internal server error: ${errorMessage}`,
message: 'Internal server error',
},
}),
{
Expand Down
8 changes: 6 additions & 2 deletions src/adapters/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const CORS_HEADERS = {
/**
* Get CORS headers as a plain object (for JSON responses)
*/
export function getCorsHeaders(): Record<string, string> {
return { ...CORS_HEADERS };
export function getCorsHeaders(origin: string = '*'): Record<string, string> {
return {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
}
34 changes: 10 additions & 24 deletions src/adapters/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,31 +145,21 @@ function generateCloudflareFiles(name: string, baseUrl: string): GeneratedFile[]
content: `/**
* Cloudflare Worker for MCP server
*
* Note: This requires bundling docs.json and search-index.json with the worker,
* or using Cloudflare KV/R2 for storage.
*
* For bundling, use wrangler with custom build configuration.
* Workers cannot access the filesystem, so docs and search index
* are imported as JSON modules and passed as pre-loaded data.
*/

import { createCloudflareHandler } from 'docusaurus-plugin-mcp-server/adapters';

// Option 1: Import bundled data (requires bundler configuration)
// import docs from '../build/mcp/docs.json';
// import searchIndex from '../build/mcp/search-index.json';

// Option 2: Use KV bindings (requires KV namespace configuration)
// const docs = await env.MCP_KV.get('docs', { type: 'json' });
// const searchIndex = await env.MCP_KV.get('search-index', { type: 'json' });
import docs from '../build/mcp/docs.json';
import searchIndex from '../build/mcp/search-index.json';

export default {
fetch: createCloudflareHandler({
docs,
searchIndexData: searchIndex,
name: '${name}',
version: '1.0.0',
baseUrl: '${baseUrl}',
// docsPath and indexPath are used for file-based loading
// For Workers, you'll need to configure data loading differently
docsPath: './mcp/docs.json',
indexPath: './mcp/search-index.json',
}),
};
`,
Expand All @@ -181,14 +171,10 @@ export default {
main = "workers/mcp.js"
compatibility_date = "2024-01-01"

# Uncomment to use KV for storing docs
# [[kv_namespaces]]
# binding = "MCP_KV"
# id = "your-kv-namespace-id"

# Static assets (the Docusaurus build)
# [site]
# bucket = "./build"
# Allow importing JSON files as modules
[[rules]]
type = "Data"
globs = ["**/*.json"]
`,
},
];
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

export { createVercelHandler, type VercelRequest, type VercelResponse } from './vercel.js';
export { createNetlifyHandler, type NetlifyEvent, type NetlifyContext } from './netlify.js';
export { createCloudflareHandler } from './cloudflare.js';
export { createCloudflareHandler, type CloudflareAdapterConfig } from './cloudflare.js';
export { generateAdapterFiles } from './generator.js';
export { createNodeServer, createNodeHandler } from './node.js';
export type { NodeServerOptions } from './node.js';
12 changes: 6 additions & 6 deletions src/adapters/netlify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/

import { McpDocsServer } from '../mcp/server.js';
import type { McpServerConfig } from '../types/index.js';
import type { McpServerFileConfig } from '../types/index.js';
import { getCorsHeaders } from './cors.js';

/**
Expand Down Expand Up @@ -113,12 +113,13 @@ async function responseToNetlify(
* Uses the MCP SDK's WebStandardStreamableHTTPServerTransport for
* proper protocol handling.
*/
export function createNetlifyHandler(config: McpServerConfig) {
export function createNetlifyHandler(config: McpServerFileConfig & { corsOrigin?: string }) {
let server: McpDocsServer | null = null;
const { corsOrigin, ...serverConfig } = config;

function getServer(): McpDocsServer {
if (!server) {
server = new McpDocsServer(config);
server = new McpDocsServer(serverConfig);
}
return server;
}
Expand All @@ -127,7 +128,7 @@ export function createNetlifyHandler(config: McpServerConfig) {
event: NetlifyEvent,
_context: NetlifyContext
): Promise<NetlifyResponse> {
const corsHeaders = getCorsHeaders();
const corsHeaders = getCorsHeaders(corsOrigin);
const headers = {
'Content-Type': 'application/json',
...corsHeaders,
Expand Down Expand Up @@ -180,7 +181,6 @@ export function createNetlifyHandler(config: McpServerConfig) {
// Convert back to Netlify response format with CORS headers
return await responseToNetlify(response, corsHeaders);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('MCP Server Error:', error);
return {
statusCode: 500,
Expand All @@ -190,7 +190,7 @@ export function createNetlifyHandler(config: McpServerConfig) {
id: null,
error: {
code: -32603,
message: `Internal server error: ${errorMessage}`,
message: 'Internal server error',
},
}),
};
Expand Down
33 changes: 29 additions & 4 deletions src/adapters/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,31 @@ export function createNodeHandler(options: NodeServerOptions) {
const mcpServer = getServer();
await mcpServer.handleHttpRequest(req, res, body);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error('[MCP] Request error:', error);

if (error instanceof Error && error.message === 'Request body too large') {
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
jsonrpc: '2.0',
id: null,
error: {
code: -32600,
message: 'Request body too large',
},
})
);
return;
}

res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
jsonrpc: '2.0',
id: null,
error: {
code: -32603,
message: `Internal server error: ${message}`,
message: 'Internal server error',
},
})
);
Expand All @@ -133,14 +148,24 @@ export function createNodeServer(options: NodeServerOptions): Server {
return createServer(handler);
}

const MAX_BODY_SIZE = 1024 * 1024; // 1MB

/**
* Parse the request body as JSON
*/
async function parseRequestBody(req: IncomingMessage): Promise<unknown> {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk) => {
body += chunk;
let size = 0;

req.on('data', (chunk: Buffer | string) => {
size += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
if (size > MAX_BODY_SIZE) {
req.destroy();
reject(new Error('Request body too large'));
return;
}
body += chunk.toString();
});
req.on('end', () => {
try {
Expand Down
12 changes: 6 additions & 6 deletions src/adapters/vercel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import type { IncomingMessage, ServerResponse } from 'node:http';
import { McpDocsServer } from '../mcp/server.js';
import type { McpServerConfig } from '../types/index.js';
import type { McpServerFileConfig } from '../types/index.js';
import { getCorsHeaders } from './cors.js';

/**
Expand All @@ -44,18 +44,19 @@ export interface VercelResponse extends ServerResponse {
*
* Uses the MCP SDK's StreamableHTTPServerTransport for proper protocol handling.
*/
export function createVercelHandler(config: McpServerConfig) {
export function createVercelHandler(config: McpServerFileConfig & { corsOrigin?: string }) {
let server: McpDocsServer | null = null;
const { corsOrigin, ...serverConfig } = config;

function getServer(): McpDocsServer {
if (!server) {
server = new McpDocsServer(config);
server = new McpDocsServer(serverConfig);
}
return server;
}

return async function handler(req: VercelRequest, res: VercelResponse) {
const corsHeaders = getCorsHeaders();
const corsHeaders = getCorsHeaders(corsOrigin);

// Handle CORS preflight
if (req.method === 'OPTIONS') {
Expand Down Expand Up @@ -99,14 +100,13 @@ export function createVercelHandler(config: McpServerConfig) {
// Use the SDK transport to handle the request
await mcpServer.handleHttpRequest(req, res, req.body);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('MCP Server Error:', error);
return res.status(500).json({
jsonrpc: '2.0',
id: null,
error: {
code: -32603,
message: `Internal server error: ${errorMessage}`,
message: 'Internal server error',
},
});
}
Expand Down
5 changes: 2 additions & 3 deletions src/providers/loader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import type { ContentIndexer, SearchProvider } from './types.js';
import { FlexSearchIndexer } from './indexers/flexsearch-indexer.js';
import { FlexSearchProvider } from './search/flexsearch-provider.js';

/**
* Load an indexer by name or module path.
Expand All @@ -22,8 +20,8 @@ import { FlexSearchProvider } from './search/flexsearch-provider.js';
* ```
*/
export async function loadIndexer(specifier: string): Promise<ContentIndexer> {
// Built-in FlexSearch indexer
if (specifier === 'flexsearch') {
const { FlexSearchIndexer } = await import('./indexers/flexsearch-indexer.js');
return new FlexSearchIndexer();
}

Expand Down Expand Up @@ -80,6 +78,7 @@ export async function loadIndexer(specifier: string): Promise<ContentIndexer> {
*/
export async function loadSearchProvider(specifier: string): Promise<SearchProvider> {
if (specifier === 'flexsearch') {
const { FlexSearchProvider } = await import('./search/flexsearch-provider.js');
return new FlexSearchProvider();
}

Expand Down
Loading