Babashka MCP Server

  • src
#!/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 axios from 'axios'; import { LATEST_REPORTS_QUERY, SEARCH_MALWARE_QUERY, SEARCH_INDICATORS_QUERY, SEARCH_THREAT_ACTORS_QUERY, } from './queries/reports.js'; import { USER_BY_ID_QUERY, ALL_USERS_QUERY, ALL_GROUPS_QUERY, ALL_ROLES_QUERY, ALL_CAPABILITIES_QUERY, } from './queries/users.js'; import { REPORT_BY_ID_QUERY, ALL_ATTACK_PATTERNS_QUERY, CAMPAIGN_BY_NAME_QUERY, ALL_STIX_CORE_OBJECTS_QUERY, ALL_STIX_DOMAIN_OBJECTS_QUERY, } from './queries/stix_objects.js'; import { ALL_STIX_CORE_RELATIONSHIPS_QUERY, ALL_STIX_SIGHTING_RELATIONSHIPS_QUERY, ALL_STIX_REF_RELATIONSHIPS_QUERY, ALL_STIX_RELATIONSHIPS_QUERY, } from './queries/relationships.js'; import { ALL_CONNECTORS_QUERY, ALL_STATUS_TEMPLATES_QUERY, ALL_STATUSES_QUERY, ALL_SUB_TYPES_QUERY, ALL_RETENTION_RULES_QUERY, ALL_BACKGROUND_TASKS_QUERY, ALL_FEEDS_QUERY, ALL_TAXII_COLLECTIONS_QUERY, ALL_STREAM_COLLECTIONS_QUERY, ALL_RULES_QUERY, ALL_SYNCHRONIZERS_QUERY, } from './queries/system.js'; import { FILE_BY_ID_QUERY, ALL_FILES_QUERY, ALL_INDEXED_FILES_QUERY, ALL_LOGS_QUERY, ALL_AUDITS_QUERY, ALL_ATTRIBUTES_QUERY, ALL_SCHEMA_ATTRIBUTE_NAMES_QUERY, ALL_FILTER_KEYS_SCHEMA_QUERY, } from './queries/metadata.js'; import { ALL_MARKING_DEFINITIONS_QUERY, ALL_LABELS_QUERY, ALL_EXTERNAL_REFERENCES_QUERY, ALL_KILL_CHAIN_PHASES_QUERY, } from './queries/references.js'; const OPENCTI_URL = process.env.OPENCTI_URL || 'http://localhost:8080'; const OPENCTI_TOKEN = process.env.OPENCTI_TOKEN; if (!OPENCTI_TOKEN) { throw new Error('OPENCTI_TOKEN environment variable is required'); } interface OpenCTIResponse { data: { stixObjects: Array<{ id: string; name?: string; description?: string; created_at?: string; modified_at?: string; pattern?: string; valid_from?: string; valid_until?: string; x_opencti_score?: number; [key: string]: any; }>; }; } class OpenCTIServer { private server: Server; private axiosInstance; constructor() { this.server = new Server( { name: 'opencti-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.axiosInstance = axios.create({ baseURL: OPENCTI_URL, headers: { 'Authorization': `Bearer ${OPENCTI_TOKEN}`, 'Content-Type': 'application/json', }, }); this.setupTools(); this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupTools() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // Reports { name: 'get_latest_reports', description: '獲取最新的OpenCTI報告', inputSchema: { type: 'object', properties: { first: { type: 'number', description: '返回結果數量限制', default: 10, }, }, }, }, { name: 'get_report_by_id', description: '根據ID獲取OpenCTI報告', inputSchema: { type: 'object', properties: { id: { type: 'string', description: '報告ID', }, }, required: ['id'], }, }, // Search { name: 'search_indicators', description: '搜尋OpenCTI中的指標', inputSchema: { type: 'object', properties: { query: { type: 'string', description: '搜尋關鍵字', }, first: { type: 'number', description: '返回結果數量限制', default: 10, }, }, required: ['query'], }, }, { name: 'search_malware', description: '搜尋OpenCTI中的惡意程式', inputSchema: { type: 'object', properties: { query: { type: 'string', description: '搜尋關鍵字', }, first: { type: 'number', description: '返回結果數量限制', default: 10, }, }, required: ['query'], }, }, { name: 'search_threat_actors', description: '搜尋OpenCTI中的威脅行為者', inputSchema: { type: 'object', properties: { query: { type: 'string', description: '搜尋關鍵字', }, first: { type: 'number', description: '返回結果數量限制', default: 10, }, }, required: ['query'], }, }, // Users & Groups { name: 'get_user_by_id', description: '根據ID獲取使用者資訊', inputSchema: { type: 'object', properties: { id: { type: 'string', description: '使用者ID', }, }, required: ['id'], }, }, { name: 'list_users', description: '列出所有使用者', inputSchema: { type: 'object', properties: {}, }, }, { name: 'list_groups', description: '列出所有群組', inputSchema: { type: 'object', properties: { first: { type: 'number', description: '返回結果數量限制', default: 10, }, }, }, }, // STIX Objects { name: 'list_attack_patterns', description: '列出所有攻擊模式', inputSchema: { type: 'object', properties: { first: { type: 'number', description: '返回結果數量限制', default: 10, }, }, }, }, { name: 'get_campaign_by_name', description: '根據名稱獲取行動資訊', inputSchema: { type: 'object', properties: { name: { type: 'string', description: '行動名稱', }, }, required: ['name'], }, }, // System { name: 'list_connectors', description: '列出所有連接器', inputSchema: { type: 'object', properties: {}, }, }, { name: 'list_status_templates', description: '列出所有狀態模板', inputSchema: { type: 'object', properties: {}, }, }, // Files { name: 'get_file_by_id', description: '根據ID獲取檔案資訊', inputSchema: { type: 'object', properties: { id: { type: 'string', description: '檔案ID', }, }, required: ['id'], }, }, { name: 'list_files', description: '列出所有檔案', inputSchema: { type: 'object', properties: {}, }, }, // References { name: 'list_marking_definitions', description: '列出所有標記定義', inputSchema: { type: 'object', properties: {}, }, }, { name: 'list_labels', description: '列出所有標籤', inputSchema: { type: 'object', properties: {}, }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { let query = ''; let variables: any = {}; switch (request.params.name) { // Reports case 'get_latest_reports': query = LATEST_REPORTS_QUERY; variables = { first: typeof request.params.arguments?.first === 'number' ? request.params.arguments.first : 10, }; break; case 'get_report_by_id': if (!request.params.arguments?.id) { throw new McpError(ErrorCode.InvalidParams, 'Report ID is required'); } query = REPORT_BY_ID_QUERY; variables = { id: request.params.arguments.id }; break; // Search case 'search_indicators': if (!request.params.arguments?.query) { throw new McpError(ErrorCode.InvalidParams, 'Query parameter is required'); } query = SEARCH_INDICATORS_QUERY; variables = { search: request.params.arguments.query, first: typeof request.params.arguments.first === 'number' ? request.params.arguments.first : 10, }; break; case 'search_malware': if (!request.params.arguments?.query) { throw new McpError(ErrorCode.InvalidParams, 'Query parameter is required'); } query = SEARCH_MALWARE_QUERY; variables = { search: request.params.arguments.query, first: typeof request.params.arguments.first === 'number' ? request.params.arguments.first : 10, }; break; case 'search_threat_actors': if (!request.params.arguments?.query) { throw new McpError(ErrorCode.InvalidParams, 'Query parameter is required'); } query = SEARCH_THREAT_ACTORS_QUERY; variables = { search: request.params.arguments.query, first: typeof request.params.arguments.first === 'number' ? request.params.arguments.first : 10, }; break; // Users & Groups case 'get_user_by_id': if (!request.params.arguments?.id) { throw new McpError(ErrorCode.InvalidParams, 'User ID is required'); } query = USER_BY_ID_QUERY; variables = { id: request.params.arguments.id }; break; case 'list_users': query = ALL_USERS_QUERY; break; case 'list_groups': query = ALL_GROUPS_QUERY; variables = { first: typeof request.params.arguments?.first === 'number' ? request.params.arguments.first : 10, }; break; // STIX Objects case 'list_attack_patterns': query = ALL_ATTACK_PATTERNS_QUERY; variables = { first: typeof request.params.arguments?.first === 'number' ? request.params.arguments.first : 10, }; break; case 'get_campaign_by_name': if (!request.params.arguments?.name) { throw new McpError(ErrorCode.InvalidParams, 'Campaign name is required'); } query = CAMPAIGN_BY_NAME_QUERY; variables = { name: request.params.arguments.name }; break; // System case 'list_connectors': query = ALL_CONNECTORS_QUERY; break; case 'list_status_templates': query = ALL_STATUS_TEMPLATES_QUERY; break; // Files case 'get_file_by_id': if (!request.params.arguments?.id) { throw new McpError(ErrorCode.InvalidParams, 'File ID is required'); } query = FILE_BY_ID_QUERY; variables = { id: request.params.arguments.id }; break; case 'list_files': query = ALL_FILES_QUERY; break; // References case 'list_marking_definitions': query = ALL_MARKING_DEFINITIONS_QUERY; break; case 'list_labels': query = ALL_LABELS_QUERY; break; default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } const response = await this.axiosInstance.post('/graphql', { query, variables, }); console.error('OpenCTI Response:', JSON.stringify(response.data, null, 2)); if (!response.data?.data) { throw new McpError( ErrorCode.InternalError, `Invalid response format from OpenCTI: ${JSON.stringify(response.data)}` ); } let formattedResponse; switch (request.params.name) { case 'get_latest_reports': formattedResponse = response.data.data.reports.edges.map((edge: any) => ({ id: edge.node.id, name: edge.node.name || 'Unnamed', description: edge.node.description || '', content: edge.node.content || '', published: edge.node.published, confidence: edge.node.confidence, created: edge.node.created, modified: edge.node.modified, reportTypes: edge.node.report_types || [], })); break; case 'get_report_by_id': formattedResponse = { ...response.data.data.report, name: response.data.data.report.name || 'Unnamed', description: response.data.data.report.description || '', }; break; case 'search_indicators': case 'search_malware': case 'search_threat_actors': formattedResponse = response.data.data.stixCoreObjects.edges.map((edge: any) => ({ id: edge.node.id, name: edge.node.name || 'Unnamed', description: edge.node.description || '', created: edge.node.created, modified: edge.node.modified, type: edge.node.malware_types?.join(', ') || edge.node.threat_actor_types?.join(', ') || '', family: edge.node.is_family ? 'Yes' : 'No', firstSeen: edge.node.first_seen || '', lastSeen: edge.node.last_seen || '', pattern: edge.node.pattern || '', validFrom: edge.node.valid_from || '', validUntil: edge.node.valid_until || '', score: edge.node.x_opencti_score, })); break; case 'get_user_by_id': formattedResponse = { ...response.data.data.user, name: response.data.data.user.name || 'Unnamed', }; break; case 'list_users': formattedResponse = response.data.data.users.edges.map((edge: any) => ({ id: edge.node.id, name: edge.node.name || 'Unnamed', email: edge.node.user_email, firstname: edge.node.firstname, lastname: edge.node.lastname, created: edge.node.created_at, modified: edge.node.updated_at, })); break; case 'list_groups': formattedResponse = response.data.data.groups.edges.map((edge: any) => ({ id: edge.node.id, name: edge.node.name || 'Unnamed', description: edge.node.description || '', members: edge.node.members?.edges?.map((memberEdge: any) => ({ id: memberEdge.node.id, name: memberEdge.node.name, email: memberEdge.node.user_email, })) || [], })); break; case 'list_attack_patterns': formattedResponse = response.data.data.attackPatterns.edges.map((edge: any) => ({ id: edge.node.id, name: edge.node.name || 'Unnamed', description: edge.node.description || '', created: edge.node.created_at, modified: edge.node.updated_at, killChainPhases: edge.node.killChainPhases?.edges?.map((phaseEdge: any) => ({ id: phaseEdge.node.id, name: phaseEdge.node.phase_name, })) || [], })); break; case 'list_connectors': formattedResponse = response.data.data.connectors.map((connector: any) => ({ id: connector.id, name: connector.name || 'Unnamed', type: connector.connector_type, scope: connector.connector_scope, state: connector.connector_state, active: connector.active, updated: connector.updated_at, created: connector.created_at, })); break; case 'list_status_templates': formattedResponse = response.data.data.statusTemplates.edges.map((edge: any) => ({ id: edge.node.id, name: edge.node.name || 'Unnamed', color: edge.node.color, usages: edge.node.usages, })); break; case 'list_marking_definitions': formattedResponse = response.data.data.markingDefinitions.edges.map((edge: any) => ({ id: edge.node.id, definition: edge.node.definition, color: edge.node.x_opencti_color, order: edge.node.x_opencti_order, })); break; case 'list_labels': formattedResponse = response.data.data.labels.edges.map((edge: any) => ({ id: edge.node.id, value: edge.node.value, color: edge.node.color, })); break; default: formattedResponse = response.data.data; } return { content: [{ type: 'text', text: JSON.stringify(formattedResponse, null, 2) }] }; } catch (error) { if (axios.isAxiosError(error)) { console.error('Axios Error:', error.response?.data); return { content: [{ type: 'text', text: `OpenCTI API error: ${JSON.stringify(error.response?.data) || error.message}` }], isError: true, }; } throw error; } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('OpenCTI MCP server running on stdio'); } } const server = new OpenCTIServer(); server.run().catch(console.error);