qbo-mcp-ts
Version:
TypeScript QuickBooks Online MCP Server with enhanced features and dual transport support
295 lines • 10.4 kB
JavaScript
"use strict";
/**
* SSE (Server-Sent Events) Transport implementation for MCP
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SSETransport = void 0;
const express_1 = __importDefault(require("express"));
const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
const logger_1 = require("../utils/logger");
/**
* SSE Transport for production deployments
*/
class SSETransport {
server;
app;
httpServer;
// private transport?: SSEServerTransport;
config;
connections = new Set();
constructor(server, config) {
this.server = server;
this.config = config;
this.app = (0, express_1.default)();
logger_1.logger.info('Initializing SSE transport', {
port: config.port,
host: config.host,
});
this.setupMiddleware();
this.setupRoutes();
}
/**
* Set up Express middleware
*/
setupMiddleware() {
// JSON body parser
this.app.use(express_1.default.json({ limit: '10mb' }));
// CORS configuration
if (this.config.cors) {
this.app.use((req, res, next) => {
const origin = Array.isArray(this.config.cors.origin)
? this.config.cors.origin.join(',')
: this.config.cors.origin;
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (this.config.cors.credentials) {
res.header('Access-Control-Allow-Credentials', 'true');
}
if (req.method === 'OPTIONS') {
res.sendStatus(204);
}
else {
next();
}
});
}
// Rate limiting
if (this.config.rateLimit) {
const limiter = (0, express_rate_limit_1.default)({
windowMs: this.config.rateLimit.windowMs,
max: this.config.rateLimit.max,
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
this.app.use('/sse', limiter);
}
// Request logging
this.app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger_1.logger.http(`${req.method} ${req.path}`, {
statusCode: res.statusCode,
duration,
ip: req.ip,
userAgent: req.get('user-agent'),
});
});
next();
});
// Error handling
this.app.use((err, _req, res, _next) => {
logger_1.logger.error('Express error', err);
res.status(500).json({
error: 'Internal server error',
message: err.message,
});
});
}
/**
* Set up Express routes
*/
setupRoutes() {
// Health check endpoint
if (this.config.healthCheckPath) {
this.app.get(this.config.healthCheckPath, (_req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
connections: this.connections.size,
});
});
}
// SSE endpoint
this.app.get('/sse', async (_req, res) => {
logger_1.logger.info('SSE client connected', {
ip: _req.ip,
userAgent: _req.get('user-agent'),
});
// Set SSE headers
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no', // Disable Nginx buffering
});
// Add to connections set
this.connections.add(res);
// Send initial ping
res.write(':ping\n\n');
// Set up keep-alive
const keepAlive = setInterval(() => {
res.write(':ping\n\n');
}, 30000); // Every 30 seconds
// Handle client disconnect
_req.on('close', () => {
clearInterval(keepAlive);
this.connections.delete(res);
logger_1.logger.info('SSE client disconnected');
});
// Create SSE transport for this connection
const transport = new sse_js_1.SSEServerTransport('/message', res);
await this.server.connect(transport);
});
// Message endpoint for SSE
this.app.post('/message', async (_req, res) => {
try {
// Forward message to MCP server
const response = await this.handleMessage(_req.body);
res.json(response);
}
catch (error) {
logger_1.logger.error('Failed to handle message', error);
res.status(500).json({
error: 'Failed to process message',
});
}
});
// Server info endpoint
this.app.get('/info', (_req, res) => {
res.json({
name: 'QBOMCP-TS',
version: '2.0.0',
transport: 'sse',
features: {
accounting: true,
naturalLanguage: true,
caching: true,
retryLogic: true,
},
});
});
// List available tools
this.app.get('/tools', async (_req, res) => {
try {
// TODO: Implement listTools method
const tools = [];
res.json(tools);
}
catch (error) {
logger_1.logger.error('Failed to list tools', error);
res.status(500).json({
error: 'Failed to list tools',
});
}
});
// List available resources
this.app.get('/resources', async (_req, res) => {
try {
// TODO: Implement listResources method
const resources = [];
res.json(resources);
}
catch (error) {
logger_1.logger.error('Failed to list resources', error);
res.status(500).json({
error: 'Failed to list resources',
});
}
});
// 404 handler
this.app.use((_req, res) => {
res.status(404).json({
error: 'Not found',
path: _req.path,
});
});
}
/**
* Handle incoming messages
*/
async handleMessage(_message) {
// This will be handled by the MCP server through the SSE transport
return { success: true };
}
/**
* Start the SSE transport
*/
async start() {
try {
const port = this.config.port || 3000;
const host = this.config.host || '0.0.0.0';
logger_1.logger.info(`Starting SSE transport on ${host}:${port}`);
// Create HTTP server
this.httpServer = this.app.listen(port, host, () => {
logger_1.logger.info(`SSE transport listening on http://${host}:${port}`);
logger_1.logger.info(`Health check: http://${host}:${port}${this.config.healthCheckPath}`);
logger_1.logger.info(`SSE endpoint: http://${host}:${port}/sse`);
});
// Handle server errors
this.httpServer.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
logger_1.logger.error(`Port ${port} is already in use`);
}
else {
logger_1.logger.error('HTTP server error', error);
}
process.exit(1);
});
// Handle process termination
process.on('SIGINT', () => this.shutdown());
process.on('SIGTERM', () => this.shutdown());
}
catch (error) {
logger_1.logger.error('Failed to start SSE transport', error);
throw error;
}
}
/**
* Broadcast message to all connected clients
*/
broadcast(event, data) {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
this.connections.forEach((res) => {
res.write(message);
});
}
/**
* Get connected clients count
*/
getConnectionCount() {
return this.connections.size;
}
/**
* Shutdown the transport
*/
async shutdown() {
logger_1.logger.info('Shutting down SSE transport');
try {
// Close all SSE connections
this.connections.forEach((res) => {
res.write('event: shutdown\ndata: {}\n\n');
res.end();
});
this.connections.clear();
// Close HTTP server
if (this.httpServer) {
await new Promise((resolve, reject) => {
this.httpServer.close((err) => {
if (err)
reject(err);
else
resolve();
});
});
}
// Close MCP server
await this.server.close();
logger_1.logger.info('SSE transport shutdown complete');
process.exit(0);
}
catch (error) {
logger_1.logger.error('Error during SSE transport shutdown', error);
process.exit(1);
}
}
}
exports.SSETransport = SSETransport;
//# sourceMappingURL=sse.js.map