leanmcp-builder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLeanMCP 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
| Feature | Vanilla MCP | LeanMCP SDK |
|---|---|---|
| Tool definition | Manual schema | |
| Input validation | Manual | Automatic with |
| Service discovery | Manual registration | Auto-discovery from |
| Authentication | DIY | |
| User input | Not built-in | |
| Secrets | DIY | |
| 特性 | 原生MCP | LeanMCP SDK |
|---|---|---|
| 工具定义 | 手动编写Schema | 使用 |
| 输入验证 | 手动实现 | 通过 |
| 服务发现 | 手动注册 | 自动从 |
| 认证功能 | 自行实现 | 使用 |
| 用户输入 | 无内置支持 | 使用 |
| 密钥管理 | 自行实现 | 使用 |
Core Packages
核心包
bash
undefinedbash
undefinedMinimal 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
undefinednpm install -g @leanmcp/cli
undefinedProject 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
└── .envmy-leanmcp-server/
├── main.ts # 入口文件(极简配置)
├── mcp/ # 自动发现的服务目录
│ ├── example/
│ │ └── index.ts # 导出ExampleService
│ └── myservice/
│ └── index.ts # 导出MyService
├── package.json
├── tsconfig.json
└── .envRequired 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:
- -> tool name:
async calculate(...)calculate - -> tool name:
async sendMessage(...)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,@Resourcedecorators@Prompt - Use for input validation
@SchemaConstraint - Use for optional fields
@Optional() - 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
最佳实践
- Always provide descriptions - Helps LLMs understand tool purpose
- Use inputClass for complex inputs - Automatic schema generation
- Return structured data - Objects with clear field names
- Handle errors gracefully - Throw descriptive errors
- Keep tools focused - One tool = one clear action
- Organize by domain - One service per business domain
- Use schema constraints - Validate inputs with min/max/enum
- 始终提供描述 - 帮助大语言模型理解工具用途
- 复杂输入使用inputClass - 自动生成Schema
- 返回结构化数据 - 使用字段清晰的对象
- 优雅处理错误 - 抛出描述性错误信息
- 保持工具聚焦 - 一个工具对应一个明确操作
- 按领域组织服务 - 一个服务对应一个业务领域
- 使用Schema约束 - 通过min/max/enum验证输入