当前位置: 首页 > news >正文

TypeScript + Zod:手把手教你从零搭建一个带输入验证的MCP计算器服务器

TypeScript + Zod:构建企业级MCP计算器服务的防御性编程实践

在当今的AI工具链生态中,Model Context Protocol(MCP)作为连接AI助手与外部工具的标准桥梁,其服务端的健壮性直接决定了整个系统的可靠性。本文将带您深入探索如何运用TypeScript的类型系统和Zod验证库,打造一个具备工业级防御能力的MCP计算器服务。不同于基础功能实现,我们将重点关注生产环境中必须考虑的输入验证、错误处理和可观测性等关键维度。

1. 工程化项目初始化与配置

1.1 现代TypeScript项目脚手架

从零开始搭建符合企业标准的TypeScript项目结构:

# 创建项目目录并初始化 mkdir mcp-calculator-enterprise cd mcp-calculator-enterprise npm init -y # 安装生产依赖 npm install @modelcontextprotocol/sdk zod winston reflect-metadata # 开发依赖 npm install -D typescript @types/node ts-node-dev eslint prettier

推荐的项目结构设计:

mcp-calculator-enterprise/ ├── src/ │ ├── core/ # 核心验证逻辑 │ │ ├── schemas/ # Zod验证规则 │ │ └── exceptions/ # 自定义异常 │ ├── tools/ # 计算器工具实现 │ ├── utils/ # 辅助函数 │ └── server.ts # 服务器入口 ├── test/ # 测试用例 ├── .eslintrc # ESLint配置 ├── tsconfig.json # TypeScript配置 └── package.json

1.2 增强型TypeScript配置

tsconfig.json的推荐生产配置:

{ "compilerOptions": { "target": "ES2022", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "noUnusedLocals": true, "noUnusedParameters": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }

2. 防御性输入验证体系设计

2.1 多层级验证策略

构建分层的验证体系是确保服务健壮性的第一道防线:

  1. 基础类型验证:确保输入符合基本数据类型要求
  2. 业务规则验证:检查数值范围、格式等业务约束
  3. 上下文验证:验证多个参数间的逻辑关系
// src/core/schemas/calculator.ts import { z } from "zod"; export const NumericStringSchema = z.string().regex(/^-?\d*\.?\d+$/).transform(Number); export const PositiveNumberSchema = z.number().positive(); export const NonZeroNumberSchema = z.number().refine(v => v !== 0, { message: "Value cannot be zero" }); export const DivisionParamsSchema = z.object({ dividend: z.union([NumericStringSchema, z.number()]), divisor: z.union([NumericStringSchema, NonZeroNumberSchema]) }).refine(data => { // 上下文验证:被除数必须大于除数 return data.dividend > data.divisor; }, { message: "Dividend must be greater than divisor", path: ["dividend"] });

2.2 自定义错误处理中间件

实现统一的错误响应格式:

// src/core/exceptions/validation-error.ts export class ValidationError extends Error { constructor( public readonly issues: z.ZodIssue[], public readonly context?: Record<string, unknown> ) { super("Validation failed"); this.name = "ValidationError"; } toResponse() { return { status: "error", errors: this.issues.map(issue => ({ code: issue.code, path: issue.path.join("."), message: issue.message })), meta: this.context }; } }

3. 生产级工具实现模式

3.1 带验证的计算器工具封装

// src/tools/calculator.ts import { McpTool } from "@modelcontextprotocol/sdk"; import * as schemas from "../core/schemas/calculator"; import { ValidationError } from "../core/exceptions"; export const createCalculatorTool = (): McpTool => { return { name: "advanced-calculator", description: "Enhanced calculator with validation and logging", parameters: { operation: z.enum(["add", "subtract", "multiply", "divide"]), operands: z.array(z.union([ schemas.NumericStringSchema, z.number() ])).min(2) }, execute: async ({ operation, operands }) => { try { // 二次验证确保运行时安全 const validated = schemas.OperationSchema.parse({ operation, operands }); let result: number; switch (validated.operation) { case "add": result = validated.operands.reduce((a, b) => a + b); break; case "subtract": result = validated.operands.reduce((a, b) => a - b); break; case "multiply": result = validated.operands.reduce((a, b) => a * b); break; case "divide": if (validated.operands.slice(1).some(x => x === 0)) { throw new ValidationError( [{ code: "custom", path: ["operands"], message: "Division by zero attempted" }], { operands: validated.operands } ); } result = validated.operands.reduce((a, b) => a / b); break; } return { content: [{ type: "text", text: `${validated.operands.join(` ${operation} `)} = ${result}` }], metadata: { calculation: { operation, operands: validated.operands, result } } }; } catch (error) { if (error instanceof z.ZodError) { throw new ValidationError(error.issues); } throw error; } } }; };

3.2 操作日志与审计追踪

集成Winston日志系统实现结构化日志:

// src/utils/logger.ts import winston from "winston"; const { combine, timestamp, json } = winston.format; export const logger = winston.createLogger({ level: "info", format: combine( timestamp(), json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: "logs/calculator-service.log", maxsize: 5 * 1024 * 1024 // 5MB }) ] }); // 在工具中使用 logger.info("Calculation performed", { operation: "divide", operands: [10, 2], result: 5, timestamp: new Date().toISOString() });

4. 服务器安全增强配置

4.1 速率限制与防滥用

// src/server.ts import { McpServer } from "@modelcontextprotocol/sdk"; import rateLimit from "express-rate-limit"; const server = new McpServer({ name: "secure-calculator", middleware: [ rateLimit({ windowMs: 15 * 60 * 1000, // 15分钟 max: 100, // 每个IP限制100次请求 standardHeaders: true, legacyHeaders: false, message: { status: "error", message: "Too many requests, please try again later" } }) ] });

4.2 输入净化与XSS防护

import DOMPurify from "dompurify"; import { JSDOM } from "jsdom"; const { window } = new JSDOM(""); const purify = DOMPurify(window); function sanitizeInput(input: unknown): string { if (typeof input !== "string") { return String(input); } return purify.sanitize(input, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }); }

