开发体验

用装饰器构建 AI 工具,太爽了

忘掉你所知道的关于构建 MCP 服务器的一切。TypeScript 装饰器将 AI 工具开发从繁琐的配置转变为纯粹、富有表现力的代码。体验 @zhama/mcp-server 装饰器驱动开发的乐趣。

Zhama AI 团队
19 min read
用装饰器构建 AI 工具,太爽了

装饰器就是魔法。

不是那种虚假的、花里胡哨的魔法。是真正的魔法 - 复杂的操作变得简单,冗长的代码变得优雅,而开发变得...有趣

当我们在 Zhama 构建 @zhama/mcp-server 时,我们只有一个目标:让为 Claude Desktop 构建 AI 工具的感觉,不再像是与配置搏斗,而更像是在写诗。

答案?TypeScript 装饰器。

让我展示为什么用装饰器构建 AI 工具是扩展 Claude 能力最令人愉悦的方式。

一切改变的那一刻

我记得那个确切的时刻。凌晨 2 点,我正喝着第五杯咖啡,盯着 200 行 JSON Schema 定义,只是为了一个简单的邮件工具。我的大脑快要融化了。

然后我想:"如果我可以直接...描述我想要的呢?"

@Tool({
  name: 'send_email',
  description: '发送邮件',
  parameters: [
    { name: 'to', type: 'string', required: true },
    { name: 'subject', type: 'string', required: true },
    { name: 'body', type: 'string', required: true }
  ]
})
class EmailTool extends BaseTool {
  async executeInternal(params) {
    // 只需发送邮件
    return await sendEmail(params);
  }
}

**就这样。**没有 schema 对象。没有处理器注册。没有请求路由。只有纯粹的、声明式的美。

我删除了 180 行样板代码,开心地去睡觉了。

是什么让装饰器如此优秀?

1. 它们读起来像自然语言

看看这个:

@Tool({
  name: 'get_weather',
  description: '获取任何城市的当前天气',
  parameters: [
    { name: 'city', type: 'string', description: '城市名称', required: true }
  ]
})

**你能读懂这个。**你不需要是 TypeScript 专家。你不需要理解 JSON Schema。你就是...理解它。

对比传统方法:

const weatherToolSchema = {
  type: 'object',
  properties: {
    city: {
      type: 'string',
      description: '城市名称'
    }
  },
  required: ['city']
};

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: 'get_weather',
    description: '获取任何城市的当前天气',
    inputSchema: weatherToolSchema
  }]
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'get_weather') {
    // 终于,你的实际代码
  }
});

**你更愿意读哪个?**更愿意写哪个?

2. 它们把相关的东西放在一起

在传统方法中,你的工具定义散落在多个地方:

  • Schema 定义:第 50 行
  • 处理器注册:第 120 行
  • 执行逻辑:第 200 行
  • 错误处理:第 250 行

想要改变一个参数?访问四个不同的地方。

使用装饰器,所有东西都在一个地方:

@Tool({
  name: 'search_database',
  description: '在产品数据库中搜索商品',
  parameters: [
    { name: 'query', type: 'string', description: '搜索查询', required: true },
    { name: 'limit', type: 'number', description: '最大结果数', required: false }
  ]
})
class SearchTool extends BaseTool {
  protected toolDefinition = {
    name: 'search_database',
    description: '搜索产品',
    parameters: []
  };

  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    const { query, limit = 10 } = params as { query: string; limit?: number };
    
    // 验证
    if (query.length < 2) {
      throw new Error('查询必须至少 2 个字符');
    }
    
    // 执行
    const results = await this.database.search(query, limit);
    
    // 响应
    return {
      query,
      count: results.length,
      results
    };
  }
}

**关于这个工具的一切都在这里。**定义、验证、逻辑、响应。不用在文件中寻找。

3. 它们支持组合

装饰器组合得很漂亮。想添加认证?日志?速率限制?堆叠装饰器:

@Tool({
  name: 'delete_user',
  description: '删除用户账户(仅管理员)',
  parameters: [
    { name: 'userId', type: 'string', required: true }
  ]
})
@RequireAuth('admin')
@RateLimit({ maxCalls: 10, windowMs: 60000 })
@AuditLog()
class DeleteUserTool extends BaseTool {
  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    // 你的逻辑 - 认证、速率限制和日志都是自动的
    return await this.userService.delete(params.userId);
  }
}

**每个装饰器添加一个能力。**简洁、模块化、可组合。

让我感到愉悦的真实示例

让我分享一些我用 @zhama/mcp-server 构建的工具,它们让我在编码时微笑。

示例 1:一行数据库查询

@Tool({
  name: 'count_users',
  description: '统计数据库中的用户总数',
  parameters: []
})
class CountUsersTool extends BaseTool {
  protected toolDefinition = { name: 'count_users', description: '统计用户', parameters: [] };
  
