返回市场
涅斯特-mcp

涅斯特-mcp

作者:omnihash2 星标更新:2025-06-06

项目介绍

Omnihash + Nest

这是一个用于实现Model Context Protocol(MCP)服务器的NestJS模块。该模块提供了将MCP协议集成到NestJS应用程序中的强大功能,支持Server-Sent Events(SSE)进行实时通信和工具执行。

功能

  • 🔄 支持Server-Sent Events(SSE)
  • 🛠️ 工具注册与执行
  • 📝 使用JSON-RPC消息格式
  • ❤️ 心跳机制
  • ✅ 使用Zod模式验证
  • 🚀 与NestJS轻松集成

安装

yarn add @omnihash/nestjs-mcp
# 或者
npm install @omnihash/nestjs-mcp

使用方法

1. 导入模块

有三种使用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 工具

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

开发

先决条件

  • Node.js 18 或更高版本
  • npm 或 yarn

设置

  1. 克隆仓库
git clone git@github.com:omnihash/nestjs-mcp.git
cd nestjs-mcp
  1. 安装依赖
nvm use
yarn install
  1. 运行示例服务器
yarn start:dev

运行测试

# 单元测试
yarn test

# 端到端测试
yarn test:e2e

# 测试覆盖率
yarn test:cov

贡献

  1. 分叉仓库
  2. 创建你的特性分支 (git checkout -b feature/amazing-feature)
  3. 提交你的更改 (git commit -m '添加一些惊人的功能')
  4. 推送到分支 (git push origin feature/amazing-feature)
  5. 打开拉取请求

本地开发

要在另一个项目中本地开发和测试模块:

  1. 链接包:
cd nestjs-mcp
npm link
  1. 在你的项目中:
cd your-project
npm link @omnihash/nestjs-mcp
  1. 添加到你的项目的package.json
{
  "dependencies": {
    "@omnihash/nestjs-mcp": "*"
  }
}