leanmcp-builder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

LeanMCP Builder Skill

LeanMCP 构建器技能

This skill guides you in building MCP servers using the LeanMCP SDK - a decorator-based TypeScript framework for elegant MCP development.
本技能将指导你使用LeanMCP SDK构建MCP服务器——这是一个基于装饰器的TypeScript框架,可实现优雅的MCP开发。

When to Use This Skill

何时使用此技能

  • User asks for "leanmcp server"
  • User wants "MCP with decorators"
  • User needs "authenticated MCP server"
  • User wants "simpler MCP development"
  • User mentions "@leanmcp/core"
  • User needs "user input collection" or "elicitation"
  • User wants "environment injection" for multi-tenant secrets
  • 用户询问“leanmcp server”
  • 用户需要“带装饰器的MCP”
  • 用户需要“带认证的MCP服务器”
  • 用户想要“更简单的MCP开发方式”
  • 用户提及“@leanmcp/core”
  • 用户需要“用户输入收集”或“信息获取(elicitation)”功能
  • 用户想要为多租户密钥实现“环境注入”

LeanMCP vs Vanilla MCP

LeanMCP 对比原生MCP

FeatureVanilla MCPLeanMCP SDK
Tool definitionManual schema
@Tool
decorator with auto-schema
Input validationManualAutomatic with
@SchemaConstraint
Service discoveryManual registrationAuto-discovery from
./mcp
directory
AuthenticationDIY
@Authenticated
decorator
User inputNot built-in
@Elicitation
decorator
SecretsDIY
@RequireEnv
+
getEnv()
特性原生MCPLeanMCP SDK
工具定义手动编写Schema使用
@Tool
装饰器自动生成Schema
输入验证手动实现通过
@SchemaConstraint
自动验证
服务发现手动注册自动从
./mcp
目录发现服务
认证功能自行实现使用
@Authenticated
装饰器
用户输入无内置支持使用
@Elicitation
装饰器
密钥管理自行实现使用
@RequireEnv
+
getEnv()

Core Packages

核心包

bash
undefined
bash
undefined

Minimal setup

最小化安装

npm install @leanmcp/core
npm install @leanmcp/core

With authentication

包含认证功能

npm install @leanmcp/auth
npm install @leanmcp/auth

With user input forms

包含用户输入表单功能

npm install @leanmcp/elicitation
npm install @leanmcp/elicitation

With user secrets

包含用户密钥管理

npm install @leanmcp/env-injection
npm install @leanmcp/env-injection

CLI (global or dev)

CLI工具(全局或开发依赖)

npm install -g @leanmcp/cli
undefined
npm install -g @leanmcp/cli
undefined

Project Structure

项目结构

my-leanmcp-server/
├── main.ts                 # Entry point (minimal)
├── mcp/                    # Auto-discovered services
│   ├── example/
│   │   └── index.ts        # Exports ExampleService
│   └── myservice/
│       └── index.ts        # Exports MyService
├── package.json
├── tsconfig.json
└── .env
my-leanmcp-server/
├── main.ts                 # 入口文件(极简配置)
├── mcp/                    # 自动发现的服务目录
│   ├── example/
│   │   └── index.ts        # 导出ExampleService
│   └── myservice/
│       └── index.ts        # 导出MyService
├── package.json
├── tsconfig.json
└── .env

Required package.json

必需的package.json配置

json
{
  "name": "my-leanmcp-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "leanmcp dev",
    "start": "leanmcp start",
    "build": "leanmcp build"
  },
  "dependencies": {
    "@leanmcp/core": "latest",
    "dotenv": "^16.5.0"
  },
  "devDependencies": {
    "@leanmcp/cli": "latest",
    "@types/node": "^20.0.0",
    "typescript": "^5.6.3"
  }
}
json
{
  "name": "my-leanmcp-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "leanmcp dev",
    "start": "leanmcp start",
    "build": "leanmcp build"
  },
  "dependencies": {
    "@leanmcp/core": "latest",
    "dotenv": "^16.5.0"
  },
  "devDependencies": {
    "@leanmcp/cli": "latest",
    "@types/node": "^20.0.0",
    "typescript": "^5.6.3"
  }
}

TypeScript Configuration

TypeScript配置

json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["**/*.ts"]
}
json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["**/*.ts"]
}

Entry Point (main.ts)

入口文件(main.ts)

typescript
import dotenv from 'dotenv';
import { createHTTPServer } from '@leanmcp/core';

dotenv.config();

