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
JavaScript
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