okta-mcp-server
Version:
Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching
246 lines • 9.76 kB
JavaScript
import { logger } from '../../utils/logger.js';
import { progressTracker } from './progress-tracker.js';
import { RateLimiter } from './rate-limiter.js';
export class BatchProcessor {
client;
options;
rateLimiter;
constructor(client, options) {
this.client = client;
this.options = options;
// Initialize rate limiter for Okta API (default 600 requests per minute)
this.rateLimiter = new RateLimiter({
max: options.rateLimitPerMinute,
window: 60 * 1000, // 1 minute
});
}
/**
* Process items in batches with rate limiting
*/
async processBatches(items, operationId, processor) {
const results = [];
const totalBatches = Math.ceil(items.length / this.options.batchSize);
progressTracker.startOperation(operationId, totalBatches);
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
const start = batchIndex * this.options.batchSize;
const end = Math.min(start + this.options.batchSize, items.length);
const batch = items.slice(start, end);
logger.info(`Processing batch ${batchIndex + 1}/${totalBatches}`, {
operationId,
batchSize: batch.length,
progress: `${start + 1}-${end}/${items.length}`,
});
progressTracker.updateProgress(operationId, {
currentBatch: batchIndex + 1,
});
// Process batch items in parallel
const batchPromises = batch.map(async (item, index) => {
const globalIndex = start + index;
// Check rate limit before each request
const rateLimitResult = await this.rateLimiter.consume('okta-api');
if (!rateLimitResult.allowed) {
// Wait for rate limit reset
const waitTime = rateLimitResult.reset - Date.now();
logger.warn(`Rate limit reached, waiting ${waitTime}ms`, {
operationId,
remaining: rateLimitResult.remaining,
reset: new Date(rateLimitResult.reset).toISOString(),
});
await this.delay(waitTime);
// Retry after waiting
await this.rateLimiter.consume('okta-api');
}
try {
const result = await processor(item, globalIndex);
progressTracker.incrementProgress(operationId, result.success, !result.success && result.identifier
? {
identifier: result.identifier,
error: result.error?.message || 'Unknown error',
details: result.error,
}
: undefined);
return result;
}
catch (error) {
const errorResult = {
success: false,
error: error instanceof Error ? error : new Error('Unknown error'),
};
progressTracker.incrementProgress(operationId, false, {
error: errorResult.error?.message || 'Unknown error',
details: error,
});
if (!this.options.continueOnError) {
throw error;
}
return errorResult;
}
});
// Wait for batch to complete
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// Add delay between batches if specified
if (this.options.delayBetweenBatches && batchIndex < totalBatches - 1) {
logger.debug(`Waiting ${this.options.delayBetweenBatches}ms between batches`, {
operationId,
});
await this.delay(this.options.delayBetweenBatches);
}
// Check if operation was cancelled
const operation = progressTracker.getOperation(operationId);
if (operation?.status === 'cancelled') {
logger.info('Operation cancelled by user', { operationId });
break;
}
}
return results;
}
/**
* Process items with automatic retry on failure
*/
async processWithRetry(item, processor, maxRetries = 3, retryDelay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const data = await processor(item);
return { success: true, data };
}
catch (error) {
lastError = error instanceof Error ? error : new Error('Unknown error');
logger.warn(`Attempt ${attempt}/${maxRetries} failed`, {
error: lastError.message,
willRetry: attempt < maxRetries,
});
if (attempt < maxRetries) {
// Exponential backoff
const delay = retryDelay * Math.pow(2, attempt - 1);
await this.delay(delay);
}
}
}
return {
success: false,
error: lastError || new Error('Max retries exceeded'),
};
}
/**
* Helper to delay execution
*/
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Create users in batches
*/
async createUsersInBatches(users, operationId, options = {}) {
return this.processBatches(users, operationId, async (user, index) => {
try {
const queryParams = {
activate: options.activate ?? true,
provider: options.provider ?? false,
};
if (options.nextLogin !== undefined) {
queryParams.nextLogin = options.nextLogin;
}
const createdUser = await this.client.createUser(user, queryParams);
return {
success: true,
data: createdUser,
identifier: user.profile['email'] || user.profile['login'],
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to create user'),
identifier: user.profile['email'] ||
user.profile['login'] ||
`User ${index + 1}`,
};
}
});
}
/**
* Update users in batches
*/
async updateUsersInBatches(updates, operationId) {
return this.processBatches(updates, operationId, async (update) => {
try {
const updatedUser = await this.client.updateUser(update.userId, update.userData);
return {
success: true,
data: updatedUser,
identifier: update.userId,
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to update user'),
identifier: update.userId,
};
}
});
}
/**
* Delete/deactivate users in batches
*/
async deleteUsersInBatches(userIds, operationId, options) {
return this.processBatches(userIds, operationId, async (userId) => {
try {
if (options.action === 'deactivate') {
await this.client.deactivateUser(userId, options.sendEmail);
}
else {
// First deactivate, then delete
await this.client.deactivateUser(userId, false);
await this.client.deleteUser(userId);
}
return {
success: true,
identifier: userId,
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to delete user'),
identifier: userId,
};
}
});
}
/**
* Fetch all users matching a filter
*/
async *fetchAllUsers(filter, search) {
let after;
let hasMore = true;
while (hasMore) {
const params = {
limit: 200, // Max page size
};
if (after !== undefined)
params.after = after;
if (filter !== undefined)
params.filter = filter;
if (search !== undefined)
params.search = search;
const response = await this.client.listUsers(params);
yield response.data;
// Check for next page
const linkHeader = response.headers?.link;
if (linkHeader && linkHeader.includes('rel="next"')) {
const match = linkHeader.match(/<[^>]+after=([^&>]+)[^>]*>;\s*rel="next"/);
after = match ? match[1] : undefined;
hasMore = !!after;
}
else {
hasMore = false;
}
// Rate limit between pages
await this.rateLimiter.consume('okta-api');
}
}
}
//# sourceMappingURL=batch-processor.js.map