UNPKG

okta-mcp-server

Version:

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

471 lines 19.7 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { CachedOktaClient } from '../lib/cached-okta-client.js'; import { OktaClientFactory } from '../mocks/okta-client-factory.js'; import { CacheFactory } from '../infrastructure/cache/index.js'; import { AuditFactory, AuditMiddleware } from '../infrastructure/audit/index.js'; import { tools } from '../tools/index.js'; import { resources } from '../resources/index.js'; import { protocolSafeLogger as logger } from '../utils/protocol-safe-logger.js'; import { NotFoundError, OktaError, } from '../utils/errors.js'; import { getErrorMessage, getSafeError } from '../utils/error-utils.js'; import { Container } from './container.js'; import { EventBus } from './event-bus.js'; import { env } from '../config/index.js'; import { SimpleAuthMiddleware } from './middleware/oauth-middleware.js'; import { ReadOnlyMiddleware } from './middleware/read-only-middleware.js'; /** * Core MCP Server for Okta operations * Implements the Model Context Protocol with Okta integration */ export class OktaMCPServer { server; okta; cache; auditLogger; auditMiddleware; authMiddleware; readOnlyMiddleware; container; eventBus; config; constructor(config) { this.config = config; this.eventBus = new EventBus(); this.container = new Container(); } async initialize() { this.server = new Server({ name: this.config.name || 'okta-mcp-server', version: this.config.version || '1.0.0', }, { capabilities: { tools: {}, resources: {}, }, }); // Container and EventBus already initialized in constructor // Initialize cache and audit synchronously (will be properly initialized in start()) this.cache = null; // Will be initialized in start() this.auditLogger = null; // Will be initialized in start() this.auditMiddleware = null; // Will be initialized in start() // Initialize Okta client using factory const mockConfig = { userCount: parseInt(process.env['MOCK_USER_COUNT'] || '1000', 10), groupCount: parseInt(process.env['MOCK_GROUP_COUNT'] || '100', 10), appCount: parseInt(process.env['MOCK_APP_COUNT'] || '50', 10), policyCount: parseInt(process.env['MOCK_POLICY_COUNT'] || '20', 10), enableAuth: process.env['MOCK_ENABLE_AUTH'] !== 'false', enableRateLimit: process.env['MOCK_ENABLE_RATE_LIMIT'] !== 'false', enableErrors: process.env['MOCK_ENABLE_ERRORS'] !== 'false', enableDelays: process.env['MOCK_ENABLE_DELAYS'] !== 'false', errorRate: process.env['MOCK_ERROR_RATE'] ? parseFloat(process.env['MOCK_ERROR_RATE']) : 0.02, networkConditions: process.env['MOCK_NETWORK_CONDITIONS'] || 'normal', }; if (process.env['MOCK_SEED']) { mockConfig.seed = parseInt(process.env['MOCK_SEED'], 10); } const baseClient = await OktaClientFactory.create({ useMock: env.USE_MOCK_OKTA, realConfig: this.config.okta, mockConfig, }); // Store base client temporarily (use type assertion for MockOktaClient compatibility) this.okta = baseClient; // Register services in container this.registerServices(); // Set up request handlers this.registerHandlers(); } /** * Initialize cache based on configuration */ async initializeCache() { try { this.cache = await CacheFactory.create(this.config.cache || { enabled: false }, this.eventBus); logger.info('Cache initialized successfully'); } catch (error) { logger.error('Failed to initialize cache, falling back to memory cache:', getSafeError(error)); // Fall back to memory cache const { MemoryCache } = await import('../infrastructure/cache/memory-cache.js'); this.cache = new MemoryCache({ maxSize: 100, eventBus: this.eventBus }); } } /** * Initialize audit logging system */ async initializeAudit() { try { // Create audit logger with configuration this.auditLogger = AuditFactory.createLogger({ eventBus: this.eventBus, dbPath: this.config.audit?.dbPath, retentionPolicy: this.config.audit?.retention, enableIntegrityChecks: this.config.audit?.enableIntegrityChecks !== false, performanceTracking: this.config.audit?.performanceTracking !== false, }); // Create audit middleware this.auditMiddleware = AuditFactory.createMiddleware(this.auditLogger, { performanceThreshold: this.config.audit?.performanceThreshold, }); logger.info('Audit system initialized successfully'); } catch (error) { logger.error('Failed to initialize audit system:', getSafeError(error)); // Audit is critical for compliance - don't fall back silently throw error; } } /** * Initialize simple auth middleware for MCP OAuth protection */ async initializeAuth() { // Check if MCP auth protection is enabled if (!this.config.auth?.required) { logger.debug('MCP OAuth protection is disabled'); return; } try { this.authMiddleware = new SimpleAuthMiddleware(this.config.auth); logger.info('Auth middleware initialized successfully'); } catch (error) { logger.error('Failed to initialize auth middleware:', getSafeError(error)); // Auth is optional - continue without it this.authMiddleware = undefined; } } /** * Initialize read-only middleware for production safety */ async initializeReadOnlyMode() { try { this.readOnlyMiddleware = new ReadOnlyMiddleware(this.config); if (this.config.features?.readOnlyMode) { logger.warn('🔒 READ-ONLY MODE ACTIVE: Write operations are disabled for safety'); } } catch (error) { logger.error('Failed to initialize read-only middleware:', getSafeError(error)); throw error; // Read-only mode is critical for safety } } /** * Register services in the dependency injection container */ registerServices() { this.container.register('okta', () => this.okta); this.container.register('cache', () => this.cache); this.container.register('eventBus', () => this.eventBus); this.container.register('logger', () => logger); this.container.register('config', () => this.config); this.container.register('auditLogger', () => this.auditLogger); this.container.register('auditMiddleware', () => this.auditMiddleware); this.container.register('authMiddleware', () => this.authMiddleware); this.container.register('readOnlyMiddleware', () => this.readOnlyMiddleware); } /** * Wrap request handler with auth validation */ wrapWithAuth(handler) { return async (request) => { // If auth middleware is not enabled, proceed normally if (!this.authMiddleware) { return handler(request); } try { // Validate authentication await this.authMiddleware.authenticate(request); // Call the original handler return await handler(request); } catch (error) { // Re-throw auth errors as-is (they're already MCP errors) throw error; } }; } /** * Register MCP request handlers */ registerHandlers() { // List tools handler this.server.setRequestHandler(ListToolsRequestSchema, this.wrapWithAuth(async (request) => { logger.debug('Listing tools'); this.eventBus.emit('tools:list', { request }); return { tools }; })); // List resources handler this.server.setRequestHandler(ListResourcesRequestSchema, this.wrapWithAuth(async (request) => { logger.debug('Listing resources'); this.eventBus.emit('resources:list', { request }); return { resources }; })); // Read resource handler with audit logging this.server.setRequestHandler(ReadResourceRequestSchema, this.wrapWithAuth(async (request) => { const { uri } = request.params; logger.debug(`Reading resource: ${uri}`); this.eventBus.emit('resources:read', { uri, request }); const startTime = Date.now(); const match = uri.match(/^okta:\/\/([^/]+)(?:\/(.+))?/); const resourceType = match?.[1] || 'unknown'; const resourceId = match?.[2] || 'all'; try { // Dynamic resource handler loading const handler = await this.loadResourceHandler(uri); const content = await handler(uri, this.container); // Log successful resource read await this.auditLogger.log({ timestamp: new Date(startTime), actor: AuditMiddleware.createActor(request), action: { type: 'resource.read', method: resourceType, result: 'success', }, resource: { type: resourceType, id: resourceId, }, request: { method: 'read', path: uri, }, response: { status: 200, }, performance: { duration: Date.now() - startTime, }, context: AuditMiddleware.createContext(request), }); return { contents: [content], }; } catch (error) { // Log error await this.auditLogger.log({ timestamp: new Date(startTime), actor: AuditMiddleware.createActor(request), action: { type: 'resource.read', method: resourceType, result: 'error', errorCode: error.code || 'UNKNOWN', errorMessage: getErrorMessage(error), }, resource: { type: resourceType, id: resourceId, }, request: { method: 'read', path: uri, }, response: { status: error.status || 500, error: String(error), }, performance: { duration: Date.now() - startTime, }, context: AuditMiddleware.createContext(request), }); if (error instanceof NotFoundError) { throw error; } throw new OktaError(`Failed to read resource: ${getErrorMessage(error)}`); } })); // Call tool handler with audit logging this.server.setRequestHandler(CallToolRequestSchema, this.wrapWithAuth(async (request) => { const { name, arguments: args } = request.params; logger.debug(`Calling tool: ${name}`, { args }); this.eventBus.emit('tools:call', { name, args, request }); const startTime = Date.now(); let result; let error; try { // Check read-only mode before executing tool this.readOnlyMiddleware.checkOperation(name); // Import the centralized tool handler const { handleToolCall } = await import('../tools/handlers.js'); // Determine if this tool needs Container or IOktaClient const needsContainer = name.startsWith('queryAudit') || name.startsWith('getAudit') || name.startsWith('exportAudit') || name.startsWith('checkAudit') || name.startsWith('generateCompliance') || name === 'getReadOnlyStatus'; // Pass container for tools that need it, okta client for others result = await handleToolCall(name, args, needsContainer ? this.container : this.okta); // Log successful tool call await this.auditLogger.log({ timestamp: new Date(startTime), actor: AuditMiddleware.createActor(request), action: { type: 'tool.call', method: name, result: result.isError ? 'failure' : 'success', }, request: { method: name, parameters: args, }, response: { status: result.isError ? 400 : 200, }, performance: { duration: Date.now() - startTime, }, context: AuditMiddleware.createContext(request), }); return result; } catch (err) { error = err; // Log error await this.auditLogger.log({ timestamp: new Date(startTime), actor: AuditMiddleware.createActor(request), action: { type: 'tool.call', method: name, result: 'error', errorCode: err.code || 'UNKNOWN', errorMessage: getErrorMessage(err), }, request: { method: name, parameters: args, }, response: { status: 500, error: String(err), }, performance: { duration: Date.now() - startTime, }, context: AuditMiddleware.createContext(request), }); if (error instanceof NotFoundError) { throw error; } throw new OktaError(`Failed to execute tool: ${getErrorMessage(error)}`); } })); } /** * Register OAuth metadata endpoint for MCP OAuth discovery */ registerOAuthMetadataEndpoint() { // Register the oauth/metadata handler this.server.setRequestHandler('oauth/metadata', async () => { if (!this.config.auth) { throw new McpError(ErrorCode.InvalidRequest, 'OAuth protection is not configured for this server'); } return { issuer: this.config.auth.issuer || 'https://your-auth-provider.com', audience: this.config.auth.audience || 'https://okta-mcp-server', required: this.config.auth.required, }; }); } /** * Dynamically load a resource handler */ async loadResourceHandler(uri) { // Extract resource type from URI (e.g., okta://users -> users) const match = uri.match(/^okta:\/\/([^/]+)/); if (!match?.[1]) { throw new NotFoundError(`Invalid resource URI: ${uri}`); } const resourceType = match[1]; try { // Dynamic import of resource handler const module = await import(`../resources/handlers/${resourceType}.js`); return (module.default || module[`handle${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)}`]); } catch (error) { throw new NotFoundError(`Resource handler not found for: ${resourceType}`); } } /** * Start the server with stdio transport */ async start() { try { logger.info(`Starting ${this.config.name} v${this.config.version}...`); // Initialize the server first await this.initialize(); // Initialize cache await this.initializeCache(); // Initialize audit system await this.initializeAudit(); // Initialize auth middleware await this.initializeAuth(); // Initialize read-only middleware await this.initializeReadOnlyMode(); // Register OAuth metadata endpoint if auth is configured if (this.config.auth) { this.registerOAuthMetadataEndpoint(); } // Wrap Okta client with caching if enabled if (this.config.cache?.enabled && this.config.features?.caching !== false) { const baseClient = this.okta; this.okta = new CachedOktaClient({ client: baseClient, cache: this.cache, defaultTtl: this.config.cache?.ttl || 300, }); // Update container registration with proper type this.container.registerValue('okta', this.okta); } const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info(`${this.config.name} running on stdio`); this.eventBus.emit('server:started', { config: this.config }); } catch (error) { logger.error('Failed to start server:', getSafeError(error)); this.eventBus.emit('server:error', { error: getSafeError(error) }); throw error; } } /** * Stop the server gracefully */ async stop() { logger.info('Stopping server...'); this.eventBus.emit('server:stopping', {}); // Close connections await this.okta.close(); // Close cache if it has a close method if (this.cache && 'close' in this.cache && typeof this.cache.close === 'function') { await this.cache.close(); } // Close audit logger if (this.auditLogger && typeof this.auditLogger.close === 'function') { this.auditLogger.close(); } logger.info('Server stopped'); this.eventBus.emit('server:stopped', {}); } /** * Get the event bus for external event handling */ getEventBus() { return this.eventBus; } /** * Get the dependency injection container */ getContainer() { return this.container; } } //# sourceMappingURL=server.js.map