UNPKG

okta-mcp-server

Version:

Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching

193 lines 7 kB
import { logger } from '../../utils/logger.js'; import { buildQueryParams } from './utils.js'; /** * Parses the URI to extract query parameters for logs */ function parseLogsUri(uri) { const url = new URL(uri, 'okta://'); const params = url.searchParams; return { since: params.get('since') || undefined, until: params.get('until') || undefined, filter: params.get('filter') || undefined, q: params.get('q') || undefined, limit: params.get('limit') ? parseInt(params.get('limit'), 10) : 100, sortOrder: params.get('sortOrder') || 'DESCENDING', after: params.get('after') || undefined, }; } /** * Async generator for streaming system logs efficiently * Implements real-time log tailing with backpressure handling */ async function* streamLogs(okta, options) { const pageSize = Math.min(options.limit || 100, 1000); // Okta max is 1000 for logs let after = options.after; let totalFetched = 0; const maxToFetch = options.limit || Infinity; // Default to last 24 hours if no time range specified const since = options.since || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); const until = options.until || new Date().toISOString(); try { while (totalFetched < maxToFetch) { const currentPageSize = Math.min(pageSize, maxToFetch - totalFetched); logger.debug(`Fetching logs page with size ${currentPageSize}, after: ${after || 'start'}`); const response = await okta.getLogs(buildQueryParams({ since, until, filter: options.filter, q: options.q, limit: currentPageSize, sortOrder: options.sortOrder, after, })); const logs = response.data; // Yield the current page of logs yield logs; totalFetched += logs.length; // Check if there are more pages if (!response.headers?.link || logs.length < currentPageSize) { break; } // Parse the 'after' cursor from the Link header const linkHeader = response.headers.link; const afterMatch = linkHeader.match(/after=([^&>]+)/); if (!afterMatch) { break; } after = afterMatch[1]; } } catch (error) { logger.error('Error streaming logs:', error); throw error; } } /** * Handle system logs resource with real-time streaming capability */ export default async function handleLogsResource(uri, container) { const okta = container.resolve('okta'); const options = parseLogsUri(uri); logger.info(`Handling logs resource with options:`, options); try { // For real-time log streaming, we'll use a different approach if (uri.includes('stream=true')) { return handleStreamingLogs(uri, container, options); } // For regular log retrieval, fetch the first page const firstPageSize = Math.min(options.limit || 100, 1000); const since = options.since || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); const until = options.until || new Date().toISOString(); const firstPageResponse = await okta.getLogs(buildQueryParams({ since, until, filter: options.filter, q: options.q, limit: firstPageSize, sortOrder: options.sortOrder, after: options.after, })); const logs = firstPageResponse.data; // Build the response content const content = { uri, mimeType: 'application/json', text: JSON.stringify({ logs, metadata: { count: logs.length, timeRange: { since, until, }, filters: { filter: options.filter, q: options.q, sortOrder: options.sortOrder, }, }, }, null, 2), }; // TODO: Add support for pagination and streaming links when MCP supports it return content; } catch (error) { logger.error('Failed to handle logs resource:', error); throw error; } } /** * Handle streaming logs for real-time monitoring */ async function handleStreamingLogs(uri, container, options) { const okta = container.resolve('okta'); // For streaming, we'll return NDJSON (newline-delimited JSON) const logs = []; let count = 0; const maxLogs = Math.min(options.limit || 1000, 10000); // Cap at 10k for streaming // Collect logs for the stream for await (const logBatch of streamLogs(okta, { ...options, limit: maxLogs })) { for (const log of logBatch) { logs.push(log); count++; if (count >= maxLogs) break; } if (count >= maxLogs) break; } // Convert to NDJSON format const ndjson = logs.map((log) => JSON.stringify(log)).join('\n'); return { uri, mimeType: 'application/x-ndjson', text: ndjson, }; } /** * Stream all logs efficiently for bulk operations or monitoring * This is an additional export for programmatic use */ export async function* streamAllLogs(container, options = {}) { const okta = container.resolve('okta'); for await (const logBatch of streamLogs(okta, options)) { for (const log of logBatch) { yield log; } } } /** * Monitor logs in real-time with polling * This function polls for new logs at regular intervals */ export async function* monitorLogs(container, options = {}) { const okta = container.resolve('okta'); const pollingInterval = options.pollingInterval || 5000; // Default 5 seconds let lastSeen = new Date().toISOString(); while (true) { try { const response = await okta.getLogs(buildQueryParams({ since: lastSeen, filter: options.filter, q: options.q, limit: 100, sortOrder: 'ASCENDING', })); const logs = response.data; for (const log of logs) { yield log; // Update lastSeen to the timestamp of the last log lastSeen = log.published; } // Wait before next poll await new Promise((resolve) => setTimeout(resolve, pollingInterval)); } catch (error) { logger.error('Error monitoring logs:', error); // Wait a bit longer on error before retrying await new Promise((resolve) => setTimeout(resolve, pollingInterval * 2)); } } } //# sourceMappingURL=logs.js.map