Skip to main content
Glama
http-server.tsโ€ข26 kB
#!/usr/bin/env node /** * HTTP-based MCP Server for Smithery deployment * Implements Streamable HTTP transport as required by Smithery */ import http from 'http'; import { URL } from 'url'; const PORT = process.env.PORT || 8081; interface StrapiConfig { STRAPI_API_URL: string; STRAPI_API_KEY: string; STRAPI_API_PREFIX?: string; STRAPI_SERVER_NAME?: string; } // Parse base64-encoded config from URL parameter function parseConfig(configParam?: string): StrapiConfig | null { if (!configParam) return null; try { const decoded = Buffer.from(configParam, 'base64').toString('utf-8'); const config = JSON.parse(decoded); // Validate required fields if (!config.STRAPI_API_URL || !config.STRAPI_API_KEY) { return null; } return { STRAPI_API_URL: config.STRAPI_API_URL, STRAPI_API_KEY: config.STRAPI_API_KEY, STRAPI_API_PREFIX: config.STRAPI_API_PREFIX || '/api', STRAPI_SERVER_NAME: config.STRAPI_SERVER_NAME || 'default' }; } catch (error) { console.error('Failed to parse config:', error); return null; } } // Make REST request to Strapi async function makeRestRequest( config: StrapiConfig, endpoint: string, method: string = 'GET', params?: Record<string, any>, body?: Record<string, any> ): Promise<{ data: any; statusCode: number; meta?: any }> { let url = `${config.STRAPI_API_URL}${config.STRAPI_API_PREFIX}/${endpoint}`; // Parse query parameters if provided if (params) { const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { searchParams.append(key, String(value)); } } const queryString = searchParams.toString(); if (queryString) { url = `${url}?${queryString}`; } } const headers = { 'Authorization': `Bearer ${config.STRAPI_API_KEY}`, 'Content-Type': 'application/json', }; const requestOptions: RequestInit = { method, headers, }; if (body && (method === 'POST' || method === 'PUT')) { requestOptions.body = JSON.stringify(body); } console.log(`Making REST request: ${method} ${url}`); try { const response = await fetch(url, requestOptions); if (!response.ok) { let errorMessage = `Request failed with status: ${response.status}`; try { const errorData = await response.json(); if (errorData && typeof errorData === 'object' && 'error' in errorData) { errorMessage += ` - ${errorData.error?.message || JSON.stringify(errorData.error)}`; } } catch { errorMessage += ` - ${response.statusText}`; } throw new Error(errorMessage); } const responseData = await response.json(); return { data: responseData.data || responseData, statusCode: response.status, meta: responseData.meta || {} }; } catch (error) { console.error(`REST request failed:`, error); throw error; } } // Set CORS headers function setCorsHeaders(res: http.ServerResponse) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, *'); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id, mcp-protocol-version'); } // Get MCP tools list function getMCPTools() { return [ { name: 'strapi_list_servers', description: 'List all configured Strapi servers', inputSchema: { type: 'object', properties: {}, }, }, { name: 'strapi_get_content_types', description: 'Get content type schemas from a Strapi server', inputSchema: { type: 'object', properties: { server: { type: 'string', description: 'Server name to query', }, }, required: ['server'], }, }, { name: 'strapi_rest', description: 'Execute REST API operations on Strapi', inputSchema: { type: 'object', properties: { server: { type: 'string', description: 'Server name' }, endpoint: { type: 'string', description: 'API endpoint' }, method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'] }, data: { type: 'object', description: 'Request body data' }, }, required: ['server', 'endpoint', 'method'], }, }, { name: 'strapi_upload_media', description: 'Upload media files to Strapi', inputSchema: { type: 'object', properties: { server: { type: 'string', description: 'Server name' }, file_data: { type: 'string', description: 'Base64 encoded file data' }, filename: { type: 'string', description: 'File name' }, }, required: ['server', 'file_data', 'filename'], }, }, { name: 'strapi_get_components', description: 'Get component schemas from Strapi', inputSchema: { type: 'object', properties: { server: { type: 'string', description: 'Server name' }, }, required: ['server'], }, }, ]; } // HTTP server const httpServer = http.createServer(async (req, res) => { setCorsHeaders(res); // Handle preflight requests if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } const url = new URL(req.url!, `http://localhost:${PORT}`); if (url.pathname === '/mcp') { // Always use dummy config for MCP requests to ensure scanning works const config: StrapiConfig = { STRAPI_API_URL: 'https://demo.strapi.io', STRAPI_API_KEY: 'demo-key', STRAPI_API_PREFIX: '/api', STRAPI_SERVER_NAME: 'demo' }; // Try to parse real config if provided, but fallback to dummy const configParam = url.searchParams.get('config'); if (configParam) { const parsedConfig = parseConfig(configParam); if (parsedConfig) { // Use real config if valid Object.assign(config, parsedConfig); } } console.log('๐Ÿ” MCP request received:', { method: req.method, url: req.url, hasConfigParam: !!configParam, serverName: config.STRAPI_SERVER_NAME, userAgent: req.headers['user-agent'], timestamp: new Date().toISOString() }); // Handle MCP requests if (req.method === 'GET') { // GET request to /mcp - return server info for scanning console.log('๐Ÿ“ก Responding to GET /mcp (scanner request)'); const serverInfo = { name: 'strapi-mcp-server', version: '2.7.1', description: 'Strapi MCP Server with HTTP transport', protocol: 'mcp', transport: 'http', capabilities: { tools: getMCPTools().length, }, tools: getMCPTools().map(tool => ({ name: tool.name, description: tool.description, })), endpoints: { mcp: '/mcp', health: '/', }, configuration: { required: ['STRAPI_API_URL', 'STRAPI_API_KEY'], optional: ['STRAPI_API_PREFIX', 'STRAPI_SERVER_NAME'], }, }; console.log('๐Ÿ“ค Sending server info:', { toolCount: serverInfo.tools.length }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(serverInfo)); return; } else if (req.method === 'POST') { let body = ''; req.on('data', chunk => body += chunk); req.on('end', async () => { try { const request = JSON.parse(body); console.log('๐Ÿ“จ MCP POST request:', { method: request.method, id: request.id }); // Simple MCP protocol handling let response; if (request.method === 'initialize') { console.log('๐Ÿš€ Handling initialize request'); response = { jsonrpc: '2.0', id: request.id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, resources: {}, prompts: {}, }, serverInfo: { name: 'strapi-mcp-server', version: '2.7.1', }, }, }; } else if (request.method === 'notifications/initialized') { console.log('โœ… Handling notifications/initialized'); // This is a notification, no response needed res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(''); return; } else if (request.method === 'ping') { console.log('๐Ÿ“ Handling ping request'); response = { jsonrpc: '2.0', id: request.id, result: {} }; } else if (request.method === 'tools/list') { console.log('๐Ÿ”ง Handling tools/list request'); const tools = getMCPTools(); console.log('๐Ÿ“‹ Returning tools:', tools.map(t => t.name)); response = { jsonrpc: '2.0', id: request.id, result: { tools: tools, }, }; } else if (request.method === 'resources/list') { console.log('๐Ÿ“š Handling resources/list request'); response = { jsonrpc: '2.0', id: request.id, result: { resources: [] }, }; } else if (request.method === 'prompts/list') { console.log('๐Ÿ’ฌ Handling prompts/list request'); response = { jsonrpc: '2.0', id: request.id, result: { prompts: [] }, }; } else if (request.method === 'tools/call') { // Simple tool call handling const toolName = request.params?.name; let result; switch (toolName) { case 'strapi_list_servers': result = { content: [ { type: 'text', text: JSON.stringify({ servers: [config.STRAPI_SERVER_NAME], message: 'Strapi MCP Server is configured and ready', config: { api_url: config.STRAPI_API_URL, api_prefix: config.STRAPI_API_PREFIX, }, }, null, 2), }, ], }; break; case 'strapi_get_content_types': try { const args = request.params?.arguments || {}; const { server } = args; console.log(`๐Ÿ”„ Getting content types from server: ${server || config.STRAPI_SERVER_NAME}`); // Try different possible endpoints for content types let data; let endpoint = 'content-type-builder/content-types'; try { data = await makeRestRequest(config, endpoint, 'GET'); } catch (error) { // Try alternative endpoint endpoint = 'v1/content-type-builder/content-types'; data = await makeRestRequest(config, endpoint, 'GET'); } const contentTypes = data.data || data || {}; const contentTypesList = Object.values(contentTypes); result = { content: [ { type: 'text', text: JSON.stringify({ contentTypes, contentTypesList, totalCount: contentTypesList.length, serverName: server || config.STRAPI_SERVER_NAME }, null, 2), }, ], }; } catch (error) { console.error('โŒ Get content types failed:', error); result = { content: [ { type: 'text', text: JSON.stringify({ contentTypes: {}, contentTypesList: [], totalCount: 0, serverName: request.params?.arguments?.server || config.STRAPI_SERVER_NAME, error: error instanceof Error ? error.message : 'Unknown error' }, null, 2), }, ], }; } break; case 'strapi_rest': try { const args = request.params?.arguments || {}; const { server, endpoint, method = 'GET', params, body, data } = args; if (!endpoint) { throw new Error('Endpoint is required'); } // Clean endpoint (remove leading slash if present) const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint; // Handle data parameter - can be string (JSON) or object let requestBody = body || data; if (typeof requestBody === 'string' && requestBody.trim().startsWith('{')) { try { requestBody = JSON.parse(requestBody); } catch (parseError) { console.warn('Failed to parse data as JSON, using as string:', parseError); } } console.log(`๐Ÿ”„ Executing REST request: ${method} ${cleanEndpoint}`); console.log(`๐Ÿ“ฆ Request body:`, requestBody); const apiResponse = await makeRestRequest(config, cleanEndpoint, method, params, requestBody); // Structure response according to the output schema const responseData = { data: apiResponse.data, meta: apiResponse.meta, method, endpoint: cleanEndpoint, statusCode: apiResponse.statusCode }; result = { content: [ { type: 'text', text: JSON.stringify(responseData, null, 2), }, ], }; } catch (error) { console.error('โŒ REST request failed:', error); // Structure error response according to the output schema const errorResponse = { data: { error: error instanceof Error ? error.message : 'Unknown error' }, meta: {}, method: request.params?.arguments?.method || 'GET', endpoint: request.params?.arguments?.endpoint || 'unknown', statusCode: 500 // Error status }; result = { content: [ { type: 'text', text: JSON.stringify(errorResponse, null, 2), }, ], }; } break; case 'strapi_get_components': try { const args = request.params?.arguments || {}; const { server } = args; console.log(`๐Ÿ”„ Getting components from server: ${server || config.STRAPI_SERVER_NAME}`); // Try different possible endpoints for components let data; let endpoint = 'content-type-builder/components'; try { data = await makeRestRequest(config, endpoint, 'GET'); } catch (error) { // Try alternative endpoint endpoint = 'v1/content-type-builder/components'; data = await makeRestRequest(config, endpoint, 'GET'); } const components = data.data || data || []; const categories = [...new Set(components.map((comp: any) => comp.category).filter(Boolean))]; result = { content: [ { type: 'text', text: JSON.stringify({ components, categories, totalCount: Array.isArray(components) ? components.length : 0, serverName: server || config.STRAPI_SERVER_NAME }, null, 2), }, ], }; } catch (error) { console.error('โŒ Get components failed:', error); result = { content: [ { type: 'text', text: JSON.stringify({ components: [], categories: [], totalCount: 0, serverName: request.params?.arguments?.server || config.STRAPI_SERVER_NAME, error: error instanceof Error ? error.message : 'Unknown error' }, null, 2), }, ], }; } break; case 'strapi_upload_media': try { const args = request.params?.arguments || {}; const { server, file_data, filename } = args; if (!file_data || !filename) { throw new Error('file_data and filename are required'); } console.log(`๐Ÿ”„ Uploading media file: ${filename} to server: ${server || config.STRAPI_SERVER_NAME}`); let buffer: Buffer; // Handle different input formats if (file_data.startsWith('http://') || file_data.startsWith('https://')) { // If it's a URL, fetch the file first console.log(`๐Ÿ“ฅ Downloading file from URL: ${file_data}`); const fileResponse = await fetch(file_data); if (!fileResponse.ok) { throw new Error(`Failed to download file from URL: ${fileResponse.status}`); } const arrayBuffer = await fileResponse.arrayBuffer(); buffer = Buffer.from(arrayBuffer); } else { // Assume it's base64 encoded data try { buffer = Buffer.from(file_data, 'base64'); } catch (error) { throw new Error('Invalid file_data format. Expected base64 encoded data or URL'); } } // Create form data const formData = new FormData(); const blob = new Blob([buffer]); formData.append('files', blob, filename); // Try different upload endpoints let uploadData; let uploadUrl = `${config.STRAPI_API_URL}${config.STRAPI_API_PREFIX}/upload`; try { console.log(`๐Ÿ“ค Trying upload to: ${uploadUrl}`); const uploadResponse = await fetch(uploadUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${config.STRAPI_API_KEY}`, }, body: formData, }); if (!uploadResponse.ok) { // Try alternative endpoint with v1 prefix uploadUrl = `${config.STRAPI_API_URL}${config.STRAPI_API_PREFIX}/v1/upload`; console.log(`๐Ÿ“ค Trying alternative upload to: ${uploadUrl}`); const altUploadResponse = await fetch(uploadUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${config.STRAPI_API_KEY}`, }, body: formData, }); if (!altUploadResponse.ok) { throw new Error(`Upload failed with status: ${altUploadResponse.status} - ${altUploadResponse.statusText}`); } uploadData = await altUploadResponse.json(); } else { uploadData = await uploadResponse.json(); } } catch (fetchError) { throw new Error(`Upload failed: ${fetchError instanceof Error ? fetchError.message : 'Unknown error'}`); } const uploadedFiles = Array.isArray(uploadData) ? uploadData : [uploadData]; result = { content: [ { type: 'text', text: JSON.stringify({ uploadedFiles, totalUploaded: uploadedFiles.length, errors: [] }, null, 2), }, ], }; } catch (error) { console.error('โŒ Upload media failed:', error); result = { content: [ { type: 'text', text: JSON.stringify({ uploadedFiles: [], totalUploaded: 0, errors: [{ filename: request.params?.arguments?.filename || 'unknown', error: error instanceof Error ? error.message : 'Unknown error' }] }, null, 2), }, ], }; } break; default: result = { content: [ { type: 'text', text: JSON.stringify({ error: `Tool ${toolName} not fully implemented in HTTP mode`, available_tools: getMCPTools().map(t => t.name), message: 'This is a basic HTTP implementation for Smithery scanning', }, null, 2), }, ], }; } response = { jsonrpc: '2.0', id: request.id, result, }; } else { console.log('โŒ Unknown method:', request.method); response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: 'Method not found', }, }; } console.log('๐Ÿ“ค Sending response:', { method: request.method, success: !response.error }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(response)); } catch (error) { console.error('โŒ Error handling MCP request:', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : 'Unknown error', }, })); } }); } else { res.writeHead(405, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Method not allowed' })); } } else if (url.pathname === '/' || url.pathname === '/health') { // Health check endpoint res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'healthy', message: 'Strapi MCP Server HTTP endpoint', name: 'strapi-mcp-server', version: '2.7.1', protocol: 'mcp', transport: 'http', endpoints: { mcp: '/mcp', health: '/', }, tools: getMCPTools().map(tool => ({ name: tool.name, description: tool.description, })), configuration: { required: ['STRAPI_API_URL', 'STRAPI_API_KEY'], optional: ['STRAPI_API_PREFIX', 'STRAPI_SERVER_NAME'], }, })); } else { // 404 for unknown paths res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Not found', message: 'Available endpoints: /, /health, /mcp', })); } }); httpServer.listen(PORT, () => { console.log(`๐ŸŒ Strapi MCP Server listening on port ${PORT}`); console.log(`๐Ÿ“ก MCP endpoint: http://localhost:${PORT}/mcp`); console.log(`๐Ÿ” Health check: http://localhost:${PORT}/`); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('๐Ÿ“ด Shutting down HTTP server...'); httpServer.close(() => { process.exit(0); }); });

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/AdityaRapid/Strapi_MCP'

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