wiki-query
PApaperclipai/paperclip101k2,948 new stars in the last 7 days
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.
This skill guides you in building MCP servers using the Leanmcp SDK - a decorator-based TypeScript framework for elegant MCP development.
| Feature | Vanilla MCP | Leanmcp SDK |
|---|---|---|
| Tool definition | Manual schema | @Tool decorator with auto-schema |
| Input validation | Manual | Automatic with @SchemaConstraint |
| Service discovery | Manual registration | Auto-discovery from ./mcp directory |
| Authentication | DIY | @Authenticated decorator |
| User input | Not built-in | @Elicitation decorator |
| Secrets | DIY | @RequireEnv + getEnv() |
# 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/cli
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
{
"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,
});
Marks a method as an MCP tool with automatic schema generation.
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');
}
}
}
Tool name is derived from the method name:
async calculate(...) -> tool name: calculateasync sendMessage(...) -> tool name: 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.`
}
}]
};
}
}
Add authentication to your service:
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
Collect structured user input:
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
Inject user-specific secrets:
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 decorators@SchemaConstraint for input validation@Optional() for optional fields@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 };
}
Errors are automatically caught and formatted as MCP error responses.
PApaperclipai/paperclip101k2,948 new stars in the last 7 days
NEnexu-io/open-design101k1,126 new stars in the last 7 days
BYbytedance/deer-flow84k368 new stars in the last 7 days
BYbytedance/deer-flow84k368 new stars in the last 7 days
COcoreyhaines31/marketingskills54k958 new stars in the last 7 days
IMImbad0202/academic-research-skills52k972 new stars in the last 7 days