2025年10月15日
39 分钟阅读

mcp服务的node实现

自己实现一个mcp-server

typescript
// mcp.controller.ts import { Controller, Post, Body, Logger } from '@nestjs/common'; import { ApiTags, ApiResponse } from '@nestjs/swagger'; import { MCPService } from './mcp.service'; import { MCPRequestDto, MCPResponseDto } from './dto/mcp.dto'; @ApiTags('MCP') @Controller('mcp') export class MCPController { private readonly logger = new Logger(MCPController.name); constructor(private readonly mcpService: MCPService) {} @Post('call') @ApiResponse({ status: 200, description: 'MCP调用成功' }) @ApiResponse({ status: 400, description: '请求参数错误' }) async handleMCPCall(@Body() request: MCPRequestDto): Promise<MCPResponseDto> { this.logger.log(`Received MCP call: ${request.method}`); try { const result = await this.mcpService.executeMethod(request); return { result }; } catch (error) { this.logger.error(`MCP call failed: ${error.message}`); return { error: { message: error.message, code: error.code } }; } } }
typescript
// mcp.service.ts import { Injectable, Inject } from '@nestjs/common'; import { ToolService } from './services/tool.service'; import { PromptService } from './services/prompt.service'; import { ResourceService } from './services/resource.service'; import { MCPRequestDto } from './dto/mcp.dto'; @Injectable() export class MCPService { constructor( private readonly toolService: ToolService, private readonly promptService: PromptService, private readonly resourceService: ResourceService, ) {} async executeMethod(request: MCPRequestDto): Promise<any> { const { method, params } = request; switch (method) { case 'tools/list': return await this.toolService.listTools(); case 'tools/call': return await this.toolService.callTool(params); case 'prompts/list': return await this.promptService.listPrompts(); case 'prompts/get': return await this.promptService.getPrompt(params); case 'resources/list': return await this.resourceService.listResources(); case 'resources/read': return await this.resourceService.readResource(params); default: throw new Error(`Unknown method: ${method}`); } } }
typescript
// services/tool.service.ts import { Injectable } from '@nestjs/common'; export interface ToolDefinition { name: string; description: string; inputSchema: any; } @Injectable() export class ToolService { private readonly tools: ToolDefinition[] = [ { name: 'calculator', description: 'Perform mathematical calculations', inputSchema: { type: 'object', properties: { expression: { type: 'string' } } } }, { name: 'web_search', description: 'Search the web for information', inputSchema: { type: 'object', properties: { query: { type: 'string' } } } } ]; async listTools(): Promise<ToolDefinition[]> { return this.tools; } async callTool(params: any): Promise<any> { const { name, arguments: args } = params; const tool = this.tools.find(t => t.name === name); if (!tool) { throw new Error(`Tool not found: ${name}`); } // 验证参数 await this.validateToolArguments(tool, args); // 执行工具逻辑 return await this.executeTool(name, args); } private async validateToolArguments(tool: ToolDefinition, args: any): Promise<void> { // 使用class-validator或自定义验证逻辑 } private async executeTool(name: string, args: any): Promise<any> { switch (name) { case 'calculator': return this.executeCalculator(args); case 'web_search': return this.executeWebSearch(args); default: throw new Error(`Tool execution not implemented: ${name}`); } } private executeCalculator(args: any): any { // 计算器实现 return { result: eval(args.expression) }; } private async executeWebSearch(args: any): Promise<any> { // 搜索实现 return { results: [] }; } }

使用 @modelcontextprotocol/sdk

[!WARNING]
截止到2025-10-15 zod版本必须为3,否则schema会报错

