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

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

npm version npm downloads Ask DeepWiki

Why LeanMCP?

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

Core Principles

  • 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 MCPs is Easy. Production MCPs are Hard.

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.

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

License

MIT

Links

Acknowledgments

About

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

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages