@leanmcp/core
v0.3.3
Published
Core library implementing decorators, reflection, and MCP runtime server
Maintainers
Readme
@leanmcp/core
Core library for building Model Context Protocol (MCP) servers with TypeScript decorators and declarative schema definition.
Features
- Type-safe decorators -
@Tool,@Prompt,@Resourcewith full TypeScript support - Schema generation - Define JSON Schema declaratively using
@SchemaConstraintdecorators on class properties - Streamable HTTP transport - Production-ready HTTP server with session management
- Input validation - Built-in AJV validation for all inputs
- Clean API - Function names automatically become tool/prompt/resource names
- MCP compliant - Built on official
@modelcontextprotocol/sdk
Installation
npm install @leanmcp/corePeer Dependencies
For HTTP server support:
npm install express corsQuick Start
1. Define Your Service with Class-Based Schema
import { Tool, SchemaConstraint, Optional } from "@leanmcp/core";
// Define input schema as a class
class AnalyzeSentimentInput {
@SchemaConstraint({
description: 'Text to analyze',
minLength: 1
})
text!: string;
@Optional()
@SchemaConstraint({
description: 'Language code',
enum: ['en', 'es', 'fr'],
default: 'en'
})
language?: string;
}
// Define output schema
class AnalyzeSentimentOutput {
@SchemaConstraint({ enum: ['positive', 'negative', 'neutral'] })
sentiment!: string;
@SchemaConstraint({ minimum: -1, maximum: 1 })
score!: number;
}
export class SentimentService {
@Tool({
description: 'Analyze sentiment of text',
inputClass: AnalyzeSentimentInput
})
async analyzeSentiment(input: AnalyzeSentimentInput): Promise<AnalyzeSentimentOutput> {
// Your implementation
return {
sentiment: 'positive',
score: 0.8,
confidence: 0.95
};
}
}2. Create and Start Server
Option A: Zero-Config Auto-Discovery (Recommended)
The simplest way to create an HTTP server with auto-discovery:
import { createHTTPServer } from "@leanmcp/core";
// Create and start HTTP server with auto-discovery
await createHTTPServer({
name: "my-mcp-server",
version: "1.0.0",
port: 3000,
cors: true,
logging: true
});
console.log('\nMCP Server running');
console.log('HTTP endpoint: http://localhost:3000/mcp');
console.log('Health check: http://localhost:3000/health');What happens automatically:
- Services are discovered from
./mcpdirectory - HTTP server is created and started
- Session management is configured
- CORS is enabled (if specified)
Directory Structure:
your-project/
├── main.ts
└── mcp/
├── sentiment/
│ └── index.ts # export class SentimentService
├── weather/
│ └── index.ts # export class WeatherService
└── database/
└── index.ts # export class DatabaseServiceOption B: Factory Pattern (Advanced)
For advanced use cases requiring manual service registration or custom configuration:
import { createHTTPServer, MCPServer } from "@leanmcp/core";
import { SentimentService } from "./services/sentiment";
// Create MCP server with factory function
const serverFactory = async () => {
const server = new MCPServer({
name: "my-mcp-server",
version: "1.0.0",
logging: true,
autoDiscover: false // Disable auto-discovery for manual registration
});
// Register services manually
server.registerService(new SentimentService());
return server.getServer();
};
// Start HTTP server with factory
await createHTTPServer(serverFactory, {
port: 3000,
cors: true,
logging: true
});Decorators
@Tool
Marks a method as an MCP tool (callable function). Use inputClass to specify the input schema class.
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 };
}@Prompt
Marks a method as an MCP prompt template. Input schema is automatically inferred from parameter type.
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 for automatic schema generation.
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;
}@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
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; // Enable verbose debug logs (default: false)
autoDiscover?: boolean; // Enable auto-discovery (default: true)
mcpDir?: string; // Custom mcp directory path (optional)
});
// Manual registration
server.registerService(instance: any): void;
// Get underlying MCP SDK server
server.getServer(): Server;Options:
logging: Enable basic logging for server operationsdebug: Enable verbose debug logs showing detailed service registration (requireslogging: true)autoDiscover: Automatically discover and register services from./mcpdirectory (default:true)mcpDir: Custom path to the mcp directory (default: auto-detected./mcp)
Zero-Config Auto-Discovery
Services are automatically discovered and registered from the ./mcp directory when the server is created:
Basic Usage (Simplified API):
import { createHTTPServer } from "@leanmcp/core";
await createHTTPServer({
name: "my-server",
version: "1.0.0",
port: 3000,
logging: true // Enable logging
});With Debug Logging:
await createHTTPServer({
name: "my-server",
version: "1.0.0",
port: 3000,
logging: true,
debug: true // Show detailed service registration logs
});With Shared Dependencies:
For services that need shared dependencies, create a config.ts (example) file in your mcp directory:
// mcp/config.ts
import { AuthProvider } from "@leanmcp/auth";
if (!process.env.COGNITO_USER_POOL_ID || !process.env.COGNITO_CLIENT_ID) {
throw new Error('Missing required Cognito configuration');
}
export const authProvider = new AuthProvider('cognito', {
region: process.env.AWS_REGION || 'us-east-1',
userPoolId: process.env.COGNITO_USER_POOL_ID,
clientId: process.env.COGNITO_CLIENT_ID,
clientSecret: process.env.COGNITO_CLIENT_SECRET
});
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 {
constructor() {
// No parameters needed - use environment or imported config
}
@Tool({ description: 'Send a message' })
async sendMessage(args: any) {
// Implementation
}
}Your main file stays clean:
import { createHTTPServer } from "@leanmcp/core";
await createHTTPServer({
name: "my-server",
version: "1.0.0",
port: 3000,
logging: true
});
// Services are automatically discovered and registeredHow It Works:
- Automatically discovers and registers services from the
./mcpdirectory during server initialization - Recursively scans for
index.tsorindex.jsfiles - Dynamically imports each file and looks for exported classes
- Instantiates services with no-args constructors
- Registers all discovered services with their decorated methods
Directory Structure:
your-project/
├── main.ts
└── mcp/
├── config.ts # Optional: shared dependencies
├── slack/
│ └── index.ts # export class SlackService
├── database/
│ └── index.ts # export class DatabaseService
└── auth/
└── index.ts # export class AuthServicecreateHTTPServer
Create and start an HTTP server with streamable transport.
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)
});Factory Pattern (Advanced):
await createHTTPServer(
serverFactory: () => Server | Promise<Server>,
options: {
port?: number; // Port number (default: 3001)
cors?: boolean | object; // Enable CORS (default: false)
logging?: boolean; // Enable HTTP request logging (default: false)
sessionTimeout?: number; // Session timeout in ms (optional)
}
);CORS Configuration:
// Simple CORS (allow all origins - not recommended for production)
await createHTTPServer({
name: "my-server",
version: "1.0.0",
cors: true
});
// Advanced CORS configuration
await createHTTPServer({
name: "my-server",
version: "1.0.0",
cors: {
origin: 'https://example.com', // Specific origin
credentials: true // Allow credentials
}
});Schema Generation
Generate JSON Schema from TypeScript classes:
import { classToJsonSchemaWithConstraints } from "@leanmcp/core";
const schema = classToJsonSchemaWithConstraints(MyInputClass);HTTP Endpoints
When using createHTTPServer, the following endpoints are available:
POST /mcp- MCP protocol endpoint (accepts JSON-RPC 2.0 messages)GET /health- Health check endpointGET /- Welcome message
Environment Variables
PORT=3000 # Server port (optional)
NODE_ENV=production # Environment (optional)Error Handling
All tools automatically handle errors and return them in MCP format:
class DivideInput {
@SchemaConstraint({ description: 'Numerator' })
a!: number;
@SchemaConstraint({ description: 'Denominator' })
b!: number;
}
@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 };
}Errors are returned as:
{
"content": [{"type": "text", "text": "Error: Division by zero"}],
"isError": true
}TypeScript Support
Full TypeScript support with type inference:
class MyInput {
@SchemaConstraint({ description: 'Input field' })
field!: string;
}
class MyOutput {
result!: string;
}
// Input schema defined via inputClass, output type inferred from return type
@Tool({
description: 'My tool',
inputClass: MyInput
})
async myTool(input: MyInput): Promise<MyOutput> {
// TypeScript knows the exact types
const result: MyOutput = {
result: input.field.toUpperCase()
// Full autocomplete and type checking
};
return result;
}Key Points:
- Input schema is defined using
inputClassin the@Tooldecorator - Output schema is inferred from the return type
- For tools with no input parameters, omit the
inputClassoption - Use
@SchemaConstraintdecorators to add validation and documentation to your input classes
License
MIT
Related Packages
- @leanmcp/cli - CLI tool for creating new projects
- @leanmcp/auth - Authentication decorators and providers
- @leanmcp/utils - Utility functions
