#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import axios, { Method } from 'axios';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import { AuthManager } from './auth/index.js';
import { loadConfig, validateConfig, ServerConfig } from './config.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Load environment variables from .env file in the project root
dotenv.config({ path: path.resolve(__dirname, '../.env') });
class ApiMcpServer {
private server: Server;
private authManager: AuthManager;
private config: ServerConfig;
private openApiSpec: any;
constructor(config: ServerConfig, openApiSpec: any) {
this.config = config;
this.openApiSpec = openApiSpec;
this.authManager = new AuthManager();
this.server = new Server(
{
name: config.serverName || 'any-api-mcp',
version: config.serverVersion || '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
this.authManager.cleanup();
await this.server.close();
process.exit(0);
});
}
private async authenticate(): Promise<void> {
try {
await this.authManager.initialize(this.config.auth, this.config.apiUrl);
if (this.authManager.hasAuth()) {
console.error('[Server] Authentication configured successfully.');
} else {
console.error('[Server] Running without authentication.');
}
} catch (error: any) {
console.error('[Server] Authentication failed:', error.message);
// No salimos - el servidor puede funcionar sin auth en algunos casos
}
}
private getOperationId(method: string, path: string, operation: any): string {
if (operation.operationId) {
return operation.operationId;
}
// Fallback: generate from method and path
const cleanPath = path.replace(/[\/{}]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
return `${method.toLowerCase()}_${cleanPath}`;
}
private convertOpenApiSchemaToMcpSchema(parameter: any): any {
// Basic conversion, can be expanded
const schema = parameter.schema || {};
return {
type: schema.type || 'string',
description: parameter.description,
...(schema.enum ? { enum: schema.enum } : {}),
...(schema.default ? { default: schema.default } : {}),
};
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = [];
for (const [pathKey, pathItem] of Object.entries(this.openApiSpec.paths)) {
for (const [method, operation] of Object.entries(pathItem as any)) {
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) {
const op = operation as any;
const toolName = this.getOperationId(method, pathKey, op);
// Skip auth-related operations if they match common patterns
const skipPatterns = ['getToken', 'auth_token', 'login', 'authenticate'];
if (skipPatterns.some(pattern => toolName.toLowerCase().includes(pattern.toLowerCase()))) {
continue;
}
const properties: any = {};
const required: string[] = [];
// Handle parameters (query, path, header, cookie)
if (op.parameters) {
for (const param of op.parameters) {
properties[param.name] = this.convertOpenApiSchemaToMcpSchema(param);
if (param.required) {
required.push(param.name);
}
}
}
// Handle request body
if (op.requestBody && op.requestBody.content && op.requestBody.content['application/json']) {
// For simplicity in this generic implementation, we'll add a 'body' parameter
properties['body'] = {
type: 'object',
description: 'Request body content',
};
if (op.requestBody.required) {
required.push('body');
}
}
tools.push({
name: toolName,
description: op.summary || op.description || `${method.toUpperCase()} ${pathKey}`,
inputSchema: {
type: 'object',
properties,
required,
},
});
}
}
}
return { tools };
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const args = request.params.arguments || {};
// Find the operation matching the tool name
let targetPath = '';
let targetMethod = '';
let targetOperation: any = null;
for (const [pathKey, pathItem] of Object.entries(this.openApiSpec.paths)) {
for (const [method, operation] of Object.entries(pathItem as any)) {
if (this.getOperationId(method, pathKey, operation) === toolName) {
targetPath = pathKey;
targetMethod = method;
targetOperation = operation;
break;
}
}
if (targetOperation) break;
}
if (!targetOperation) {
throw new McpError(ErrorCode.MethodNotFound, `Tool ${toolName} not found`);
}
try {
// Construct URL
let url = targetPath;
const queryParams: any = {};
const headers: any = {
'Content-Type': 'application/json',
};
// Get auth headers and query params
const authResult = await this.authManager.getAuthResult();
if (authResult.headers) {
Object.assign(headers, authResult.headers);
}
if (authResult.queryParams) {
Object.assign(queryParams, authResult.queryParams);
}
// Handle cookies if present
if (authResult.cookies && Object.keys(authResult.cookies).length > 0) {
const cookieString = Object.entries(authResult.cookies)
.map(([key, value]) => `${key}=${value}`)
.join('; ');
headers['Cookie'] = cookieString;
}
// Separate args into path params, query params, and body
if (targetOperation.parameters) {
for (const param of targetOperation.parameters) {
if (args[param.name] !== undefined) {
if (param.in === 'path') {
url = url.replace(`{${param.name}}`, String(args[param.name]));
} else if (param.in === 'query') {
queryParams[param.name] = args[param.name];
} else if (param.in === 'header') {
headers[param.name] = args[param.name];
}
}
}
}
const body = args['body'];
const response = await axios({
method: targetMethod as Method,
url: `${this.config.apiUrl}${url}`,
params: queryParams,
data: body,
headers,
validateStatus: () => true, // Don't throw on error status
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
status: response.status,
data: response.data,
}, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error executing request: ${error.message}`,
},
],
isError: true,
};
}
});
}
async run() {
await this.authenticate();
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Any API MCP Server running on stdio');
}
}
// Main entry point
async function main() {
// Load configuration
const config = loadConfig();
// Validate configuration
const errors = validateConfig(config);
if (errors.length > 0) {
console.error('[Config] Configuration errors:');
errors.forEach(e => console.error(` - ${e}`));
process.exit(1);
}
// Load OpenAPI spec
let openApiSpec: any;
try {
const fileContent = fs.readFileSync(config.openApiSpecPath, 'utf-8');
openApiSpec = JSON.parse(fileContent);
console.error(`[Server] Loaded OpenAPI spec from ${config.openApiSpecPath}`);
} catch (error) {
console.error('[Server] Error loading OpenAPI spec:', error);
process.exit(1);
}
// Start server
const server = new ApiMcpServer(config, openApiSpec);
await server.run();
}
main().catch(console.error);