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

张开发
2026/4/21 23:26:54 15 分钟阅读

分享文章

TypeScript + Zod:手把手教你从零搭建一个带输入验证的MCP计算器服务器
TypeScript Zod构建企业级MCP计算器服务的防御性编程实践在当今的AI工具链生态中Model Context ProtocolMCP作为连接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.json1.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 多层级验证策略构建分层的验证体系是确保服务健壮性的第一道防线基础类型验证确保输入符合基本数据类型要求业务规则验证检查数值范围、格式等业务约束上下文验证验证多个参数间的逻辑关系// 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?: Recordstring, 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 --onlyproduction COPY dist/ ./dist/ COPY logs/ ./logs/ ENV NODE_ENVproduction 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_ENVproduction - LOG_LEVELinfo

更多文章