  protected async executeInternal(): Promise<unknown> {
    return { count: await this.db.users.count() };
  }
}

**这就是整个工具。**七行实际代码。在 Claude 中完美工作。

对比传统方法:60+ 行,包含 schema 定义、处理器注册、响应格式化。

示例 2:优雅的 API 包装器

@Tool({
  name: 'shorten_url',
  description: '使用 bit.ly 创建短链接',
  parameters: [
    { name: 'url', type: 'string', description: '要缩短的 URL', required: true }
  ]
})
class ShortenUrlTool extends BaseTool {
  protected toolDefinition = { name: 'shorten_url', description: '缩短 URL', parameters: [] };

  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    const { url } = params as { url: string };
    
    const response = await fetch('https://api-ssl.bitly.com/v4/shorten', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.BITLY_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ long_url: url })
    });

    const data = await response.json();
    return {
      original: url,
      shortened: data.link,
      success: true
    };
  }
}

**优美。**装饰器描述接口,方法实现逻辑。清晰的关注点分离。

示例 3:强大的验证器

@Tool({
  name: 'register_user',
  description: '注册新用户账户',
  parameters: [
    { name: 'email', type: 'string', description: '用户邮箱', required: true },
    { name: 'username', type: 'string', description: '用户名', required: true },
    { name: 'password', type: 'string', description: '密码', required: true }
  ]
})
class RegisterUserTool extends BaseTool {
  protected toolDefinition = { name: 'register_user', description: '注册用户', parameters: [] };

  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    const { email, username, password } = params as {
      email: string;
      username: string;
      password: string;
    };

    // 验证(自动错误格式化)
    if (!this.isValidEmail(email)) {
      throw new Error('邮箱格式无效');
    }

    if (username.length < 3) {
      throw new Error('用户名必须至少 3 个字符');
    }

    if (password.length < 8) {
      throw new Error('密码必须至少 8 个字符');
    }

    // 注册
    const user = await this.userService.create({
      email,
      username,
      passwordHash: await this.hashPassword(password)
    });

    return {
      success: true,
      userId: user.id,
      message: '用户注册成功'
    };
  }

  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  private async hashPassword(password: string): Promise<string> {
    // 密码哈希逻辑
    return await bcrypt.hash(password, 10);
  }
}

**一切都在它应该在的地方。**验证逻辑和工具在一起。辅助方法是私有的。简洁、可维护、可测试。

模式库

一旦你开始用装饰器思考,模式就会出现。这是我的个人收藏:

模式 1:可选参数默认值

@Tool({
  name: 'list_files',
  description: '列出目录中的文件',
  parameters: [
    { name: 'path', type: 'string', description: '目录路径', required: true },
    { name: 'limit', type: 'number', description: '返回的最大文件数', required: false }
  ]
})
class ListFilesTool extends BaseTool {
  protected toolDefinition = { name: 'list_files', description: '列出文件', parameters: [] };

  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    const { path, limit = 100 } = params as { path: string; limit?: number };
    
    const files = await fs.readdir(path);
    return {
      path,
      files: files.slice(0, limit),
      total: files.length,
      limited: files.length > limit
    };
  }
}

**带默认值的解构。**如此简洁,如此 JavaScript。

模式 2:类型安全的枚举

@Tool({
  name: 'convert_temperature',
  description: '在单位之间转换温度',
  parameters: [
    { name: 'value', type: 'number', description: '温度值', required: true },
    { name: 'from', type: 'string', description: '源单位:celsius、fahrenheit、kelvin', required: true },
    { name: 'to', type: 'string', description: '目标单位:celsius、fahrenheit、kelvin', required: true }
  ]
})
class ConvertTemperatureTool extends BaseTool {
  protected toolDefinition = { name: 'convert_temperature', description: '转换温度', parameters: [] };

  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    const { value, from, to } = params as {
      value: number;
      from: 'celsius' | 'fahrenheit' | 'kelvin';
      to: 'celsius' | 'fahrenheit' | 'kelvin';
    };

    const conversions = {
      celsius: {
        fahrenheit: (v: number) => (v * 9/5) + 32,
        kelvin: (v: number) => v + 273.15
      },
      fahrenheit: {
        celsius: (v: number) => (v - 32) * 5/9,
        kelvin: (v: number) => ((v - 32) * 5/9) + 273.15
      },
      kelvin: {
        celsius: (v: number) => v - 273.15,
        fahrenheit: (v: number) => ((v - 273.15) * 9/5) + 32
      }
    };

    if (from === to) {
      return { value, unit: to };
    }

    const convert = conversions[from]?.[to];
    if (!convert) {
      throw new Error(`无法从 ${from} 转换到 ${to}`);
    }

    return {
      original: { value, unit: from },
      converted: { value: convert(value), unit: to }
    };
  }
}