// Services are automatically discovered from ./mcp directory
await createHTTPServer({
  name: 'my-leanmcp-server',
  version: '1.0.0',
  port: 3001,
  cors: true,
  logging: true,
});
typescript
import dotenv from 'dotenv';
import { createHTTPServer } from '@leanmcp/core';

dotenv.config();

// 服务将自动从./mcp目录发现
await createHTTPServer({
  name: 'my-leanmcp-server',
  version: '1.0.0',
  port: 3001,
  cors: true,
  logging: true,
});

@Tool Decorator

@Tool 装饰器

Marks a method as an MCP tool with automatic schema generation.
将方法标记为MCP工具,并自动生成Schema。

Basic Tool

基础工具

typescript
import { Tool } from '@leanmcp/core';

export class MyService {
  @Tool({ description: 'Echo a message back' })
  async echo(args: { message: string }) {
    return { echoed: args.message };
  }
}
typescript
import { Tool } from '@leanmcp/core';

export class MyService {
  @Tool({ description: '将消息回显' })
  async echo(args: { message: string }) {
    return { echoed: args.message };
  }
}

Tool with Input Class (Recommended)

带输入类的工具(推荐)

typescript
import { Tool, SchemaConstraint, Optional } from '@leanmcp/core';

class CalculateInput {
  @SchemaConstraint({ description: 'First number', minimum: -1000000 })
  a!: number;

  @SchemaConstraint({ description: 'Second number', minimum: -1000000 })
  b!: number;

  @Optional()
  @SchemaConstraint({ 
    description: 'Operation to perform',
    enum: ['add', 'subtract', 'multiply', 'divide'],
    default: 'add'
  })
  operation?: string;
}

export class MathService {
  @Tool({ 
    description: 'Perform arithmetic operations',
    inputClass: CalculateInput
  })
  async calculate(input: CalculateInput) {
    const a = Number(input.a);
    const b = Number(input.b);
    
    switch (input.operation || 'add') {
      case 'add': return { result: a + b };
      case 'subtract': return { result: a - b };
      case 'multiply': return { result: a * b };
      case 'divide': 
        if (b === 0) throw new Error('Division by zero');
        return { result: a / b };
      default: throw new Error('Invalid operation');
    }
  }
}
typescript
import { Tool, SchemaConstraint, Optional } from '@leanmcp/core';

class CalculateInput {
  @SchemaConstraint({ description: '第一个数字', minimum: -1000000 })
  a!: number;

  @SchemaConstraint({ description: '第二个数字', minimum: -1000000 })
  b!: number;

  @Optional()
  @SchemaConstraint({ 
    description: '要执行的运算',
    enum: ['add', 'subtract', 'multiply', 'divide'],
    default: 'add'
  })
  operation?: string;
}

export class MathService {
  @Tool({ 
    description: '执行算术运算',
    inputClass: CalculateInput
  })
  async calculate(input: CalculateInput) {
    const a = Number(input.a);
    const b = Number(input.b);
    
    switch (input.operation || 'add') {
      case 'add': return { result: a + b };
      case 'subtract': return { result: a - b };
      case 'multiply': return { result: a * b };
      case 'divide': 
        if (b === 0) throw new Error('Division by zero');
        return { result: a / b };
      default: throw new Error('Invalid operation');
    }
  }
}

Tool Naming

工具命名

Tool name is derived from the method name:
  • async calculate(...)
    -> tool name:
    calculate
  • async sendMessage(...)
    -> tool name:
    sendMessage
工具名称由方法名自动生成:
  • async calculate(...)
    -> 工具名称:
    calculate
  • async sendMessage(...)
    -> 工具名称:
    sendMessage

@Resource Decorator

@Resource 装饰器

typescript
import { Resource } from '@leanmcp/core';

export class InfoService {
  @Resource({ description: 'Get server information' })
  async serverInfo() {
    return {
      contents: [{
        uri: 'server://info',
        mimeType: 'application/json',
        text: JSON.stringify({
          name: 'my-server',
          version: '1.0.0',
          uptime: process.uptime()
        })
      }]
    };
  }
}
typescript
import { Resource } from '@leanmcp/core';

export class InfoService {
  @Resource({ description: '获取服务器信息' })
  async serverInfo() {
    return {
      contents: [{
        uri: 'server://info',
        mimeType: 'application/json',
        text: JSON.stringify({
          name: 'my-server',
          version: '1.0.0',
          uptime: process.uptime()
        })
      }]
    };
  }
}

@Prompt Decorator

@Prompt 装饰器

typescript
import { Prompt } from '@leanmcp/core';