bash
pnpm i @modelcontextprotocol/sdk zod@3
typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; const server = new McpServer({ name: 'webSearch', version: '1.0.0', capabilities: { resources: {}, tools: {}, }, }); server.tool('webSearch', 'webSearch', { input: z.string() }, async (state) => { const input = state.input; // 获取用户输入的搜索内容 try { const response = await axios.post( process.env.ZHI_PU_QING_YAN_BASE_URL as string, // 智谱青言的 API 地址 { tool: process.env.ZHI_PU_QING_YAN_TOOL as string, // 使用的工具名称 messages: [{ role: 'user', content: input }], stream: false, }, { headers: { Authorization: process.env.ZHI_PU_QING_YAN_KEY, // API 密钥 }, } ); const resData: string[] = []; for (const choice of response.data.choices) { for (const message of choice.message.tool_calls) { const searchResults = message.search_result; if (!searchResults) { continue; } for (const result of searchResults) { resData.push(result.content); // 收集搜索结果的内容 } } } return { content: [ { type: 'text', text: resData.join('\n\n\n'), // 将所有搜索结果拼接成一个字符串 }, ], }; } catch (error: any) { return { content: [ { type: 'text', text: error.message, // 返回错误信息 }, ], }; } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Web Search MCP Server running on stdio'); } main().catch((error) => { console.error('Fatal error in main():', error); process.exit(1); });

express + @modelcontextprotocol/sdk

typescript
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; import { z } from 'zod'; // Create an MCP server const server = new McpServer({ name: 'demo-server', version: '1.0.0' }); // Add an addition tool server.registerTool( 'add', { title: 'Addition Tool', description: 'Add two numbers', inputSchema: { 'a': z.number(), b: z.number() }, outputSchema: { 'result': z.number() } }, async ({ a, b }: { a: number, b: number }) => { const output = { result: a + b }; return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output }; } ); // Add a dynamic greeting resource server.registerResource( 'greeting', new ResourceTemplate('greeting://{name}', { list: undefined }), { title: 'Greeting Resource', // Display name for UI description: 'Dynamic greeting generator' }, async (uri, { name }) => ({ contents: [ { uri: uri.href, text: `Hello, ${name}!` } ] }) ); // Set up Express and HTTP transport const app = express(); app.use(express.json()); app.post('/mcp', async (req, res) => { // Create a new transport for each request to prevent request ID collisions const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true }); res.on('close', () => { transport.close(); }); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); const port = parseInt(process.env.PORT || '3000'); app.listen(port, () => { console.log(`Demo MCP Server running on http://localhost:${port}/mcp`); }).on('error', error => { console.error('Server error:', error); process.exit(1); });

nestjs+mcp-nest

typescript
// app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { McpModule } from '@rekog/mcp-nest'; import { AppService } from './app.service'; import { GreetingResource } from './resources/greeting.resource'; import { GreetingTool } from './resources/greeting.tool'; import { GreetingPrompt } from './resources/greeting.prompt'; @Module({ imports: [ McpModule.forRoot({ name: 'workflow-mcp-server', version: '1.0.0', }), ], controllers: [AppController], providers: [AppService, GreetingTool, GreetingResource, GreetingPrompt], }) export class AppModule {}
typescript
// greeting.tool.ts import type { Request } from 'express'; import { Injectable } from '@nestjs/common'; import { type Context, Tool } from '@rekog/mcp-nest'; import { Progress } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; const informalGreetings = { en: 'Hey', es: 'Qué tal', fr: 'Salut', de: 'Hi', it: 'Ciao', pt: 'Oi', ja: 'やあ', ko: '안녕', zh: '嗨', }; const languageNames = { en: 'English', es: 'Spanish', fr: 'French', de: 'German', it: 'Italian', pt: 'Portuguese', ja: 'Japanese', ko: 'Korean', zh: 'Chinese', }; @Injectable() export class GreetingTool { constructor() {} @Tool({ name: 'greet-logged-in-user', description: 'Greets the currently logged-in user using their name from the request', annotations: { title: 'Greet Logged-in User Tool', destructiveHint: false, readOnlyHint: true, idempotentHint: true, openWorldHint: false, }, }) async greetLoggedInUser(args, context: Context, request) { // Try to extract user name from request (commonly request.user or request.session.user) let name; if (request.user && typeof request.user === 'object') { name = request.user.displayName || request.user.username || request.user.name; } if (!name) { return { content: [ { type: 'text', text: 'Error: No logged-in user found in the request.', }, ], }; } return `Hello, ${name}!`; } @Tool({ name: 'greet-world', description: 'Returns a simple Hello, World! message', }) greetWorld() { console.log('greet world called'); return 'Hello, World!'; } @Tool({ name: 'greet-user', description: "Returns a personalized greeting in the user's preferred language", parameters: z.object({ name: z .string() .describe('The name of the person to greet') .nonempty('Name is required'), language: z .string() .describe('Language code (e.g., "en", "es", "fr", "de")') .nonempty('Language is required'), }), annotations: { title: 'Multi-language Greeting Tool', destructiveHint: false, readOnlyHint: true, idempotentHint: true, openWorldHint: false, }, }) async sayHello({ name, language }, context: Context, request: Request) { const greetingWord = informalGreetings[language] || informalGreetings['en']; const greeting = `${greetingWord}, ${name}!`; const totalSteps = 5; for (let i = 0; i < totalSteps; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); await context.reportProgress({ progress: (i + 1) * 20, total: 100, } as Progress); } return greeting; } @Tool({ name: 'greet-user-interactive', description: 'Returns a personalized greeting with interactive language selection', parameters: z.object({ name: z.string().describe('The first name of the person to greet'), }), annotations: { title: 'Interactive Greeting Tool', destructiveHint: false, readOnlyHint: true, idempotentHint: true, openWorldHint: false, }, }) async sayHelloElicitation({ name }, context: Context, request: Request) { try { const res = context.mcpServer.server.getClientCapabilities(); if (!res?.elicitation) { const result = { content: [ { type: 'text', text: 'Elicitation is not supported by the client. Thus this tool cannot be used.', }, ], }; return result; } const response = await context.mcpServer.server.elicitInput({ message: 'Please select your preferred language', requestedSchema: { type: 'object', properties: { language: { type: 'string', enum: ['en', 'es', 'fr', 'de', 'it', 'pt', 'ja', 'ko', 'zh'], description: 'Your preferred language for the greeting', }, }, }, }); let selectedLanguage = 'en'; switch (response.action) { case 'accept': { selectedLanguage = (response?.content?.language as string) || 'en'; break; } case 'decline': case 'cancel': selectedLanguage = 'en'; break; default: selectedLanguage = 'en'; } const greetingWord = informalGreetings[selectedLanguage] || informalGreetings['en']; const greeting = `${greetingWord}, ${name}!`; const result = { content: [{ type: 'text', text: greeting }], }; return result; } catch (error) { const result = { content: [{ type: 'text', text: `Error: ${error.message}` }], }; return result; } } @Tool({ name: 'greet-user-structured', description: 'Returns a structured greeting message with language metadata', parameters: z.object({ name: z.string().describe('The name of the person to greet'), language: z .string() .describe('Language code (e.g., "en", "es", "fr", "de")'), }), outputSchema: z.object({ greeting: z.string(), language: z.string(), languageName: z.string(), }), annotations: { title: 'Structured Greeting Tool', destructiveHint: false, readOnlyHint: true, idempotentHint: true, openWorldHint: false, }, }) async sayHelloStructured( { name, language }, context: Context, request: Request, ) { if (!name || !language) { console.log( '[greeting.tool.ts] Exiting sayHelloStructured (missing args)', ); return { content: [ { type: 'text', text: 'Error: Missing required parameters name and language.', }, ], }; } const greetingWord = informalGreetings[language] || informalGreetings['en']; const languageName = languageNames[language] || languageNames['en']; const greeting = `${greetingWord}, ${name}!`; const structuredContent = { greeting, language: language || 'en', languageName, }; const result = { structuredContent, content: [ { type: 'text', text: JSON.stringify(structuredContent, null, 2), }, ], }; return result; } }

mcp测试

bash
npx @modelcontextprotocol/inspector@0.16.2 --cli http://localhost:3030/mcp --transport http --method tools/list

或者直接运行一个web界面

bash
npx @modelcontextprotocol/inspector
评论区 (0)
你的临时ID:
暂无评论,来发表第一条评论吧!