@mrtkrcm/acp-claude-code
Version:
ACP (Agent Client Protocol) bridge for Claude Code
224 lines • 8.92 kB
JavaScript
import { globalMcpManager } from './mcp-connection-manager.js';
import { createLogger } from './logger.js';
export class McpToolBatcher {
pendingBatches = new Map();
executingBatches = new Set();
logger;
constructor() {
this.logger = createLogger('MCP-Batcher');
}
/**
* Add tool calls to a batch for optimized execution
*/
addToBatch(batchId, toolCalls) {
const existing = this.pendingBatches.get(batchId) || [];
existing.push(...toolCalls);
this.pendingBatches.set(batchId, existing);
this.logger.debug(`Added ${toolCalls.length} tools to batch ${batchId}. Total: ${existing.length}`);
}
/**
* Execute a batch of MCP tool calls with optimization strategies
*/
async executeBatch(batchId, options = { strategy: 'parallel' }) {
const toolCalls = this.pendingBatches.get(batchId);
if (!toolCalls || toolCalls.length === 0) {
return [];
}
if (this.executingBatches.has(batchId)) {
throw new Error(`Batch ${batchId} is already executing`);
}
this.executingBatches.add(batchId);
this.pendingBatches.delete(batchId);
try {
const startTime = Date.now();
this.logger.info(`Executing batch ${batchId} with ${toolCalls.length} tools using ${options.strategy} strategy`);
let results;
switch (options.strategy) {
case 'parallel':
results = await this.executeParallel(toolCalls, options);
break;
case 'sequential':
results = await this.executeSequential(toolCalls);
break;
case 'dependency-aware':
results = await this.executeDependencyAware(toolCalls, options);
break;
default:
throw new Error(`Unknown batch strategy: ${options.strategy}`);
}
const totalTime = Date.now() - startTime;
this.logger.info(`Batch ${batchId} completed in ${totalTime}ms. Success rate: ${results.filter(r => r.success).length}/${results.length}`);
return results;
}
finally {
this.executingBatches.delete(batchId);
}
}
async executeParallel(toolCalls, options) {
const { maxConcurrency = 5 } = options;
const results = [];
// Group by server to optimize connection usage
const serverGroups = new Map();
for (const call of toolCalls) {
const group = serverGroups.get(call.serverName) || [];
group.push(call);
serverGroups.set(call.serverName, group);
}
const concurrencyLimiter = new Map();
const executeCall = async (call) => {
const startTime = Date.now();
try {
// Limit concurrency per server
const current = concurrencyLimiter.get(call.serverName) || 0;
const serverLimit = Math.ceil(maxConcurrency / serverGroups.size);
if (current >= serverLimit) {
await new Promise(resolve => setTimeout(resolve, 100));
}
concurrencyLimiter.set(call.serverName, current + 1);
const result = await globalMcpManager.executeToolWithPooling(call.serverName, call.toolName, call.args, { timeout: call.timeout });
return {
id: call.id,
success: true,
result,
executionTime: Date.now() - startTime,
};
}
catch (error) {
return {
id: call.id,
success: false,
error: error instanceof Error ? error.message : String(error),
executionTime: Date.now() - startTime,
};
}
finally {
const current = concurrencyLimiter.get(call.serverName) || 1;
concurrencyLimiter.set(call.serverName, current - 1);
}
};
// Execute with controlled concurrency
const promises = [];
const executing = new Set();
for (const call of toolCalls) {
const promise = executeCall(call);
promises.push(promise);
executing.add(promise);
if (executing.size >= maxConcurrency) {
const completed = await Promise.race(executing);
executing.delete(Promise.resolve(completed));
}
}
const allResults = await Promise.all(promises);
return allResults;
}
async executeSequential(toolCalls) {
const results = [];
for (const call of toolCalls) {
const startTime = Date.now();
try {
const result = await globalMcpManager.executeToolWithPooling(call.serverName, call.toolName, call.args, { timeout: call.timeout });
results.push({
id: call.id,
success: true,
result,
executionTime: Date.now() - startTime,
});
}
catch (error) {
results.push({
id: call.id,
success: false,
error: error instanceof Error ? error.message : String(error),
executionTime: Date.now() - startTime,
});
}
}
return results;
}
async executeDependencyAware(toolCalls, options) {
const results = new Map();
const completed = new Set();
const executing = new Set();
// Build dependency graph
const dependents = new Map();
for (const call of toolCalls) {
if (call.dependencies) {
for (const dep of call.dependencies) {
const deps = dependents.get(dep) || [];
deps.push(call.id);
dependents.set(dep, deps);
}
}
}
const canExecute = (call) => {
if (executing.has(call.id) || completed.has(call.id)) {
return false;
}
if (!call.dependencies) {
return true;
}
return call.dependencies.every(dep => completed.has(dep));
};
const executeWave = async () => {
const readyCalls = toolCalls.filter(canExecute);
if (readyCalls.length === 0) {
return;
}
const promises = readyCalls.slice(0, options.maxConcurrency || 3).map(async (call) => {
executing.add(call.id);
const startTime = Date.now();
try {
const result = await globalMcpManager.executeToolWithPooling(call.serverName, call.toolName, call.args, { timeout: call.timeout });
const batchResult = {
id: call.id,
success: true,
result,
executionTime: Date.now() - startTime,
};
results.set(call.id, batchResult);
completed.add(call.id);
}
catch (error) {
const batchResult = {
id: call.id,
success: false,
error: error instanceof Error ? error.message : String(error),
executionTime: Date.now() - startTime,
};
results.set(call.id, batchResult);
completed.add(call.id); // Still mark as completed to unblock dependents
}
finally {
executing.delete(call.id);
}
});
await Promise.all(promises);
};
// Execute waves until all tools are processed
while (completed.size < toolCalls.length && executing.size === 0) {
await executeWave();
}
return Array.from(results.values());
}
/**
* Get statistics about batch execution
*/
getBatchStats() {
const totalPendingTools = Array.from(this.pendingBatches.values())
.reduce((sum, batch) => sum + batch.length, 0);
return {
pendingBatches: this.pendingBatches.size,
executingBatches: this.executingBatches.size,
totalPendingTools,
};
}
/**
* Clear all pending batches
*/
clearPendingBatches() {
this.pendingBatches.clear();
this.logger.info('Cleared all pending batches');
}
}
export const globalToolBatcher = new McpToolBatcher();
//# sourceMappingURL=mcp-tool-batcher.js.map