export class PromptService {
  @Prompt({ description: 'Generate a greeting prompt' })
  async greeting(args: { name?: string }) {
    return {
      messages: [{
        role: 'user' as const,
        content: {
          type: 'text' as const,
          text: `Hello ${args.name || 'there'}! Welcome to my server.`
        }
      }]
    };
  }
}
typescript
import { Prompt } from '@leanmcp/core';

export class PromptService {
  @Prompt({ description: '生成问候提示词' })
  async greeting(args: { name?: string }) {
    return {
      messages: [{
        role: 'user' as const,
        content: {
          type: 'text' as const,
          text: `Hello ${args.name || 'there'}! Welcome to my server.`
        }
      }]
    };
  }
}

@Authenticated Decorator

@Authenticated 装饰器

Add authentication to your service:
typescript
import { Tool } from '@leanmcp/core';
import { Authenticated, AuthProvider, authUser } from '@leanmcp/auth';

const authProvider = new AuthProvider('cognito', {
  region: 'us-east-1',
  userPoolId: 'us-east-1_XXXXXXXXX',
  clientId: 'your-client-id'
});
await authProvider.init();

@Authenticated(authProvider)
export class SecureService {
  @Tool({ description: 'Get user data' })
  async getUserData() {
    // authUser is automatically available
    return { 
      userId: authUser.sub,
      email: authUser.email 
    };
  }
}
Supported providers: AWS Cognito, Clerk, Auth0, LeanMCP
为服务添加认证功能:
typescript
import { Tool } from '@leanmcp/core';
import { Authenticated, AuthProvider, authUser } from '@leanmcp/auth';

const authProvider = new AuthProvider('cognito', {
  region: 'us-east-1',
  userPoolId: 'us-east-1_XXXXXXXXX',
  clientId: 'your-client-id'
});
await authProvider.init();

@Authenticated(authProvider)
export class SecureService {
  @Tool({ description: '获取用户数据' })
  async getUserData() {
    // authUser会自动可用
    return { 
      userId: authUser.sub,
      email: authUser.email 
    };
  }
}
支持的提供商: AWS Cognito, Clerk, Auth0, LeanMCP

@Elicitation Decorator

@Elicitation 装饰器

Collect structured user input:
typescript
import { Tool } from '@leanmcp/core';
import { Elicitation } from '@leanmcp/elicitation';

export class ChannelService {
  @Tool({ description: 'Create a new channel' })
  @Elicitation({
    title: 'Channel Details',
    fields: [
      { name: 'channelName', label: 'Channel Name', type: 'text', required: true },
      { name: 'isPrivate', label: 'Private Channel', type: 'boolean', defaultValue: false },
      { name: 'topic', label: 'Topic', type: 'textarea' }
    ]
  })
  async createChannel(args: { channelName: string; isPrivate: boolean; topic?: string }) {
    return { success: true, channel: args.channelName };
  }
}
Field types: text, textarea, number, boolean, select, multiselect, email, url, date
收集结构化用户输入:
typescript
import { Tool } from '@leanmcp/core';
import { Elicitation } from '@leanmcp/elicitation';

export class ChannelService {
  @Tool({ description: '创建新频道' })
  @Elicitation({
    title: '频道详情',
    fields: [
      { name: 'channelName', label: '频道名称', type: 'text', required: true },
      { name: 'isPrivate', label: '私密频道', type: 'boolean', defaultValue: false },
      { name: 'topic', label: '频道主题', type: 'textarea' }
    ]
  })
  async createChannel(args: { channelName: string; isPrivate: boolean; topic?: string }) {
    return { success: true, channel: args.channelName };
  }
}
字段类型: text, textarea, number, boolean, select, multiselect, email, url, date

@RequireEnv Decorator

@RequireEnv 装饰器

Inject user-specific secrets:
typescript
import { Tool } from '@leanmcp/core';
import { Authenticated } from '@leanmcp/auth';
import { RequireEnv, getEnv } from '@leanmcp/env-injection';

@Authenticated(authProvider, { projectId: 'my-project' })
export class SlackService {
  @Tool({ description: 'Send Slack message' })
  @RequireEnv(['SLACK_TOKEN', 'SLACK_CHANNEL'])
  async sendMessage(args: { message: string }) {
    const token = getEnv('SLACK_TOKEN')!;
    const channel = getEnv('SLACK_CHANNEL')!;
    
    await fetch('https://slack.com/api/chat.postMessage', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ channel, text: args.message })
    });
    
    return { success: true };
  }
}
注入用户专属密钥:
typescript
import { Tool } from '@leanmcp/core';
import { Authenticated } from '@leanmcp/auth';
import { RequireEnv, getEnv } from '@leanmcp/env-injection';

