test-client.ts•6.92 kB
#!/usr/bin/env node
import { randomUUID } from 'crypto';
import { createConnection, Socket } from 'net';
import { RegistrationProtocol, RegistrationMessageType, RegistrationStatus } from '../discovery/registrationProtocol.js';
/**
* Test client for MCP Bridge Server
* Simulates a client connecting to the bridge server
*/
class TestClient {
private socket: Socket | null = null;
private protocol: RegistrationProtocol;
private clientId: string;
private clientType: 'claude' | 'cline';
private socketPath: string;
private connected = false;
constructor(options: {
clientType: 'claude' | 'cline';
socketPath: string;
clientId?: string;
}) {
this.clientType = options.clientType;
this.socketPath = options.socketPath;
this.clientId = options.clientId || `${this.clientType}-${randomUUID()}`;
this.protocol = new RegistrationProtocol();
}
/**
* Connect to the bridge server
*/
public async connect(): Promise<void> {
return new Promise<void>((resolve, reject) => {
console.log(`Connecting to bridge server at ${this.socketPath}...`);
this.socket = createConnection(this.socketPath);
this.socket.on('connect', () => {
console.log('Connected to bridge server');
this.connected = true;
// Send registration message
this.sendRegistration()
.then(() => resolve())
.catch(reject);
});
this.socket.on('data', (data) => {
this.handleMessage(data.toString());
});
this.socket.on('error', (error) => {
console.error('Socket error:', error);
reject(error);
});
this.socket.on('close', () => {
console.log('Connection closed');
this.connected = false;
});
});
}
/**
* Send registration message
*/
private async sendRegistration(): Promise<void> {
if (!this.socket || !this.connected) {
throw new Error('Not connected to bridge server');
}
console.log(`Registering as ${this.clientType} client with ID ${this.clientId}`);
// Create registration message
const message = this.protocol.createRegisterMessage(
this.clientType,
{
supportedMethods: ['tools/call', 'tools/discover_client'],
supportedTransports: ['unix-socket'],
maxConcurrentTasks: 5,
targetType: this.clientType,
features: {
autoStart: true,
reconnect: true,
healthCheck: true
}
},
'unix-socket',
this.clientId
);
// Send message
return new Promise<void>((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not initialized'));
return;
}
this.socket.write(JSON.stringify(message) + '\n', (error) => {
if (error) {
reject(error);
} else {
console.log('Registration message sent');
resolve();
}
});
});
}
/**
* Handle incoming message
*/
private handleMessage(message: string): void {
try {
console.log('Received message:', message);
// Parse message
const parsedMessage = this.protocol.parseMessage(message);
if (!parsedMessage) {
console.error('Failed to parse message');
return;
}
// Handle registration response
if (parsedMessage.type === RegistrationMessageType.REGISTER_RESPONSE) {
const responseMsg = parsedMessage as any; // Type assertion to access properties
if (responseMsg.status === RegistrationStatus.SUCCESS) {
console.log(`Registration successful, assigned client ID: ${responseMsg.clientId}`);
this.clientId = responseMsg.clientId;
// Start sending heartbeats
this.startHeartbeats();
} else {
console.error(`Registration failed: ${responseMsg.error}`);
}
}
// Handle heartbeat response
if (parsedMessage.type === RegistrationMessageType.HEARTBEAT_RESPONSE) {
const responseMsg = parsedMessage as any; // Type assertion to access properties
if (responseMsg.status === RegistrationStatus.SUCCESS) {
console.log('Heartbeat acknowledged');
} else {
console.error(`Heartbeat failed: ${responseMsg.error}`);
}
}
} catch (error) {
console.error('Error handling message:', error);
}
}
/**
* Start sending heartbeats
*/
private startHeartbeats(): void {
setInterval(() => {
if (this.connected) {
this.sendHeartbeat().catch(console.error);
}
}, 30000); // Every 30 seconds
}
/**
* Send heartbeat message
*/
private async sendHeartbeat(): Promise<void> {
if (!this.socket || !this.connected) {
return;
}
console.log('Sending heartbeat...');
// Create heartbeat message
const message = this.protocol.createHeartbeatMessage(this.clientId);
// Send message
return new Promise<void>((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not initialized'));
return;
}
this.socket.write(JSON.stringify(message) + '\n', (error) => {
if (error) {
reject(error);
} else {
console.log('Heartbeat sent');
resolve();
}
});
});
}
/**
* Disconnect from the bridge server
*/
public async disconnect(): Promise<void> {
if (!this.socket || !this.connected) {
return;
}
console.log('Disconnecting from bridge server...');
// Create disconnect message
const message = this.protocol.createDisconnectMessage(
this.clientId,
'Client initiated disconnect'
);
// Send message
return new Promise<void>((resolve) => {
if (!this.socket) {
resolve();
return;
}
this.socket.write(JSON.stringify(message) + '\n', () => {
this.socket?.end(() => {
this.socket = null;
this.connected = false;
console.log('Disconnected from bridge server');
resolve();
});
});
});
}
}
/**
* Main function
*/
async function main(): Promise<void> {
// Parse command line arguments
const args = process.argv.slice(2);
const clientType = args[0] as 'claude' | 'cline' || 'claude';
const socketPath = args[1] || '/tmp/mcp-bridge.sock';
// Create test client
const client = new TestClient({
clientType,
socketPath
});
// Handle process termination
process.on('SIGINT', async () => {
console.log('Received SIGINT, shutting down...');
await client.disconnect();
process.exit(0);
});
// Connect to bridge server
try {
await client.connect();
console.log('Test client running, press Ctrl+C to exit');
} catch (error) {
console.error('Failed to connect:', error);
process.exit(1);
}
}
// Run main function
main().catch(console.error);