index-http.ts•6.91 kB
import 'dotenv/config';
import express, { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { makeSpiderfootClientFromEnv } from './spiderfootClient.js';
// Build a new server instance with tools registered (same tools as stdio index)
function buildServer() {
const server = new McpServer({ name: 'spiderfoot-mcp-server', version: '0.1.0' });
const sf = makeSpiderfootClientFromEnv();
const StartScanSchema = z.object({
scanname: z.string().min(1),
scantarget: z.string().min(1),
modulelist: z.string().optional(),
typelist: z.string().optional(),
usecase: z.enum(['all', 'investigate', 'passive', 'footprint']).optional(),
});
server.registerTool(
'spiderfoot_ping',
{ title: 'Ping', description: 'Ping SpiderFoot server to verify it is responding.', inputSchema: {} },
async () => ({ content: [{ type: 'text', text: JSON.stringify(await sf.ping()) }] })
);
server.registerTool(
'spiderfoot_modules',
{ title: 'Modules', description: 'List available SpiderFoot modules.', inputSchema: {} },
async () => ({ content: [{ type: 'text', text: JSON.stringify(await sf.modules()) }] })
);
server.registerTool(
'spiderfoot_event_types',
{ title: 'Event Types', description: 'List available SpiderFoot event types.', inputSchema: {} },
async () => ({ content: [{ type: 'text', text: JSON.stringify(await sf.eventTypes()) }] })
);
server.registerTool(
'spiderfoot_scans',
{ title: 'Scans', description: 'List all scans (past and present).', inputSchema: {} },
async () => ({ content: [{ type: 'text', text: JSON.stringify(await sf.scans()) }] })
);
server.registerTool(
'spiderfoot_scan_info',
{ title: 'Scan Info', description: 'Retrieve scan metadata/config for a scan ID.', inputSchema: { id: z.string() } },
async ({ id }) => ({ content: [{ type: 'text', text: JSON.stringify(await sf.scanInfo(id)) }] })
);
server.registerTool(
'spiderfoot_start_scan',
{ title: 'Start Scan', description: 'Start a new scan against a target.', inputSchema: StartScanSchema.shape },
async (input) => {
const allow = (process.env.ALLOW_START_SCAN ?? 'true').toLowerCase();
if (!(allow === 'true' || allow === '1' || allow === 'yes')) {
return { content: [{ type: 'text', text: 'Starting scans is disabled by configuration (ALLOW_START_SCAN=false).' }], isError: true };
}
const res = await sf.startScan(input as any);
return { content: [{ type: 'text', text: JSON.stringify(res) }] };
}
);
server.registerTool(
'spiderfoot_scan_data',
{ title: 'Scan Data', description: 'Fetch scan event results for a scan ID.', inputSchema: { id: z.string(), eventType: z.string().optional() } },
async ({ id, eventType }) => ({ content: [{ type: 'text', text: JSON.stringify(await sf.scanEventResults({ id, eventType })) }] })
);
server.registerTool(
'spiderfoot_scan_data_unique',
{ title: 'Scan Data Unique', description: 'Fetch unique scan event results.', inputSchema: { id: z.string(), eventType: z.string().optional() } },
async ({ id, eventType }) => ({ content: [{ type: 'text', text: JSON.stringify(await sf.scanEventResultsUnique({ id, eventType })) }] })
);
server.registerTool(
'spiderfoot_scan_logs',
{ title: 'Scan Logs', description: 'Fetch/poll scan logs for a given scan ID.', inputSchema: { id: z.string(), limit: z.number().optional(), reverse: z.enum(['0','1']).optional(), rowId: z.number().optional() } },
async ({ id, limit, reverse, rowId }) => ({ content: [{ type: 'text', text: JSON.stringify(await sf.scanLogs({ id, limit, reverse, rowId })) }] })
);
server.registerTool(
'spiderfoot_export_json',
{ title: 'Export JSON', description: 'Export scan results in JSON for CSV of IDs.', inputSchema: { ids: z.string() } },
async ({ ids }) => ({ content: [{ type: 'text', text: JSON.stringify(await sf.exportJson(ids)) }] })
);
return server;
}
async function main() {
const app = express();
app.use(express.json());
// Session map
const transports: Record<string, StreamableHTTPServerTransport> = {};
app.post('/mcp', async (req: Request, res: Response) => {
try {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
// For local usage, consider enabling DNS rebinding protection and allowedHosts
// enableDnsRebindingProtection: true,
// allowedHosts: ['127.0.0.1', 'localhost']
});
transport.onclose = () => {
if (transport.sessionId) delete transports[transport.sessionId];
};
const server = buildServer();
await server.connect(transport);
if (transport.sessionId) transports[transport.sessionId] = transport;
} else {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
id: null,
});
return;
}
await transport.handleRequest(req, res, req.body);
} catch (err) {
// eslint-disable-next-line no-console
console.error('HTTP /mcp error', err);
if (!res.headersSent) res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
await transports[sessionId].handleRequest(req, res);
});
app.delete('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId || !transports[sessionId]) {
res.status(400).send('Invalid or missing session ID');
return;
}
await transports[sessionId].handleRequest(req, res);
});
const port = Number(process.env.MCP_HTTP_PORT || 3000);
app.listen(port, () => {
// eslint-disable-next-line no-console
console.log(`[spiderfoot-mcp-server:http] listening on http://0.0.0.0:${port}/mcp`);
});
}
main().catch((err) => {
// eslint-disable-next-line no-console
console.error('Failed to start HTTP MCP server:', err);
process.exit(1);
});