@Authenticated(authProvider, { projectId: 'my-project' })
export class SlackService {
  @Tool({ description: '发送Slack消息' })
  @RequireEnv(['SLACK_TOKEN', 'SLACK_CHANNEL'])
  async sendMessage(args: { message: string }) {
    const token = getEnv('SLACK_TOKEN')!;
    const channel = getEnv('SLACK_CHANNEL')!;
    
    await fetch('https://slack.com/api/chat.postMessage', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ channel, text: args.message })
    });
    
    return { success: true };
  }
}

Complete Service Example

完整服务示例

typescript
// mcp/products/index.ts
import { Tool, Resource, SchemaConstraint, Optional } from '@leanmcp/core';

class CreateProductInput {
  @SchemaConstraint({ description: 'Product name', minLength: 1 })
  name!: string;

  @SchemaConstraint({ description: 'Price in dollars', minimum: 0 })
  price!: number;

  @Optional()
  @SchemaConstraint({ description: 'Product description' })
  description?: string;
}

class UpdateProductInput {
  @SchemaConstraint({ description: 'Product ID' })
  id!: string;

  @Optional()
  @SchemaConstraint({ description: 'Product name' })
  name?: string;

  @Optional()
  @SchemaConstraint({ description: 'Price in dollars', minimum: 0 })
  price?: number;
}

export class ProductsService {
  private products: Map<string, any> = new Map();

  @Tool({ description: 'Create a new product', inputClass: CreateProductInput })
  async createProduct(input: CreateProductInput) {
    const id = crypto.randomUUID();
    const product = { id, ...input, createdAt: new Date().toISOString() };
    this.products.set(id, product);
    return { success: true, product };
  }

  @Tool({ description: 'List all products' })
  async listProducts() {
    return { products: Array.from(this.products.values()) };
  }

  @Tool({ description: 'Update a product', inputClass: UpdateProductInput })
  async updateProduct(input: UpdateProductInput) {
    const product = this.products.get(input.id);
    if (!product) throw new Error('Product not found');
    
    if (input.name) product.name = input.name;
    if (input.price) product.price = input.price;
    product.updatedAt = new Date().toISOString();
    
    return { success: true, product };
  }

  @Tool({ description: 'Delete a product' })
  async deleteProduct(args: { id: string }) {
    if (!this.products.has(args.id)) throw new Error('Product not found');
    this.products.delete(args.id);
    return { success: true };
  }

  @Resource({ description: 'Get product statistics' })
  async productStats() {
    const products = Array.from(this.products.values());
    return {
      contents: [{
        uri: 'products://stats',
        mimeType: 'application/json',
        text: JSON.stringify({
          total: products.length,
          totalValue: products.reduce((sum, p) => sum + p.price, 0)
        })
      }]
    };
  }
}
typescript
// mcp/products/index.ts
import { Tool, Resource, SchemaConstraint, Optional } from '@leanmcp/core';

class CreateProductInput {
  @SchemaConstraint({ description: '产品名称', minLength: 1 })
  name!: string;

  @SchemaConstraint({ description: '产品价格(美元)', minimum: 0 })
  price!: number;

  @Optional()
  @SchemaConstraint({ description: '产品描述' })
  description?: string;
}

class UpdateProductInput {
  @SchemaConstraint({ description: '产品ID' })
  id!: string;

  @Optional()
  @SchemaConstraint({ description: '产品名称' })
  name?: string;

  @Optional()
  @SchemaConstraint({ description: '产品价格(美元)', minimum: 0 })
  price?: number;
}

export class ProductsService {
  private products: Map<string, any> = new Map();

  @Tool({ description: '创建新产品', inputClass: CreateProductInput })
  async createProduct(input: CreateProductInput) {
    const id = crypto.randomUUID();
    const product = { id, ...input, createdAt: new Date().toISOString() };
    this.products.set(id, product);
    return { success: true, product };
  }

  @Tool({ description: '列出所有产品' })
  async listProducts() {
    return { products: Array.from(this.products.values()) };
  }

  @Tool({ description: '更新产品信息', inputClass: UpdateProductInput })
  async updateProduct(input: UpdateProductInput) {
    const product = this.products.get(input.id);
    if (!product) throw new Error('Product not found');
    
    if (input.name) product.name = input.name;
    if (input.price) product.price = input.price;
    product.updatedAt = new Date().toISOString();
    
    return { success: true, product };
  }