**类型安全 + 查找表。**优雅且安全。

模式 3:异步链

@Tool({
  name: 'process_image',
  description: '下载、调整大小并上传图片',
  parameters: [
    { name: 'url', type: 'string', description: '图片 URL', required: true },
    { name: 'width', type: 'number', description: '目标宽度', required: true },
    { name: 'height', type: 'number', description: '目标高度', required: true }
  ]
})
class ProcessImageTool extends BaseTool {
  protected toolDefinition = { name: 'process_image', description: '处理图片', parameters: [] };

  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    const { url, width, height } = params as {
      url: string;
      width: number;
      height: number;
    };

    // 优雅的异步管道
    const originalBuffer = await this.downloadImage(url);
    const resizedBuffer = await this.resizeImage(originalBuffer, width, height);
    const uploadedUrl = await this.uploadImage(resizedBuffer);

    return {
      original: url,
      processed: uploadedUrl,
      dimensions: { width, height },
      success: true
    };
  }

  private async downloadImage(url: string): Promise<Buffer> {
    const response = await fetch(url);
    return Buffer.from(await response.arrayBuffer());
  }

  private async resizeImage(buffer: Buffer, width: number, height: number): Promise<Buffer> {
    // 图片处理逻辑
    return buffer; // 简化版
  }

  private async uploadImage(buffer: Buffer): Promise<string> {
    // 上传逻辑
    return 'https://cdn.example.com/processed.jpg'; // 简化版
  }
}

**Async/await 链从上到下阅读。**没有回调地狱,没有 promise 链,只有清晰的流程。

模式 4:错误上下文

@Tool({
  name: 'charge_payment',
  description: '处理支付收费',
  parameters: [
    { name: 'amount', type: 'number', description: '金额(分)', required: true },
    { name: 'currency', type: 'string', description: '货币代码', required: true },
    { name: 'customerId', type: 'string', description: '客户 ID', required: true }
  ]
})
class ChargePaymentTool extends BaseTool {
  protected toolDefinition = { name: 'charge_payment', description: '收费', parameters: [] };

  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    const { amount, currency, customerId } = params as {
      amount: number;
      currency: string;
      customerId: string;
    };

    try {
      // 尝试收费
      const charge = await this.stripe.charges.create({
        amount,
        currency,
        customer: customerId
      });

      return {
        success: true,
        chargeId: charge.id,
        amount,
        currency
      };
    } catch (error) {
      // 丰富的错误上下文
      this.logger.error('支付失败', {
        amount,
        currency,
        customerId,
        error: error.message
      });

      throw new Error(
        `支付失败:${error.message}。` +
        `客户:${customerId},金额:${amount/100} ${currency.toUpperCase()}`
      );
    }
  }
}

**带上下文的错误处理。**当出错时,你确切地知道发生了什么。

高级装饰器技术

一旦你熟悉了基础知识,试试这些高级模式:

自定义装饰器

为常见模式创建自己的装饰器:

// 用于缓存的自定义装饰器
function Cached(ttlSeconds: number) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const cache = new Map<string, { data: any; expires: number }>();

    descriptor.value = async function (...args: any[]) {
      const key = JSON.stringify(args);
      const cached = cache.get(key);

      if (cached && cached.expires > Date.now()) {
        return cached.data;
      }

      const result = await originalMethod.apply(this, args);
      cache.set(key, {
        data: result,
        expires: Date.now() + (ttlSeconds * 1000)
      });

      return result;
    };

    return descriptor;
  };
}

// 使用它
@Tool({
  name: 'get_exchange_rate',
  description: '获取货币汇率',
  parameters: [
    { name: 'from', type: 'string', required: true },
    { name: 'to', type: 'string', required: true }
  ]
})
class ExchangeRateTool extends BaseTool {
  protected toolDefinition = { name: 'get_exchange_rate', description: '获取汇率', parameters: [] };

  @Cached(300) // 缓存 5 分钟
  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    const { from, to } = params as { from: string; to: string };
    
    const response = await fetch(`https://api.exchangerate.com/${from}/${to}`);
    const data = await response.json();
    
    return { from, to, rate: data.rate, timestamp: Date.now() };
  }
}

装饰器组合

堆叠装饰器以获得强大的组合:

@Tool({
  name: 'admin_action',
  description: '执行管理操作',
  parameters: [
    { name: 'action', type: 'string', required: true },
    { name: 'target', type: 'string', required: true }
  ]
})
@RequireAuth('admin')           // 必须是管理员
@RateLimit(5, 60000)            // 每分钟最多 5 次
@AuditLog()                     // 记录所有操作
@Retry(3, 1000)                 // 重试 3 次,延迟 1 秒
class AdminActionTool extends BaseTool {
  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    // 所有装饰器魔法自动发生
    return await this.performAction(params);
  }
}

