TypeScript toolkit for building Model Context Protocol (MCP) servers with production-minded defaults.
Links
- Docs: https://docs.leanmcp.com
- Build & Deploy: https://ship.leanmcp.com
- Observability Platform: https://app.leanmcp.com
- npm packages: https://www.npmjs.com/search?q=%40leanmcp
- GitHub org: https://github.com/LeanMCP
LeanMCP Core is a TypeScript SDK + CLI for building Model Context Protocol (MCP) servers that are intended to run in production.
It focuses on the server side of MCP: tools, resources, prompts, schemas, and runtime behavior.
LeanMCP does not replace the MCP specification. It provides infrastructure and conventions MCP servers need once they move beyond local demos.
Create and run a local MCP server in seconds:
npx @leanmcp/cli create my-server
cd my-server
npm run devYour MCP server is now running with:
- schema validation
- tool and resource registration
- a predictable local development workflow
LeanMCP is modular. Start with the core packages, then add capabilities as needed.
| Package | Purpose | Install |
|---|---|---|
| @leanmcp/cli | Project scaffolding and local dev / deploy workflow | npm install -g @leanmcp/cli or npx @leanmcp/cli |
| @leanmcp/core | MCP server runtime, decorators, schema validation | npm install @leanmcp/core |
| Package | Purpose | When to use | Install |
|---|---|---|---|
| @leanmcp/auth | Authentication and access control | Real users, permissions, multi-user MCP servers | npm install @leanmcp/auth |
| @leanmcp/elicitation | Structured user input during execution | Tools need guided or multi-step input | npm install @leanmcp/elicitation |
| @leanmcp/ui | UI components for MCP Apps | Interactive MCP experiences (advanced) | npm install @leanmcp/ui |
| @leanmcp/env-injection | Request-scoped environment / secret injection | Multi-tenant secrets, per-request config | npm install @leanmcp/env-injection |
| @leanmcp/utils | Shared utilities | Extending or building on LeanMCP internals | npm install @leanmcp/utils |
If you are unsure where to start, install @leanmcp/cli and @leanmcp/core only.
-
npm install -g @leanmcp/cliornpx @leanmcp/cliScaffolds projects and runs local development and deployment workflows. -
npm install @leanmcp/coreThe MCP server runtime: decorators, schema validation, and conventions.
Optional capabilities layer on top of the core:
npm install @leanmcp/authfor identity and permissionsnpm install @leanmcp/elicitationfor structured user inputnpm install @leanmcp/uifor MCP-native UI surfacesnpm install @leanmcp/env-injectionfor request-scoped secrets
|
|
|
|
|
@tool("search_docs")
async searchDocs(query: string) {
return await this.vectorStore.search(query)
}@requireAuth()
@tool("get_user_data")
async getUserData() {
...
}const input = await elicit({
type: "form",
fields: [...]
})These snippets show common patterns only. Full API details live in the documentation.
- Installation
- Quick Start
- Core Concepts
- CLI Commands
- Decorators
- Project Structure
- API Reference
- Examples
- Development
- Contributing
npm install -g @leanmcp/clinpm install @leanmcp/core
npm install --save-dev @leanmcp/clinpx @leanmcp/cli create my-mcp-server
cd my-mcp-server
npm installThis generates a clean project structure:
my-mcp-server/
├── main.ts # Entry point with HTTP server
├── package.json # Dependencies
├── tsconfig.json # TypeScript config
└── mcp/ # Services directory
└── example/
└── index.ts # Example service
The generated mcp/example/index.ts shows class-based schema validation:
import { Tool, Optional, SchemaConstraint } from "@leanmcp/core";
// Define input schema as a TypeScript class
class AnalyzeSentimentInput {
@SchemaConstraint({
description: 'Text to analyze',
minLength: 1
})
text!: string;
@Optional()
@SchemaConstraint({
description: 'Language code',
enum: ['en', 'es', 'fr', 'de'],
default: 'en'
})
language?: string;
}
// Define output schema
class AnalyzeSentimentOutput {
@SchemaConstraint({ enum: ['positive', 'negative', 'neutral'] })
sentiment!: string;
@SchemaConstraint({ minimum: -1, maximum: 1 })
score!: number;
@SchemaConstraint({ minimum: 0, maximum: 1 })
confidence!: number;
}
export class SentimentService {
@Tool({
description: 'Analyze sentiment of text',
inputClass: AnalyzeSentimentInput
})
async analyzeSentiment(args: AnalyzeSentimentInput): Promise<AnalyzeSentimentOutput> {
const sentiment = this.detectSentiment(args.text);
return {
sentiment: sentiment > 0 ? 'positive' : sentiment < 0 ? 'negative' : 'neutral',
score: sentiment,
confidence: Math.abs(sentiment)
};
}
private detectSentiment(text: string): number {
// Simple keyword-based sentiment analysis
const positiveWords = ['good', 'great', 'excellent', 'amazing', 'love'];
const negativeWords = ['bad', 'terrible', 'awful', 'horrible', 'hate'];
let score = 0;
const words = text.toLowerCase().split(/\s+/);
words.forEach(word => {
if (positiveWords.includes(word)) score += 0.3;
if (negativeWords.includes(word)) score -= 0.3;
});
return Math.max(-1, Math.min(1, score));
}
}npm startYour MCP server starts on http://localhost:8080 with:
- HTTP endpoint:
http://localhost:8080/mcp - Health check:
http://localhost:8080/health
Callable functions that perform actions (like API endpoints).
class AddInput {
@SchemaConstraint({ description: 'First number' })
a!: number;
@SchemaConstraint({ description: 'Second number' })
b!: number;
}
@Tool({
description: 'Calculate sum of two numbers',
inputClass: AddInput
})
async add(input: AddInput): Promise<{ result: number }> {
return { result: input.a + input.b };
}
// Tool name: "add" (from function name)Reusable prompt templates for LLM interactions.
@Prompt({ description: 'Generate a greeting prompt' })
greetingPrompt(args: { name?: string }) {
return {
messages: [{
role: 'user',
content: { type: 'text', text: `Say hello to ${args.name || 'there'}!` }
}]
};
}
// Prompt name: "greetingPrompt" (from function name)Data endpoints that provide information (like REST GET endpoints).
@Resource({ description: 'Service statistics' })
getStats() {
return {
uptime: process.uptime(),
requestCount: 1523
};
}
// Resource URI: "servicename://getStats" (auto-generated)The LeanMCP CLI provides an interactive experience for creating and managing MCP projects.
Creates a new MCP server project with interactive setup:
leanmcp create my-mcp-serverInteractive prompts:
- Auto-install dependencies (optional)
- Start dev server after creation (optional)
Generated structure:
my-mcp-server/
├── main.ts # Entry point with HTTP server
├── package.json # Project dependencies
├── tsconfig.json # TypeScript configuration
├── .gitignore # Git ignore rules
├── .dockerignore # Docker ignore rules
├── .env # Environment variables
├── .env.local # Local overrides
└── mcp/ # Services directory
└── example/
└── index.ts # Example service
Adds a new service to an existing project with auto-registration:
leanmcp add weatherWhat it does:
- Creates
mcp/weather/index.tswith boilerplate (Tool, Prompt, Resource examples) - Auto-registers the service in
main.ts - Ready to customize and use immediately
For complete CLI documentation including all commands, options, and advanced usage, see @leanmcp/cli README.
| Decorator | Purpose | Usage |
|---|---|---|
@Tool |
Callable function | @Tool({ description?: string, inputClass?: Class }) |
@Prompt |
Prompt template | @Prompt({ description?: string }) |
@Resource |
Data endpoint | @Resource({ description?: string }) |
| Decorator | Purpose | Usage |
|---|---|---|
@Optional |
Mark property as optional | Property decorator |
@SchemaConstraint |
Add validation rules | Property decorator with constraints |
Available Constraints:
- String:
minLength,maxLength,pattern,enum,format,description,default - Number:
minimum,maximum,description,default - Array:
minItems,maxItems,description - Common:
description,default
Example:
class UserInput {
@SchemaConstraint({
description: 'User email address',
format: 'email'
})
email!: string;
@Optional()
@SchemaConstraint({
description: 'User age',
minimum: 18,
maximum: 120
})
age?: number;
@SchemaConstraint({
description: 'User roles',
enum: ['admin', 'user', 'guest'],
default: 'user'
})
role!: string;
}Simplified API (Recommended):
import { createHTTPServer } from "@leanmcp/core";
// Services are automatically discovered from ./mcp directory
await createHTTPServer({
name: "my-mcp-server",
version: "1.0.0",
port: 8080,
cors: true,
logging: true
});Factory Pattern (Advanced):
import { createHTTPServer, MCPServer } from "@leanmcp/core";
import { ExampleService } from "./mcp/example/index.js";
const serverFactory = async () => {
const server = new MCPServer({
name: "my-mcp-server",
version: "1.0.0",
autoDiscover: false
});
server.registerService(new ExampleService());
return server.getServer();
};
await createHTTPServer(serverFactory, {
port: 8080,
cors: true
});import { Tool, Prompt, Resource } from "@leanmcp/core";
class ToolInput {
@SchemaConstraint({ description: 'Input parameter' })
param!: string;
}
export class ServiceName {
@Tool({
description: 'Tool description',
inputClass: ToolInput
})
async toolMethod(args: ToolInput) {
// Tool implementation
return { result: 'success' };
}
@Prompt({ description: 'Prompt description' })
promptMethod(args: { param?: string }) {
// Prompt implementation
return {
messages: [{
role: 'user',
content: { type: 'text', text: 'Prompt text' }
}]
};
}
@Resource({ description: 'Resource description' })
resourceMethod() {
// Resource implementation
return { data: 'value' };
}
}Creates and starts an HTTP server with MCP support.
Simplified API (Recommended):
await createHTTPServer({
name: string; // Server name (required)
version: string; // Server version (required)
port?: number; // Port number (default: 3001)
cors?: boolean | object; // Enable CORS (default: false)
logging?: boolean; // Enable logging (default: false)
debug?: boolean; // Enable debug logs (default: false)
autoDiscover?: boolean; // Auto-discover services (default: true)
mcpDir?: string; // Custom mcp directory path (optional)
sessionTimeout?: number; // Session timeout in ms (optional)
});Example:
import { createHTTPServer } from "@leanmcp/core";
// Services automatically discovered from ./mcp directory
await createHTTPServer({
name: "my-mcp-server",
version: "1.0.0",
port: 3000,
cors: true,
logging: true
});Factory Pattern (Advanced):
import { createHTTPServer, MCPServer } from "@leanmcp/core";
import { MyService } from "./mcp/myservice/index.js";
const serverFactory = async () => {
const server = new MCPServer({
name: "my-mcp-server",
version: "1.0.0",
autoDiscover: false
});
server.registerService(new MyService());
return server.getServer();
};
await createHTTPServer(serverFactory, {
port: 3000,
cors: true
});Main server class for manual service registration.
Constructor Options:
const server = new MCPServer({
name: string; // Server name (required)
version: string; // Server version (required)
logging?: boolean; // Enable logging (default: false)
debug?: boolean; // Enable debug logs (default: false)
autoDiscover?: boolean; // Auto-discover services (default: true)
mcpDir?: string; // Custom mcp directory path (optional)
});Methods:
registerService(instance)- Manually register a service instancegetServer()- Get the underlying MCP SDK server
Example:
import { MCPServer } from "@leanmcp/core";
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
autoDiscover: false
});
server.registerService(new WeatherService());
server.registerService(new PaymentService());import { Tool, Prompt, Resource, SchemaConstraint, Optional } from "@leanmcp/core";
class WeatherInput {
@SchemaConstraint({
description: 'City name',
minLength: 1
})
city!: string;
@Optional()
@SchemaConstraint({
description: 'Units',
enum: ['metric', 'imperial'],
default: 'metric'
})
units?: string;
}
class WeatherOutput {
@SchemaConstraint({ description: 'Temperature value' })
temperature!: number;
@SchemaConstraint({
description: 'Weather conditions',
enum: ['sunny', 'cloudy', 'rainy', 'snowy']
})
conditions!: string;
@SchemaConstraint({
description: 'Humidity percentage',
minimum: 0,
maximum: 100
})
humidity!: number;
}
export class WeatherService {
@Tool({
description: 'Get current weather for a city',
inputClass: WeatherInput
})
async getCurrentWeather(args: WeatherInput): Promise<WeatherOutput> {
// Simulate API call
return {
temperature: 72,
conditions: 'sunny',
humidity: 65
};
}
@Prompt({ description: 'Generate weather query prompt' })
weatherPrompt(args: { city?: string }) {
return {
messages: [{
role: 'user',
content: {
type: 'text',
text: `What's the weather forecast for ${args.city || 'the city'}?`
}
}]
};
}
@Resource({ description: 'Supported cities list' })
getSupportedCities() {
return {
cities: ['New York', 'London', 'Tokyo', 'Paris', 'Sydney'],
count: 5
};
}
}import { Tool, SchemaConstraint } from "@leanmcp/core";
class CalculatorInput {
@SchemaConstraint({
description: 'First number',
minimum: -1000000,
maximum: 1000000
})
a!: number;
@SchemaConstraint({
description: 'Second number',
minimum: -1000000,
maximum: 1000000
})
b!: number;
}
class CalculatorOutput {
@SchemaConstraint({ description: 'Calculation result' })
result!: number;
}
export class CalculatorService {
@Tool({
description: 'Add two numbers',
inputClass: CalculatorInput
})
async add(args: CalculatorInput): Promise<CalculatorOutput> {
return { result: args.a + args.b };
}
@Tool({
description: 'Subtract two numbers',
inputClass: CalculatorInput
})
async subtract(args: CalculatorInput): Promise<CalculatorOutput> {
return { result: args.a - args.b };
}
@Tool({
description: 'Multiply two numbers',
inputClass: CalculatorInput
})
async multiply(args: CalculatorInput): Promise<CalculatorOutput> {
return { result: args.a * args.b };
}
@Tool({
description: 'Divide two numbers',
inputClass: CalculatorInput
})
async divide(args: CalculatorInput): Promise<CalculatorOutput> {
if (args.b === 0) {
throw new Error('Division by zero');
}
return { result: args.a / args.b };
}
}import { Tool, SchemaConstraint } from "@leanmcp/core";
import { AuthProvider, Authenticated } from "@leanmcp/auth";
// Initialize auth provider
const authProvider = new AuthProvider('cognito', {
region: process.env.AWS_REGION,
userPoolId: process.env.COGNITO_USER_POOL_ID,
clientId: process.env.COGNITO_CLIENT_ID
});
await authProvider.init();
// Input class - no token field needed
class SendMessageInput {
@SchemaConstraint({
description: 'Channel to send message to',
minLength: 1
})
channel!: string;
@SchemaConstraint({
description: 'Message text',
minLength: 1
})
text!: string;
}
// Protect entire service with authentication
@Authenticated(authProvider)
export class SlackService {
@Tool({
description: 'Send message to Slack channel',
inputClass: SendMessageInput
})
async sendMessage(args: SendMessageInput) {
// Token automatically validated from _meta.authorization.token
// Only business arguments are passed here
return {
success: true,
channel: args.channel,
timestamp: Date.now().toString()
};
}
}Client Usage:
// Call with authentication
await mcpClient.callTool({
name: "sendMessage",
arguments: {
channel: "#general",
text: "Hello world"
},
_meta: {
authorization: {
type: "bearer",
token: "your-jwt-token"
}
}
});See examples/slack-with-auth for a complete working example.
# Clone the repository
git clone https://github.com/leanmcp/leanmcp-sdk.git
cd leanmcp-sdk
# Install dependencies
npm install
# Build all packages
npm run buildleanmcp-sdk/
├── package.json # Root workspace config
├── tsconfig.base.json # Shared TypeScript config
├── turbo.json # Turborepo configuration
└── packages/
├── cli/ # @leanmcp/cli - CLI binary
├── core/ # @leanmcp/core - Core decorators & runtime
├── auth/ # @leanmcp/auth - Authentication with @Authenticated decorator
└── utils/ # @leanmcp/utils - Utilities (planned)
# Build core package
cd packages/core
npm run build
# Build CLI package
cd packages/cli
npm run build# Create a test project
npx @leanmcp/cli create test-project
cd test-project
# Link local development version
npm link ../../packages/core
npm link ../../packages/cli
# Run the test project
npm start- Compile-time validation - Catch errors before runtime
- Autocomplete - Full IntelliSense support in VS Code
- Refactoring - Safe renames and changes across your codebase
- No duplication - Define schemas once using TypeScript types
- Type inference - Automatic schema generation from decorators
Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
# Run tests
npm test
# Run linter
npm run lint
# Build all packages
npm run buildNew to open source? Perfect! We have plenty of good first issues waiting for you.
|
Fork & Contribute
|
Good First Issues
|
Join Community Chat with maintainers and contributors |
- Documentation: Help make our guides clearer
- Examples: Add new service examples (weather, payments, etc.)
- Auth Integrations: Add support for new auth providers
- Bug Fixes: Fix reported issues
- Tests: Improve test coverage
- Features: Propose and implement new capabilities
See our Contributing Guide for detailed instructions.
MIT License - see LICENSE file for details
- Built on top of Model Context Protocol (MCP)
- Uses reflect-metadata for decorator support
- Inspired by the amazing MCP community
