UNPKG

@iqai/mcp-near-agent

Version:
290 lines (287 loc) โ€ข 10.4 kB
import { EventEmitter } from "node:events"; import dedent from "dedent"; import { env } from "../env.js"; export class EventProcessor extends EventEmitter { static DEFAULT_MAX_TOKENS = 1000; static DEFAULT_TIMEOUT = 2 * 60 * 1000; // 2 minutes account = null; server = null; processingQueue = new Map(); processingStats = { totalProcessed: 0, successful: 0, failed: 0, averageProcessingTime: 0, }; constructor(account, server) { super(); this.account = account || null; this.server = server || null; } /** * Set the NEAR account for blockchain interactions */ setAccount(account) { this.account = account; } /** * Set the MCP server for creating messages */ setServer(server) { this.server = server; } /** * Process an event by requesting sampling from MCP client and sending response to blockchain */ async processEvent(context) { const startTime = Date.now(); const { event, subscription } = context; console.log(`๐Ÿ”„ Processing event '${event.eventType}' with request ID: ${event.requestId}`); try { // Add to processing queue this.processingQueue.set(event.requestId, context); this.emit("queue:added", event.requestId, context); // Request sampling from MCP client this.emit("event:mcp-request", context); const mcpResponse = await this.requestMCPSampling(event, subscription); this.emit("event:mcp-response", context, mcpResponse); if (!mcpResponse || !mcpResponse.content?.text) { throw new Error("No valid response from MCP client"); } // Send response back to blockchain const txHash = await this.sendBlockchainResponse(event.requestId, mcpResponse.content.text, subscription); this.emit("event:blockchain-response", context, txHash); const processingTime = Date.now() - startTime; const result = { success: true, response: mcpResponse.content.text, requestId: event.requestId, subscription, event, processingTime, }; this.updateStats(true, processingTime); this.emit("event:processed", result); console.log(`โœ… Successfully processed event ${event.requestId} in ${processingTime}ms`); return result; } catch (error) { const processingTime = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : "Unknown error"; const result = { success: false, error: errorMessage, requestId: event.requestId, subscription, event, processingTime, }; this.updateStats(false, processingTime); this.emit("event:processing-error", result); console.error(`โŒ Failed to process event ${event.requestId}:`, error); return result; } finally { // Remove from processing queue this.processingQueue.delete(event.requestId); this.emit("queue:removed", event.requestId); } } /** * Request sampling from MCP client using server.createMessage */ async requestMCPSampling(event, subscription) { if (!this.server) { throw new Error("MCP server not set. Call setServer() first."); } try { const eventContent = this.formatEventForSampling(event); console.log(`๐Ÿค– Requesting MCP sampling for event: ${event.eventType}`); const samplingRequest = { messages: [ { role: "user", content: { type: "text", text: eventContent, }, }, ], includeContext: "thisServer", maxTokens: EventProcessor.DEFAULT_MAX_TOKENS, }; // Set timeout for MCP request const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error("MCP sampling timeout")), EventProcessor.DEFAULT_TIMEOUT); }); const samplingPromise = this.server.createMessage(samplingRequest); const response = await Promise.race([samplingPromise, timeoutPromise]); console.log(`๐Ÿ“ Received MCP response for event: ${event.eventType}`); return response; } catch (error) { console.error("โŒ Error requesting MCP sampling:", error); throw new Error(`MCP sampling failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Format event data for MCP sampling */ formatEventForSampling(event) { const eventInfo = { eventType: event.eventType, requestId: event.requestId, sender: event.sender, timestamp: new Date(event.timestamp).toISOString(), payload: event.payload, }; return dedent ` Please process the following NEAR blockchain event: Event Type: ${eventInfo.eventType} Request ID: ${eventInfo.requestId} Sender: ${eventInfo.sender} Timestamp: ${eventInfo.timestamp} Event Data: ${JSON.stringify(eventInfo.payload, null, 2)} Please analyze this event and provide a concise response that can be sent back to the blockchain contract.`; } /** * Send response back to the NEAR blockchain */ async sendBlockchainResponse(requestId, response, subscription) { if (!this.account) { throw new Error("NEAR account not set. Call setAccount() first."); } try { console.log(`๐Ÿ“ค Sending response to blockchain for request: ${requestId}`); const result = await this.account.functionCall({ contractId: subscription.contractId, methodName: subscription.responseMethodName, args: { data_id: requestId, [subscription.responseParameterName]: response, timestamp: Date.now(), }, gas: BigInt(env.NEAR_GAS_LIMIT || "300000000000000"), }); const txHash = result.transaction.hash; console.log(`โœ… Blockchain response sent successfully. Transaction: ${txHash}`); return txHash; } catch (error) { console.error("โŒ Error sending blockchain response:", error); throw new Error(`Blockchain response failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * Update processing statistics */ updateStats(success, processingTime) { this.processingStats.totalProcessed++; if (success) { this.processingStats.successful++; } else { this.processingStats.failed++; } // Update average processing time const totalTime = this.processingStats.averageProcessingTime * (this.processingStats.totalProcessed - 1) + processingTime; this.processingStats.averageProcessingTime = totalTime / this.processingStats.totalProcessed; // Emit stats update this.emit("stats:updated", this.getStats()); } /** * Get current processing queue status */ getQueueStatus() { return { queueSize: this.processingQueue.size, processingItems: Array.from(this.processingQueue.entries()).map(([requestId, context]) => ({ requestId, eventType: context.event.eventType, contractId: context.subscription.contractId, timestamp: context.event.timestamp, })), }; } /** * Get processing statistics */ getStats() { return { ...this.processingStats, successRate: this.processingStats.totalProcessed > 0 ? (this.processingStats.successful / this.processingStats.totalProcessed) * 100 : 0, currentQueueSize: this.processingQueue.size, }; } /** * Check if a request is currently being processed */ isProcessing(requestId) { return this.processingQueue.has(requestId); } /** * Get all currently processing request IDs */ getProcessingRequestIds() { return Array.from(this.processingQueue.keys()); } /** * Cancel processing for a specific request (if still in queue) */ cancelProcessing(requestId) { if (this.processingQueue.has(requestId)) { this.processingQueue.delete(requestId); this.emit("queue:removed", requestId); console.log(`๐Ÿšซ Cancelled processing for request: ${requestId}`); return true; } return false; } /** * Reset statistics */ resetStats() { this.processingStats = { totalProcessed: 0, successful: 0, failed: 0, averageProcessingTime: 0, }; this.emit("stats:updated", this.getStats()); console.log("๐Ÿ“Š Processing statistics reset"); } /** * Cleanup resources */ cleanup() { console.log("๐Ÿงน Cleaning up EventProcessor..."); this.processingQueue.clear(); this.removeAllListeners(); this.account = null; this.server = null; console.log("โœ… EventProcessor cleaned up"); } // Type-safe event listener methods // Type-safe event listener methods on(event, listener) { return super.on(event, listener); } emit(event, ...args) { return super.emit(event, ...args); } off(event, listener) { return super.off(event, listener); } once(event, listener) { return super.once(event, listener); } } //# sourceMappingURL=event-processor.js.map