@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
214 lines (181 loc) • 5.79 kB
text/typescript
/**
* Connection manager for handling multiple concurrent MCP connections
*/
interface Connection {
id: string;
createdAt: Date;
lastUsed: Date;
requestCount: number;
isActive: boolean;
}
class ConnectionManager {
private connections = new Map<string, Connection>();
private requestQueue = new Map<
string,
Array<{
resolve: (value: any) => void;
reject: (reason?: any) => void;
request: any;
}>
>();
private activeRequests = new Map<string, number>();
private readonly maxConcurrentRequests = 10;
private readonly connectionTimeout = 300000; // 5 minutes
/**
* Register a new connection
*/
registerConnection(connectionId: string): void {
this.connections.set(connectionId, {
id: connectionId,
createdAt: new Date(),
lastUsed: new Date(),
requestCount: 0,
isActive: true,
});
// Initialize request tracking
this.activeRequests.set(connectionId, 0);
this.requestQueue.set(connectionId, []);
// Connection registered successfully
}
/**
* Handle incoming request with queuing and concurrency control
*/
async handleRequest<T>(
connectionId: string,
requestHandler: () => Promise<T>
): Promise<T> {
// Register connection if not exists
if (!this.connections.has(connectionId)) {
this.registerConnection(connectionId);
}
const connection = this.connections.get(connectionId)!;
connection.lastUsed = new Date();
connection.requestCount++;
// Check if we can process immediately
const currentActive = this.activeRequests.get(connectionId) || 0;
if (currentActive >= this.maxConcurrentRequests) {
// Queue the request
return new Promise<T>((resolve, reject) => {
const queue = this.requestQueue.get(connectionId) || [];
queue.push({ resolve, reject, request: requestHandler });
this.requestQueue.set(connectionId, queue);
});
}
return this.executeRequest(connectionId, requestHandler);
}
/**
* Execute a request with proper tracking
*/
private async executeRequest<T>(
connectionId: string,
requestHandler: () => Promise<T>
): Promise<T> {
// Increment active request count
const currentActive = this.activeRequests.get(connectionId) || 0;
this.activeRequests.set(connectionId, currentActive + 1);
try {
const result = await requestHandler();
return result;
} finally {
// Decrement active request count
const newActive = (this.activeRequests.get(connectionId) || 1) - 1;
this.activeRequests.set(connectionId, newActive);
// Process next queued request if any
this.processQueue(connectionId);
}
}
/**
* Process queued requests for a connection
*/
private processQueue(connectionId: string): void {
const queue = this.requestQueue.get(connectionId) || [];
const currentActive = this.activeRequests.get(connectionId) || 0;
if (queue.length > 0 && currentActive < this.maxConcurrentRequests) {
const { resolve, reject, request } = queue.shift()!;
this.requestQueue.set(connectionId, queue);
this.executeRequest(connectionId, request)
.then((result: unknown) => resolve(result))
.catch((error: any) => reject(error));
}
}
/**
* Close and cleanup a connection
*/
closeConnection(connectionId: string): void {
const connection = this.connections.get(connectionId);
if (connection) {
connection.isActive = false;
// Reject any queued requests
const queue = this.requestQueue.get(connectionId) || [];
queue.forEach(({ reject }) => {
reject(new Error('Connection closed'));
});
// Cleanup
this.connections.delete(connectionId);
this.activeRequests.delete(connectionId);
this.requestQueue.delete(connectionId);
// Connection closed and cleaned up
}
}
/**
* Get connection statistics
*/
getStats(): {
totalConnections: number;
activeConnections: number;
totalRequests: number;
queuedRequests: number;
} {
const activeConnections = Array.from(this.connections.values()).filter(
conn => conn.isActive
).length;
const totalRequests = Array.from(this.connections.values()).reduce(
(sum, conn) => sum + conn.requestCount,
0
);
const queuedRequests = Array.from(this.requestQueue.values()).reduce(
(sum, queue) => sum + queue.length,
0
);
return {
totalConnections: this.connections.size,
activeConnections,
totalRequests,
queuedRequests,
};
}
/**
* Cleanup stale connections
*/
cleanupStaleConnections(): void {
const now = new Date();
const staleConnections: string[] = [];
this.connections.forEach((connection, id) => {
const timeSinceLastUse = now.getTime() - connection.lastUsed.getTime();
if (timeSinceLastUse > this.connectionTimeout) {
staleConnections.push(id);
}
});
staleConnections.forEach(id => this.closeConnection(id));
if (staleConnections.length > 0) {
// Stale connections cleaned up successfully
}
}
}
// Singleton instance
export const connectionManager = new ConnectionManager();
// Cleanup interval - store reference for testing
let cleanupInterval: ReturnType<typeof setInterval> | null = null;
// Only set interval if not in test environment
if (process.env.NODE_ENV !== 'test') {
cleanupInterval = setInterval(() => {
connectionManager.cleanupStaleConnections();
}, 60000); // Every minute
}
// Export function to clear interval for testing
export const clearCleanupInterval = () => {
if (cleanupInterval) {
clearInterval(cleanupInterval);
cleanupInterval = null;
}
};