/**
* MCP Server 实现
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { SearXNGService } from './services/searxng.js';
import { CreeperService } from './services/creeper.js';
import { SummarizerService } from './services/summarizer.js';
import { getConfig } from './utils/config.js';
import { logger } from './utils/logger.js';
import {
webSearchInputSchema,
executeWebSearch,
} from './tools/web-search.js';
export async function createServer(): Promise<{
server: Server;
searxngService: SearXNGService;
creeperService: CreeperService;
}> {
const config = getConfig();
logger.setLevel(config.service.logLevel);
// 初始化新服务
const searxngService = new SearXNGService({
baseUrl: config.searxng.baseUrl,
timeout: config.searxng.timeout,
});
const creeperService = new CreeperService({
scriptPath: config.creeper.path,
pythonPath: config.creeper.pythonPath,
concurrency: config.creeper.concurrency,
timeout: config.creeper.timeout,
}, config.fileSave);
// 测试服务连接
logger.info('Testing SearXNG connection...');
const searxngOk = await searxngService.testConnection();
if (!searxngOk) {
logger.warn('SearXNG connection test failed, but continuing...');
}
logger.info('Testing Creeper environment...');
const creeperOk = await creeperService.testEnvironment();
if (!creeperOk) {
logger.warn('Creeper environment test failed, but continuing...');
}
// 初始化其他服务
const summarizerService = new SummarizerService({
apiKey: config.summary.apiKey,
baseUrl: config.summary.baseUrl,
model: config.summary.model,
maxTokens: config.summary.maxTokens,
timeout: config.summary.timeout,
maxRetries: config.summary.maxRetries,
});
// 创建 MCP Server
const server = new Server(
{
name: 'web-analysis-mcp',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// 注册工具列表处理器
server.setRequestHandler(ListToolsRequestSchema, async () => {
logger.info('=== LISTING TOOLS ===');
return {
tools: [
{
name: 'web_search',
description:
'使用 SearXNG 搜索网络内容,通过 Creeper 爬取网页,并返回经过 LLM 总结的结果。适用于需要获取最新网络信息的场景。',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索查询(不能为空或仅包含空格字符)',
},
max_results: {
type: 'number',
description: '最大结果数量 (1-20)',
default: 10,
minimum: 1,
maximum: 20,
},
language: {
type: 'string',
description: '搜索语言',
default: 'zh',
},
time_range: {
type: 'string',
enum: ['day', 'week', 'month', 'year'],
description: '时间范围',
},
include_domains: {
type: 'array',
items: { type: 'string' },
description: '只搜索这些域名(白名单)',
},
exclude_domains: {
type: 'array',
items: { type: 'string' },
description: '排除这些域名(黑名单)',
},
save_to_file: {
type: 'boolean',
description: '是否将爬取的内容保存到本地文件(需要启用 SAVE_CONTENT_ENABLED 配置)',
default: false,
},
},
required: ['query'],
},
},
],
};
});
// 注册工具调用处理器
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
logger.info('=== TOOL CALLED ===', { name, args });
logger.info('Tool called', { name, args });
try {
let result: string;
switch (name) {
case 'web_search': {
const input = webSearchInputSchema.parse(args);
result = await executeWebSearch(
input,
searxngService,
creeperService,
summarizerService,
{
filter: config.filter,
filterLlm: config.filterLlm,
mapReduce: config.mapReduce,
maxContentLength: config.service.maxContentLength,
}
);
break;
}
default:
throw new Error(`Unknown tool: ${name}`);
}
return {
content: [
{
type: 'text',
text: result,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error('Tool execution failed', { name, error: errorMessage });
return {
content: [
{
type: 'text',
text: `执行失败: ${errorMessage}`,
},
],
isError: true,
};
}
});
return { server, searxngService, creeperService };
}
export async function runServer(): Promise<void> {
const { server, searxngService, creeperService } = await createServer();
const transport = new StdioServerTransport();
// 处理进程退出时的清理
const cleanup = async () => {
logger.info('Shutting down server...');
// Creeper 子进程会自动清理
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
logger.info('Starting Web Analysis MCP Server');
logger.info('Using SearXNG + Creeper + LLM architecture');
await server.connect(transport);
logger.info('Server connected and ready');
}