为什么这在规模化时很重要

Zhama,我们在多个 Claude 集成中维护 50+ 个 AI 工具。装饰器不仅仅是好 - 它们是必不可少的

代码审查速度

装饰器之前:

开发者:"能审查一下我的新工具吗?"
审查者:*滚动 150 行*
审查者:"参数验证在哪里?"
开发者:"第 87 行"
审查者:"错误处理在哪里?"
开发者:"120-145 行"
审查者:"实际逻辑在哪里?"
开发者:"60-75 行"

使用装饰器:

开发者:"能审查一下我的新工具吗?"
审查者:*读 40 行*
审查者:"看起来不错,批准"

代码审查时间:20 分钟 → 5 分钟

入职速度

新开发者的第一天:

之前:

  • 第 1-2 小时:理解服务器设置
  • 第 3-4 小时:学习请求处理器模式
  • 第 5-6 小时:研究 schema 定义
  • 第 7-8 小时:构建第一个工具
  • 总计:8 小时

之后:

  • 第 1 小时:"这是一个装饰器示例"
  • 第 2 小时:构建第一个工具
  • 总计:2 小时

Bug 密度

**装饰器之前:**每 1000 行代码 15 个 bug

  • 主要在样板代码中:错误的 schema、缺少验证、响应格式化

**使用装饰器:**每 1000 行代码 3 个 bug

  • 全部在业务逻辑中,易于修复

**Bug 减少 5 倍。**不是因为我们是更好的程序员 - 而是因为需要搞砸的代码更少了。

愉悦因子

让我诚实地说:编程应该让人感觉良好。

不是每一天都会完美,但当你在构建东西时,工具应该帮助,而不是阻碍。

装饰器让我开心,因为:

  1. 我少想机制 - 没有 schema、处理器、格式化的心理负担
  2. 我看到全局 - 关于工具的一切一眼可见
  3. 我交付更快 - 5 分钟而不是 5 小时
  4. 我犯更少错误 - 更少的代码 = 更少的 bug
  5. 我享受过程 - 写代码感觉像在打磨,而不是在配置

**那种"好得难以置信"的感觉?**是真的。@zhama/mcp-server 兑现了它。

开始装饰器驱动开发

准备好亲自体验了吗?

安装

npm install @zhama/mcp-server

你的第一个装饰器工具

import { createMCPServer, BaseTool, Tool } from '@zhama/mcp-server';

@Tool({
  name: 'hello',
  description: '向某人问好',
  parameters: [
    { name: 'name', type: 'string', description: '要问候的名字', required: true }
  ]
})
class HelloTool extends BaseTool {
  protected toolDefinition = {
    name: 'hello',
    description: '打招呼',
    parameters: []
  };

  protected async executeInternal(params: Record<string, unknown>): Promise<unknown> {
    const { name } = params as { name: string };
    return {
      greeting: `你好,${name}!👋`,
      timestamp: new Date().toISOString()
    };
  }
}

// 创建并运行
createMCPServer('hello-server', '1.0.0')
  .enableTools()
  .addTool(new HelloTool())
  .runStdio();

保存这个。构建它。看它工作。

当你看到 Claude 使用你的装饰器定义的工具时,你会明白的。

社区正在增长

使用 @zhama/mcp-server 的开发者已经构建了:

  • 数据库工具 - 用装饰器查询、插入、更新
  • API 集成 - 优雅地包装任何 REST API
  • 文件处理器 - 上传、下载、转换文件
  • 通知系统 - 邮件、短信、Slack、Discord
  • 分析工具 - 查询和可视化数据
  • 自动化工作流 - 链接多个操作

每一个都使用装饰器。每一个开发者都喜欢它。

接下来是什么?

我们正在添加更多装饰器功能:

  • @Validate() - 参数验证规则
  • @Transform() - 自动数据转换
  • @Cache() - 响应缓存
  • @Retry() - 自动重试逻辑
  • @RateLimit() - 请求节流
  • @Auth() - 认证要求

装饰器模式可以无限扩展。

结论:愉悦是一个功能

当我们构建 @zhama/mcp-server 时,我们不仅想让开发更快。我们想让它令人愉悦

装饰器实现了这一点。它们转变了:

  • 配置 → 声明
  • 样板代码 → 清晰
  • 复杂性 → 简单性
  • 工作 → 手艺

**结果?**开发者在编写 AI 工具时微笑。

这不是夸张。这是我们在 Zhama 的体验和社区的反馈。

亲自试试:

npm install @zhama/mcp-server

五分钟后,你也会微笑。


Zhama 用 ❤️ 和装饰器打造

访问:

分享你的装饰器驱动工具!标记我们或在 GitHub 上开启讨论 - 我们很想看到你构建的东西!

标签

TypeScript装饰器MCPClaude Desktop开发者体验简洁代码最佳实践