  @Tool({ description: '删除产品' })
  async deleteProduct(args: { id: string }) {
    if (!this.products.has(args.id)) throw new Error('Product not found');
    this.products.delete(args.id);
    return { success: true };
  }

  @Resource({ description: '获取产品统计数据' })
  async productStats() {
    const products = Array.from(this.products.values());
    return {
      contents: [{
        uri: 'products://stats',
        mimeType: 'application/json',
        text: JSON.stringify({
          total: products.length,
          totalValue: products.reduce((sum, p) => sum + p.price, 0)
        })
      }]
    };
  }
}

Server Configuration Options

服务器配置选项

typescript
import { MCPServer, createHTTPServer } from '@leanmcp/core';

// Simple API (recommended)
await createHTTPServer({
  name: 'my-server',
  version: '1.0.0',
  port: 3001,
  cors: true,
  logging: true
});

// Advanced: Factory pattern with manual registration
const serverFactory = async () => {
  const server = new MCPServer({
    name: 'my-server',
    version: '1.0.0',
    autoDiscover: false,  // Disable auto-discovery
    logging: true
  });
  
  server.registerService(new MyService());
  return server.getServer();
};

await createHTTPServer(serverFactory, {
  port: 3001,
  cors: true
});
typescript
import { MCPServer, createHTTPServer } from '@leanmcp/core';

// 简单API(推荐)
await createHTTPServer({
  name: 'my-server',
  version: '1.0.0',
  port: 3001,
  cors: true,
  logging: true
});

// 进阶:工厂模式+手动注册
const serverFactory = async () => {
  const server = new MCPServer({
    name: 'my-server',
    version: '1.0.0',
    autoDiscover: false,  // 禁用自动发现
    logging: true
  });
  
  server.registerService(new MyService());
  return server.getServer();
};

await createHTTPServer(serverFactory, {
  port: 3001,
  cors: true
});

Editing Guidelines

编辑指南

DO

建议做法

  • Add new services in
    mcp/servicename/index.ts
  • Use
    @Tool
    ,
    @Resource
    ,
    @Prompt
    decorators
  • Use
    @SchemaConstraint
    for input validation
  • Use
    @Optional()
    for optional fields
  • Export service classes from index.ts
  • Follow decorator patterns consistently
  • mcp/servicename/index.ts
    中添加新服务
  • 使用
    @Tool
    @Resource
    @Prompt
    装饰器
  • 使用
    @SchemaConstraint
    进行输入验证
  • 为可选字段使用
    @Optional()
  • 从index.ts导出服务类
  • 始终遵循装饰器模式

DON'T

不建议做法

  • Wrap services in AppProvider (CLI handles this)
  • Use direct imports for components in
    @UIApp
  • Completely rewrite existing files
  • Add terminal/command instructions
  • Remove existing working code
  • 用AppProvider包裹服务(CLI会处理此操作)
  • @UIApp
    中直接导入组件
  • 完全重写现有文件
  • 添加终端/命令指令
  • 删除现有可运行代码

Error Handling

错误处理

typescript
@Tool({ description: 'Divide two numbers' })
async divide(input: { a: number; b: number }) {
  if (input.b === 0) {
    throw new Error('Division by zero is not allowed');
  }
  return { result: input.a / input.b };
}
Errors are automatically caught and formatted as MCP error responses.
typescript
@Tool({ description: '两数相除' })
async divide(input: { a: number; b: number }) {
  if (input.b === 0) {
    throw new Error('Division by zero is not allowed');
  }
  return { result: input.a / input.b };
}
错误会被自动捕获并格式化为MCP错误响应。

Best Practices

最佳实践

  1. Always provide descriptions - Helps LLMs understand tool purpose
  2. Use inputClass for complex inputs - Automatic schema generation
  3. Return structured data - Objects with clear field names
  4. Handle errors gracefully - Throw descriptive errors
  5. Keep tools focused - One tool = one clear action
  6. Organize by domain - One service per business domain
  7. Use schema constraints - Validate inputs with min/max/enum
  1. 始终提供描述 - 帮助大语言模型理解工具用途
  2. 复杂输入使用inputClass - 自动生成Schema
  3. 返回结构化数据 - 使用字段清晰的对象
  4. 优雅处理错误 - 抛出描述性错误信息
  5. 保持工具聚焦 - 一个工具对应一个明确操作
  6. 按领域组织服务 - 一个服务对应一个业务领域
  7. 使用Schema约束 - 通过min/max/enum验证输入