Skip to main content
Glama

Quran MCP Server

by Prince77-7
http-server.ts•12.7 kB
/** * Quran MCP Server for Cloudflare Workers * Implements MCP Protocol 2025-03-26 with Streamable HTTP Transport * * Streamable HTTP Transport: * - Single endpoint /mcp for both GET and POST * - POST: Send JSON-RPC messages, receive JSON or SSE stream * - GET: Open SSE stream for server-initiated messages * - Session management with Mcp-Session-Id header (optional) * * Backward compatible with legacy REST endpoints */ import { executeTool } from './shared/tool-executor.js'; import { tools } from './shared/tools-definition.js'; // CORS headers for cross-origin requests const CORS_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-ID', 'Access-Control-Max-Age': '86400', }; // JSON headers const JSON_HEADERS = { 'Content-Type': 'application/json', ...CORS_HEADERS, }; // SSE headers (for future use if we implement SSE streaming) // const SSE_HEADERS = { // 'Content-Type': 'text/event-stream', // 'Cache-Control': 'no-cache', // 'Connection': 'keep-alive', // ...CORS_HEADERS, // }; // JSON-RPC 2.0 types interface JsonRpcRequest { jsonrpc: '2.0'; id?: string | number | null; method: string; params?: any; } interface JsonRpcResponse { jsonrpc: '2.0'; id: string | number | null; result?: any; error?: { code: number; message: string; data?: any; }; } // MCP Protocol constants - Updated to 2025-03-26 Streamable HTTP const MCP_VERSION = '2025-03-26'; const SERVER_INFO = { name: 'quran-mcp-server', version: '2.0.0', protocolVersion: MCP_VERSION, }; /** * Create JSON-RPC 2.0 success response */ function createJsonRpcResponse(id: string | number | null, result: any): JsonRpcResponse { return { jsonrpc: '2.0', id, result, }; } /** * Create JSON-RPC 2.0 error response */ function createJsonRpcError( id: string | number | null, code: number, message: string, data?: any ): JsonRpcResponse { return { jsonrpc: '2.0', id, error: { code, message, data, }, }; } /** * Handle MCP initialize request * Per 2025-03-26 spec: Returns server info and capabilities */ function handleMcpInitialize(id: string | number | null, _params: any): JsonRpcResponse { return createJsonRpcResponse(id, { protocolVersion: MCP_VERSION, serverInfo: SERVER_INFO, capabilities: { tools: {}, logging: {}, }, }); } /** * Handle MCP tools/list request */ function handleMcpToolsList(id: string | number | null): JsonRpcResponse { return createJsonRpcResponse(id, { tools: tools, }); } /** * Handle MCP tools/call request */ async function handleMcpToolsCall(id: string | number | null, params: any): Promise<JsonRpcResponse> { try { const { name, arguments: args } = params; if (!name) { return createJsonRpcError(id, -32602, 'Invalid params: missing tool name'); } const result = await executeTool(name, args || {}); if (result.success) { return createJsonRpcResponse(id, { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2), }, ], }); } else { return createJsonRpcError( id, -32000, result.error?.message || 'Tool execution failed', result.error ); } } catch (error) { return createJsonRpcError( id, -32603, 'Internal error', { message: (error as Error).message } ); } } /** * Handle MCP JSON-RPC request */ async function handleMcpJsonRpc(request: JsonRpcRequest): Promise<JsonRpcResponse> { const { method, params } = request; // Get the id, preserving undefined vs null vs actual values // According to JSON-RPC 2.0: if id is missing, it's a notification (no response needed) // But MCP requires responses, so we treat missing id as id: 1 const id = request.id !== undefined ? request.id : 1; // Validate JSON-RPC version if (request.jsonrpc !== '2.0') { return createJsonRpcError(id, -32600, 'Invalid Request: jsonrpc must be "2.0"'); } // Route to appropriate handler switch (method) { case 'initialize': return handleMcpInitialize(id, params); case 'tools/list': return handleMcpToolsList(id); case 'tools/call': return await handleMcpToolsCall(id, params); case 'ping': return createJsonRpcResponse(id, {}); default: return createJsonRpcError(id, -32601, `Method not found: ${method}`); } } /** * Handle POST /mcp - MCP JSON-RPC endpoint (Streamable HTTP 2025-03-26) * * Per spec: * - Client MUST include Accept header with application/json and text/event-stream * - For requests: Server returns either JSON or SSE stream * - For notifications/responses: Server returns 202 Accepted */ async function handleMcpPost(request: Request): Promise<Response> { try { const body = await request.json() as JsonRpcRequest | JsonRpcRequest[]; // Handle batch requests (array of requests) if (Array.isArray(body)) { // Check if all are notifications/responses (no requests) const hasRequests = body.some(msg => msg.method && msg.id !== undefined ); if (!hasRequests) { // All notifications/responses - return 202 Accepted return new Response(null, { status: 202, headers: CORS_HEADERS, }); } // Has requests - process and return JSON (we don't support SSE for now) const responses = await Promise.all( body.map(msg => handleMcpJsonRpc(msg)) ); return new Response(JSON.stringify(responses), { status: 200, headers: JSON_HEADERS, }); } // Single message const message = body as JsonRpcRequest; // Check if it's a notification or response (no id or no method) if (!message.method || (message.method && message.id === undefined)) { // Notification or response - return 202 Accepted return new Response(null, { status: 202, headers: CORS_HEADERS, }); } // It's a request - process and return JSON const response = await handleMcpJsonRpc(message); return new Response(JSON.stringify(response), { status: 200, headers: JSON_HEADERS, }); } catch (error) { const errorResponse = createJsonRpcError( null, -32700, 'Parse error', { message: (error as Error).message } ); return new Response(JSON.stringify(errorResponse), { status: 400, headers: JSON_HEADERS, }); } } /** * Handle GET /mcp - Per 2025-03-26 spec: Return 405 Method Not Allowed * * The 2025-03-26 spec allows GET to open an SSE stream for server-initiated messages, * but this is optional. For simplicity, we return 405 to indicate we don't support * server-initiated messages via GET. * * Clients should use POST for all requests. */ function handleMcpGet(request: Request): Response { // Check if client is requesting SSE stream const acceptHeader = request.headers.get('Accept') || ''; if (acceptHeader.includes('text/event-stream')) { // Client wants SSE stream, but we don't support server-initiated messages return new Response('Method Not Allowed: Server does not support SSE streams for server-initiated messages', { status: 405, headers: JSON_HEADERS, }); } // Return server info for non-SSE requests const info = { name: 'Quran MCP Server', version: '2.0.0', description: 'MCP-compliant JSON-RPC 2.0 server for Islamic resources', protocol: 'Model Context Protocol (MCP)', protocolVersion: MCP_VERSION, transport: 'streamable-http', authentication: 'none', public: true, endpoint: '/mcp', methods: ['POST'], usage: { initialize: 'POST /mcp with {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}', tools_list: 'POST /mcp with {"jsonrpc":"2.0","id":2,"method":"tools/list"}', tools_call: 'POST /mcp with {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{...}}', }, documentation: 'https://github.com/Prince77-7/quranMCP', tools_count: tools.length, }; return new Response(JSON.stringify(info, null, 2), { status: 200, headers: JSON_HEADERS, }); } /** * Handle OPTIONS preflight requests */ function handleOptions(): Response { return new Response(null, { status: 204, headers: CORS_HEADERS, }); } /** * Handle GET / - Server info and documentation */ function handleRoot(): Response { const info = { name: 'Quran MCP Server', version: '2.0.0', description: 'MCP-compliant JSON-RPC 2.0 server for Islamic resources - Quran, Hadith, Tafsir, and Recitations', protocol: 'Model Context Protocol (MCP)', protocolVersion: MCP_VERSION, transport: 'http', authentication: 'none', public: true, endpoints: { '/': 'Server information (this page)', '/health': 'Health check endpoint', '/mcp': 'POST - MCP JSON-RPC 2.0 endpoint (initialize, tools/list, tools/call)', '/tools': 'GET - List all available tools (legacy REST)', '/api/call': 'POST - Call a tool with JSON body (legacy REST)', }, documentation: 'https://github.com/Prince77-7/quranMCP', features: [ 'MCP-compliant JSON-RPC 2.0 protocol', 'Public access - no authentication required', 'Search Quran by keywords/phrases', 'Search Hadith collections', 'Get Tafsir (commentary)', 'Audio recitations', 'Multiple translations', 'Topic-based search', ], tools_count: tools.length, mcp_compatible: true, }; return new Response(JSON.stringify(info, null, 2), { status: 200, headers: JSON_HEADERS, }); } /** * Handle GET /health - Health check */ function handleHealth(): Response { return new Response(JSON.stringify({ status: 'healthy', timestamp: new Date().toISOString() }), { status: 200, headers: JSON_HEADERS, }); } /** * Handle GET /tools - List all tools */ function handleListTools(): Response { return new Response(JSON.stringify({ tools }, null, 2), { status: 200, headers: JSON_HEADERS, }); } /** * Handle POST /api/call - Call a tool */ async function handleCallTool(request: Request): Promise<Response> { try { const body = await request.json() as { tool: string; arguments: any }; const { tool, arguments: args } = body; if (!tool) { return new Response( JSON.stringify({ error: 'Missing "tool" parameter', code: 'INVALID_REQUEST' }), { status: 400, headers: JSON_HEADERS } ); } const result = await executeTool(tool, args || {}); if (result.success) { return new Response( JSON.stringify({ success: true, data: result.data }), { status: 200, headers: JSON_HEADERS } ); } else { return new Response( JSON.stringify({ success: false, error: result.error }), { status: 400, headers: JSON_HEADERS } ); } } catch (error) { return new Response( JSON.stringify({ success: false, error: { message: (error as Error).message, code: 'INTERNAL_ERROR', }, }), { status: 500, headers: JSON_HEADERS } ); } } /** * Main request handler for Cloudflare Workers */ export default { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const path = url.pathname; const method = request.method; // Handle OPTIONS preflight if (method === 'OPTIONS') { return handleOptions(); } // Route requests if (method === 'GET') { if (path === '/' || path === '') { return handleRoot(); } else if (path === '/health') { return handleHealth(); } else if (path === '/tools') { return handleListTools(); } else if (path === '/mcp') { // GET /mcp returns server info or 405 for SSE requests return handleMcpGet(request); } } else if (method === 'POST') { if (path === '/mcp') { // POST /mcp for MCP JSON-RPC 2.0 protocol return handleMcpPost(request); } else if (path === '/api/call') { // Legacy REST endpoint for backward compatibility return handleCallTool(request); } } // 404 Not Found return new Response( JSON.stringify({ error: 'Not Found', path, method, hint: 'Use POST /mcp for MCP protocol or see / for documentation' }), { status: 404, headers: JSON_HEADERS } ); }, };

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/Prince77-7/quranMCP'

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