#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { FinixContext } from './types.js';
import { FinixClient } from './utils/finixClient.js';
import { allTools } from './tools/index.js';
class FinixMCPServer {
private server: Server;
private context: FinixContext;
private client: FinixClient;
private tools: Map<string, any>;
constructor() {
this.context = {
username: process.env.FINIX_USERNAME,
password: process.env.FINIX_PASSWORD,
environment: process.env.FINIX_ENVIRONMENT || 'Sandbox',
application: process.env.FINIX_APPLICATION_ID,
};
this.client = new FinixClient(this.context);
this.tools = new Map();
// Initialize tools
for (const toolFactory of allTools) {
const tool = toolFactory(this.context);
this.tools.set(tool.method, tool);
}
this.server = new Server(
{
name: 'finix-mcp-server',
version: '2.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: this.getToolSchemas(),
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
try {
// Validate parameters using Zod schema
const validatedArgs = tool.parameters.parse(args);
// Execute the tool
const result = await tool.execute(this.client, this.context, validatedArgs);
// Handle different result types
if (typeof result === 'string') {
return {
content: [
{
type: 'text',
text: result,
},
],
};
} else {
// Return structured data as formatted JSON
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error executing ${name}: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
});
}
private getToolSchemas(): Tool[] {
return Array.from(this.tools.values()).map(tool => ({
name: tool.method,
description: tool.description,
inputSchema: zodToJsonSchema(tool.parameters),
}));
}
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
const hasCredentials = this.client.hasCredentials();
const toolCount = this.tools.size;
const apiToolCount = Array.from(this.tools.values()).filter(t =>
t.actions && Object.keys(t.actions).some(key => key !== 'documentation')
).length;
console.error(`Finix MCP Server v2.0 running...`);
console.error(`Environment: ${this.context.environment}`);
console.error(`Total tools available: ${toolCount}`);
console.error(`Documentation search: ✓ Available`);
console.error(`API operations: ${hasCredentials ? `✓ Available (${apiToolCount} tools)` : '✗ Requires FINIX_USERNAME & FINIX_PASSWORD'}`);
if (!hasCredentials) {
console.error(`Set FINIX_USERNAME and FINIX_PASSWORD environment variables to enable API operations`);
}
}
}
// Simple Zod to JSON Schema converter for basic schemas
function zodToJsonSchema(schema: any): any {
if (!schema || !schema._def) {
return { type: 'object' };
}
if (schema._def.typeName === 'ZodObject') {
const properties: any = {};
const required: string[] = [];
const shape = schema._def.shape();
for (const [key, value] of Object.entries(shape)) {
const fieldDef = (value as any)._def;
properties[key] = zodFieldToJsonSchema(value as any);
if (fieldDef.typeName !== 'ZodOptional' && fieldDef.typeName !== 'ZodDefault') {
required.push(key);
}
}
return {
type: 'object',
properties,
required: required.length > 0 ? required : undefined,
};
}
return { type: 'object' };
}
function zodFieldToJsonSchema(field: any): any {
if (!field || !field._def) {
return { type: 'string' };
}
const def = field._def;
switch (def.typeName) {
case 'ZodString':
const stringSchema: any = { type: 'string' };
if (def.description) stringSchema.description = def.description;
return stringSchema;
case 'ZodNumber':
const numberSchema: any = { type: 'number' };
if (def.description) numberSchema.description = def.description;
if (def.checks) {
for (const check of def.checks) {
if (check.kind === 'min') numberSchema.minimum = check.value;
if (check.kind === 'max') numberSchema.maximum = check.value;
if (check.kind === 'int') numberSchema.type = 'integer';
}
}
return numberSchema;
case 'ZodBoolean':
const booleanSchema: any = { type: 'boolean' };
if (def.description) booleanSchema.description = def.description;
return booleanSchema;
case 'ZodEnum':
const enumSchema: any = { type: 'string', enum: def.values };
if (def.description) enumSchema.description = def.description;
return enumSchema;
case 'ZodArray':
const arraySchema: any = { type: 'array', items: zodFieldToJsonSchema(def.type) };
if (def.description) arraySchema.description = def.description;
return arraySchema;
case 'ZodOptional':
return zodFieldToJsonSchema(def.innerType);
case 'ZodDefault':
const defaultSchema = zodFieldToJsonSchema(def.innerType);
defaultSchema.default = def.defaultValue();
return defaultSchema;
case 'ZodObject':
return zodToJsonSchema(field);
default:
const unknownSchema: any = { type: 'string' };
if (def.description) unknownSchema.description = def.description;
return unknownSchema;
}
}
async function main() {
try {
const server = new FinixMCPServer();
await server.start();
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
main().catch((error) => {
console.error('Unhandled error:', error);
process.exit(1);
});