@iqai/mcp-near-agent
Version:
Mcp server for Near
290 lines (287 loc) โข 10.4 kB
JavaScript
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