mcp和mcp-server
首先需要明确mcp是什么:
MCP本质上是一套模型无关的、用于构建可互操作AI应用的工程协议,其核心组件Server/Client与LLM的智能决策并无直接关联。
MCP由三个部分组成,host(端口),Client(客户端),server(服务器)。它引入了host的概念。区别于传统的client-server架构。
host的主要分工为和LLM进行直接交互。比如上下文的完整性,动态构筑和拼接Pormpt,解析LLM的响应,根据AI决策生成于mcp-server交互的指令。它负责了用户与ai交互的最前沿的感知。
Client是一个无状态的协议中间件,主要负责host和server的沟通。负责协议握手、会话管理和心跳维持,它不关心业务逻辑也不理解AI意图,仅作为标准化通信通道。
Server是一个标准的网络服务,它提供一组确定性的的、可供远程调用的能力,这些能力包括文件读取,工具调用等。它接收标准化的信息,执行对应的能力,返回确定的结果,它的行为是确定的可以预测的。
这里暂且只记录mcp-server。
首先是一个基础版本的server服务:它支持在本地调试,但是因为未曾暴露端口仅能在和Client处于同一位置时才可以进行对应的工具调用,有些局限性,但是方便调试与熟悉内容。
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
// Load env and use typed access
import { env } from './config.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { create, all } from 'mathjs';
import OpenAI from "openai";
// 创建 MCP 服务器(使用低层 Server 避免对等依赖 zod)
const server = new Server(
{
name: 'my-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
}
}
);
const openai = new OpenAI({
apiKey: env.OPENAI_API_KEY ?? env.DASHSCOPE_API_KEY,
// 以下是北京地域base_url,如果使用新加坡地域的模型,需要将base_url替换为:https://dashscope-intl.aliyuncs.com/compatible-mode/v1
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
});
// 工具列表(按照新规范使用 inputSchema)
const tools = [
{
name: 'greet',
description: '打招呼',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: '姓名',
},
},
required: ['name'],
},
},
{
name: 'calculate',
description: '计算',
inputSchema: {
type: 'object',
properties: {
expression: {
type: 'string',
description: '数学表达式',
},
},
required: ['expression'],
},
},
{
name: 'ask-AI',
description: '询问 AI',
inputSchema: {
type: 'object',
properties: {
question: {
type: 'string',
description: '问题',
},
},
required: ['question'],
},
}
];
// 处理工具列表请求
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
}
});
// 处理工具调用请求
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'greet':
const { name: personName } = args as { name: string };
return {
content: [
{
type: 'text',
text: `Hello, ${personName}!`,
}
]
};
case 'calculate':
try {
const { expression } = args as { expression: string };
const result = safeEval(expression || "0");
return {
content: [
{
type: 'text',
text: `${expression} = ${result}`,
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `计算错误: ${error.message}`,
}
]
};
}
case 'ask-AI':
const { question } = args as { question: string };
// TODO: 调用 AI 模型
const completion = await openai.chat.completions.create({
model: "qwen3-vl-plus",
messages: [
{
role: "user",
content: question,
}
],
temperature: 0.7,
});
return {
content: [
{
type: 'text',
text: completion.choices[0].message.content,
}
]
};
default:
throw new Error(`未知工具: ${name}`);
}
});
// 安全计算函数
function safeEval(expression: string): number {
try {
// 创建 math 实例并计算表达式
const math = create(all, {
precision: 14
});
return math.evaluate(expression);
} catch (error: any) {
throw new Error(`无效的数学表达式: ${expression}, 错误: ${error?.message || String(error)}`);
}
}
// 启动服务器
async function startServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.log('NODE.JS MCP SERVER STARTED');
console.log('传输方式:stdio');
console.log('工具列表:', tools.map(tool => tool.name).join(','));
}
startServer().catch(error => {
console.error('启动服务器失败:', error);
process.exit(1);
});
其次,为了能够提供远程的服务,需要对它进行一些改造,主要内容是通讯协议方面由Stdio修改为stream,暴露一些服务端口来方便部署于其他位置的Client进行访问通信;
// server.js - 修复版 const http = require('http'); const https = require('https'); const fs = require('fs'); const path = require('path'); const os = require('os'); // 工具管理器函数 function createToolManager() { const tools = new Map(); return { /** * 注册工具 */ registerTool(name, description, parameters, executeFn) { tools.set(name, { name, description, parameters, execute: executeFn }); console.log(`🛠️ Tool registered: ${name}`); return this; }, /** * 注册工具对象 */ registerToolObject(tool) { tools.set(tool.name, tool); console.log(`🛠️ Tool registered: ${tool.name}`); return this; }, /** * 获取工具 */ getTool(name) { return tools.get(name); }, /** * 获取所有工具 */ getAllTools() { return Array.from(tools.values()).map(tool => ({ name: tool.name, description: tool.description, parameters: tool.parameters || {} })); }, /** * 执行工具 */ async executeTool(name, params) { const tool = tools.get(name); if (!tool) { throw new Error(`Tool "${name}" not found`); } try { return await tool.execute(params); } catch (error) { throw new Error(`Tool execution failed: ${error.message}`); } }, /** * 注册内置工具 */ registerBuiltinTools() { // 计算器工具 this.registerTool( 'calculator', 'Perform mathematical calculations', { expression: { type: 'string', description: 'Mathematical expression to evaluate', required: true } }, async (params) => { const { expression } = params; const sanitized = expression.replace(/[^0-9+\-*/().\s]/g, ''); try { const result = eval(sanitized); if (typeof result !== 'number' || !isFinite(result)) { throw new Error('Invalid calculation result'); } return { result, expression, formatted: `${expression} = ${result}` }; } catch (error) { throw new Error(`Calculation error: ${error.message}`); } } ); // 时间工具 this.registerTool( 'get_current_time', 'Get the current date and time', { format: { type: 'string', description: 'Time format (ISO, local, timestamp, custom)', default: 'ISO', enum: ['ISO', 'local', 'timestamp'] }, timezone: { type: 'string', description: 'Timezone (e.g., "UTC", "America/New_York")', default: 'UTC' } }, async (params) => { const { format = 'ISO', timezone = 'UTC' } = params; const now = new Date(); let formatted; switch (format.toLowerCase()) { case 'iso': formatted = now.toISOString(); break; case 'local': formatted = now.toLocaleString('en-US', { timeZone: timezone }); break; case 'timestamp': formatted = now.getTime().toString(); break; default: formatted = now.toISOString(); } return { timestamp: now.getTime(), formatted, timezone, date: now.toDateString(), time: now.toTimeString() }; } ); // HTTP请求工具 this.registerTool( 'http_request', 'Make HTTP requests', { url: { type: 'string', description: 'Request URL', required: true }, method: { type: 'string', description: 'HTTP method', default: 'GET', enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] }, headers: { type: 'object', description: 'Request headers', default: {} }, body: { type: 'any', description: 'Request body', default: null } }, async (params) => { const { url, method = 'GET', headers = {}, body = null } = params; return new Promise((resolve, reject) => { const parsedUrl = new URL(url); const client = parsedUrl.protocol === 'https:' ? https : http; const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), path: parsedUrl.pathname + parsedUrl.search, method, headers: { 'User-Agent': 'MCP-Server/1.0', ...headers } }; const req = client.request(options, (res) => { let responseData = ''; const responseHeaders = { ...res.headers }; res.on('data', chunk => responseData += chunk); res.on('end', () => { let data; try { data = JSON.parse(responseData); } catch { data = responseData; } resolve({ status: res.statusCode, statusText: res.statusMessage, headers: responseHeaders, data }); }); }); req.on('error', reject); if (body) { if (typeof body === 'object') { req.write(JSON.stringify(body)); } else { req.write(body); } } req.end(); }); } ); // 天气工具(模拟) this.registerTool( 'weather', 'Get weather information for a city', { city: { type: 'string', description: 'City name', required: true }, country: { type: 'string', description: 'Country code (e.g., "us", "cn")', default: '' }, units: { type: 'string', description: 'Temperature units (metric, imperial)', default: 'metric', enum: ['metric', 'imperial'] } }, async (params) => { const { city, country = '', units = 'metric' } = params; // 模拟天气数据 return { city: country ? `${city}, ${country}` : city, temperature: units === 'metric' ? 25 : 77, description: 'Sunny', humidity: 65, wind_speed: units === 'metric' ? 15 : 9.3, units, timestamp: new Date().toISOString() }; } ); // 文件系统工具 this.registerTool( 'filesystem', 'Perform file system operations', { action: { type: 'string', description: 'Action to perform', required: true, enum: ['read', 'write', 'list', 'delete', 'stat'] }, path: { type: 'string', description: 'File or directory path', required: true }, content: { type: 'string', description: 'Content to write (for write action)', default: '' }, encoding: { type: 'string', description: 'File encoding', default: 'utf-8' } }, async (params) => { const { action, path: filePath, content = '', encoding = 'utf-8' } = params; try { let result; switch (action) { case 'read': result = await fs.promises.readFile(filePath, encoding); break; case 'write': await fs.promises.writeFile(filePath, content, encoding); result = { written: content.length, path: filePath }; break; case 'list': const files = await fs.promises.readdir(filePath); result = files.map(file => ({ name: file, isDirectory: fs.statSync(path.join(filePath, file)).isDirectory() })); break; case 'delete': await fs.promises.unlink(filePath); result = { deleted: filePath }; break; case 'stat': const stats = await fs.promises.stat(filePath); result = { size: stats.size, mtime: stats.mtime, isFile: stats.isFile(), isDirectory: stats.isDirectory() }; break; default: throw new Error(`Unsupported action: ${action}`); } return { success: true, action, result }; } catch (error) { return { success: false, action, error: error.message }; } } ); // 系统信息工具 this.registerTool( 'system_info', 'Get system information', { type: { type: 'string', description: 'Type of information to get', default: 'all', enum: ['all', 'memory', 'cpu', 'network'] } }, async (params) => { const { type = 'all' } = params; const info = { platform: os.platform(), arch: os.arch(), uptime: os.uptime(), timestamp: new Date().toISOString() }; if (type === 'all' || type === 'memory') { info.memory = { total: os.totalmem(), free: os.freemem(), used: os.totalmem() - os.freemem(), usage: ((os.totalmem() - os.freemem()) / os.totalmem() * 100).toFixed(2) + '%' }; } if (type === 'all' || type === 'cpu') { info.cpus = os.cpus().map(cpu => ({ model: cpu.model, speed: cpu.speed, times: cpu.times })); info.loadavg = os.loadavg(); } if (type === 'all' || type === 'network') { info.network = os.networkInterfaces(); } return info; } ); return this; } }; } // AI 服务函数 function createAIService(config) { const aiConfig = { apiKey: config.apiKey, endpoint: config.endpoint || 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: config.model || 'gpt-3.5-turbo', temperature: config.temperature || 0.7, timeoutMs: Number( (config && config.timeoutMs != null ? config.timeoutMs : process.env.AI_API_TIMEOUT_MS) || 15000 ), }; /** * 调用 AI API */ async function callAIAPI(messages, options = {}) { const requestData = JSON.stringify({ model: options.model || aiConfig.model, messages, temperature: options.temperature || aiConfig.temperature, max_tokens: options.max_tokens, stream: options.stream || false }); return new Promise((resolve, reject) => { const url = new URL(aiConfig.endpoint); const req = https.request({ hostname: url.hostname, port: url.port || 443, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${aiConfig.apiKey}`, ...(options.stream && { 'Accept': 'text/event-stream' }) }, timeout: aiConfig.timeoutMs, }, (res) => { let data = ''; res.on('data', (chunk) => { if (options.stream && options.onChunk) { options.onChunk(chunk.toString()); } else { data += chunk; } }); res.on('end', () => { if (options.stream) { resolve({ complete: true }); } else { try { console.log('DEBUG: Raw AI Response:', data); const response = JSON.parse(data); if (res.statusCode !== 200) { reject(new Error(`AI API error: ${response.error?.message || 'Unknown error'}`)); return; } resolve(response.choices?.[0]?.message?.content || ''); } catch (error) { console.error('DEBUG: JSON Parse Error. Data was:', data); reject(new Error('Failed to parse AI response')); } } }); }); // 超时保护:上游长时间无响应时中断 req.setTimeout(aiConfig.timeoutMs, () => { req.destroy(new Error('Upstream timeout')); }); req.on('error', reject); req.write(requestData); req.end(); }); } /** * 流式聊天 */ async function streamChat(messages, options = {}) { return callAIAPI(messages, { ...options, stream: true }); } return { callAIAPI, streamChat }; } // 主服务器创建函数 function createMCPServer(config = {}) { const server = http.createServer(); const port = config.port || 3000; const toolManager = createToolManager(); const aiService = config.ai ? createAIService(config.ai) : null; // 增加最大监听器数量 server.setMaxListeners(20); // 默认配置 const defaultConfig = { cors: true, logging: true, bodyLimit: '1mb', ...config }; // 自动注册内置工具 toolManager.registerBuiltinTools(); /** * 设置响应头 */ function setResponseHeaders(res) { res.setHeader('Content-Type', 'application/json'); if (defaultConfig.cors) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); } } /** * 解析请求体 */ function parseRequestBody(req) { return new Promise((resolve, reject) => { if (req.method === 'GET' || req.method === 'HEAD') { resolve({}); return; } const contentType = req.headers['content-type'] || ''; let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { try { if (!body) { resolve({}); return; } console.log('DEBUG: Received body:', body); console.log('DEBUG: Content-Type:', contentType); if (contentType.includes('application/json')) { resolve(JSON.parse(body)); } else { resolve({ raw: body }); } } catch (error) { console.error('DEBUG: Parse error:', error.message, 'Body was:', body); reject(new Error('Invalid request body')); } }); req.on('error', reject); }); } /** * 发送 JSON 响应 */ function sendJson(res, statusCode, data) { res.statusCode = statusCode; res.end(JSON.stringify(data, null, 2)); } /** * 处理请求的主函数 */ async function handleRequest(req, res) { const startTime = Date.now(); try { // 设置响应头 setResponseHeaders(res); // 处理 OPTIONS 预检请求 if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // 解析 URL const url = new URL(req.url, `http://${req.headers.host}`); const pathname = url.pathname; // 解析请求体 const body = await parseRequestBody(req); // 路由处理 if (req.method === 'GET' && pathname === '/') { sendJson(res, 200, { name: 'MCP Server', version: '1.0.0', endpoints: { '/health': 'GET - Health check', '/tools': 'GET - List all tools', '/tools/:name': 'POST - Execute a tool', '/chat': 'POST - Chat with AI', '/chat/stream': 'POST - Stream chat with AI' }, timestamp: new Date().toISOString() }); return; } if (req.method === 'GET' && pathname === '/health') { sendJson(res, 200, { status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage() }); return; } if (req.method === 'GET' && pathname === '/tools') { sendJson(res, 200, { tools: toolManager.getAllTools(), count: toolManager.getAllTools().length }); return; } if (req.method === 'POST' && pathname.startsWith('/tools/')) { const toolName = pathname.substring('/tools/'.length); try { const result = await toolManager.executeTool(toolName, body); sendJson(res, 200, { success: true, tool: toolName, result }); } catch (error) { sendJson(res, 400, { success: false, tool: toolName, error: error.message }); } return; } if (req.method === 'POST' && pathname === '/chat') { if (!aiService) { sendJson(res, 500, { error: 'AI service not configured' }); return; } try { const { messages, model, temperature, stream } = body; if (!messages || !Array.isArray(messages)) { sendJson(res, 400, { error: 'Messages array is required' }); return; } if (stream) { // 流式响应 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' }); await aiService.streamChat(messages, { model, temperature, onChunk: (chunk) => { res.write(`data: ${JSON.stringify({ chunk })}\n\n`); } }); res.write('data: [DONE]\n\n'); res.end(); } else { // 普通响应 const response = await aiService.callAIAPI(messages, { model, temperature }); sendJson(res, 200, { id: `chatcmpl-${Date.now()}`, object: 'chat.completion', created: Math.floor(Date.now() / 1000), model: model || 'gpt-3.5-turbo', choices: [{ index: 0, message: { role: 'assistant', content: response }, finish_reason: 'stop' }], usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } }); } } catch (error) { const msg = (error && error.message) ? String(error.message) : String(error); if (/timeout/i.test(msg)) { sendJson(res, 504, { error: msg }); } else { sendJson(res, 500, { error: msg }); } } return; } // 404 处理 sendJson(res, 404, { error: 'Not Found', path: pathname }); } catch (error) { console.error('Request error:', error); sendJson(res, 500, { error: 'Internal Server Error', message: error.message }); } finally { // 记录日志 if (defaultConfig.logging) { const duration = Date.now() - startTime; console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ${res.statusCode} - ${duration}ms`); } } } /** * 启动服务器 */ function start(callback) { // 设置请求处理器 - 只设置一次! server.on('request', handleRequest); server.listen(port, (err) => { if (callback) callback(err, server); if (defaultConfig.logging) { console.log(`🚀 MCP Server running on http://localhost:${port}`); console.log('📡 Available endpoints:'); console.log(` GET / - Server info`); console.log(` GET /health - Health check`); console.log(` GET /tools - List available tools`); console.log(` POST /tools/:name - Execute a tool`); console.log(` POST /chat - Chat with AI`); console.log(` POST /chat/stream - Stream chat with AI`); console.log(''); console.log(`🛠️ Available tools: ${toolManager.getAllTools().map(t => t.name).join(', ')}`); } }); return server; } /** * 停止服务器 */ function stop(callback) { server.close(callback); if (defaultConfig.logging) { console.log('🛑 MCP Server stopped'); } } /** * 注册工具 */ function registerTool(name, description, parameters, executeFn) { toolManager.registerTool(name, description, parameters, executeFn); } /** * 注册工具对象 */ function registerToolObject(tool) { toolManager.registerToolObject(tool); } /** * 执行工具 */ async function executeTool(name, params) { return toolManager.executeTool(name, params); } /** * 获取所有工具 */ function getTools() { return toolManager.getAllTools(); } return { start, stop, registerTool, registerToolObject, executeTool, getTools }; } module.exports = { createMCPServer, createToolManager, createAIService };
截至此时,这个mcp-Server才算是一个完整的可供调用的服务。如果想要运行可以在此页面新增
// 如果直接运行此文件 if (require.main === module) { require('dotenv/config'); const port = Number(process.env.PORT || 3000); const aiKey = process.env.AI_API_KEY || ''; const aiEndpoint = process.env.AI_API_ENDPOINT; const aiModel = process.env.AI_MODEL || 'gpt-3.5-turbo'; const aiTemperature = Number(process.env.AI_TEMPERATURE || 0.7); const aiTimeoutMs = Number(process.env.AI_API_TIMEOUT_MS || 15000); console.log('🚀 Starting server directly from server.js...'); console.log('DEBUG: AI_API_KEY length:', aiKey ? aiKey.length : 0); const config = { port, ai: aiKey ? { apiKey: aiKey, endpoint: aiEndpoint, model: aiModel, temperature: aiTemperature, timeoutMs: aiTimeoutMs, } : undefined }; const mcpServer = createMCPServer(config); mcpServer.start((err) => { if (err) { console.error('Failed to start server:', err); process.exit(1); } console.log('✅ Server is up and running!'); }); }
这样就可以通过node server.js来运行,
如果不想修改server可以通过index.js,运行方式为 node index.js
// index.js require('dotenv/config'); const { createMCPServer } = require('./server'); // 环境变量读取与默认值 const port = Number(process.env.PORT || 3000);
const host = process.env.HOST || '0.0.0.0'; const aiKey = process.env.AI_API_KEY || ''; const aiEndpoint = process.env.AI_API_ENDPOINT; // 可选:自定义上游(如代理/兼容端点) const aiModel = process.env.AI_MODEL || 'gpt-3.5-turbo'; const aiTemperature = Number(process.env.AI_TEMPERATURE || 0.7); const aiTimeoutMs = Number(process.env.AI_API_TIMEOUT_MS || 15000); // 仅当提供了 API Key 时启用 AI 服务,避免误调用上游导致超时 console.log('DEBUG: AI_API_KEY length:', aiKey ? aiKey.length : 0); console.log('DEBUG: AI_API_KEY starts with:', aiKey ? aiKey.substring(0, 3) : 'N/A'); const ai = aiKey ? { apiKey: aiKey, endpoint: aiEndpoint, model: aiModel, temperature: aiTemperature, timeoutMs: aiTimeoutMs, } : undefined; const config = { port,
host, cors: true, logging: true, ai, }; const mcpServer = createMCPServer(config); mcpServer.start((err) => { if (err) { console.error('Failed to start server:', err); process.exit(1); } console.log('✅ Server started successfully!'); }); // 优雅关闭 process.on('SIGINT', () => { console.log('\n🔻 Shutting down server...'); mcpServer.stop(() => { console.log('✅ Server stopped'); process.exit(0); }); });
新增工具可以使用
mcpServer.registerTool(name: any, description: any, parameters: any, executeFn: any)

浙公网安备 33010602011771号