@mseep/mcp-maigret
Version:
MCP server for maigret - OSINT username search across social networks
248 lines (247 loc) • 10.8 kB
JavaScript
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 { exec } from 'child_process';
import { promisify } from 'util';
import { existsSync, mkdirSync } from 'fs';
import { join } from 'path';
const execAsync = promisify(exec);
const DOCKER_IMAGE = 'soxoj/maigret:latest';
function sanitizeFilename(filename) {
return filename.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_');
}
function isSearchUsernameArgs(args) {
if (!args || typeof args !== 'object')
return false;
const a = args;
return typeof a.username === 'string' &&
(a.format === undefined || ['txt', 'html', 'pdf', 'json', 'csv', 'xmind'].includes(a.format)) &&
(a.use_all_sites === undefined || typeof a.use_all_sites === 'boolean') &&
(a.tags === undefined || (Array.isArray(a.tags) && a.tags.every(t => typeof t === 'string')));
}
function isParseUrlArgs(args) {
if (!args || typeof args !== 'object')
return false;
const a = args;
return typeof a.url === 'string' &&
(a.format === undefined || ['txt', 'html', 'pdf', 'json', 'csv', 'xmind'].includes(a.format));
}
class MaigretServer {
constructor() {
if (!process.env.MAIGRET_REPORTS_DIR) {
throw new Error('MAIGRET_REPORTS_DIR environment variable must be set');
}
this.reportsDir = process.env.MAIGRET_REPORTS_DIR;
this.server = new Server({
name: 'maigret-server',
version: '0.1.0',
capabilities: {
tools: {}
},
timeout: 600000 // 10 minutes in milliseconds
});
console.error('Using reports directory:', this.reportsDir);
// Create reports directory if it doesn't exist
if (!existsSync(this.reportsDir)) {
console.error('Creating reports directory...');
mkdirSync(this.reportsDir, { recursive: true });
}
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
// Trigger setup immediately
this.ensureSetup().catch(error => {
console.error('Failed to setup maigret:', error);
process.exit(1);
});
}
async execCommand(command) {
console.error('Executing command:', command);
try {
const result = await execAsync(command, {
maxBuffer: 10 * 1024 * 1024
});
console.error('Command output:', result.stdout);
if (result.stderr)
console.error('Command stderr:', result.stderr);
return result;
}
catch (error) {
console.error('Command failed:', error);
throw error;
}
}
async ensureSetup() {
try {
console.error('Checking Docker...');
try {
await this.execCommand('docker --version');
}
catch (error) {
throw new Error('Docker is not installed or not running. Please install Docker and try again.');
}
console.error('Checking if maigret image exists...');
try {
await this.execCommand(`docker image inspect ${DOCKER_IMAGE}`);
console.error('Maigret image found');
}
catch (error) {
console.error('Maigret image not found, pulling...');
await this.execCommand(`docker pull ${DOCKER_IMAGE}`);
console.error('Maigret image pulled successfully');
}
}
catch (error) {
console.error('Setup failed:', error);
throw error;
}
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'search_username',
description: 'Search for a username across social networks and sites',
inputSchema: {
type: 'object',
properties: {
username: {
type: 'string',
description: 'Username to search for'
},
format: {
type: 'string',
enum: ['txt', 'html', 'pdf', 'json', 'csv', 'xmind'],
description: 'Output format',
default: 'pdf'
},
use_all_sites: {
type: 'boolean',
description: 'Use all available sites instead of top 500',
default: false
},
tags: {
type: 'array',
items: {
type: 'string'
},
description: 'Filter sites by tags (e.g. photo, dating, us)',
default: []
}
},
required: ['username']
}
},
{
name: 'parse_url',
description: 'Parse a URL to extract information and search for associated usernames',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
format: 'uri',
description: 'URL to parse and analyze'
},
format: {
type: 'string',
enum: ['txt', 'html', 'pdf', 'json', 'csv', 'xmind'],
description: 'Output format',
default: 'pdf'
}
},
required: ['url']
}
}
]
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
await this.ensureSetup();
switch (request.params.name) {
case 'search_username': {
if (!isSearchUsernameArgs(request.params.arguments)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for search_username');
}
const { username, format = 'pdf', use_all_sites = false, tags = [] } = request.params.arguments;
const safeUsername = sanitizeFilename(username);
const reportPath = join(this.reportsDir, `report_${safeUsername}.${format}`);
// Build command arguments
const args = [
username,
`--${format}`,
'--no-color',
'--no-progressbar',
'-n', '200' // Increase max connections from default 100 to 200
];
if (use_all_sites) {
args.push('-a');
}
if (tags.length > 0) {
args.push('--tags', tags.join(','));
}
// Run maigret in Docker
const { stdout, stderr } = await this.execCommand(`docker run --rm -v "${this.reportsDir}:/app/reports" ${DOCKER_IMAGE} ${args.join(' ')}`);
return {
content: [
{
type: 'text',
text: `Report saved to: ${reportPath}\n\n${stdout}${stderr ? `\nErrors:\n${stderr}` : ''}`
}
]
};
}
case 'parse_url': {
if (!isParseUrlArgs(request.params.arguments)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid arguments for parse_url');
}
const { url, format = 'pdf' } = request.params.arguments;
const args = [
'--parse', url,
`--${format}`,
'--no-color',
'--no-progressbar',
'--timeout', '60', // 60 second timeout per request
'-n', '200' // Increase max connections from default 100 to 200
];
// Run maigret in Docker
const { stdout, stderr } = await this.execCommand(`docker run --rm -v "${this.reportsDir}:/app/reports" ${DOCKER_IMAGE} ${args.join(' ')}`);
return {
content: [
{
type: 'text',
text: stdout + (stderr ? `\nErrors:\n${stderr}` : '')
}
]
};
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `Error executing maigret: ${errorMessage}`
}
],
isError: true
};
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Maigret MCP server running on stdio');
}
}
const server = new MaigretServer();
server.run().catch(console.error);