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);
});
});