Huntress-MCP-Server

#!/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, { AxiosInstance } from 'axios'; // Environment variables for authentication const API_KEY = process.env.HUNTRESS_API_KEY; const API_SECRET = process.env.HUNTRESS_API_SECRET; if (!API_KEY || !API_SECRET) { throw new Error('HUNTRESS_API_KEY and HUNTRESS_API_SECRET environment variables are required'); } interface RequestParams { [key: string]: any; } class HuntressServer { private server: Server; private axiosInstance: AxiosInstance; private lastRequestTime: number = 0; private requestCount: number = 0; constructor() { this.server = new Server( { name: 'huntress-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); // Initialize axios instance with base configuration this.axiosInstance = axios.create({ baseURL: 'https://api.huntress.io/v1', headers: { Authorization: `Basic ${Buffer.from(`${API_KEY}:${API_SECRET}`).toString('base64')}`, }, }); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private async checkRateLimit() { const now = Date.now(); if (now - this.lastRequestTime >= 60000) { // Reset if a minute has passed this.requestCount = 0; this.lastRequestTime = now; } else if (this.requestCount >= 60) { // Wait until a minute has passed since first request const waitTime = 60000 - (now - this.lastRequestTime); await new Promise(resolve => setTimeout(resolve, waitTime)); this.requestCount = 0; this.lastRequestTime = Date.now(); } this.requestCount++; } private async makeRequest(endpoint: string, params: RequestParams = {}) { await this.checkRateLimit(); try { const response = await this.axiosInstance.get(endpoint, { params }); return response.data; } catch (error: any) { if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `Huntress API error: ${error.response?.data?.message || error.message}` ); } throw error; } } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'get_account_info', description: 'Get information about the current account', inputSchema: { type: 'object', properties: {}, }, }, { name: 'list_organizations', description: 'List organizations in the account', inputSchema: { type: 'object', properties: { page: { type: 'integer', description: 'Page number (starts at 1)', minimum: 1, }, limit: { type: 'integer', description: 'Number of results per page (1-500)', minimum: 1, maximum: 500, }, }, }, }, { name: 'get_organization', description: 'Get details of a specific organization', inputSchema: { type: 'object', properties: { organization_id: { type: 'integer', description: 'Organization ID', }, }, required: ['organization_id'], }, }, { name: 'list_agents', description: 'List agents in the account', inputSchema: { type: 'object', properties: { page: { type: 'integer', description: 'Page number (starts at 1)', minimum: 1, }, limit: { type: 'integer', description: 'Number of results per page (1-500)', minimum: 1, maximum: 500, }, organization_id: { type: 'integer', description: 'Filter by organization ID', }, platform: { type: 'string', description: 'Filter by platform (darwin or windows)', enum: ['darwin', 'windows'], }, }, }, }, { name: 'get_agent', description: 'Get details of a specific agent', inputSchema: { type: 'object', properties: { agent_id: { type: 'integer', description: 'Agent ID', }, }, required: ['agent_id'], }, }, { name: 'list_incident_reports', description: 'List incident reports', inputSchema: { type: 'object', properties: { page: { type: 'integer', description: 'Page number (starts at 1)', minimum: 1, }, limit: { type: 'integer', description: 'Number of results per page (1-500)', minimum: 1, maximum: 500, }, organization_id: { type: 'integer', description: 'Filter by organization ID', }, status: { type: 'string', description: 'Filter by status', enum: ['sent', 'closed', 'dismissed'], }, severity: { type: 'string', description: 'Filter by severity', enum: ['low', 'high', 'critical'], }, }, }, }, { name: 'get_incident_report', description: 'Get details of a specific incident report', inputSchema: { type: 'object', properties: { report_id: { type: 'integer', description: 'Incident Report ID', }, }, required: ['report_id'], }, }, { name: 'list_summary_reports', description: 'List summary reports', inputSchema: { type: 'object', properties: { page: { type: 'integer', description: 'Page number (starts at 1)', minimum: 1, }, limit: { type: 'integer', description: 'Number of results per page (1-500)', minimum: 1, maximum: 500, }, organization_id: { type: 'integer', description: 'Filter by organization ID', }, type: { type: 'string', description: 'Filter by report type', enum: ['monthly_summary', 'quarterly_summary', 'yearly_summary'], }, }, }, }, { name: 'get_summary_report', description: 'Get details of a specific summary report', inputSchema: { type: 'object', properties: { report_id: { type: 'integer', description: 'Summary Report ID', }, }, required: ['report_id'], }, }, { name: 'list_billing_reports', description: 'List billing reports', inputSchema: { type: 'object', properties: { page: { type: 'integer', description: 'Page number (starts at 1)', minimum: 1, }, limit: { type: 'integer', description: 'Number of results per page (1-500)', minimum: 1, maximum: 500, }, status: { type: 'string', description: 'Filter by status', enum: ['open', 'paid', 'failed', 'partial_refund', 'full_refund'], }, }, }, }, { name: 'get_billing_report', description: 'Get details of a specific billing report', inputSchema: { type: 'object', properties: { report_id: { type: 'integer', description: 'Billing Report ID', }, }, required: ['report_id'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args = {} } = request.params; try { let response; switch (name) { case 'get_account_info': response = await this.makeRequest('/account'); break; case 'list_organizations': response = await this.makeRequest('/organizations', args); break; case 'get_organization': if (!args.organization_id) { throw new McpError(ErrorCode.InvalidParams, 'organization_id is required'); } response = await this.makeRequest(`/organizations/${args.organization_id}`); break; case 'list_agents': response = await this.makeRequest('/agents', args); break; case 'get_agent': if (!args.agent_id) { throw new McpError(ErrorCode.InvalidParams, 'agent_id is required'); } response = await this.makeRequest(`/agents/${args.agent_id}`); break; case 'list_incident_reports': response = await this.makeRequest('/incident_reports', args); break; case 'get_incident_report': if (!args.report_id) { throw new McpError(ErrorCode.InvalidParams, 'report_id is required'); } response = await this.makeRequest(`/incident_reports/${args.report_id}`); break; case 'list_summary_reports': response = await this.makeRequest('/reports', args); break; case 'get_summary_report': if (!args.report_id) { throw new McpError(ErrorCode.InvalidParams, 'report_id is required'); } response = await this.makeRequest(`/reports/${args.report_id}`); break; case 'list_billing_reports': response = await this.makeRequest('/billing_reports', args); break; case 'get_billing_report': if (!args.report_id) { throw new McpError(ErrorCode.InvalidParams, 'report_id is required'); } response = await this.makeRequest(`/billing_reports/${args.report_id}`); break; default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } return { content: [ { type: 'text', text: JSON.stringify(response, null, 2), }, ], }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Error executing ${name}: ${error}` ); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Huntress MCP server running on stdio'); } } const server = new HuntressServer(); server.run().catch(console.error);