Skip to content

TypeScript SDK for building Model Context Protocol servers with built-in support for Auth, Elicitation, and MCP-Apps (including ChatGPT Apps).

License

Notifications You must be signed in to change notification settings

LeanMCP/leanmcp-sdk

Repository files navigation

LeanMCP Logo

MIT License Documentation Website Follow on X Discord Community
Python repo PyPI version PyPI downloads Python docs
TypeScript repo npm version npm downloads

LeanMCP Core (SDK + CLI)

TypeScript toolkit for building Model Context Protocol (MCP) servers with production-minded defaults.

Links


What is LeanMCP Core?

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.


Quick Start

Create and run a local MCP server in seconds:

npx @leanmcp/cli create my-server
cd my-server
npm run dev

Your MCP server is now running with:

  • schema validation
  • tool and resource registration
  • a predictable local development workflow

Packages (What to install)

LeanMCP is modular. Start with the core packages, then add capabilities as needed.

Required for every MCP server

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

Optional capability packages

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.


How the pieces fit

  • npm install -g @leanmcp/cli or npx @leanmcp/cli Scaffolds projects and runs local development and deployment workflows.

  • npm install @leanmcp/core The MCP server runtime: decorators, schema validation, and conventions.

Optional capabilities layer on top of the core:

  • npm install @leanmcp/auth for identity and permissions
  • npm install @leanmcp/elicitation for structured user input
  • npm install @leanmcp/ui for MCP-native UI surfaces
  • npm install @leanmcp/env-injection for request-scoped secrets

Choose your path (common setups)

1. Minimal MCP server (local development)

2. Authenticated MCP server

  • Add: @leanmcp/auth
  • Use when: MCP server has real users and access control

3. Interactive tool execution

4. MCP Apps / UI (advanced)

  • Add: @leanmcp/ui
  • Use when: building interactive MCP interfaces, not just JSON output

5. Multi-tenant secrets (advanced)


Common patterns

Define a tool

@tool("search_docs")
async searchDocs(query: string) {
  return await this.vectorStore.search(query)
}

Require authentication

@requireAuth()
@tool("get_user_data")
async getUserData() {
  ...
}

Ask for structured input

const input = await elicit({
  type: "form",
  fields: [...]
})

These snippets show common patterns only. Full API details live in the documentation.


Table of Contents

Installation

Global CLI Installation

npm install -g @leanmcp/cli

Project-Level Installation

npm install @leanmcp/core
npm install --save-dev @leanmcp/cli

Quick Start

1. Create a new project

npx @leanmcp/cli create my-mcp-server
cd my-mcp-server
npm install

This 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

2. Define your 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));
  }
}

3. Run your server

npm start

Your MCP server starts on http://localhost:8080 with:

  • HTTP endpoint: http://localhost:8080/mcp
  • Health check: http://localhost:8080/health

Core Concepts

Tools

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)

Prompts

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)

Resources

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)

CLI Commands

The LeanMCP CLI provides an interactive experience for creating and managing MCP projects.

leanmcp create <project-name>

Creates a new MCP server project with interactive setup:

leanmcp create my-mcp-server

Interactive 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

leanmcp add <service-name>

Adds a new service to an existing project with auto-registration:

leanmcp add weather

What it does:

  • Creates mcp/weather/index.ts with boilerplate (Tool, Prompt, Resource examples)
  • Auto-registers the service in main.ts
  • Ready to customize and use immediately

More CLI Features

For complete CLI documentation including all commands, options, and advanced usage, see @leanmcp/cli README.

Decorators

Core Decorators

Decorator Purpose Usage
@Tool Callable function @Tool({ description?: string, inputClass?: Class })
@Prompt Prompt template @Prompt({ description?: string })
@Resource Data endpoint @Resource({ description?: string })

Schema Decorators

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;
}

Project Structure

Main Entry Point (main.ts)

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
});

Service Structure (mcp/service-name/index.ts)

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' };
  }
}

API Reference

createHTTPServer(options | serverFactory, options?)

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
});

MCPServer

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 instance
  • getServer() - 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());

Examples

Complete Weather Service

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
    };
  }
}

Calculator Service with Validation

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 };
  }
}

Authenticated Service with AWS Cognito

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.

Development

Setting Up the Monorepo

# Clone the repository
git clone https://github.com/leanmcp/leanmcp-sdk.git
cd leanmcp-sdk

# Install dependencies
npm install

# Build all packages
npm run build

Monorepo Structure

leanmcp-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)

Building Individual Packages

# Build core package
cd packages/core
npm run build

# Build CLI package
cd packages/cli
npm run build

Testing Your Changes

# 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

Type Safety Benefits

  • 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

Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development Workflow

# Run tests
npm test

# Run linter
npm run lint

# Build all packages
npm run build

We Actively Welcome Contributors

If LeanMCP is useful to you, please give us a star!

GitHub stars GitHub forks

Contributing is Easy

New to open source? Perfect! We have plenty of good first issues waiting for you.

Fork & Contribute

  1. Fork the repo
  2. Create a branch
  3. Make changes
  4. Submit PR

Fork Now →

Good First Issues

  • 📖 Documentation improvements
  • 🔧 Example additions
  • 🔐 Auth provider integrations
  • 🧪 Test coverage

Browse Issues →

Join Community

Chat with maintainers and contributors

Join Discord →

What You Can Contribute

  • 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.


License

MIT License - see LICENSE file for details

Links

Acknowledgments