UNPKG

urlhaus-mcp-server

Version:

MCP server for accessing URLhaus malicious URL database from abuse.ch - provides threat intelligence for cybersecurity research

375 lines (374 loc) 15.6 kB
#!/usr/bin/env node /** * URLhaus MCP Server * * This MCP server provides access to URLhaus (https://urlhaus.abuse.ch/), * a project from abuse.ch that collects and shares malicious URLs used for * malware distribution. * * URLhaus API endpoints: * - Recent URLs: Get the most recent malicious URLs * - URL Info: Get detailed information about a specific URL * - Host Info: Get information about URLs hosted on a specific host * - URL Search: Search for URLs by various criteria * - Payload Info: Get information about malware payloads * - Tag Info: Get URLs associated with specific tags * * The URLhaus API is free to use and doesn't require authentication, * but has rate limits to prevent abuse. */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios from 'axios'; class URLhausServer { server; axiosInstance; constructor() { this.server = new Server({ name: "urlhaus-server", version: "0.1.0", }, { capabilities: { tools: {}, }, }); // URLhaus API configuration this.axiosInstance = axios.create({ baseURL: 'https://urlhaus-api.abuse.ch/v1', timeout: 30000, headers: { 'User-Agent': 'URLhaus-MCP-Server/0.1.0', 'Content-Type': 'application/x-www-form-urlencoded', }, }); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'get_recent_urls', description: 'Get the most recent malicious URLs from URLhaus (up to 1000 entries)', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Number of URLs to retrieve (1-1000, default: 100)', minimum: 1, maximum: 1000, }, }, }, }, { name: 'lookup_url', description: 'Get detailed information about a specific URL', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The URL to look up (must be a complete URL with protocol)', }, }, required: ['url'], }, }, { name: 'lookup_host', description: 'Get information about URLs hosted on a specific host/domain', inputSchema: { type: 'object', properties: { host: { type: 'string', description: 'The hostname or domain to look up (e.g., "example.com")', }, }, required: ['host'], }, }, { name: 'lookup_payload', description: 'Get information about a malware payload by its hash', inputSchema: { type: 'object', properties: { hash: { type: 'string', description: 'MD5 or SHA256 hash of the malware payload', }, }, required: ['hash'], }, }, { name: 'get_urls_by_tag', description: 'Get URLs associated with a specific malware tag/family', inputSchema: { type: 'object', properties: { tag: { type: 'string', description: 'Malware tag/family (e.g., "emotet", "trickbot", "cobalt_strike")', }, limit: { type: 'number', description: 'Number of results to return (1-1000, default: 100)', minimum: 1, maximum: 1000, }, }, required: ['tag'], }, }, { name: 'get_urls_by_signature', description: 'Get URLs associated with a specific malware signature', inputSchema: { type: 'object', properties: { signature: { type: 'string', description: 'Malware signature name', }, limit: { type: 'number', description: 'Number of results to return (1-1000, default: 100)', minimum: 1, maximum: 1000, }, }, required: ['signature'], }, }, { name: 'get_payloads', description: 'Get recent malware payloads from URLhaus', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Number of payloads to retrieve (1-1000, default: 100)', minimum: 1, maximum: 1000, }, }, }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case 'get_recent_urls': return await this.getRecentUrls(request.params.arguments); case 'lookup_url': return await this.lookupUrl(request.params.arguments); case 'lookup_host': return await this.lookupHost(request.params.arguments); case 'lookup_payload': return await this.lookupPayload(request.params.arguments); case 'get_urls_by_tag': return await this.getUrlsByTag(request.params.arguments); case 'get_urls_by_signature': return await this.getUrlsBySignature(request.params.arguments); case 'get_payloads': return await this.getPayloads(request.params.arguments); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } } catch (error) { if (axios.isAxiosError(error)) { const statusCode = error.response?.status; const errorMessage = error.response?.data?.query_status || error.message; if (statusCode === 429) { return { content: [{ type: 'text', text: `Rate limit exceeded. Please try again later.` }], isError: true, }; } return { content: [{ type: 'text', text: `URLhaus API error (${statusCode}): ${errorMessage}` }], isError: true, }; } throw error; } }); } async getRecentUrls(args) { const limit = Math.min(Number(args?.limit || 100), 1000); const response = await this.axiosInstance.get('/urls/recent/', { params: { limit } }); const data = response.data; return { content: [{ type: 'text', text: JSON.stringify({ query_status: data.query_status, urls_count: data.urls?.length || 0, urls: data.urls?.slice(0, limit) || [], summary: `Retrieved ${data.urls?.length || 0} recent malicious URLs` }, null, 2) }] }; } async lookupUrl(args) { const url = String(args?.url || '').trim(); if (!url) { throw new McpError(ErrorCode.InvalidParams, 'URL parameter is required'); } const formData = new URLSearchParams(); formData.append('url', url); const response = await this.axiosInstance.post('/url/', formData); const data = response.data; return { content: [{ type: 'text', text: JSON.stringify({ query_status: data.query_status, url_info: data, summary: data.query_status === 'ok' ? `URL found in URLhaus database` : `URL not found in URLhaus database` }, null, 2) }] }; } async lookupHost(args) { const host = String(args?.host || '').trim(); if (!host) { throw new McpError(ErrorCode.InvalidParams, 'Host parameter is required'); } const formData = new URLSearchParams(); formData.append('host', host); const response = await this.axiosInstance.post('/host/', formData); const data = response.data; return { content: [{ type: 'text', text: JSON.stringify({ query_status: data.query_status, host_info: data, urls_count: data.urls?.length || 0, summary: data.query_status === 'ok' ? `Found ${data.urls?.length || 0} URLs for host ${host}` : `No data found for host ${host}` }, null, 2) }] }; } async lookupPayload(args) { const hash = String(args?.hash || '').trim(); if (!hash) { throw new McpError(ErrorCode.InvalidParams, 'Hash parameter is required'); } const formData = new URLSearchParams(); formData.append('hash', hash); const response = await this.axiosInstance.post('/payload/', formData); const data = response.data; return { content: [{ type: 'text', text: JSON.stringify({ query_status: data.query_status, payload_info: data, summary: data.query_status === 'ok' ? `Payload found in URLhaus database` : `Payload not found in URLhaus database` }, null, 2) }] }; } async getUrlsByTag(args) { const tag = String(args?.tag || '').trim(); const limit = Math.min(Number(args?.limit || 100), 1000); if (!tag) { throw new McpError(ErrorCode.InvalidParams, 'Tag parameter is required'); } const formData = new URLSearchParams(); formData.append('tag', tag); formData.append('limit', limit.toString()); const response = await this.axiosInstance.post('/tag/', formData); const data = response.data; return { content: [{ type: 'text', text: JSON.stringify({ query_status: data.query_status, tag: tag, urls_count: data.urls?.length || 0, urls: data.urls?.slice(0, limit) || [], summary: `Found ${data.urls?.length || 0} URLs tagged with "${tag}"` }, null, 2) }] }; } async getUrlsBySignature(args) { const signature = String(args?.signature || '').trim(); const limit = Math.min(Number(args?.limit || 100), 1000); if (!signature) { throw new McpError(ErrorCode.InvalidParams, 'Signature parameter is required'); } const formData = new URLSearchParams(); formData.append('signature', signature); formData.append('limit', limit.toString()); const response = await this.axiosInstance.post('/signature/', formData); const data = response.data; return { content: [{ type: 'text', text: JSON.stringify({ query_status: data.query_status, signature: signature, urls_count: data.urls?.length || 0, urls: data.urls?.slice(0, limit) || [], summary: `Found ${data.urls?.length || 0} URLs with signature "${signature}"` }, null, 2) }] }; } async getPayloads(args) { const limit = Math.min(Number(args?.limit || 100), 1000); const response = await this.axiosInstance.get('/payloads/recent/', { params: { limit } }); const data = response.data; return { content: [{ type: 'text', text: JSON.stringify({ query_status: data.query_status, payloads_count: data.payloads?.length || 0, payloads: data.payloads?.slice(0, limit) || [], summary: `Retrieved ${data.payloads?.length || 0} recent malware payloads` }, null, 2) }] }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('URLhaus MCP server running on stdio'); } } const server = new URLhausServer(); server.run().catch(console.error);