anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
564 lines • 19.4 kB
JavaScript
"use strict";
/**
* MCP (Model Context Protocol) Client Implementation
*
* Core client for connecting to MCP servers and managing LLM provider interactions
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.MCPConnectionImpl = exports.MCPConnectionManager = exports.MCPClient = void 0;
const events_1 = require("events");
const types_1 = require("./types");
/**
* Connection manager for individual MCP connections
*/
class MCPConnectionManager extends events_1.EventEmitter {
constructor(config) {
super();
this.config = config;
this.connections = new Map();
this.connectionAttempts = new Map();
this.lastHeartbeat = new Map();
this.startHeartbeatMonitoring();
}
/**
* Create connection to MCP server
*/
async connect(providerId, endpoint) {
try {
const connection = new MCPConnectionImpl(providerId, endpoint, this.config);
await connection.connect();
this.connections.set(providerId, connection);
this.connectionAttempts.set(providerId, 0);
this.lastHeartbeat.set(providerId, new Date());
// Set up connection event handlers
connection.on('disconnect', () => this.handleDisconnection(providerId));
connection.on('error', (error) => this.handleConnectionError(providerId, error));
connection.on('heartbeat', () => this.lastHeartbeat.set(providerId, new Date()));
this.emit('connected', providerId);
return connection;
}
catch (error) {
const attempts = this.connectionAttempts.get(providerId) || 0;
this.connectionAttempts.set(providerId, attempts + 1);
if (attempts < this.config.retryAttempts) {
// Exponential backoff retry
const delay = Math.min(this.config.retryDelay * Math.pow(this.config.backoffMultiplier, attempts), this.config.maxRetryDelay);
setTimeout(() => this.connect(providerId, endpoint), delay);
}
throw new types_1.MCPError({
code: types_1.MCPErrorCode.NETWORK_ERROR,
message: `Failed to connect to provider ${providerId}: ${error}`,
timestamp: new Date(),
provider: providerId,
retryable: attempts < this.config.retryAttempts
});
}
}
/**
* Get connection by provider ID
*/
getConnection(providerId) {
return this.connections.get(providerId);
}
/**
* Get all active connections
*/
getAllConnections() {
return new Map(this.connections);
}
/**
* Disconnect from provider
*/
async disconnect(providerId) {
const connection = this.connections.get(providerId);
if (connection) {
await connection.disconnect();
this.connections.delete(providerId);
this.connectionAttempts.delete(providerId);
this.lastHeartbeat.delete(providerId);
this.emit('disconnected', providerId);
}
}
/**
* Disconnect from all providers
*/
async disconnectAll() {
const disconnectPromises = Array.from(this.connections.keys()).map((providerId) => this.disconnect(providerId));
await Promise.all(disconnectPromises);
}
/**
* Handle connection disconnection
*/
handleDisconnection(providerId) {
this.connections.delete(providerId);
this.emit('disconnected', providerId);
// Attempt reconnection if configured
if (this.config.keepAlive) {
this.emit('reconnecting', providerId);
}
}
/**
* Handle connection errors
*/
handleConnectionError(providerId, error) {
this.emit('error', providerId, error);
}
/**
* Monitor connection health via heartbeats
*/
startHeartbeatMonitoring() {
if (!this.config.heartbeatInterval)
return;
setInterval(() => {
const now = new Date();
for (const [providerId, lastHeartbeat] of this.lastHeartbeat.entries()) {
const timeSinceHeartbeat = now.getTime() - lastHeartbeat.getTime();
if (timeSinceHeartbeat > this.config.heartbeatInterval * 2) {
// Connection appears dead
this.handleDisconnection(providerId);
}
}
}, this.config.heartbeatInterval);
}
}
exports.MCPConnectionManager = MCPConnectionManager;
/**
* Concrete implementation of MCPConnection
*/
class MCPConnectionImpl extends events_1.EventEmitter {
constructor(providerId, endpoint, config) {
super();
this.endpoint = endpoint;
this.config = config;
this.status = types_1.ConnectionStatus.DISCONNECTED;
this.lastHeartbeat = new Date();
this.createdAt = new Date();
this.ws = null;
this.requestId = 0;
this.pendingRequests = new Map();
this.id = `conn-${providerId}-${Date.now()}`;
this.providerId = providerId;
this.metadata = {
endpoint: this.endpoint,
version: '1.0.0',
features: [],
retryCount: 0
};
}
/**
* Connect to MCP server
*/
async connect() {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.endpoint);
this.ws.onopen = () => {
this.status = types_1.ConnectionStatus.CONNECTED;
this.startHeartbeat();
resolve();
};
this.ws.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data));
};
this.ws.onclose = () => {
this.status = types_1.ConnectionStatus.DISCONNECTED;
this.emit('disconnect');
};
this.ws.onerror = (error) => {
reject(new Error(`WebSocket error: ${error}`));
};
// Connection timeout
setTimeout(() => {
if (this.ws?.readyState !== WebSocket.OPEN) {
reject(new Error('Connection timeout'));
}
}, this.config.timeout);
}
catch (error) {
reject(error);
}
});
}
/**
* Disconnect from MCP server
*/
async disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.status = types_1.ConnectionStatus.DISCONNECTED;
// Clear pending requests
for (const [requestId, { reject, timeout }] of this.pendingRequests.entries()) {
clearTimeout(timeout);
reject(new types_1.MCPError({
code: types_1.MCPErrorCode.NETWORK_ERROR,
message: 'Connection closed',
timestamp: new Date(),
requestId,
retryable: false
}));
}
this.pendingRequests.clear();
}
/**
* Send LLM request and await response
*/
async sendRequest(request) {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.NETWORK_ERROR,
message: 'Connection not available',
timestamp: new Date(),
provider: this.providerId,
retryable: true
});
}
const requestId = `${this.providerId}-${++this.requestId}`;
const message = {
id: requestId,
type: types_1.MCPMessageType.REQUEST,
timestamp: new Date(),
sender: 'client',
recipient: this.providerId,
payload: request,
metadata: {
requestType: request.type,
agentDID: request.agentDID,
sessionId: request.sessionId
}
};
return new Promise((resolve, reject) => {
// Set up timeout
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new types_1.MCPError({
code: types_1.MCPErrorCode.TIMEOUT,
message: `Request timeout after ${this.config.timeout}ms`,
timestamp: new Date(),
requestId,
provider: this.providerId,
retryable: true
}));
}, this.config.timeout);
// Store pending request
this.pendingRequests.set(requestId, { resolve, reject, timeout });
// Send message
this.ws.send(JSON.stringify(message));
});
}
/**
* Send streaming LLM request
*/
async *streamRequest(request) {
// Implementation for streaming would depend on the MCP server protocol
// This is a placeholder that would need to be implemented based on
// the actual MCP streaming specification
throw new Error('Streaming not yet implemented');
}
/**
* Check connection health
*/
async health() {
const start = Date.now();
try {
await this.sendHeartbeat();
const latency = Date.now() - start;
return {
status: 'healthy',
latency
};
}
catch (error) {
return {
status: 'unhealthy'
};
}
}
/**
* Handle incoming messages from MCP server
*/
handleMessage(message) {
switch (message.type) {
case types_1.MCPMessageType.RESPONSE:
this.handleResponse(message);
break;
case types_1.MCPMessageType.ERROR:
this.handleError(message);
break;
case types_1.MCPMessageType.HEARTBEAT:
this.lastHeartbeat = new Date();
this.emit('heartbeat');
break;
case types_1.MCPMessageType.NOTIFICATION:
this.emit('notification', message.payload);
break;
}
}
/**
* Handle response messages
*/
handleResponse(message) {
const pending = this.pendingRequests.get(message.id);
if (pending) {
clearTimeout(pending.timeout);
this.pendingRequests.delete(message.id);
pending.resolve(message.payload);
}
}
/**
* Handle error messages
*/
handleError(message) {
const pending = this.pendingRequests.get(message.id);
if (pending) {
clearTimeout(pending.timeout);
this.pendingRequests.delete(message.id);
pending.reject(message.payload);
}
}
/**
* Send heartbeat to maintain connection
*/
async sendHeartbeat() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const heartbeat = {
id: `heartbeat-${Date.now()}`,
type: types_1.MCPMessageType.HEARTBEAT,
timestamp: new Date(),
sender: 'client',
recipient: this.providerId,
payload: {}
};
this.ws.send(JSON.stringify(heartbeat));
}
}
/**
* Start periodic heartbeat
*/
startHeartbeat() {
if (this.config.heartbeatInterval) {
setInterval(() => {
this.sendHeartbeat();
}, this.config.heartbeatInterval);
}
}
}
exports.MCPConnectionImpl = MCPConnectionImpl;
/**
* Main MCP Client class
*/
class MCPClient extends events_1.EventEmitter {
constructor(config) {
super();
this.config = config;
this.providers = new Map();
this.requestMetrics = new Map();
this.connectionManager = new MCPConnectionManager(config.client);
// Set up connection manager event handlers
this.connectionManager.on('connected', (providerId) => this.emit('connected', providerId));
this.connectionManager.on('disconnected', (providerId) => this.emit('disconnected', providerId));
this.connectionManager.on('error', (providerId, error) => this.emit('error', providerId, error));
}
/**
* Initialize MCP client and connect to providers
*/
async initialize() {
// Register and connect to all configured providers
for (const providerConfig of this.config.providers) {
if (providerConfig.enabled) {
await this.addProvider(providerConfig);
}
}
// Set default provider if not set
if (!this.defaultProvider && this.providers.size > 0) {
this.defaultProvider = Array.from(this.providers.keys())[0];
}
}
/**
* Add and connect to a provider
*/
async addProvider(providerConfig) {
const provider = {
id: providerConfig.id,
name: providerConfig.id,
version: '1.0.0',
description: `${providerConfig.id} provider`,
capabilities: {
completion: true,
streaming: false,
functionCalling: true,
embeddings: false,
moderation: false,
multimodal: false,
codeGeneration: true,
jsonMode: true
},
models: [],
rateLimits: providerConfig.rateLimits || {
requestsPerMinute: 60,
tokensPerMinute: 100000,
requestsPerDay: 1000,
tokensPerDay: 1000000,
concurrentRequests: 10
},
config: providerConfig,
status: types_1.ProviderStatus.AVAILABLE
};
this.providers.set(provider.id, provider);
await this.connectionManager.connect(provider.id, providerConfig.endpoint);
}
/**
* Send request to specific provider
*/
async sendRequest(request, providerId) {
const targetProvider = providerId || this.defaultProvider;
if (!targetProvider) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.PROVIDER_UNAVAILABLE,
message: 'No provider available',
timestamp: new Date(),
retryable: false
});
}
const connection = this.connectionManager.getConnection(targetProvider);
if (!connection) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.PROVIDER_UNAVAILABLE,
message: `Provider ${targetProvider} not connected`,
timestamp: new Date(),
provider: targetProvider,
retryable: true
});
}
// Add request metadata
const enhancedRequest = {
...request,
metadata: {
...request.metadata,
requestId: `${targetProvider}-${Date.now()}`,
timestamp: new Date(),
source: 'mcp-client'
}
};
try {
const response = await connection.sendRequest(enhancedRequest);
// Track usage metrics
if (response.usage) {
const providerMetrics = this.requestMetrics.get(targetProvider) || [];
providerMetrics.push(response.usage);
this.requestMetrics.set(targetProvider, providerMetrics);
}
return response;
}
catch (error) {
// Handle provider failover if enabled
if (error instanceof types_1.MCPError && error.retryable && this.config.providers.length > 1) {
const alternativeProviders = Array.from(this.providers.keys())
.filter(id => id !== targetProvider);
if (alternativeProviders.length > 0) {
return this.sendRequest(request, alternativeProviders[0]);
}
}
throw error;
}
}
/**
* Send streaming request
*/
async *streamRequest(request, providerId) {
const targetProvider = providerId || this.defaultProvider;
if (!targetProvider) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.PROVIDER_UNAVAILABLE,
message: 'No provider available',
timestamp: new Date(),
retryable: false
});
}
const connection = this.connectionManager.getConnection(targetProvider);
if (!connection) {
throw new types_1.MCPError({
code: types_1.MCPErrorCode.PROVIDER_UNAVAILABLE,
message: `Provider ${targetProvider} not connected`,
timestamp: new Date(),
provider: targetProvider,
retryable: true
});
}
yield* connection.streamRequest(request);
}
/**
* Get available providers
*/
getAvailableProviders() {
return Array.from(this.providers.keys()).filter((providerId) => this.connectionManager.getConnection(providerId) !== undefined);
}
/**
* Get provider information
*/
getProvider(providerId) {
return this.providers.get(providerId);
}
/**
* Get usage statistics for provider
*/
getUsageStats(providerId) {
return this.requestMetrics.get(providerId) || [];
}
/**
* Set default provider
*/
setDefaultProvider(providerId) {
if (this.providers.has(providerId)) {
this.defaultProvider = providerId;
}
else {
throw new Error(`Provider ${providerId} not available`);
}
}
/**
* Health check for all providers
*/
async healthCheck() {
const results = new Map();
for (const providerId of this.providers.keys()) {
const connection = this.connectionManager.getConnection(providerId);
if (connection) {
try {
const health = await connection.health();
results.set(providerId, health);
}
catch (error) {
results.set(providerId, { status: 'unhealthy', error: error.message });
}
}
else {
results.set(providerId, { status: 'disconnected' });
}
}
return results;
}
/**
* Get connection for provider
*/
async getConnection(providerId) {
return this.connectionManager.getConnection(providerId) || null;
}
/**
* Get provider capabilities
*/
async getProviderCapabilities(providerId) {
const provider = this.providers.get(providerId);
return provider?.capabilities || {};
}
/**
* Shutdown MCP client
*/
async shutdown() {
await this.connectionManager.disconnectAll();
this.removeAllListeners();
}
}
exports.MCPClient = MCPClient;
//# sourceMappingURL=client.js.map