mcp-maigret
by BurtTheCoder
- mcp-maigret
- 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 { exec } from 'child_process';
import { promisify } from 'util';
import { platform } from 'os';
import { existsSync, mkdirSync } from 'fs';
import { join } from 'path';
const execAsync = promisify(exec);
const DOCKER_IMAGE = 'soxoj/maigret:latest';
interface SearchUsernameArgs {
username: string;
format?: 'txt' | 'html' | 'pdf' | 'json' | 'csv' | 'xmind';
use_all_sites?: boolean;
tags?: string[];
}
interface ParseUrlArgs {
url: string;
format?: 'txt' | 'html' | 'pdf' | 'json' | 'csv' | 'xmind';
}
interface ExecResult {
stdout: string;
stderr: string;
}
function sanitizeFilename(filename: string): string {
return filename.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_');
}
function isSearchUsernameArgs(args: unknown): args is SearchUsernameArgs {
if (!args || typeof args !== 'object') return false;
const a = args as Record<string, unknown>;
return typeof a.username === 'string' &&
(a.format === undefined || ['txt', 'html', 'pdf', 'json', 'csv', 'xmind'].includes(a.format as string)) &&
(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: unknown): args is ParseUrlArgs {
if (!args || typeof args !== 'object') return false;
const a = args as Record<string, unknown>;
return typeof a.url === 'string' &&
(a.format === undefined || ['txt', 'html', 'pdf', 'json', 'csv', 'xmind'].includes(a.format as string));
}
class MaigretServer {
private server: Server;
private reportsDir: string;
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);
});
}
private async execCommand(command: string): Promise<ExecResult> {
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;
}
}
private async ensureSetup(): Promise<void> {
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;
}
}
private 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);