okta-mcp-server
Version:
Model Context Protocol (MCP) server for Okta API operations with support for bulk operations and caching
193 lines • 7 kB
JavaScript
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