5. 测试策略与质量保障

5.1 单元测试示例

使用Jest编写验证逻辑测试:

// test/schemas/calculator.test.ts import { z } from "zod"; import * as schemas from "../../src/core/schemas/calculator"; describe("Calculator Schemas", () => { describe("NumericStringSchema", () => { it("should validate numeric strings", () => { expect(schemas.NumericStringSchema.parse("123")).toBe(123); expect(schemas.NumericStringSchema.parse("-12.34")).toBe(-12.34); }); it("should reject non-numeric strings", () => { expect(() => schemas.NumericStringSchema.parse("abc")).toThrow(); }); }); describe("DivisionParamsSchema", () => { it("should validate proper division parameters", () => { const validInput = { dividend: 10, divisor: 2 }; expect(schemas.DivisionParamsSchema.parse(validInput)).toEqual(validInput); }); it("should reject when dividend is less than divisor", () => { const invalidInput = { dividend: 2, divisor: 10 }; expect(() => schemas.DivisionParamsSchema.parse(invalidInput)).toThrow(); }); }); });

5.2 集成测试方案

// test/integration/calculator.test.ts import { StdioServerTransport } from "@modelcontextprotocol/sdk"; import { createCalculatorServer } from "../../src/server"; import { logger } from "../../src/utils/logger"; describe("Calculator Server Integration", () => { let server: McpServer; let transport: StdioServerTransport; beforeAll(async () => { server = createCalculatorServer(); transport = new StdioServerTransport(); await server.connect(transport); logger.silent = true; // 测试时静默日志 }); afterAll(async () => { await server.disconnect(); logger.silent = false; }); it("should handle valid addition request", async () => { const response = await transport.send({ tool: "advanced-calculator", parameters: { operation: "add", operands: [2, 3] } }); expect(response.content[0].text).toBe("2 + 3 = 5"); }); it("should reject division by zero", async () => { const response = await transport.send({ tool: "advanced-calculator", parameters: { operation: "divide", operands: [1, 0] } }); expect(response.content[0].text).toContain("Division by zero"); }); });

