TypeScript SDK for building Model Context Protocol servers
with built-in support for Auth, Elicitation, and MCP-Apps (including ChatGPT Apps).
A basic MCP connects tools to AI agents. But production means solving real problems:
| Problem | LeanMCP Solution |
|---|---|
| Auth | Integrate with Auth0, Supabase, Cognito, Firebase, or custom |
| Multi-tenancy | Per-user API keys and permissions |
| Elicitation | Handle user input during tool execution |
| Audit | Logging, monitoring, production observability |
- Developer Experience first — decorators, auto-discovery
- Convention over configuration — sensible defaults
- Type-safe by default — TypeScript + schema validation
- Production-ready — HTTP transport, session management
Building a basic MCP that connects tools to an AI agent is straightforward — define your tools, add descriptions, done. But the make-or-break features that separate a toy from production are much harder:
- Authentication — OAuth integration, token validation, scope management
- Elicitation — User input collection with validation
- Payments — Stripe integration, subscription checks, usage-based billing
- MCP Apps & UI — Rendering UI components inside ChatGPT, Claude, and other clients
These features require deep MCP protocol knowledge and weeks of implementation. LeanMCP handles them out of the box with @leanmcp/auth, @leanmcp/elicitation, and built-in UI support.
- 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 buildMIT
- Built on top of Model Context Protocol (MCP)
- Uses reflect-metadata for decorator support