Loading...
Loading...
Build MCP servers using the LeanMCP SDK with decorator-based TypeScript. Use this skill when users ask for "leanmcp", "MCP with decorators", "MCP with authentication", "MCP with elicitation", "MCP with environment injection", or want a simpler, more elegant way to build MCP servers. LeanMCP provides automatic schema generation, dependency injection, authentication, and user input collection.
npx skill4agent add leanmcp-community/skills leanmcp-builder| 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 | |
# Minimal setup
npm install @leanmcp/core
# With authentication
npm install @leanmcp/auth
# With user input forms
npm install @leanmcp/elicitation
# With user secrets
npm install @leanmcp/env-injection
# CLI (global or dev)
npm install -g @leanmcp/climy-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{
"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"
}
}{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["**/*.ts"]
}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,
});import { Tool } from '@leanmcp/core';
export class MyService {
@Tool({ description: 'Echo a message back' })
async echo(args: { message: string }) {
return { echoed: args.message };
}
}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');
}
}
}async calculate(...)calculateasync sendMessage(...)sendMessageimport { 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()
})
}]
};
}
}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.`
}
}]
};
}
}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
};
}
}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 };
}
}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 };
}
}// 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)
})
}]
};
}
}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
});mcp/servicename/index.ts@Tool@Resource@Prompt@SchemaConstraint@Optional()@UIApp@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 };
}