UNPKG

stellar-cyber-mcp-agents

Version:

Model Context Protocol (MCP) server for Stellar Cyber security operations with specialized multi-agent analysis capabilities

1,035 lines • 254 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import dotenv from 'dotenv'; import axios from 'axios'; dotenv.config(); const STELLAR_API_URL = process.env.STELLAR_API_URL; const STELLAR_API_TOKEN = process.env.STELLAR_API_TOKEN; // Warn about missing configuration but don't prevent startup if (!STELLAR_API_URL || !STELLAR_API_TOKEN) { console.warn('Warning: STELLAR_API_URL and/or STELLAR_API_TOKEN environment variables are not set'); console.warn('Tools will not function properly without proper configuration'); console.warn('Please set these variables in your MCP client configuration or environment'); } class StellarAuthManager { accessToken = ''; refreshToken = ''; tokenExpires = 0; apiUrl; apiKey; constructor(apiUrl, apiKey) { this.apiUrl = apiUrl; this.apiKey = apiKey; } async authenticate() { try { console.error('[Auth] Starting authentication with API key...'); // Use the correct authentication endpoint from investigation-agent.ts const authEndpoint = '/connect/api/v1/access_token'; try { console.error(`[Auth] Using endpoint: ${authEndpoint}`); // Use the correct authentication method from investigation-agent.ts const response = await axios.post(`${this.apiUrl}${authEndpoint}`, {}, { headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, timeout: 10000, }); const data = response.data; if (data.access_token || data.token) { console.error('[Auth] Success! Got access token from /connect/api/v1/access_token'); this.accessToken = data.access_token || data.token || ''; this.refreshToken = data.refresh_token || ''; this.tokenExpires = Date.now() + ((data.expires_in || 3600) * 1000); return true; } } catch (error) { console.error(`[Auth] Authentication failed: ${error.response?.status} ${error.response?.statusText}`); if (error.response?.data) { console.error(`[Auth] Error details: ${JSON.stringify(error.response.data)}`); } } return false; } catch (error) { console.error('[Auth] Authentication error:', error.message); return false; } } async getValidAccessToken() { // Check if we have a token if (!this.accessToken) { console.error('[Auth] No access token, authenticating...'); const success = await this.authenticate(); if (!success) { throw new Error('Authentication failed'); } } // Check if token is expired or about to expire (within 5 minutes) if (Date.now() >= (this.tokenExpires - 300000)) { console.error('[Auth] Token expired or expiring soon, refreshing...'); if (this.refreshToken) { await this.refreshAccessToken(); } else { await this.authenticate(); } } return this.accessToken; } async refreshAccessToken() { try { console.error('[Auth] Attempting token refresh...'); // Use the same endpoint for refresh as in investigation-agent.ts const response = await axios.post(`${this.apiUrl}/connect/api/v1/access_token`, {}, { headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }, timeout: 10000, }); const data = response.data; if (data.access_token || data.token) { console.error('[Auth] Token refreshed successfully'); this.accessToken = data.access_token || data.token || ''; this.refreshToken = data.refresh_token || this.refreshToken; this.tokenExpires = Date.now() + ((data.expires_in || 3600) * 1000); } else { throw new Error('No access token in refresh response'); } } catch (error) { console.error('[Auth] Token refresh error:', error.message); // Fall back to re-authentication await this.authenticate(); } } async makeAuthenticatedRequest(method, url, options = {}) { const token = await this.getValidAccessToken(); const config = { method, url, ...options, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', ...options.headers, }, }; try { return await axios(config); } catch (error) { // If we get a 401, try to re-authenticate once if (error.response?.status === 401) { console.error('[Auth] Got 401, re-authenticating...'); const success = await this.authenticate(); if (success) { // Update the token and retry config.headers.Authorization = `Bearer ${this.accessToken}`; return await axios(config); } } throw error; } } } class StellarCyberMCPServer { server; authManager; constructor() { this.authManager = new StellarAuthManager(STELLAR_API_URL || '', STELLAR_API_TOKEN || ''); this.server = new Server({ name: 'stellar-cyber-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); this.setupErrorHandling(); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'get_case_details', description: 'Retrieve detailed information about a specific Stellar Cyber case', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, }, required: ['caseId'], }, }, { name: 'search_cases', description: 'Search for cases using various criteria', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query or filter criteria', }, limit: { type: 'number', description: 'Maximum number of cases to return', default: 10, }, }, required: ['query'], }, }, { name: 'get_system_status', description: 'Check the status of the Stellar Cyber API connection', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_case_timeline', description: 'Get a chronological timeline of events for a specific case', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, }, required: ['caseId'], }, }, { name: 'get_case_observables', description: 'Get observables (IOCs) associated with a specific case', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, }, required: ['caseId'], }, }, { name: 'get_case_alerts', description: 'Get alerts associated with a specific case', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, }, required: ['caseId'], }, }, { name: 'find_related_cases', description: 'Find cases related to a specific case by observables, temporal patterns, or behaviors', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case to find relationships for', }, method: { type: 'string', description: 'Correlation method: observables, temporal, or behavioral', enum: ['observables', 'temporal', 'behavioral'], default: 'observables', }, }, required: ['caseId'], }, }, { name: 'investigate_case', description: 'Perform a comprehensive investigation of a case with analysis and recommendations', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case to investigate', }, }, required: ['caseId'], }, }, { name: 'get_case_activities', description: 'Get activities and actions taken on a specific case', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, }, required: ['caseId'], }, }, { name: 'get_case_comments', description: 'Get comments and notes added to a specific case', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, }, required: ['caseId'], }, }, { name: 'get_case_scores', description: 'Get risk scores and threat assessments for a specific case', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, }, required: ['caseId'], }, }, { name: 'analyze_network_activity', description: 'Analyze network activity for a case including connections, protocols, and suspicious patterns', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, timeRange: { type: 'object', description: 'Optional time range filter', properties: { start: { type: 'string', description: 'Start timestamp' }, end: { type: 'string', description: 'End timestamp' } } } }, required: ['caseId'], }, }, { name: 'detect_lateral_movement', description: 'Detect lateral movement patterns within internal network infrastructure', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, timeRange: { type: 'object', description: 'Optional time range filter', properties: { start: { type: 'string', description: 'Start timestamp' }, end: { type: 'string', description: 'End timestamp' } } } }, required: ['caseId'], }, }, { name: 'analyze_case_observables', description: 'Perform enhanced analysis of case observables with threat intelligence and risk scoring', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The unique identifier for the case', }, observableTypes: { type: 'array', items: { type: 'string' }, description: 'Optional filter for specific observable types (ip, domain, hash, email, etc.)' }, includeReputation: { type: 'boolean', description: 'Include reputation scoring analysis', default: true } }, required: ['caseId'], }, }, { name: 'correlate_cases', description: 'Find and analyze relationships between cases based on shared indicators and attack patterns', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The primary case ID to find correlations for', }, timeWindow: { type: 'object', description: 'Time window for correlation analysis', properties: { days: { type: 'number', description: 'Number of days to look back/forward (default: 30)' }, start: { type: 'string', description: 'Start date (ISO format)' }, end: { type: 'string', description: 'End date (ISO format)' } } }, similarityThreshold: { type: 'number', description: 'Minimum similarity score for correlation (0.0-1.0, default: 0.3)', minimum: 0, maximum: 1 }, maxResults: { type: 'number', description: 'Maximum number of correlated cases to return (default: 20)', minimum: 1, maximum: 100 } }, required: ['caseId'], }, }, { name: 'analyze_malware', description: 'Comprehensive malware analysis of artifacts with static analysis, YARA scanning, and family identification', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The case ID containing malware artifacts to analyze', }, options: { type: 'object', description: 'Analysis options and preferences', properties: { staticAnalysis: { type: 'boolean', description: 'Enable static analysis (default: true)' }, yaraScanning: { type: 'boolean', description: 'Enable YARA rule scanning (default: true)' }, virusTotalLookup: { type: 'boolean', description: 'Enable VirusTotal lookup (default: true)' }, sandboxAnalysis: { type: 'boolean', description: 'Enable sandbox analysis (default: false)' }, generateIOCs: { type: 'boolean', description: 'Generate indicators of compromise (default: true)' } } } }, required: ['caseId'], }, }, { name: 'analyze_artifact', description: 'Analyze individual artifact for malware indicators and behavior patterns', inputSchema: { type: 'object', properties: { artifactId: { type: 'string', description: 'The artifact ID or hash to analyze', }, artifactType: { type: 'string', enum: ['file', 'hash', 'url', 'domain', 'ip', 'email'], description: 'Type of artifact to analyze' }, options: { type: 'object', description: 'Analysis options', properties: { deepAnalysis: { type: 'boolean', description: 'Enable deep analysis (default: false)' }, includeMetadata: { type: 'boolean', description: 'Include detailed metadata (default: true)' } } } }, required: ['artifactId', 'artifactType'], }, }, { name: 'sandbox_analysis', description: 'Perform dynamic sandbox analysis of malware samples in isolated environment', inputSchema: { type: 'object', properties: { artifactId: { type: 'string', description: 'The artifact ID to analyze in sandbox', }, sandboxType: { type: 'string', enum: ['windows', 'linux', 'auto'], description: 'Sandbox environment type (default: auto)', default: 'auto' }, timeout: { type: 'number', description: 'Analysis timeout in seconds (default: 300)', minimum: 60, maximum: 1800, default: 300 } }, required: ['artifactId'], }, }, { name: 'identify_family', description: 'Identify malware family and variant classification for analyzed artifacts', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The case ID to analyze for malware families', }, confidence: { type: 'number', description: 'Minimum confidence threshold for family identification (0.0-1.0, default: 0.7)', minimum: 0, maximum: 1, default: 0.7 } }, required: ['caseId'], }, }, { name: 'generate_iocs', description: 'Generate indicators of compromise (IOCs) from malware analysis results', inputSchema: { type: 'object', properties: { caseId: { type: 'string', description: 'The case ID to generate IOCs for', }, iocTypes: { type: 'array', items: { type: 'string', enum: ['hash', 'ip', 'domain', 'url', 'registry', 'file', 'network'] }, description: 'Types of IOCs to generate (default: all types)' }, format: { type: 'string', enum: ['json', 'stix', 'csv', 'text'], description: 'Output format for IOCs (default: json)', default: 'json' } }, required: ['caseId'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; switch (name) { case 'get_case_details': return await this.getCaseDetails(args?.caseId); case 'search_cases': return await this.searchCases(args?.query, args?.limit); case 'get_system_status': return await this.getSystemStatus(); case 'get_case_timeline': return await this.getCaseTimeline(args?.caseId); case 'get_case_observables': return await this.getCaseObservables(args?.caseId); case 'get_case_alerts': return await this.getCaseAlerts(args?.caseId); case 'find_related_cases': return await this.findRelatedCases(args?.caseId, args?.method); case 'investigate_case': return await this.investigateCase(args?.caseId); case 'get_case_activities': return await this.getCaseActivities(args?.caseId); case 'get_case_comments': return await this.getCaseComments(args?.caseId); case 'get_case_scores': return await this.getCaseScores(args?.caseId); case 'analyze_network_activity': return await this.analyzeNetworkActivity(args?.caseId, args?.timeRange); case 'detect_lateral_movement': return await this.detectLateralMovement(args?.caseId, args?.timeRange); case 'analyze_case_observables': return await this.analyzeCaseObservables(args?.caseId, args?.observableTypes, args?.includeReputation); case 'correlate_cases': return await this.correlateCases(args?.caseId, args?.timeWindow, args?.similarityThreshold, args?.maxResults); case 'analyze_malware': return await this.analyzeMalware(args?.caseId, args?.options); case 'analyze_artifact': return await this.analyzeArtifact(args?.artifactId, args?.artifactType, args?.options); case 'sandbox_analysis': return await this.sandboxAnalysis(args?.artifactId, args?.sandboxType, args?.timeout); case 'identify_family': return await this.identifyFamily(args?.caseId, args?.confidence); case 'generate_iocs': return await this.generateIOCs(args?.caseId, args?.iocTypes, args?.format); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message}`); } }); } getApiUrl() { if (!STELLAR_API_URL) { throw new McpError(ErrorCode.InternalError, 'Configuration error: STELLAR_API_URL environment variable is not set. Please check your MCP client configuration.'); } return STELLAR_API_URL; } extractObservables(responseData) { // Extract observables from different response formats if (responseData?.data?.observables && Array.isArray(responseData.data.observables)) { return responseData.data.observables; } else if (responseData?.observables && Array.isArray(responseData.observables)) { return responseData.observables; } else if (Array.isArray(responseData)) { return responseData; } return []; } async getCaseDetails(caseId) { if (!caseId) { throw new McpError(ErrorCode.InvalidParams, 'Case ID is required'); } try { const response = await this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}`); return { content: [ { type: 'text', text: `Case Details for ${caseId}:\n\n${JSON.stringify(response.data, null, 2)}`, }, ], }; } catch (error) { const errorMessage = error.response?.data?.message || error.message; return { content: [ { type: 'text', text: `Error retrieving case ${caseId}: ${errorMessage}`, }, ], }; } } async searchCases(query, limit = 10) { if (!query) { throw new McpError(ErrorCode.InvalidParams, 'Search query is required'); } try { const response = await this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases`, { params: { limit: Math.min(limit, 20), // API fails with 500 error above 20 sort: '-created_at' }, }); let cases = response.data.data?.cases || []; // Apply client-side filtering if query is provided (like investigation-agent.ts) if (query && query.trim() && query !== '*') { const searchTerm = query.toLowerCase(); cases = cases.filter((caseItem) => { return ((caseItem.name && caseItem.name.toLowerCase().includes(searchTerm)) || (caseItem.description && caseItem.description.toLowerCase().includes(searchTerm)) || (caseItem._id && caseItem._id.toString().includes(searchTerm)) || (caseItem.status && caseItem.status.toLowerCase().includes(searchTerm)) || (caseItem.severity && caseItem.severity.toLowerCase().includes(searchTerm))); }); } const resultsText = cases.length > 0 ? cases.map((c) => `- Case ${c._id || 'unknown'}: ${c.name || 'No name'} (${c.status || 'Unknown status'})`).join('\n') : 'No cases found matching your search criteria.'; return { content: [ { type: 'text', text: `Search Results (${cases.length} cases found):\n\n${resultsText}`, }, ], }; } catch (error) { const errorMessage = error.response?.data?.message || error.message; return { content: [ { type: 'text', text: `Error searching cases: ${errorMessage}`, }, ], }; } } async getSystemStatus() { try { // First, ensure authentication is working const authSuccess = await this.authManager.authenticate(); if (!authSuccess) { return { content: [ { type: 'text', text: `āŒ Authentication Failed:\n\nšŸ”— Endpoint: ${this.getApiUrl()}\nā— Error: Unable to authenticate with provided token\n\nPlease check:\n- API token is valid\n- Token has proper permissions\n- API URL is correct`, }, ], }; } // Try multiple endpoints to verify system health const testEndpoints = [ '/connect/api/v1/user/profile', '/connect/api/v1/health', '/connect/api/v1/cases' ]; const results = []; for (const endpoint of testEndpoints) { try { const response = await this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}${endpoint}`, { timeout: 5000, params: endpoint === '/connect/api/v1/cases' ? { limit: 1 } : {} }); results.push(`āœ… ${endpoint}: ${response.status} ${response.statusText}`); } catch (error) { results.push(`āŒ ${endpoint}: ${error.response?.status || 'ERROR'} ${error.response?.statusText || error.message}`); } } return { content: [ { type: 'text', text: `Stellar Cyber System Status:\n\nšŸ” Authentication: āœ… Success\nšŸ“ Endpoint: ${this.getApiUrl()}\n\nšŸ“Š Endpoint Health:\n${results.join('\n')}`, }, ], }; } catch (error) { const errorMessage = error.response?.data?.message || error.message; return { content: [ { type: 'text', text: `āŒ System Status Check Failed:\n\nšŸ”— Endpoint: ${this.getApiUrl()}\nā— Error: ${errorMessage}\n\nPlease check:\n- API URL is correct\n- API token is valid\n- Network connectivity`, }, ], }; } } async getCaseTimeline(caseId) { if (!caseId) { throw new McpError(ErrorCode.InvalidParams, 'Case ID is required'); } try { const response = await this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}/activities`); const activities = response.data.data || response.data.activities || []; if (activities.length === 0) { return { content: [{ type: 'text', text: `No timeline activities found for case ${caseId}.` }] }; } // Sort activities by timestamp activities.sort((a, b) => { const aTime = new Date(a.timestamp || a.created_at || a.createdAt).getTime(); const bTime = new Date(b.timestamp || b.created_at || b.createdAt).getTime(); return aTime - bTime; }); const timelineText = activities.map((activity, index) => { const timestamp = activity.timestamp || activity.created_at || activity.createdAt || 'Unknown time'; const action = activity.action || activity.type || 'Unknown action'; const details = activity.description || activity.details || ''; const user = activity.user || activity.actor || 'System'; return `${index + 1}. [${timestamp}] ${action} by ${user}${details ? ': ' + details : ''}`; }).join('\n'); return { content: [{ type: 'text', text: `Timeline for Case ${caseId} (${activities.length} activities):\n\n${timelineText}` }] }; } catch (error) { const errorMessage = error.response?.data?.message || error.message; return { content: [{ type: 'text', text: `Error retrieving timeline for case ${caseId}: ${errorMessage}` }] }; } } async getCaseObservables(caseId) { if (!caseId) { throw new McpError(ErrorCode.InvalidParams, 'Case ID is required'); } try { const response = await this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}/observables`); // Debug: Check the actual response structure // Debug: Observables response structure (console output disabled for MCP) let observables = []; // Try multiple possible data structure paths if (response.data.data?.observables && Array.isArray(response.data.data.observables)) { observables = response.data.data.observables; } else if (response.data.observables && Array.isArray(response.data.observables)) { observables = response.data.observables; } else if (response.data.data && Array.isArray(response.data.data)) { observables = response.data.data; } else if (Array.isArray(response.data)) { observables = response.data; } else { // If we can't find an array, show the structure for debugging return { content: [{ type: 'text', text: `Observables data structure debug for case ${caseId}:\n\nReceived data type: ${typeof response.data}\nData keys: ${response.data ? Object.keys(response.data).join(', ') : 'none'}\n\nFull response: ${JSON.stringify(response.data, null, 2)}` }] }; } if (!observables || observables.length === 0) { return { content: [{ type: 'text', text: `No observables found for case ${caseId}.` }] }; } const observableText = observables.map((obs, index) => { const type = obs.type || obs.observable_type || 'unknown'; const value = obs.value || obs.indicator || obs.observable_value || 'unknown'; const tlp = obs.tlp ? ` (TLP: ${obs.tlp})` : ''; const tags = (obs.tags && Array.isArray(obs.tags) && obs.tags.length > 0) ? ` [${obs.tags.join(', ')}]` : ''; return `${index + 1}. ${type}: ${value}${tlp}${tags}`; }).join('\n'); return { content: [{ type: 'text', text: `Observables for Case ${caseId} (${observables.length} items):\n\n${observableText}` }] }; } catch (error) { const errorMessage = error.response?.data?.message || error.message; return { content: [{ type: 'text', text: `Error retrieving observables for case ${caseId}: ${errorMessage}` }] }; } } async getCaseAlerts(caseId) { if (!caseId) { throw new McpError(ErrorCode.InvalidParams, 'Case ID is required'); } try { const response = await this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}/alerts`, { params: { skip: 0, limit: 50 } }); const alerts = response.data.data?.docs || response.data.alerts || []; if (alerts.length === 0) { return { content: [{ type: 'text', text: `No alerts found for case ${caseId}.` }] }; } const alertText = alerts.map((alert, index) => { const name = alert.name || alert.title || 'Unnamed alert'; const severity = alert.severity || 'unknown'; const timestamp = alert.timestamp || alert.created_at || alert.createdAt || 'unknown time'; const source = alert.source || alert.detector || 'unknown source'; return `${index + 1}. [${severity.toUpperCase()}] ${name} - ${source} (${timestamp})`; }).join('\n'); return { content: [{ type: 'text', text: `Alerts for Case ${caseId} (${alerts.length} items):\n\n${alertText}` }] }; } catch (error) { const errorMessage = error.response?.data?.message || error.message; return { content: [{ type: 'text', text: `Error retrieving alerts for case ${caseId}: ${errorMessage}` }] }; } } async findRelatedCases(caseId, method = 'observables') { if (!caseId) { throw new McpError(ErrorCode.InvalidParams, 'Case ID is required'); } try { // Try the dedicated related cases endpoint first const queryParams = method ? `?method=${method}` : ''; const response = await this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}/related${queryParams}`); const relatedCases = response.data.relatedCases || response.data.data || []; if (relatedCases.length === 0) { return { content: [{ type: 'text', text: `No related cases found for case ${caseId} using ${method} correlation.` }] }; } const relatedText = relatedCases.map((relatedId, index) => { return `${index + 1}. Case ${relatedId}`; }).join('\n'); return { content: [{ type: 'text', text: `Cases related to ${caseId} (${method} correlation, ${relatedCases.length} found):\n\n${relatedText}` }] }; } catch (error) { // If related endpoint doesn't exist, provide manual guidance (no cascading API calls) if (error.response?.status === 404 || error.response?.status === 500) { // Related cases endpoint unavailable, providing manual guidance try { // Get basic case info for guidance const currentCase = await this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}`); let guidanceText = `**Related Cases Analysis** (Endpoint unavailable)\n`; guidanceText += `Case ${caseId}: ${currentCase.data.name || 'Unknown'}\n`; guidanceText += `Severity: ${currentCase.data.severity || 'Unknown'}\n`; guidanceText += `Status: ${currentCase.data.status || 'Unknown'}\n\n`; guidanceText += `**Manual Search Suggestions:**\n`; guidanceText += `1. Use search_cases with case name keywords\n`; guidanceText += `2. Filter by same severity level: "${currentCase.data.severity || 'N/A'}"\n`; guidanceText += `3. Search cases from same time period\n`; guidanceText += `4. Look for cases with similar status: "${currentCase.data.status || 'N/A'}"\n\n`; guidanceText += `**Alternative Tools:**\n`; guidanceText += `- analyze_case_observables: Extract key indicators from this case\n`; guidanceText += `- search_cases: Search by specific terms or criteria\n`; guidanceText += `- correlate_cases: Attempt correlation (may have limited results)\n\n`; guidanceText += `**Recommended Workflow:**\n`; guidanceText += `1. Run analyze_case_observables to get key indicators\n`; guidanceText += `2. Use search_cases to find cases with those indicators\n`; guidanceText += `3. Manually review results for similarities\n`; return { content: [{ type: 'text', text: guidanceText }] }; } catch (basicError) { let basicFallback = `**Related Cases Analysis** (Service unavailable)\n`; basicFallback += `Case ${caseId} - Related cases service is currently unavailable.\n\n`; basicFallback += `**Alternative approaches:**\n`; basicFallback += `1. Use 'search_cases' with specific keywords\n`; basicFallback += `2. Try 'analyze_case_observables' to extract key indicators\n`; basicFallback += `3. Search by case attributes (severity, status, timeframe)\n`; basicFallback += `4. Manual review of recent cases with similar characteristics\n\n`; basicFallback += `**Note:** Related cases functionality may be restored after service recovery.\n`; return { content: [{ type: 'text', text: basicFallback }] }; } } // For other errors, provide general guidance const errorMessage = error.response?.data?.message || error.message; let fallbackText = `Error finding related cases for ${caseId}: ${errorMessage}\n\n`; fallbackText += `**Alternative approaches:**\n`; fallbackText += `1. Try using the 'correlate_cases' tool instead\n`; fallbackText += `2. Use 'search_cases' with specific criteria\n`; fallbackText += `3. Analyze case observables first, then search for matches\n`; fallbackText += `4. Use search_cases with relevant keywords from this case\n`; fallbackText += `5. Search for cases with same severity or status\n`; return { content: [{ type: 'text', text: fallbackText }] }; } } async investigateCase(caseId) { if (!caseId) { throw new McpError(ErrorCode.InvalidParams, 'Case ID is required'); } try { // Gather comprehensive case information const [caseDetails, observables, alerts, activities] = await Promise.allSettled([ this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}`), this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}/observables`), this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}/alerts`, { params: { limit: 10 } }), this.authManager.makeAuthenticatedRequest('GET', `${this.getApiUrl()}/connect/api/v1/cases/${caseId}/activities`) ]); let investigation = `Comprehensive Investigation of Case ${caseId}\n`; investigation += `=====================================\n\n`; // Case Details if (caseDetails.status === 'fulfilled') { const details = caseDetails.value.data; investigation += `**Case Overview:**\n`; investigation += `- Name: ${details.name || 'Unknown'}\n`; investigation += `- Status: ${details.status || 'Unknown'}\n`; investigation += `- Severity: ${details.severity || 'Unknown'}\n`; investigation += `- Created: ${details.created_at || details.createdAt || 'Unknown'}\n`; investigation += `- Score: ${details.score || 'N/A'}\n\n`; } // Observables Analysis if (observables.status === 'fulfilled') { let obs = []; const obsData = observables.value.data; // Try multiple possible data structure paths if (obsData.data?.observables && Array.isArray(obsData.data.observables)) { obs = obsData.data.observables; } else if (obsData.observables && Array.isArray(obsData.observables)) { obs = obsData.observables; } else if (obsData.data && Array.isArray(obsData.data)) { obs = obsData.data; } else if (Array.isArray(obsData)) { obs = obsData; } investigation += `**Observables (${obs.length} f