mcp服务的node实现

自己实现一个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)