Skip to main content
Glama
noops888

Cloudflare AutoRAG MCP Server

by noops888
server.ts21.9 kB
import { z } from 'zod'; /** * Cloudflare AutoRAG MCP Server * Provides search and aiSearch tools for Cloudflare AutoRAG instances */ // Note: Filters are not supported in Workers bindings - only available via REST API interface Env { AI: { autorag: (name: string) => { search: (params: AutoRAGSearchParams) => Promise<AutoRAGSearchResponse>; aiSearch: (params: AutoRAGAiSearchParams) => Promise<AutoRAGAiSearchResponse>; }; }; AUTORAG_NAME?: string; // Default AutoRAG instance (optional if using AUTORAG_INSTANCES) // Support for multiple AutoRAG instances AUTORAG_INSTANCES?: string; // Comma-separated list of AutoRAG instance names AUTORAG_DESCRIPTIONS?: string; // Comma-separated list of descriptions } interface AutoRAGSearchParams { query: string; rewrite_query?: boolean; max_num_results?: number; ranking_options?: { score_threshold?: number; }; // filters parameter removed - not supported in Workers bindings } interface AutoRAGAiSearchParams { query: string; rewrite_query?: boolean; max_num_results?: number; ranking_options?: { score_threshold?: number; }; cursor?: string; // Added for pagination support // filters parameter removed - not supported in Workers bindings } interface AutoRAGSearchResponse { object: string; search_query: string; data: Array<{ file_id: string; content: string; score: number; metadata?: Record<string, any>; }>; } interface AutoRAGAiSearchResponse { object: string; search_query: string; response: string; data: Array<{ file_id: string; filename: string; score: number; attributes?: Record<string, any>; content: Array<{ id: string; type: string; text: string; }>; }>; has_more: boolean; next_page: string | null; } interface JsonRpcRequest { jsonrpc: string; id?: string | number | null; method: string; params?: any; } interface JsonRpcResponse { jsonrpc: string; id?: string | number | null; result?: any; error?: { code: number; message: string; data?: any; }; } interface Tool { name: string; description: string; inputSchema: any; } interface McpServerInfo { name: string; version: string; } interface McpCapabilities { tools?: {}; logging?: {}; } class WorkersMcpServer { private serverInfo: McpServerInfo; private capabilities: McpCapabilities; private tools: Map<string, { description: string; inputSchema: any; handler: (params: any) => Promise<{ content: Array<{ type: string; text: string }> }>; }> = new Map(); constructor(serverInfo: McpServerInfo, options: { capabilities: McpCapabilities }) { this.serverInfo = serverInfo; this.capabilities = options.capabilities; } addTool( name: string, description: string, inputSchema: z.ZodSchema, handler: (params: any) => Promise<{ content: Array<{ type: string; text: string }> }> ) { this.tools.set(name, { description, inputSchema: this.zodToJsonSchema(inputSchema), handler }); } private zodToJsonSchema(schema: z.ZodSchema): any { // Handle union types if (schema instanceof z.ZodUnion) { const types = schema._def.options.map((opt: z.ZodSchema) => this.zodToJsonSchema(opt)); return { oneOf: types }; } // Basic Zod to JSON Schema conversion if (schema instanceof z.ZodObject) { const properties: Record<string, any> = {}; const required: string[] = []; const shape = schema.shape; for (const [key, value] of Object.entries(shape)) { if (value instanceof z.ZodString) { properties[key] = { type: 'string', description: value.description }; if (!value.isOptional()) required.push(key); } else if (value instanceof z.ZodNumber) { properties[key] = { type: 'number', description: value.description }; if (!value.isOptional()) required.push(key); } else if (value instanceof z.ZodBoolean) { properties[key] = { type: 'boolean', description: value.description }; if (!value.isOptional()) required.push(key); } else if (value instanceof z.ZodRecord) { properties[key] = { type: 'object', description: value.description }; if (!value.isOptional()) required.push(key); } else if (value instanceof z.ZodObject) { // Handle nested objects like FiltersSchema properties[key] = this.zodToJsonSchema(value); if (!value.isOptional()) required.push(key); } else if (value instanceof z.ZodOptional) { const innerType = value._def.innerType; if (innerType instanceof z.ZodString) { properties[key] = { type: 'string', description: innerType.description }; } else if (innerType instanceof z.ZodNumber) { properties[key] = { type: 'number', description: innerType.description }; } else if (innerType instanceof z.ZodBoolean) { properties[key] = { type: 'boolean', description: innerType.description }; } else if (innerType instanceof z.ZodRecord) { properties[key] = { type: 'object', description: innerType.description }; } else if (innerType instanceof z.ZodObject) { // Handle nested objects inside optional properties[key] = this.zodToJsonSchema(innerType); } } } return { type: 'object', properties, required }; } // Handle primitives if (schema instanceof z.ZodString) { return { type: 'string', description: schema.description }; } if (schema instanceof z.ZodNumber) { return { type: 'number', description: schema.description }; } if (schema instanceof z.ZodBoolean) { return { type: 'boolean', description: schema.description }; } return { type: 'object' }; } async handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> { const { method, params, id } = request; try { switch (method) { case 'initialize': return { jsonrpc: '2.0', id, result: { protocolVersion: '2024-11-05', capabilities: this.capabilities, serverInfo: this.serverInfo } }; case 'tools/list': const tools: Tool[] = Array.from(this.tools.entries()).map(([name, tool]) => ({ name, description: tool.description, inputSchema: tool.inputSchema })); return { jsonrpc: '2.0', id, result: { tools } }; case 'tools/call': const { name, arguments: args } = params; const tool = this.tools.get(name); if (!tool) { return { jsonrpc: '2.0', id, error: { code: -32601, message: `Tool '${name}' not found` } }; } // Check for filters parameter and provide helpful error if (args && args.filters) { return { jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params', data: 'Metadata filtering is not supported in Workers bindings. Please use the REST API for filtered queries. See: https://developers.cloudflare.com/autorag/usage/rest-api/' } }; } const result = await tool.handler(args); return { jsonrpc: '2.0', id, result }; case 'resources/list': return { jsonrpc: '2.0', id, result: { resources: [] } }; case 'prompts/list': return { jsonrpc: '2.0', id, result: { prompts: [] } }; default: return { jsonrpc: '2.0', id, error: { code: -32601, message: method ? `Method '${method}' not supported` : 'Method is required' } }; } } catch (error) { return { jsonrpc: '2.0', id, error: { code: -32603, message: `Internal error: ${error instanceof Error ? error.message : String(error)}` } }; } } } function createServer(env: Env): WorkersMcpServer { const server = new WorkersMcpServer({ name: 'cloudflare-autorag-mcp', version: '2.0.0', }, { capabilities: { tools: {}, logging: {} } }); // Helper function to get available AutoRAG instances const getAutoRAGInstances = () => { const instances: Array<{ name: string; description: string; is_default: boolean }> = []; // Check if we have multiple instances configured if (env.AUTORAG_INSTANCES) { const names = env.AUTORAG_INSTANCES.split(',').map(s => s.trim()).filter(s => s); const descriptions = env.AUTORAG_DESCRIPTIONS?.split(',').map(s => s.trim()) || []; names.forEach((name, index) => { instances.push({ name, description: descriptions[index] || `AutoRAG instance: ${name}`, is_default: index === 0 // First instance is default }); }); } else if (env.AUTORAG_NAME) { // Fallback to single instance configuration instances.push({ name: env.AUTORAG_NAME, description: 'Default AutoRAG instance', is_default: true }); } return instances; }; // Helper function to get default AutoRAG name const getDefaultAutoRAGName = () => { if (env.AUTORAG_NAME) { return env.AUTORAG_NAME; } if (env.AUTORAG_INSTANCES) { const firstInstance = env.AUTORAG_INSTANCES.split(',')[0]?.trim(); if (firstInstance) { return firstInstance; } } throw new Error('No AutoRAG instances configured'); }; // Helper function to validate AutoRAG name const validateAutoRAGName = (autoragName: string): string => { const instances = getAutoRAGInstances(); const validInstance = instances.find(i => i.name === autoragName); if (!validInstance) { throw new Error(`AutoRAG instance '${autoragName}' not found. Available instances: ${instances.map(i => i.name).join(', ')}`); } return autoragName; }; // Basic search tool server.addTool( 'autorag_basic_search', 'Basic search for documents in Cloudflare AutoRAG without query rewriting or answer generation. Query length: max 10,000 chars. Score range: 0.0-1.0 (higher = better match).', z.object({ query: z.string().max(10000).describe('The search query to find relevant documents (max 10,000 chars)'), score_threshold: z.number().min(0).max(1).optional().describe('Minimum similarity score threshold (0.0-1.0, default: 0.5)'), max_num_results: z.number().min(1).max(50).optional().describe('Maximum number of results to return (1-50, default: 10)'), autorag_name: z.string().optional().describe('Name of the AutoRAG instance to use (defaults to configured default)') }), async ({ query, score_threshold, max_num_results, autorag_name }) => { try { const searchParams: AutoRAGSearchParams = { query, rewrite_query: false, // Basic search never rewrites query ranking_options: { score_threshold: score_threshold ?? 0.5 // Default threshold of 0.5 } }; if (max_num_results !== undefined) { searchParams.max_num_results = max_num_results; } // Use specified AutoRAG instance or default const autoragInstance = autorag_name ? validateAutoRAGName(autorag_name) : getDefaultAutoRAGName(); const result = await env.AI.autorag(autoragInstance).search(searchParams); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] }; } catch (error) { return { content: [ { type: 'text', text: `Error searching AutoRAG: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); // Search with query rewriting but NO AI generation server.addTool( 'autorag_rewrite_search', 'Search for documents in Cloudflare AutoRAG with AI query rewriting but NO answer generation (returns document chunks only). Query length: max 10,000 chars. Score range: 0.0-1.0 (higher = better match).', z.object({ query: z.string().max(10000).describe('The search query to find relevant documents with AI query rewriting (max 10,000 chars)'), score_threshold: z.number().min(0).max(1).optional().describe('Minimum similarity score threshold (0.0-1.0, default: 0.5)'), max_num_results: z.number().min(1).max(50).optional().describe('Maximum number of results to return (1-50, default: 10)'), rewrite_query: z.boolean().optional().describe('Whether to rewrite the query using AI (default: true)'), autorag_name: z.string().optional().describe('Name of the AutoRAG instance to use (defaults to configured default)') }), async ({ query, score_threshold, max_num_results, rewrite_query, autorag_name }) => { try { // Use search method with configurable rewrite_query for AI-powered ranking without generation const searchParams: AutoRAGSearchParams = { query, rewrite_query: rewrite_query ?? true, // Default to true for rewrite search ranking_options: { score_threshold: score_threshold ?? 0.5 // Default threshold of 0.5 } }; if (max_num_results !== undefined) { searchParams.max_num_results = max_num_results; } // Use specified AutoRAG instance or default const autoragInstance = autorag_name ? validateAutoRAGName(autorag_name) : getDefaultAutoRAGName(); // Use search method instead of aiSearch to avoid AI generation const result = await env.AI.autorag(autoragInstance).search(searchParams); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] }; } catch (error) { return { content: [ { type: 'text', text: `Error in AutoRAG rewrite search: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); // AI Search tool with configurable AI response server.addTool( 'autorag_ai_search', 'Search documents in Cloudflare AutoRAG with AI query rewriting and optional AI-generated response. Returns document chunks and optionally AI answer. Supports pagination for large result sets. Query length: max 10,000 chars. Score range: 0.0-1.0 (higher = better match).', z.object({ query: z.string().max(10000).describe('The search query to find relevant documents with AI query rewriting (max 10,000 chars)'), score_threshold: z.number().min(0).max(1).optional().describe('Minimum similarity score threshold (0.0-1.0, default: 0.5)'), max_num_results: z.number().min(1).max(50).optional().describe('Maximum number of results to return (1-50, default: 10)'), rewrite_query: z.boolean().optional().describe('Whether to rewrite the query for better semantic matching (default: true)'), include_ai_response: z.boolean().optional().describe('Whether to include the AI-generated response in the output (default: false)'), cursor: z.string().optional().describe('Pagination cursor from previous response to fetch next page of results'), autorag_name: z.string().optional().describe('Name of the AutoRAG instance to use (defaults to configured default)') }), async ({ query, score_threshold, max_num_results, rewrite_query, include_ai_response, cursor, autorag_name }) => { try { const searchParams: AutoRAGAiSearchParams = { query, rewrite_query: rewrite_query ?? true, // Default to true for AI search ranking_options: { score_threshold: score_threshold ?? 0.5 // Default threshold of 0.5 } }; if (max_num_results !== undefined) { searchParams.max_num_results = max_num_results; } if (cursor !== undefined) { searchParams.cursor = cursor; } // Use specified AutoRAG instance or default const autoragInstance = autorag_name ? validateAutoRAGName(autorag_name) : getDefaultAutoRAGName(); // Use aiSearch method to get both AI response and document chunks const result = await env.AI.autorag(autoragInstance).aiSearch(searchParams); // Transform the response to include nextCursor for MCP compliance const responseToReturn = include_ai_response ?? false ? { ...result, nextCursor: result.next_page || undefined } : { object: result.object, search_query: result.search_query, data: result.data, has_more: result.has_more, next_page: result.next_page, nextCursor: result.next_page || undefined // Exclude 'response' field }; return { content: [ { type: 'text', text: JSON.stringify(responseToReturn, null, 2) } ] }; } catch (error) { return { content: [ { type: 'text', text: `Error in AutoRAG AI search: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); // AutoRAG management tools server.addTool( 'list_autorags', 'List all available AutoRAG instances configured in the server', z.object({}), async () => { try { const instances = getAutoRAGInstances(); return { content: [{ type: 'text', text: JSON.stringify({ autorags: instances, total: instances.length, default: instances.find(i => i.is_default)?.name || instances[0]?.name }, null, 2) }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text', text: `Error listing AutoRAG instances: ${errorMessage}` }] }; } } ); server.addTool( 'get_current_autorag', 'Get the currently configured default AutoRAG instance', z.object({}), async () => { try { const instances = getAutoRAGInstances(); const defaultInstance = instances.find(i => i.is_default); return { content: [{ type: 'text', text: JSON.stringify({ current_autorag: defaultInstance?.name || getDefaultAutoRAGName(), description: defaultInstance?.description || 'Default AutoRAG instance', is_default: true }, null, 2) }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text', text: `Error getting current AutoRAG: ${errorMessage}` }] }; } } ); return server; } export default { async fetch(request: Request, env: Env): Promise<Response> { try { // Handle CORS preflight if (request.method === 'OPTIONS') { return new Response(null, { status: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' } }); } // Only handle POST requests for MCP if (request.method !== 'POST') { return new Response(JSON.stringify({ jsonrpc: '2.0', error: { code: -32600, message: 'Invalid Request - only POST method supported' }, id: null }), { status: 405, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' } }); } const body = await request.json() as JsonRpcRequest; const server = createServer(env); const response = await server.handleRequest(body); return new Response(JSON.stringify(response), { status: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } }); } catch (error) { console.error('Error handling MCP request:', error); return new Response(JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: `Internal server error: ${error instanceof Error ? error.message : String(error)}` }, id: null }), { status: 500, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' } }); } } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/noops888/cf-autorag-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server