Skip to main content
Glama
agent.js9.87 kB
#!/usr/bin/env node /** * 简单的 Agent 来调用 MCP 服务器 * 使用 LLM API (https://llmapi.paratera.com) 来决策和执行 MCP 工具调用 */ // 使用 Node.js 18+ 原生 fetch(无需额外依赖) import { fileURLToPath } from 'url'; import { resolve } from 'path'; import { CONFIG } from './config.js'; /** * 调用 MCP 服务器 */ async function callMcpServer(method, params = {}) { const request = { jsonrpc: '2.0', id: Date.now(), method, params, }; // 调试日志(仅在开发时启用) if (process.env.DEBUG) { console.log(`[DEBUG] MCP 请求:`, JSON.stringify(request, null, 2)); } try { const response = await fetch(CONFIG.MCP_SERVER_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(request), }); if (!response.ok) { // 尝试读取错误响应体 let errorBody = ''; try { errorBody = await response.text(); } catch (e) { errorBody = '无法读取错误响应'; } throw new Error(`MCP 服务器错误: ${response.status} ${response.statusText}\n响应体: ${errorBody}`); } const data = await response.json(); if (data.error) { throw new Error(`MCP 错误: ${JSON.stringify(data.error, null, 2)}`); } return data.result; } catch (error) { throw new Error(`调用 MCP 服务器失败: ${error.message}`); } } /** * 获取 MCP 工具列表 */ async function listTools() { return await callMcpServer('tools/list'); } /** * 调用 MCP 工具 */ async function callTool(name, args) { return await callMcpServer('tools/call', { name, arguments: args, }); } /** * 调用 LLM API */ async function callLLM(messages, tools = null) { const url = `${CONFIG.LLM_API_URL}/v1/chat/completions`; const body = { model: CONFIG.MODEL, messages, temperature: 0.7, }; // 如果提供了工具,添加到请求中 if (tools && tools.length > 0) { body.tools = tools; body.tool_choice = 'auto'; } const headers = { 'Content-Type': 'application/json', }; // 如果有 API key,添加到 headers if (CONFIG.LLM_API_KEY) { headers['Authorization'] = `Bearer ${CONFIG.LLM_API_KEY}`; } try { const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`LLM API 错误: ${response.status} ${response.statusText}\n${errorText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`调用 LLM API 失败: ${error.message}`); } } /** * 将 MCP 工具格式转换为 OpenAI 工具格式 */ function convertMcpToolsToOpenAI(mcpTools) { return mcpTools.map(tool => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, })); } /** * 执行工具调用 */ async function executeToolCall(toolCall) { const { name, arguments: argsRaw } = toolCall.function; // 处理参数:如果 LLM 返回的是字符串,需要解析为对象 let args = argsRaw; if (typeof argsRaw === 'string') { try { args = JSON.parse(argsRaw); } catch (e) { throw new Error(`无法解析工具参数: ${argsRaw}`); } } console.log(`\n[执行工具] ${name}`); console.log(` 参数:`, JSON.stringify(args, null, 2)); try { const result = await callTool(name, args); // 提取工具返回的文本内容 let toolResult = ''; if (result.content && result.content.length > 0) { toolResult = result.content .filter(item => item.type === 'text') .map(item => item.text) .join('\n'); } else { toolResult = JSON.stringify(result, null, 2); } console.log(` 结果:`, toolResult.substring(0, 200) + (toolResult.length > 200 ? '...' : '')); return { role: 'tool', tool_call_id: toolCall.id, name: toolCall.function.name, content: toolResult, }; } catch (error) { console.error(` 错误:`, error.message); return { role: 'tool', tool_call_id: toolCall.id, name: toolCall.function.name, content: `错误: ${error.message}`, }; } } /** * 主 Agent 函数 */ async function runAgent(userQuery) { console.log('='.repeat(80)); console.log('Crypto MCP Agent'); console.log('='.repeat(80)); console.log(`\n用户问题: ${userQuery}\n`); try { // 1. 获取 MCP 工具列表 console.log('[步骤 1] 获取 MCP 工具列表...'); const toolsResult = await listTools(); const mcpTools = toolsResult.tools || []; console.log(` 找到 ${mcpTools.length} 个工具:`); mcpTools.forEach(tool => { console.log(` - ${tool.name}: ${tool.description}`); }); // 2. 转换为 OpenAI 工具格式 const openAITools = convertMcpToolsToOpenAI(mcpTools); // 3. 初始化对话消息 const messages = [ { role: 'system', content: `你是一个加密货币市场分析助手。你可以使用以下工具来获取实时市场数据: ${mcpTools.map(t => `- ${t.name}: ${t.description}`).join('\n')} 请根据用户的问题,选择合适的工具来获取数据,然后基于数据给出分析和回答。`, }, { role: 'user', content: userQuery, }, ]; // 4. 开始对话循环(最多 10 轮工具调用) let maxIterations = 10; let iteration = 0; while (iteration < maxIterations) { iteration++; console.log(`\n[步骤 ${iteration + 1}] 调用 LLM...`); // 调用 LLM const llmResponse = await callLLM(messages, openAITools); const choice = llmResponse.choices[0]; const message = choice.message; // 调试信息:显示 LLM 的响应 if (process.env.DEBUG) { console.log(`[DEBUG] LLM 响应:`, JSON.stringify({ hasToolCalls: !!message.tool_calls, toolCallsCount: message.tool_calls?.length || 0, hasContent: !!message.content, contentPreview: message.content?.substring(0, 100) || '无内容' }, null, 2)); } // 添加 LLM 的回复到消息历史 messages.push(message); // 检查是否有工具调用 if (message.tool_calls && message.tool_calls.length > 0) { console.log(` LLM 决定调用 ${message.tool_calls.length} 个工具`); // 执行所有工具调用 const toolResults = await Promise.all( message.tool_calls.map(toolCall => executeToolCall(toolCall)) ); // 将工具结果添加到消息历史 messages.push(...toolResults); // 如果已经执行了工具调用,下一次循环时强制 LLM 给出最终答案 // 通过设置 tool_choice 为 'none' 来避免无限循环 if (iteration >= maxIterations - 1) { console.log(`\n[警告] 即将达到最大迭代次数,最后一次调用将强制 LLM 给出最终答案`); } } else { // 没有工具调用,LLM 已经给出最终答案 if (message.content) { console.log('\n[最终回答]'); console.log('='.repeat(80)); console.log(message.content); console.log('='.repeat(80)); return message.content; } else { console.warn('\n[警告] LLM 返回的消息没有内容,尝试继续...'); // 如果消息没有内容也没有工具调用,可能是格式问题,继续循环 } } } // 如果达到最大迭代次数,尝试最后一次调用(不使用工具) console.log('\n[信息] 达到最大迭代次数,最后一次调用 LLM(不使用工具)...'); const finalResponse = await callLLM(messages, null); // 不提供工具,强制 LLM 给出答案 const finalMessage = finalResponse.choices[0].message; if (finalMessage.content) { console.log('\n[最终回答] (达到最大迭代次数后强制生成)'); console.log('='.repeat(80)); console.log(finalMessage.content); console.log('='.repeat(80)); return finalMessage.content; } // 如果还是没有内容,返回错误 throw new Error('达到最大迭代次数,LLM 未能生成最终答案'); } catch (error) { console.error('\n[错误]', error.message); if (error.stack) { console.error(error.stack); } throw error; } } // 命令行接口 async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.log('用法: node agent.js "你的问题"'); console.log('\n示例:'); console.log(' node agent.js "BTC 现在多少钱?"'); console.log(' node agent.js "ETH 今天涨了多少?"'); console.log(' node agent.js "BTC 的买卖盘深度怎么样?"'); console.log('\n环境变量:'); console.log(' LLM_API_URL - LLM API 地址 (默认: https://llmapi.paratera.com)'); console.log(' LLM_API_KEY - LLM API Key (可选)'); console.log(' MCP_SERVER_URL - MCP 服务器地址 (默认: http://localhost:8081/mcp)'); console.log(' MODEL - LLM 模型名称 (默认: gpt-3.5-turbo)'); process.exit(1); } const userQuery = args.join(' '); try { await runAgent(userQuery); } catch (error) { console.error('Agent 执行失败:', error); process.exit(1); } } // 如果直接运行此脚本(通过 node agent.js 运行) const __filename = fileURLToPath(import.meta.url); const isMainModule = process.argv[1] && resolve(process.argv[1]) === __filename; if (isMainModule) { main(); } export { runAgent, callMcpServer, callLLM };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/WilliamNing316/crypto-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server