claude-flow-tbowman01
Version:
Enterprise-grade AI agent orchestration with ruv-swarm integration (Alpha Release)
239 lines (202 loc) • 6.59 kB
text/typescript
/**
* Standard I/O transport for MCP
*/
import { createReadStream, createWriteStream } from 'node:fs';
import { stdin, stdout } from 'node:process';
import { createInterface, Interface } from 'node:readline';
import type { ITransport, RequestHandler, NotificationHandler } from './base.js';
import type { MCPRequest, MCPResponse, MCPNotification } from '../../utils/types.js';
import type { ILogger } from '../../core/logger.js';
import { MCPTransportError } from '../../utils/errors.js';
/**
* Stdio transport implementation
*/
export class StdioTransport implements ITransport {
private requestHandler?: RequestHandler;
private notificationHandler?: NotificationHandler;
private readline?: Interface;
private messageCount = 0;
private notificationCount = 0;
private running = false;
constructor(private logger: ILogger) {}
async start(): Promise<void> {
if (this.running) {
throw new MCPTransportError('Transport already running');
}
this.logger.info('Starting stdio transport');
try {
// Create readline interface for stdin
this.readline = createInterface({
input: stdin,
output: stdout,
terminal: false,
});
// Set up line handler
this.readline.on('line', (line: string) => {
this.processMessage(line.trim()).catch((error) => {
this.logger.error('Error processing message', { line, error });
});
});
this.readline.on('close', () => {
this.logger.info('Stdin closed');
this.running = false;
});
this.running = true;
this.logger.info('Stdio transport started');
} catch (error) {
throw new MCPTransportError('Failed to start stdio transport', { error });
}
}
async stop(): Promise<void> {
if (!this.running) {
return;
}
this.logger.info('Stopping stdio transport');
this.running = false;
if (this.readline) {
this.readline.close();
this.readline = undefined;
}
this.logger.info('Stdio transport stopped');
}
onRequest(handler: RequestHandler): void {
this.requestHandler = handler;
}
onNotification(handler: NotificationHandler): void {
this.notificationHandler = handler;
}
async getHealthStatus(): Promise<{
healthy: boolean;
error?: string;
metrics?: Record<string, number>;
}> {
return {
healthy: this.running,
metrics: {
messagesReceived: this.messageCount,
notificationsSent: this.notificationCount,
stdinOpen: this.readline ? 1 : 0,
},
};
}
private async processMessage(line: string): Promise<void> {
let message: any;
try {
message = JSON.parse(line);
if (!message.jsonrpc || message.jsonrpc !== '2.0') {
throw new Error('Invalid JSON-RPC version');
}
if (!message.method) {
throw new Error('Missing method');
}
} catch (error) {
this.logger.error('Failed to parse message', { line, error });
// Send error response if we can extract an ID
let id = 'unknown';
try {
const parsed = JSON.parse(line);
if (parsed.id !== undefined) {
id = parsed.id;
}
} catch {
// Ignore parse error for ID extraction
}
await this.sendResponse({
jsonrpc: '2.0',
id,
error: {
code: -32700,
message: 'Parse error',
},
});
return;
}
this.messageCount++;
// Check if this is a notification (no id field) or a request
if (message.id === undefined) {
// This is a notification
await this.handleNotification(message as MCPNotification);
} else {
// This is a request
await this.handleRequest(message as MCPRequest);
}
}
private async handleRequest(request: MCPRequest): Promise<void> {
if (!this.requestHandler) {
await this.sendResponse({
jsonrpc: '2.0',
id: request.id,
error: {
code: -32603,
message: 'No request handler registered',
},
});
return;
}
try {
const response = await this.requestHandler(request);
await this.sendResponse(response);
} catch (error) {
this.logger.error('Request handler error', { request, error });
await this.sendResponse({
jsonrpc: '2.0',
id: request.id,
error: {
code: -32603,
message: 'Internal error',
data: error instanceof Error ? error.message : String(error),
},
});
}
}
private async handleNotification(notification: MCPNotification): Promise<void> {
if (!this.notificationHandler) {
this.logger.warn('Received notification but no handler registered', {
method: notification.method,
});
return;
}
try {
await this.notificationHandler(notification);
} catch (error) {
this.logger.error('Notification handler error', { notification, error });
// Notifications don't send error responses
}
}
private async sendResponse(response: MCPResponse): Promise<void> {
try {
const json = JSON.stringify(response);
stdout.write(json + '\n');
} catch (error) {
this.logger.error('Failed to send response', { response, error });
}
}
async connect(): Promise<void> {
// For STDIO transport, connect is handled by start()
if (!this.running) {
await this.start();
}
}
async disconnect(): Promise<void> {
// For STDIO transport, disconnect is handled by stop()
await this.stop();
}
async sendRequest(request: MCPRequest): Promise<MCPResponse> {
// Send request to stdout
const json = JSON.stringify(request);
stdout.write(json + '\n');
// In STDIO transport, responses are handled asynchronously
// This would need a proper request/response correlation mechanism
throw new Error('STDIO transport sendRequest requires request/response correlation');
}
async sendNotification(notification: MCPNotification): Promise<void> {
try {
const json = JSON.stringify(notification);
stdout.write(json + '\n');
this.notificationCount++;
} catch (error) {
this.logger.error('Failed to send notification', { notification, error });
throw error;
}
}
}