dataforseo-mcp-server
Version:
A Model Context Protocol (MCP) server for the DataForSEO API, enabling modular and extensible integration of DataForSEO endpoints with support for both HTTP and SSE transports.
174 lines (173 loc) • 6.64 kB
JavaScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { DataForSEOClient } from './client/dataforseo.client.js';
import { EnabledModulesSchema } from './config/modules.config.js';
import { z } from 'zod';
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { randomUUID } from "node:crypto";
import { name, version } from './utils/version.js';
import { ModuleLoaderService } from "./utils/module-loader.js";
import { initializeFieldConfiguration } from './config/field-configuration.js';
// Initialize field configuration if provided
initializeFieldConfiguration();
console.error('Starting DataForSEO MCP Server...');
console.error(`Server name: ${name}, version: ${version}`);
function getServer(username, password) {
const server = new McpServer({
name,
version,
}, { capabilities: { logging: {} } });
// Initialize DataForSEO client
const dataForSEOConfig = {
username: username || "",
password: password || "",
};
const dataForSEOClient = new DataForSEOClient(dataForSEOConfig);
console.error('DataForSEO client initialized');
// Parse enabled modules from environment
const enabledModules = EnabledModulesSchema.parse(process.env.ENABLED_MODULES);
// Initialize modules
const modules = ModuleLoaderService.loadModules(dataForSEOClient, enabledModules);
console.error('Modules initialized');
function registerModuleTools() {
console.error('Registering tools');
console.error(modules.length);
modules.forEach(module => {
const tools = module.getTools();
Object.entries(tools).forEach(([name, tool]) => {
const typedTool = tool;
const schema = z.object(typedTool.params);
server.tool(name, typedTool.description, schema.shape, typedTool.handler);
});
});
}
registerModuleTools();
console.error('Tools registered');
return server;
}
function getSessionId() {
return randomUUID().toString();
}
async function main() {
const app = express();
app.use(express.json());
// Basic Auth Middleware
const basicAuth = (req, res, next) => {
// Check for Authorization header
const authHeader = req.headers.authorization;
console.error(authHeader);
if (!authHeader || !authHeader.startsWith('Basic ')) {
next();
return;
}
// Extract credentials
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
const [username, password] = credentials.split(':');
if (!username || !password) {
console.error('Invalid credentials');
res.status(401).json({
jsonrpc: "2.0",
error: {
code: -32001,
message: "Invalid credentials"
},
id: null
});
return;
}
// Add credentials to request
req.username = username;
req.password = password;
next();
};
// Apply basic auth to MCP endpoint
app.post('/http', basicAuth, async (req, res) => {
// In stateless mode, create a new instance of transport and server for each request
// to ensure complete isolation. A single instance would cause request ID collisions
// when multiple clients connect concurrently.
try {
console.error(Date.now().toLocaleString());
// Check if we have valid credentials
if (!req.username && !req.password) {
// If no request auth, check environment variables
const envUsername = process.env.DATAFORSEO_USERNAME;
const envPassword = process.env.DATAFORSEO_PASSWORD;
if (!envUsername || !envPassword) {
console.error('No DataForSEO credentials provided');
res.status(401).json({
jsonrpc: "2.0",
error: {
code: -32001,
message: "Authentication required. Provide DataForSEO credentials."
},
id: null
});
return;
}
// Use environment variables
req.username = envUsername;
req.password = envPassword;
}
const server = getServer(req.username, req.password);
console.error(Date.now().toLocaleString());
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined
});
await server.connect(transport);
console.error('handle request');
await transport.handleRequest(req, res, req.body);
console.error('end handle request');
req.on('close', () => {
console.error('Request closed');
transport.close();
server.close();
});
}
catch (error) {
console.error('Error handling HTTP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
app.get('/http', async (req, res) => {
console.error('Received GET HTTP request');
res.status(405).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Method not allowed."
},
id: null
});
});
app.delete('/http', async (req, res) => {
console.error('Received DELETE HTTP request');
res.status(405).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Method not allowed."
},
id: null
});
});
// Start the server
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
app.listen(PORT, () => {
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
});
}
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});