186 lines
5.3 KiB
JavaScript
186 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
import { z } from 'zod';
|
|
import { createStripeServer } from './server.js';
|
|
|
|
/**
|
|
* Environment variable schema
|
|
*/
|
|
const EnvSchema = z.object({
|
|
STRIPE_SECRET_KEY: z.string().min(1, 'STRIPE_SECRET_KEY is required'),
|
|
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
|
PORT: z.string().optional().default('3000'),
|
|
NODE_ENV: z.enum(['development', 'production', 'test']).optional().default('production'),
|
|
});
|
|
|
|
type Env = z.infer<typeof EnvSchema>;
|
|
|
|
/**
|
|
* Validate environment variables
|
|
*/
|
|
function validateEnv(): Env {
|
|
try {
|
|
return EnvSchema.parse(process.env);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
console.error('❌ Environment validation failed:');
|
|
for (const issue of error.issues) {
|
|
console.error(` - ${issue.path.join('.')}: ${issue.message}`);
|
|
}
|
|
console.error('\nRequired environment variables:');
|
|
console.error(' STRIPE_SECRET_KEY - Your Stripe secret key (starts with sk_test_ or sk_live_)');
|
|
console.error('\nOptional environment variables:');
|
|
console.error(' STRIPE_WEBHOOK_SECRET - Webhook signing secret (starts with whsec_)');
|
|
console.error(' PORT - HTTP server port for SSE transport (default: 3000)');
|
|
console.error('\nExample:');
|
|
console.error(' export STRIPE_SECRET_KEY=sk_test_xxxxx');
|
|
console.error(' npm start');
|
|
process.exit(1);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Health check endpoint data
|
|
*/
|
|
function getHealthInfo() {
|
|
return {
|
|
status: 'healthy',
|
|
service: '@mcpengine/stripe',
|
|
version: '1.0.0',
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime(),
|
|
memory: process.memoryUsage(),
|
|
node: process.version,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Setup graceful shutdown
|
|
*/
|
|
function setupGracefulShutdown(cleanup: () => Promise<void>): void {
|
|
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGUSR2'];
|
|
|
|
signals.forEach(signal => {
|
|
process.on(signal, async () => {
|
|
console.error(`\n📡 Received ${signal}, shutting down gracefully...`);
|
|
|
|
try {
|
|
await cleanup();
|
|
console.error('✅ Cleanup complete');
|
|
process.exit(0);
|
|
} catch (error) {
|
|
console.error('❌ Error during cleanup:', error);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Handle uncaught errors
|
|
process.on('uncaughtException', (error) => {
|
|
console.error('❌ Uncaught exception:', error);
|
|
process.exit(1);
|
|
});
|
|
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|
console.error('❌ Unhandled rejection at:', promise, 'reason:', reason);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start HTTP/SSE server for dual transport support
|
|
*/
|
|
async function startHttpServer(server: ReturnType<typeof createStripeServer>, port: number): Promise<void> {
|
|
const { createServer } = await import('http');
|
|
const { SSEServerTransport } = await import('@modelcontextprotocol/sdk/server/sse.js');
|
|
|
|
const httpServer = createServer(async (req, res) => {
|
|
// Health check endpoint
|
|
if (req.url === '/health' || req.url === '/') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(getHealthInfo(), null, 2));
|
|
return;
|
|
}
|
|
|
|
// SSE endpoint
|
|
if (req.url === '/sse') {
|
|
console.error('📡 SSE client connected');
|
|
|
|
const transport = new SSEServerTransport('/message', res);
|
|
await server.getServer().connect(transport);
|
|
|
|
// Handle client disconnect
|
|
req.on('close', () => {
|
|
console.error('📡 SSE client disconnected');
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 404 for other routes
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
});
|
|
|
|
httpServer.listen(port, () => {
|
|
console.error(`🌐 HTTP/SSE server listening on http://localhost:${port}`);
|
|
console.error(` Health: http://localhost:${port}/health`);
|
|
console.error(` SSE: http://localhost:${port}/sse`);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Main entry point
|
|
*/
|
|
async function main(): Promise<void> {
|
|
console.error('🚀 Starting Stripe MCP Server...\n');
|
|
|
|
// Validate environment
|
|
const env = validateEnv();
|
|
|
|
console.error('✅ Environment validated');
|
|
console.error(` API Key: ${env.STRIPE_SECRET_KEY.substring(0, 12)}...`);
|
|
if (env.STRIPE_WEBHOOK_SECRET) {
|
|
console.error(` Webhook Secret: ${env.STRIPE_WEBHOOK_SECRET.substring(0, 12)}...`);
|
|
}
|
|
console.error('');
|
|
|
|
// Create server
|
|
const server = createStripeServer({
|
|
apiKey: env.STRIPE_SECRET_KEY,
|
|
webhookSecret: env.STRIPE_WEBHOOK_SECRET,
|
|
});
|
|
|
|
// Setup graceful shutdown
|
|
setupGracefulShutdown(async () => {
|
|
await server.close();
|
|
});
|
|
|
|
// Determine transport mode
|
|
const transportMode = process.env.TRANSPORT_MODE || 'stdio';
|
|
|
|
if (transportMode === 'http' || transportMode === 'dual') {
|
|
const port = parseInt(env.PORT, 10);
|
|
await startHttpServer(server, port);
|
|
}
|
|
|
|
if (transportMode === 'stdio' || transportMode === 'dual') {
|
|
// Connect to stdio (this is the default for MCP)
|
|
await server.connectStdio();
|
|
}
|
|
|
|
if (transportMode !== 'http' && transportMode !== 'stdio' && transportMode !== 'dual') {
|
|
console.error(`❌ Unknown transport mode: ${transportMode}`);
|
|
console.error(' Valid modes: stdio, http, dual');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.error('✅ Server ready\n');
|
|
}
|
|
|
|
// Run the server
|
|
main().catch((error) => {
|
|
console.error('❌ Fatal error:', error);
|
|
process.exit(1);
|
|
});
|