文章 / AI 与自动化 / mcp服务的node实现

mcp服务的node实现

2025年10月15日39 min11444 words
mcp服务的node实现
图 1-1 cover.webp

自己实现一个mcp-server

// 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 } };
    }
  }
}
// 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}`);
    }
  }
}

// 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会报错

pnpm i @modelcontextprotocol/sdk zod@3
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

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

// 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 {}

// 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测试

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

或者直接运行一个web界面

npx @modelcontextprotocol/inspector

评论 (0)

加载评论中...
RELATED NOTES
05 entries
01给 AI Agent 设计可靠的发布系统:幂等、断点续跑与跨平台一致性阅读文章 →02docker镜像资源阅读文章 →03kafka阅读文章 →04知乎邀请指定用户阅读文章 →05postgresql 入门阅读文章 →