6. 性能优化与生产部署

6.1 编译优化配置

// package.json { "scripts": { "build": "tsc --incremental --tsBuildInfoFile .tsbuildinfo", "start": "node dist/server.js", "dev": "ts-node-dev --respawn --transpile-only src/server.ts" } }

6.2 容器化部署方案

# Dockerfile FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY dist/ ./dist/ COPY logs/ ./logs/ ENV NODE_ENV=production EXPOSE 3000 CMD ["node", "dist/server.js"]

对应的docker-compose配置:

# docker-compose.yml version: '3.8' services: calculator: build: . ports: - "3000:3000" volumes: - ./logs:/app/logs restart: unless-stopped environment: - NODE_ENV=production - LOG_LEVEL=info
http://www.jsqmd.com/news/588535/

相关文章:

  • Linux-特殊权限SUID,SGID,SBIT
  • 用MoveIt2和C++让机械臂动起来:从环境配置到避障抓取的保姆级实战
  • 告别CubeMX:手动移植FreeRTOSv202406.01到STM32F103的完整流程与HAL库适配心得
  • 12. 欧姆定律计算器
  • 别再硬扛内存了:用Gensim的Word2Vec分批次处理超大语料库(附Python代码)
  • 10个在线地图瓦片URL分享
  • 从几何直观到代数方程:KKT条件的Farkas引理证明之路
  • 告别高延迟!在4G对称NAT下,如何为RV1106自建TURN服务器实现稳定WebRTC推流
  • STM32入门——软件SPI读写W25Q64(17)
  • Docker 完全指南:从入门到生产级实践
  • 从原理到代码:手把手教你用Fmask实现卫星影像云检测(含Python示例)
  • Windows 10/11下保姆级编译QGIS 3.42.3:从OSGeo4W、Cygwin到CMake GUI的完整避坑指南
  • 别再为Modelsim仿真Xilinx IP核发愁了!手把手教你搞定FFT IP的完整流程(Vivado 2018.3 + Modelsim DE 10.6c)
  • 嵌入式Bootloader升级必备:Hex转Bin的5个实战坑点与高效脚本集成方案
  • 告别过热烦恼!用开源神器为你的戴尔G15笔记本降温30%
  • 蓝桥杯5G仿真平台保姆级通关指南:从网络规划到核心网配置,手把手带你拿分
  • Docker常用指令速查手册
  • 打破Mac局域网通信壁垒:飞秋Mac版如何实现跨平台无缝对接
  • 量子比特的魔力:从叠加态到逻辑量子比特的演进
  • LVGL实战:在Windows模拟器上集成《avilib》实现AVI视频流畅播放
  • 用树莓派和SG90舵机实现摄像头云台控制:从零调试到精准转动
  • IPC-7351标准实战:如何用Allegro快速生成符合规范的PCB封装库(附资源下载)
  • 保姆级教程:用Python把DeepSig RadioML 2018.01A数据集拆成单信噪比.mat文件
  • 中电联协议实战解析:从零到一构建充电桩业务信息交换系统
  • HC32F460 BootLoader实战:从串口接收、Flash烧录到安全跳转的完整实现
  • Zotero Linter插件:5个核心功能让文献管理效率提升90%的完整指南
  • 深入解析AOSP15 Audio HAL的HIDL实现与核心库架构
  • SiameseUIE与LangChain集成:构建智能问答系统
  • 实战分享:当HttpOnly遇上XSS,我是如何绕过防护获取Cookie的(附详细复现步骤)
  • Android Gradle Plugin升级后.aar依赖报错?手把手教你正确配置build.gradle