UNPKG

hot-content-mcp

Version:

MCP服务器,支持获取百度热搜、B站热门视频等多平台热门内容数据

839 lines (825 loc) 32.9 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { BaiduHotSearchService, BilibiliHotSearchService } from './api.js'; import { ConfigManager } from './config.js'; import * as http from 'http'; import * as url from 'url'; export class HotContentMCPServer { server; hotSearchService; bilibiliService; configManager; constructor(configPath) { // 初始化配置管理器并立即验证配置 this.configManager = new ConfigManager(configPath); // 在构造函数中就验证配置,确保配置文件存在且有效 try { this.configManager.loadConfig(); console.error('✅ 配置文件加载成功'); } catch (error) { console.error('❌ 配置文件加载失败:', error instanceof Error ? error.message : '未知错误'); console.error('💡 请确保 config.json 文件存在且包含有效的百度API配置'); console.error('📝 配置文件格式示例:'); console.error(JSON.stringify({ "baidu_api": { "id": "your_user_id", "key": "your_api_key" } }, null, 2)); throw new Error('配置验证失败,服务器无法启动'); } this.server = new Server({ name: 'hot-content-mcp', version: '2.3.0', }, { capabilities: { tools: {}, resources: {}, }, }); // 使用已验证的配置管理器创建服务 this.hotSearchService = new BaiduHotSearchService(this.configManager); this.bilibiliService = new BilibiliHotSearchService(this.configManager); this.setupToolHandlers(); this.setupResourceHandlers(); } /** * 设置工具处理器 */ setupToolHandlers() { // 列出所有可用工具 this.server.setRequestHandler(ListToolsRequestSchema, async () => { const tools = [ // 百度热搜工具 { name: 'get_baidu_hot_search', description: '获取百度热搜榜数据', inputSchema: { type: 'object', properties: { count: { type: 'number', description: '要获取的热搜条数,默认为10,最大50', minimum: 1, maximum: 50, default: 10, }, use_cache: { type: 'boolean', description: '是否使用缓存数据,默认为true', default: true, }, }, additionalProperties: false, }, }, { name: 'search_baidu_hot_search', description: '搜索包含特定关键词的百度热搜', inputSchema: { type: 'object', properties: { keyword: { type: 'string', description: '搜索关键词', minLength: 1, }, }, required: ['keyword'], additionalProperties: false, }, }, { name: 'clear_baidu_cache', description: '清除百度热搜数据缓存', inputSchema: { type: 'object', properties: {}, additionalProperties: false, }, }, // B站工具 { name: 'get_bilibili_hot', description: '获取B站热门视频数据', inputSchema: { type: 'object', properties: { count: { type: 'number', description: '要获取的视频数量,默认为10,最大50', minimum: 1, maximum: 50, default: 10, }, use_cache: { type: 'boolean', description: '是否使用缓存数据,默认为true', default: true, }, }, additionalProperties: false, }, }, { name: 'search_bilibili_videos', description: '搜索B站视频(根据标题或UP主名称)', inputSchema: { type: 'object', properties: { keyword: { type: 'string', description: '搜索关键词', minLength: 1, }, }, required: ['keyword'], additionalProperties: false, }, }, { name: 'clear_bilibili_cache', description: '清除B站视频数据缓存', inputSchema: { type: 'object', properties: {}, additionalProperties: false, }, } ]; return { tools }; }); // 处理工具调用 this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { // 百度热搜工具 case 'get_baidu_hot_search': return await this.handleGetBaiduHotSearch(args); case 'search_baidu_hot_search': return await this.handleSearchBaiduHotSearch(args); case 'clear_baidu_cache': return await this.handleClearBaiduCache(); // B站工具 case 'get_bilibili_hot': return await this.handleGetBilibiliHot(args); case 'search_bilibili_videos': return await this.handleSearchBilibiliVideos(args); case 'clear_bilibili_cache': return await this.handleClearBilibiliCache(); default: throw new Error(`未知工具: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `工具执行失败: ${error instanceof Error ? error.message : '未知错误'}`, }, ], isError: true, }; } }); } /** * 设置资源处理器 */ setupResourceHandlers() { // 列出所有可用资源 this.server.setRequestHandler(ListResourcesRequestSchema, async () => { const resources = [ // 百度热搜资源 { uri: 'baidu://hot-search/current', name: '当前百度热搜榜', description: '实时的百度热搜榜数据', mimeType: 'application/json', }, { uri: 'baidu://hot-search/top5', name: '百度热搜榜TOP5', description: '排名前5的热搜数据', mimeType: 'application/json', }, // B站资源 { uri: 'bilibili://videos/current', name: '当前B站热门视频', description: '实时的B站热门视频数据', mimeType: 'application/json', }, { uri: 'bilibili://videos/top5', name: 'B站热门视频TOP5', description: '排名前5的B站热门视频', mimeType: 'application/json', } ]; return { resources }; }); // 处理资源读取 this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; try { switch (uri) { // 百度热搜资源 case 'baidu://hot-search/current': return await this.handleReadCurrentHotSearch(); case 'baidu://hot-search/top5': return await this.handleReadTop5HotSearch(); // B站资源 case 'bilibili://videos/current': return await this.handleReadCurrentBilibiliVideos(); case 'bilibili://videos/top5': return await this.handleReadTop5BilibiliVideos(); default: throw new Error(`未知资源: ${uri}`); } } catch (error) { throw new Error(`读取资源失败: ${error instanceof Error ? error.message : '未知错误'}`); } }); } /** * 处理获取百度热搜工具 */ async handleGetBaiduHotSearch(args) { const count = args?.count ?? 10; const useCache = args?.use_cache ?? true; const data = await this.hotSearchService.getHotSearchData(useCache); const results = data.slice(0, Math.min(count, data.length)); return { content: [ { type: 'text', text: this.formatHotSearchResults(results, `百度热搜榜 TOP ${count}`), }, ], }; } /** * 处理搜索百度热搜工具 */ async handleSearchBaiduHotSearch(args) { const keyword = args.keyword; const results = await this.hotSearchService.searchHotSearch(keyword); if (results.length === 0) { return { content: [ { type: 'text', text: `没有找到包含关键词"${keyword}"的热搜。`, }, ], }; } return { content: [ { type: 'text', text: this.formatHotSearchResults(results, `搜索"${keyword}"的结果`), }, ], }; } /** * 处理清除百度缓存工具 */ async handleClearBaiduCache() { this.hotSearchService.clearCache(); return { content: [ { type: 'text', text: '✅ 百度热搜缓存已清除,下次请求将获取最新数据。', }, ], }; } /** * 处理读取当前热搜资源 */ async handleReadCurrentHotSearch() { const data = await this.hotSearchService.getHotSearchData(); return { contents: [ { uri: 'baidu://hot-search/current', mimeType: 'application/json', text: JSON.stringify(data, null, 2), }, ], }; } /** * 处理读取TOP5热搜资源 */ async handleReadTop5HotSearch() { const data = await this.hotSearchService.getHotSearchData(); const top5Data = data.slice(0, 5); return { contents: [ { uri: 'baidu://hot-search/top5', mimeType: 'application/json', text: JSON.stringify(top5Data, null, 2), }, ], }; } /** * 处理获取B站热门视频工具 */ async handleGetBilibiliHot(args) { const count = args?.count ?? 10; const useCache = args?.use_cache ?? true; const data = await this.bilibiliService.getBilibiliHotData(useCache); const results = data.slice(0, Math.min(count, data.length)); return { content: [ { type: 'text', text: this.formatBilibiliResults(results, `B站热门视频 TOP ${count}`), }, ], }; } /** * 处理搜索B站视频工具 */ async handleSearchBilibiliVideos(args) { const keyword = args.keyword; const results = await this.bilibiliService.searchBilibiliVideos(keyword); if (results.length === 0) { return { content: [ { type: 'text', text: `没有找到包含关键词"${keyword}"的B站视频。`, }, ], }; } return { content: [ { type: 'text', text: this.formatBilibiliResults(results, `B站搜索"${keyword}"的结果`), }, ], }; } /** * 处理清除B站缓存工具 */ async handleClearBilibiliCache() { this.bilibiliService.clearCache(); return { content: [ { type: 'text', text: '✅ B站缓存已清除,下次请求将获取最新数据。', }, ], }; } /** * 处理读取当前B站视频资源 */ async handleReadCurrentBilibiliVideos() { const data = await this.bilibiliService.getBilibiliHotData(); return { contents: [ { uri: 'bilibili://videos/current', mimeType: 'application/json', text: JSON.stringify(data, null, 2), }, ], }; } /** * 处理读取TOP5 B站视频资源 */ async handleReadTop5BilibiliVideos() { const data = await this.bilibiliService.getBilibiliHotData(); const top5Data = data.slice(0, 5); return { contents: [ { uri: 'bilibili://videos/top5', mimeType: 'application/json', text: JSON.stringify(top5Data, null, 2), }, ], }; } /** * 格式化热搜结果为可读文本 */ formatHotSearchResults(results, title) { let output = `## ${title}\n\n`; if (results.length === 0) { output += '暂无数据\n'; return output; } results.forEach((item, index) => { output += `### ${item.rank}. ${item.title}\n`; output += `- **热度**: ${item.hotScore}\n`; output += `- **趋势**: ${item.trend}\n`; if (item.description) { output += `- **描述**: ${item.description}\n`; } if (item.url) { output += `- **链接**: ${item.url}\n`; } output += '\n'; }); output += `*数据获取时间: ${new Date().toLocaleString('zh-CN')}*\n`; return output; } /** * 格式化B站视频结果为可读文本 */ formatBilibiliResults(results, title) { let output = `## ${title}\n\n`; if (results.length === 0) { output += '暂无数据\n'; return output; } results.forEach((item) => { output += `### ${item.rank}. ${item.title}\n`; output += `- **UP主**: ${item.author}\n`; output += `- **播放量**: ${this.formatNumber(item.views)}\n`; output += `- **点赞**: ${this.formatNumber(item.likes)}\n`; output += `- **投币**: ${this.formatNumber(item.coins)}\n`; output += `- **BV号**: ${item.bvid}\n`; if (item.description) { output += `- **描述**: ${item.description}\n`; } if (item.publishLocation) { output += `- **发布地**: ${item.publishLocation}\n`; } if (item.url) { output += `- **链接**: ${item.url}\n`; } output += `- **统计**: 弹幕${this.formatNumber(item.stats.danmaku)} | 评论${this.formatNumber(item.stats.reply)} | 收藏${this.formatNumber(item.stats.favorite)} | 分享${this.formatNumber(item.stats.share)}\n`; output += '\n'; }); output += `*数据获取时间: ${new Date().toLocaleString('zh-CN')}*\n`; return output; } /** * 格式化数字显示 */ formatNumber(num) { if (num >= 10000) { return `${(num / 10000).toFixed(1)}万`; } return num.toString(); } /** * 启动MCP服务器 */ async start(transport = 'stdio', port) { if (transport === 'sse') { await this.startSSEServer(port || 3000); } else { await this.startStdioServer(); } } /** * 启动STDIO传输 */ async startStdioServer() { const serverTransport = new StdioServerTransport(); await this.server.connect(serverTransport); console.error('📱 启动 STDIO 传输模式'); console.error('🚀 热门内容 MCP 服务器已启动'); console.error('📋 可用工具: get_baidu_hot_search, get_bilibili_hot, search_baidu_hot_search, search_bilibili_videos 等'); console.error('📚 可用资源: baidu://hot-search/*, bilibili://videos/*'); } /** * 启动SSE传输 */ async startSSEServer(port) { const httpServer = http.createServer(); httpServer.on('request', async (req, res) => { const parsedUrl = url.parse(req.url || '', true); // 设置CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } if (parsedUrl.pathname === '/') { // 提供简单的HTML客户端 res.setHeader('Content-Type', 'text/html'); res.writeHead(200); res.end(this.getTestPage()); return; } if (parsedUrl.pathname === '/sse' && req.method === 'GET') { // SSE连接 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); res.write('data: {"type":"connection","message":"Connected to Hot Content MCP Server"}\\n\\n'); // 发送服务器信息 const serverInfo = { type: 'server-info', name: 'hot-content-mcp', version: '2.3.0', tools: ['get_baidu_hot_search', 'get_bilibili_hot', 'search_baidu_hot_search', 'search_bilibili_videos'], resources: ['baidu://hot-search/current', 'bilibili://videos/current'] }; res.write(`data: ${JSON.stringify(serverInfo)}\\n\\n`); // 保持连接活跃 const keepAlive = setInterval(() => { res.write('data: {"type":"ping","timestamp":' + Date.now() + '}\\n\\n'); }, 30000); req.on('close', () => { clearInterval(keepAlive); }); return; } if (parsedUrl.pathname === '/api' && req.method === 'POST') { // 处理MCP请求 let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const request = JSON.parse(body); const response = await this.handleMCPRequest(request); res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(JSON.stringify(response)); } catch (error) { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid request' })); } }); return; } res.writeHead(404); res.end('Not found'); }); httpServer.listen(port, () => { console.error(`🌐 启动 SSE 传输模式,端口: ${port}`); console.error(`🔗 访问地址: http://localhost:${port}`); console.error('🚀 热门内容 MCP 服务器已启动'); console.error('📋 可用工具: get_baidu_hot_search, get_bilibili_hot, search_baidu_hot_search, search_bilibili_videos'); console.error('📚 可用资源: baidu://hot-search/current, bilibili://videos/current'); console.error('💡 在浏览器中访问上述地址测试SSE连接'); }); } /** * 处理MCP请求 */ async handleMCPRequest(request) { switch (request.method) { case 'tools/list': return { tools: [ { name: 'get_baidu_hot_search', description: '获取百度热搜榜数据', inputSchema: { type: 'object', properties: { count: { type: 'number', description: '要获取的热搜条数,默认为10,最大50' } } } }, { name: 'search_baidu_hot_search', description: '搜索包含特定关键词的百度热搜', inputSchema: { type: 'object', properties: { keyword: { type: 'string', description: '搜索关键词' } }, required: ['keyword'] } }, { name: 'get_bilibili_hot', description: '获取B站热门视频数据', inputSchema: { type: 'object', properties: { count: { type: 'number', description: '要获取的视频数量,默认为10,最大50' } } } } ] }; case 'tools/call': const toolName = request.params.name; const args = request.params.arguments || {}; switch (toolName) { case 'get_baidu_hot_search': const data = await this.hotSearchService.getHotSearchData(); const count = args.count || 10; const results = data.slice(0, Math.min(count, data.length)); return { content: [ { type: 'text', text: this.formatHotSearchResults(results, `百度热搜榜 TOP ${count}`) } ] }; case 'search_baidu_hot_search': const searchResults = await this.hotSearchService.searchHotSearch(args.keyword); return { content: [ { type: 'text', text: searchResults.length > 0 ? this.formatHotSearchResults(searchResults, `搜索"${args.keyword}"的结果`) : `没有找到包含关键词"${args.keyword}"的热搜。` } ] }; case 'get_bilibili_hot': const bilibiliData = await this.bilibiliService.getBilibiliHotData(); const bilibiliCount = args.count || 10; const bilibiliResults = bilibiliData.slice(0, Math.min(bilibiliCount, bilibiliData.length)); return { content: [ { type: 'text', text: this.formatBilibiliResults(bilibiliResults, `B站热门视频 TOP ${bilibiliCount}`) } ] }; default: throw new Error(`未知工具: ${toolName}`); } default: throw new Error(`不支持的方法: ${request.method}`); } } /** * 获取测试页面HTML */ getTestPage() { return ` <!DOCTYPE html> <html> <head> <title>热门内容 MCP 服务器</title> <meta charset="utf-8"> <style> body { font-family: Arial, sans-serif; margin: 20px; } .container { max-width: 800px; margin: 0 auto; } .status { padding: 10px; border-radius: 5px; margin: 10px 0; } .connected { background: #d4edda; color: #155724; } .error { background: #f8d7da; color: #721c24; } .message { background: #f8f9fa; padding: 10px; margin: 5px 0; border-left: 3px solid #007bff; } button { padding: 10px 15px; margin: 5px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; } button:hover { background: #0056b3; } #messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; } </style> </head> <body> <div class="container"> <h1>🔥 热门内容 MCP 服务器</h1> <div id="status" class="status">准备连接...</div> <div> <button onclick="testGetBaiduHotSearch()">获取百度热搜</button> <button onclick="testGetBilibiliHot()">获取B站热门</button> <button onclick="testSearch()">搜索热搜</button> <button onclick="clearMessages()">清空消息</button> </div> <div id="messages"></div> </div> <script> let eventSource; const messagesDiv = document.getElementById('messages'); const statusDiv = document.getElementById('status'); function connectSSE() { eventSource = new EventSource('/sse'); eventSource.onopen = function(event) { statusDiv.className = 'status connected'; statusDiv.textContent = '✅ 已连接到 MCP 服务器'; addMessage('连接成功', 'info'); }; eventSource.onmessage = function(event) { try { const data = JSON.parse(event.data); addMessage(JSON.stringify(data, null, 2), 'data'); } catch (e) { addMessage(event.data, 'raw'); } }; eventSource.onerror = function(event) { statusDiv.className = 'status error'; statusDiv.textContent = '❌ 连接错误'; addMessage('连接错误', 'error'); }; } async function testGetBaiduHotSearch() { try { const response = await fetch('/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'tools/call', params: { name: 'get_baidu_hot_search', arguments: { count: 5 } } }) }); const result = await response.json(); addMessage('获取百度热搜结果:', 'info'); addMessage(JSON.stringify(result, null, 2), 'success'); } catch (error) { addMessage('获取百度热搜失败: ' + error.message, 'error'); } } async function testGetBilibiliHot() { try { const response = await fetch('/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'tools/call', params: { name: 'get_bilibili_hot', arguments: { count: 5 } } }) }); const result = await response.json(); addMessage('获取B站热门结果:', 'info'); addMessage(JSON.stringify(result, null, 2), 'success'); } catch (error) { addMessage('获取B站热门失败: ' + error.message, 'error'); } } async function testSearch() { const keyword = prompt('请输入搜索关键词:'); if (!keyword) return; try { const response = await fetch('/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'tools/call', params: { name: 'search_baidu_hot_search', arguments: { keyword } } }) }); const result = await response.json(); addMessage('搜索结果:', 'info'); addMessage(JSON.stringify(result, null, 2), 'success'); } catch (error) { addMessage('搜索失败: ' + error.message, 'error'); } } function addMessage(message, type) { const div = document.createElement('div'); div.className = 'message ' + type; div.innerHTML = '<strong>' + new Date().toLocaleTimeString() + ':</strong><br><pre>' + message + '</pre>'; messagesDiv.appendChild(div); messagesDiv.scrollTop = messagesDiv.scrollHeight; } function clearMessages() { messagesDiv.innerHTML = ''; } // 自动连接 connectSSE(); </script> </body> </html> `; } /** * 获取服务器实例 */ getServer() { return this.server; } } //# sourceMappingURL=mcp-server.js.map