Skip to main content

@leanmcp/core

Core library for building Model Context Protocol (MCP) servers with TypeScript decorators and declarative schema definition.

Features

Type-Safe Decorators

@Tool, @Prompt, @Resource with full TypeScript support

Auto-Discovery

Zero-config service discovery from ./mcp directory

Schema Generation

Declarative JSON Schema with @SchemaConstraint decorators

HTTP Transport

Production-ready HTTP server with session management

Installation

npm install @leanmcp/core
For HTTP server support:
npm install express cors

Quick Start

The simplest way to create an MCP server with auto-discovery:
import { createHTTPServer } from "@leanmcp/core";

await createHTTPServer({
  name: "my-mcp-server",
  version: "1.0.0",
  port: 3001,
  cors: true,
  logging: true
});

// Services are automatically discovered from ./mcp directory
Directory Structure:
your-project/
├── main.ts
└── mcp/
    ├── sentiment/
    │   └── index.ts   # export class SentimentService
    ├── weather/
    │   └── index.ts   # export class WeatherService
    └── config.ts      # Optional: shared dependencies

Define a Service

// mcp/sentiment/index.ts
import { Tool, SchemaConstraint, Optional } from "@leanmcp/core";

class AnalyzeSentimentInput {
  @SchemaConstraint({
    description: 'Text to analyze',
    minLength: 1
  })
  text!: string;

  @Optional()
  @SchemaConstraint({
    description: 'Language code',
    enum: ['en', 'es', 'fr'],
    default: 'en'
  })
  language?: string;
}

export class SentimentService {
  @Tool({ 
    description: 'Analyze sentiment of text',
    inputClass: AnalyzeSentimentInput
  })
  async analyzeSentiment(input: AnalyzeSentimentInput) {
    return {
      sentiment: 'positive',
      score: 0.8
    };
  }
}

Decorators

@Tool

Marks a method as a callable MCP tool.
class CalculateInput {
  @SchemaConstraint({ description: 'First number' })
  a!: number;
  
  @SchemaConstraint({ description: 'Second number' })
  b!: number;
}

@Tool({ 
  description: 'Calculate sum of two numbers',
  inputClass: CalculateInput
})
async calculate(input: CalculateInput) {
  return { result: input.a + input.b };
}
Options:
OptionTypeDescription
descriptionstringTool description for the AI
inputClassClassClass defining input schema

@Prompt

Marks a method as a reusable prompt template.
class CodeReviewInput {
  @SchemaConstraint({ description: 'Code to review' })
  code!: string;
  
  @SchemaConstraint({ description: 'Programming language' })
  language!: string;
}

@Prompt({ description: 'Generate code review prompt' })
codeReview(input: CodeReviewInput) {
  return {
    messages: [{
      role: "user",
      content: {
        type: "text",
        text: `Review this ${input.language} code:\n\n${input.code}`
      }
    }]
  };
}

@Resource

Marks a method as an MCP resource (data source).
@Resource({ 
  description: 'Get system configuration', 
  mimeType: 'application/json' 
})
async getConfig() {
  return {
    version: "1.0.0",
    environment: process.env.NODE_ENV
  };
}

@SchemaConstraint

Add validation constraints to class properties.
class UserInput {
  @SchemaConstraint({
    description: 'User email',
    format: 'email',
    minLength: 5,
    maxLength: 100
  })
  email!: string;

  @SchemaConstraint({
    description: 'User age',
    minimum: 18,
    maximum: 120
  })
  age!: number;

  @Optional()
  @SchemaConstraint({
    description: 'User role',
    enum: ['admin', 'user', 'guest'],
    default: 'user'
  })
  role?: string;
}
Common constraints:
  • description, default - Documentation
  • minLength, maxLength - String length
  • minimum, maximum - Number range
  • enum - Allowed values
  • format - String format (email, uri, date, etc.)
  • pattern - Regex pattern

@Optional

Marks a property as optional in the schema.
class SearchInput {
  @SchemaConstraint({ description: 'Search query' })
  query!: string;

  @Optional()
  @SchemaConstraint({ description: 'Max results', default: 10 })
  limit?: number;
}

API Reference

createHTTPServer

Create and start an HTTP server with auto-discovery. Simplified API (Recommended):
await createHTTPServer({
  name: string;              // Server name (required)
  version: string;           // Server version (required)
  port?: number;             // Port (default: 3001)
  cors?: boolean | object;   // Enable CORS (default: false)
  logging?: boolean;         // Enable logging (default: false)
  debug?: boolean;           // Verbose debug logs (default: false)
  autoDiscover?: boolean;    // Auto-discover services (default: true)
  mcpDir?: string;           // Custom mcp directory path
  sessionTimeout?: number;   // Session timeout in ms
  stateless?: boolean;       // Stateless mode for Lambda/serverless (default: true)
  dashboard?: boolean;       // Serve dashboard UI at / (default: true)
});
Factory Pattern (Advanced):
const serverFactory = async () => {
  const server = new MCPServer({
    name: "my-server",
    version: "1.0.0",
    autoDiscover: false  // Disable for manual registration
  });
  
  server.registerService(new MyService());
  return server.getServer();
};

await createHTTPServer(serverFactory, {
  port: 3001,
  cors: true
});

MCPServer

Main server class for registering services.
const server = new MCPServer({
  name: string;           // Server name
  version: string;        // Server version
  logging?: boolean;      // Enable logging (default: false)
  debug?: boolean;        // Verbose debug logs (default: false)
  autoDiscover?: boolean; // Auto-discover from ./mcp (default: true)
  mcpDir?: string;        // Custom mcp directory path
});

server.registerService(instance);  // Manual registration
server.getServer();                // Get underlying MCP SDK server

Auto-Discovery

Services are automatically discovered from the ./mcp directory:
  1. Recursively scans for index.ts or index.js files
  2. Dynamically imports each file
  3. Looks for exported classes
  4. Instantiates with no-args constructors
  5. Registers all decorated methods

Shared Dependencies

For services needing shared configuration (auth, database, etc.), create a config.ts:
// mcp/config.ts
import { AuthProvider } from "@leanmcp/auth";

export 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();
Then import in your services:
// mcp/slack/index.ts
import { Tool } from "@leanmcp/core";
import { Authenticated } from "@leanmcp/auth";
import { authProvider } from "../config.js";

@Authenticated(authProvider)
export class SlackService {
  @Tool({ description: 'Send a message' })
  async sendMessage(args: { channel: string; message: string }) {
    // Implementation
  }
}

HTTP Endpoints

EndpointMethodDescription
/mcpPOSTMCP protocol endpoint (JSON-RPC 2.0)
/healthGETHealth check
/GETWelcome message

Error Handling

Errors are automatically caught and returned in MCP format:
@Tool({ description: 'Divide numbers', inputClass: DivideInput })
async divide(input: DivideInput) {
  if (input.b === 0) {
    throw new Error("Division by zero");
  }
  return { result: input.a / input.b };
}
Returns:
{
  "content": [{"type": "text", "text": "Error: Division by zero"}],
  "isError": true
}

Environment Variables

PORT=3001              # Server port
NODE_ENV=production    # Environment

TypeScript Support

Key Points:
  • Input schema is defined via inputClass in the decorator
  • Output type is inferred from the return type
  • For tools with no input, omit inputClass
  • Use @SchemaConstraint for validation and documentation
class MyInput {
  @SchemaConstraint({ description: 'Input field' })
  field!: string;
}

@Tool({ description: 'My tool', inputClass: MyInput })
async myTool(input: MyInput): Promise<{ result: string }> {
  return { result: input.field.toUpperCase() };
}