
这是一个用于实现Model Context Protocol(MCP)服务器的NestJS模块。该模块提供了将MCP协议集成到NestJS应用程序中的强大功能,支持Server-Sent Events(SSE)进行实时通信和工具执行。
yarn add @omnihash/nestjs-mcp
# 或者
npm install @omnihash/nestjs-mcp
有三种使用MCP模块的方式:使用装饰器、手动注册或两者结合。
首先,使用装饰器创建你的工具服务:
// math-tools.service.ts
import { Injectable } from '@nestjs/common';
import { z } from 'zod';
import { McpTool, McpTools } from '@omnihash/nestjs-mcp';
@Injectable()
@McpTools('math')
export class MathToolsService {
@McpTool({
name: 'add',
description: '加两个数',
schema: z.object({
a: z.number().describe('第一个数'),
b: z.number().describe('第二个数'),
}),
})
async add({ a, b }: { a: number; b: number }): Promise<number> {
return a + b;
}
@McpTool({
name: 'multiply',
description: '乘两个数',
schema: z.object({
a: z.number().describe('第一个数'),
b: z.number().describe('第二个数'),
}),
})
async multiply({ a, b }: { a: number; b: number }): Promise<number> {
return a * b;
}
@McpTool({
name: 'divide',
description: '除两个数',
schema: z.object({
dividend: z.number().describe('被除数'),
divisor: z.number().describe('除数'),
}),
})
async divide({
dividend,
divisor,
}: {
dividend: number;
divisor: number;
}): Promise<number> {
if (divisor === 0) {
throw new Error('不允许除以零');
}
return dividend / divisor;
}
@McpTool({
name: 'sqrt',
description: '计算平方根',
schema: z.object({
n: z.number().min(0).describe('要找平方根的数'),
}),
})
async sqrt({ n }: { n: number }): Promise<number> {
return Math.sqrt(n);
}
@McpTool({
name: 'power',
description: '计算幂',
schema: z.object({
base: z.number().describe('底数'),
exponent: z.number().describe('指数'),
}),
})
async power({
base,
exponent,
}: {
base: number;
exponent: number;
}): Promise<number> {
return Math.pow(base, exponent);
}
}
// string-tools.service.ts
@Injectable()
@McpTools('string')
export class StringToolsService {
@McpTool({
name: 'reverse',
description: '反转字符串',
schema: z.object({
text: z.string().describe('要反转的文本'),
}),
})
async reverse({ text }: { text: string }): Promise<string> {
return text.split('').reverse().join('');
}
@McpTool({
name: 'wordCount',
description: '统计文本中的单词数量',
schema: z.object({
text: z.string().describe('要统计单词的文本'),
includeNumbers: z
.boolean()
.default(false)
.describe('是否在单词计数中包含数字'),
}),
})
async wordCount({
text,
includeNumbers,
}: {
text: string;
includeNumbers: boolean;
}): Promise<{
totalWords: number;
uniqueWords: number;
wordFrequency: Record<string, number>;
}> {
const words = text.toLowerCase().match(/\b\w+\b/g) || [];
const filteredWords = includeNumbers
? words
: words.filter((word) => isNaN(Number(word)));
const frequency: Record<string, number> = {};
filteredWords.forEach((word) => {
frequency[word] = (frequency[word] || 0) + 1;
});
return {
totalWords: filteredWords.length,
uniqueWords: Object.keys(frequency).length,
wordFrequency: frequency,
};
}
@McpTool({
name: 'capitalize',
description: '每个单词首字母大写',
schema: z.object({
text: z.string().describe('要大写的文本'),
}),
})
async capitalize({ text }: { text: string }): Promise<string> {
return text.replace(/\b\w/g, (char) => char.toUpperCase());
}
@McpTool({
name: 'extractEmails',
description: '从文本中提取所有电子邮件地址',
schema: z.object({
text: z.string().describe('要搜索电子邮件的文本'),
}),
})
async extractEmails({ text }: { text: string }): Promise<string[]> {
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
return text.match(emailRegex) || [];
}
}
// 创建一个模块来提供你的工具
@Module({
providers: [MathToolsService, StringToolsService],
exports: [MathToolsService, StringToolsService],
})
export class ToolsModule {}
然后在你的app.module.ts中:
import { Module } from '@nestjs/common';
import { McpModule } from '@omnihash/nestjs-mcp';
import { ToolsModule } from './tools/tools.module';
@Module({
imports: [
McpModule.forRoot({
name: 'my-mcp-server',
version: '1.0.0',
description: '我的MCP服务器实现',
}),
ToolsModule,
],
})
export class AppModule {}
或者,你可以手动注册工具:
import { Module } from '@nestjs/common';
import { McpModule } from '@omnihash/nestjs-mcp';
import { z } from 'zod';
@Module({
imports: [
McpModule.forRootAsync({
useFactory: () => ({
name: 'my-mcp-server',
version: '1.0.0',
description: '具有手动工具的MCP服务器',
tools: [
{
name: 'greet',
schema: z.object({
name: z.string().describe('问候的名字'),
}),
handler: async ({ name }) => {
return `你好,${name}!`;
},
},
],
}),
}),
// 注意:你也可以通过导入你的ToolsModule来同时注册装饰器工具
ToolsModule,
],
})
export class AppModule {}
一旦导入了模块,以下端点将会可用:
GET /sse - 建立SSE连接POST /messages - 处理MCP消息GET /health - 健康检查端点GET /capabilities - 返回服务器能力McpModule.forRoot()方法接受以下选项:
interface McpModuleOptions {
name: string; // 服务器名称
version: string; // 服务器版本
description: string; // 服务器描述
tools: McpTool[]; // 工具数组
}
interface McpTool {
name: string; // 工具名称
schema: z.ZodObject; // 输入验证的Zod模式
handler: (params: any) => Promise<any>; // 工具实现
}
MIT
src/
├── app.module.ts # 示例应用模块
├── main.ts # 应用入口点
└── tools/ # 示例工具实现
├── api-tools.service.ts
├── math-tools.service.ts
├── string-tools.service.ts
└── tools.module.ts
// api-tools.service.ts
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { z } from 'zod';
import { McpTool, McpTools } from '@omnihash/nestjs-mcp';
@Injectable()
@McpTools('api')
export class ApiToolsService {
@McpTool({
name: 'getTodoList',
description: '根据ID获取待办事项',
schema: z.object({
id: z.string().describe('待办事项列表ID'),
}),
})
async getTodoList({ id }: { id: string }): Promise<any> {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/todos/${id}`,
);
return response.data;
}
}
@McpTool({
name: 'divide',
description: '除两个数',
schema: z.object({
dividend: z.number().describe('被除数'),
divisor: z.number().describe('除数'),
}),
})
async divide({
dividend,
divisor,
}: {
dividend: number;
divisor: number;
}): Promise<number> {
if (divisor === 0) {
throw new Error('不允许除以零');
}
return dividend / divisor;
}
@McpTool({
name: 'sqrt',
description: '计算平方根',
schema: z.object({
n: z.number().min(0).describe('要找平方根的数'),
}),
})
async sqrt({ n }: { n: number }): Promise<number> {
return Math.sqrt(n);
}
@McpTool({
name: 'power',
description: '计算幂',
schema: z.object({
base: z.number().describe('底数'),
exponent: z.number().describe('指数'),
}),
})
async power({
base,
exponent,
}: {
base: number;
exponent: number;
}): Promise<number> {
return Math.pow(base, exponent);
}
@McpTool({
name: 'capitalize',
description: '每个单词首字母大写',
schema: z.object({
text: z.string().describe('要大写的文本'),
}),
})
async capitalize({ text }: { text: string }): Promise<string> {
return text.replace(/\b\w/g, (char) => char.toUpperCase());
}
@McpTool({
name: 'extractEmails',
description: '从文本中提取所有电子邮件地址',
schema: z.object({
text: z.string().describe('要搜索电子邮件的文本'),
}),
})
async extractEmails({ text }: { text: string }): Promise<string[]> {
const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
return text.match(emailRegex) || [];
}
模块会自动发现装饰有@McpTool的服务中的工具。这意味着你可以将工具组织成逻辑组:
@Injectable()
@McpTools('math') // 工具将以前缀'math/'开头
export class MathTools {
@McpTool({
name: 'add', // 将作为'math/add'可用
description: '...',
schema: z.object({...}),
})
async add() {...}
}
@Injectable()
@McpTools('string') // 工具将以前缀'string/'开头
export class StringTools {
@McpTool({
name: 'reverse', // 将作为'string/reverse'可用
description: '...',
schema: z.object({...}),
})
async reverse() {...}
}
工具可以抛出错误,这些错误将在MCP响应中正确格式化:
@McpTool({
name: 'divide',
schema: z.object({
dividend: z.number(),
divisor: z.number(),
}),
})
async divide({ dividend, divisor }) {
if (divisor === 0) {
throw new Error('除以零'); // 将作为JSON-RPC错误返回
}
return dividend / divisor;
}
你可以使用forRootAsync进行动态配置:
@Module({
imports: [
McpModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
name: config.get('MCP_SERVER_NAME'),
version: config.get('MCP_SERVER_VERSION'),
description: config.get('MCP_SERVER_DESCRIPTION'),
}),
}),
],
})
export class AppModule {}
工具可以返回复杂对象,这些对象将自动转换为MCP内容:
@McpTool({
name: 'analyze',
schema: z.object({
text: z.string(),
}),
})
async analyze({ text }) {
return {
wordCount: text.split(/\s+/).length,
charCount: text.length,
sentiment: calculateSentiment(text),
language: detectLanguage(text),
};
}
git clone git@github.com:omnihash/nestjs-mcp.git
cd nestjs-mcp
nvm use
yarn install
yarn start:dev
# 单元测试
yarn test
# 端到端测试
yarn test:e2e
# 测试覆盖率
yarn test:cov
git checkout -b feature/amazing-feature)git commit -m '添加一些惊人的功能')git push origin feature/amazing-feature)要在另一个项目中本地开发和测试模块:
cd nestjs-mcp
npm link
cd your-project
npm link @omnihash/nestjs-mcp
package.json:{
"dependencies": {
"@omnihash/nestjs-mcp": "*"
}
}