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
JavaScript
#!/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