easy-mcp-nest
v0.5.0
Published
A NestJS-based framework for building standard Model Context Protocol (MCP) servers with tool execution via JSON-RPC 2.0
Downloads
1,044
Maintainers
Readme
EasyMCP Framework
A NestJS-based framework for building standard Model Context Protocol (MCP) servers with tool execution via JSON-RPC 2.0.
Description
EasyMCP simplifies the creation of MCP (Model Context Protocol) servers by providing a clean, type-safe framework that:
- Standard MCP Protocol: Implements the Model Context Protocol specification with JSON-RPC 2.0 over stdio
- Tool Execution: Register and execute tools that external LLM agents can discover and call
- Type Safety: Full TypeScript support with comprehensive type definitions
- Simple Configuration: Minimal setup required - just define your tools and run
Installation
npm install easy-mcp-nest
# or
pnpm add easy-mcp-nest
# or
yarn add easy-mcp-nestPeer Dependencies
EasyMCP requires the following peer dependencies to be installed:
@nestjs/common^11.0.1@nestjs/core^11.0.1@nestjs/platform-express^11.0.1
Optional peer dependencies (for specific features):
express^4.18.0 (for Express adapter)zod^3.22.0 (for TypeScript-first validation)
Install required dependencies with:
npm install @nestjs/common@^11.0.1 @nestjs/core@^11.0.1 @nestjs/platform-express@^11.0.1Install optional dependencies as needed:
# For Express adapter
npm install express@^4.18.0
# For Zod validation
npm install zod@^3.22.0Quick Start
import { EasyMCP, McpConfig } from 'easy-mcp-nest';
// Define your tools
async function getUser(args: { userId: string }): Promise<string> {
// Your tool logic here
const user = await fetchUser(args.userId);
return JSON.stringify(user);
}
// Configure EasyMCP
const config: McpConfig = {
tools: [
{
name: 'getUser',
description: 'Retrieves user details by ID',
function: getUser,
inputSchema: {
type: 'OBJECT',
properties: {
userId: {
type: 'STRING',
description: 'The unique ID of the user',
},
},
required: ['userId'],
},
},
],
};
// Initialize and run
async function bootstrap() {
await EasyMCP.initialize(config);
await EasyMCP.run();
}
bootstrap();Configuration
McpConfig
The main configuration object passed to EasyMCP.initialize():
interface McpConfig {
tools: ToolRegistrationInput[];
resources?: ResourceRegistrationInput[]; // Optional resources
prompts?: PromptRegistrationInput[]; // Optional prompts
serverInfo?: ServerInfo;
}ToolRegistrationInput
Each tool must implement:
interface ToolRegistrationInput<TArgs = Record<string, any>> {
name: string;
description: string;
/**
* Tool function that accepts args, optional cancellationToken, optional context, and optional progress callback.
* For backward compatibility, only the args parameter is required.
*/
function: (
args: TArgs,
cancellationToken?: CancellationToken,
context?: McpContext,
progress?: ProgressCallback
) => Promise<any>;
/**
* Input schema in JSON Schema 2020-12 format or Zod schema.
* If a Zod schema is provided, it will be converted to JSON Schema internally.
*/
inputSchema: JsonSchema2020_12 | z.ZodType<TArgs>;
icon?: string; // Optional icon URI
/** Required scopes/permissions for this tool */
requiredScopes?: string[];
}ServerInfo (Optional)
Optional server information for MCP initialize response:
interface ServerInfo {
name: string;
version: string;
}Example Tool
Using JSON Schema
async function searchDatabase(
args: { query: string; limit?: number },
cancellationToken?: CancellationToken,
context?: McpContext
): Promise<string> {
const results = await db.search(args.query, args.limit || 10);
return JSON.stringify(results);
}
const searchTool: ToolRegistrationInput = {
name: 'searchDatabase',
description: 'Searches the database for matching records',
function: searchDatabase,
inputSchema: {
type: 'object', // JSON Schema 2020-12 format
properties: {
query: {
type: 'string',
description: 'The search query',
},
limit: {
type: 'integer',
description: 'Maximum number of results to return',
},
},
required: ['query'],
},
requiredScopes: ['database:read'], // Optional: require specific permissions
};Using Zod Schema (Recommended)
import { z } from 'zod';
const SearchSchema = z.object({
query: z.string().describe('The search query'),
limit: z.number().int().positive().optional().describe('Maximum number of results to return'),
});
type SearchParams = z.infer<typeof SearchSchema>;
async function searchDatabase(
args: SearchParams,
cancellationToken?: CancellationToken,
context?: McpContext
): Promise<string> {
// args is fully typed as SearchParams
const results = await db.search(args.query, args.limit || 10);
return JSON.stringify(results);
}
const searchTool: ToolRegistrationInput<SearchParams> = {
name: 'searchDatabase',
description: 'Searches the database for matching records',
function: searchDatabase,
inputSchema: SearchSchema, // Direct Zod schema - no conversion needed!
requiredScopes: ['database:read'],
};ResourceRegistrationInput
Resources can now access context (user info, scopes, etc.):
interface ResourceRegistrationInput {
uri: string;
name: string;
description?: string;
mimeType?: string;
icon?: string;
/**
* Function that returns the resource content.
* Context is provided when the resource is accessed.
*/
getContent: (context?: McpContext) => Promise<string | { type: string; data: string; mimeType?: string }>;
}Example Resource
const buildingListResource: ResourceRegistrationInput = {
uri: 'building://list',
name: 'Building List',
description: 'List of buildings accessible to the user',
getContent: async (context) => {
// Context is now available!
const buildings = await getBuildingService().getBuildings(context?.userId);
return JSON.stringify(buildings);
},
};API Reference
EasyMCP Class
static initialize(config: McpConfig): Promise<void>
Initializes the EasyMCP framework with the provided configuration. Must be called before run().
static run(): Promise<void>
Starts the EasyMCP server and begins listening for JSON-RPC requests via stdio.
static getService<T>(token: string | symbol): T
Retrieves a service from the NestJS application context. Useful for advanced use cases.
static shutdown(): Promise<void>
Gracefully shuts down the EasyMCP framework, closing the NestJS application context and cleaning up resources. Should be called when the application is terminating (e.g., on SIGTERM, SIGINT signals).
// Example: Handle graceful shutdown
process.on('SIGTERM', async () => {
await EasyMCP.shutdown();
process.exit(0);
});
process.on('SIGINT', async () => {
await EasyMCP.shutdown();
process.exit(0);
});Types
Core Types:
McpConfig- Main configuration interfaceToolRegistrationInput- Tool definition interfaceServerInfo- Optional server informationJsonRpcRequest,JsonRpcResponse,JsonRpcError- JSON-RPC 2.0 typesJsonRpcErrorCode- JSON-RPC 2.0 error code enumInitializeParams,InitializeResult- MCP initialize typesListToolsResult,McpTool- MCP tools typesCallToolParams,CallToolResult- MCP tool call typesToolDefinition,ToolParameter,ToolFunction- Tool interfacesIInterfaceLayer- Interface layer interfaceMcpErrorCode- MCP error code enumVERSION,PACKAGE_NAME,getVersion(),getPackageName()- Version information utilitiesINTERFACE_LAYER_TOKEN- Token for accessing the interface layer (advanced use cases)
New Feature Types:
McpContext- Context interface for user informationCreateMcpExpressRouterOptions- Express adapter optionsOAuthProviderConfig,OAuthConfig- OAuth configurationCreateMcpServerOptions- Standalone server optionsStandaloneTransport- Transport type ('stdio' | 'http')
Architecture
EasyMCP uses a simplified architecture for standard MCP servers:
- Interface Layer: Handles JSON-RPC 2.0 communication over stdio
- Core Layer: Implements MCP protocol methods (initialize, tools/list, tools/call)
- Tool Registry: Manages tool registration and execution
Architecture Diagram
graph TB
MCPClient[MCP Client<br/>Claude Desktop, Cline, etc.] -->|stdio JSON-RPC| Interface[Interface Layer<br/>StdioGatewayService]
Interface -->|JsonRpcRequest| Core[McpServerService<br/>Core Orchestrator]
Core -->|getTools| Registry[ToolRegistryService]
Core -->|executeTool| Tools[User Tools]
Core -->|JsonRpcResponse| Interface
Interface -->|Response| MCPClientMCP Protocol Compliance
EasyMCP implements the standard Model Context Protocol (MCP) specification version 2025-11-25.
Protocol Version
- Supported Version:
2025-11-25 - Validation: The framework validates that clients use the supported protocol version during initialization
- Error Handling: Unsupported protocol versions are rejected with a clear error message
Supported Methods
EasyMCP implements the following MCP protocol methods:
initialize- Server/client handshake, returns server capabilities and protocol version- Validates client protocol version
- Returns server capabilities (currently supports tools)
- Returns server information (name and version)
tools/list- Returns all registered tools with their JSON Schema 2020-12 definitions- Returns array of tool definitions in MCP format
- Each tool includes name, description, inputSchema, and optional icon
tools/call- Executes a tool with provided arguments and returns the result- Validates tool arguments against JSON Schema 2020-12
- Executes tool function
- Returns result in MCP content format
- Handles errors according to MCP error code specification
- Supports cancellation tokens for long-running operations
resources/list- Returns all registered resources- Returns array of resource definitions with URI, name, description, mimeType, and optional icon
resources/read- Reads the content of a resource by URI- Returns resource content in MCP format
prompts/list- Returns all registered prompts- Returns array of prompt definitions with name, description, arguments, and optional icon
prompts/get- Generates prompt content from arguments- Returns prompt messages in MCP format
Error Codes
EasyMCP uses standard JSON-RPC 2.0 and MCP error codes:
-32700- Parse error-32600- Invalid request-32601- Method not found-32602- Invalid params-32603- Internal error-32001- Tool not found (MCP-specific)-32002- Tool execution error (MCP-specific)
Transport
The server communicates via JSON-RPC 2.0 over stdio (standard input/output), which is the standard transport for MCP servers. This allows the server to be used with MCP clients like Claude Desktop, Cursor, Cline, and other MCP-compatible tools.
Default Mode: Newline-Delimited JSON
By default, the server uses newline-delimited JSON (one JSON-RPC message per line) for maximum compatibility with MCP clients. This mode works seamlessly with Cursor, Claude Desktop, and other popular MCP clients.
Content-Length Framing Mode (Optional)
For strict MCP protocol compliance with Content-Length framing, set the environment variable:
MCP_USE_CONTENT_LENGTH=1This enables the MCP-specified Content-Length header format, which some clients may not parse correctly.
Features
- Tools: Full support for tool registration and execution with JSON Schema 2020-12
- Resources: Support for resource registration and reading
- Prompts: Support for prompt templates and generation
- Client Features: Basic support for Sampling, Roots, and Elicitation (client-initiated)
- Metadata/Icons: Optional icon support for tools, resources, and prompts
- Progress & Cancellation: Basic support for cancellation tokens in tool execution
- Tool Naming: Validation according to MCP 2025-11-25 naming guidelines
Limitations
- Transport: stdio transport is fully supported. HTTP transport is available via Express adapter. WebSocket transport is not available.
- Protocol Version: Only protocol version 2025-11-25 is supported. Older or newer versions will be rejected.
- Resources Subscribe/Unsubscribe: Basic resource subscription is not yet implemented (resources/list and resources/read are supported)
For more information on testing EasyMCP with real MCP clients, see Integration Testing Guide.
Error Handling
EasyMCP provides comprehensive error handling with custom error classes and clear error messages.
Error Classes
EasyMcpError- Base error class for all framework errorsConfigurationError- Configuration validation errors (thrown duringinitialize())ToolExecutionError- Tool execution failures (wrapped and returned as MCP error)ToolNotFoundError- Tool not found in registry (returned as MCP error code -32001)
Error Handling Examples
import {
EasyMCP,
ConfigurationError,
ToolExecutionError,
ToolNotFoundError
} from 'easy-mcp-nest';
// Configuration errors - caught during initialization
try {
await EasyMCP.initialize(config);
} catch (error) {
if (error instanceof ConfigurationError) {
console.error('Configuration error:', error.message);
// Example: "Tool 'myTool': name must be a non-empty string"
}
}
// Tool execution errors - handled automatically by MCP protocol
// When a tool throws an error, it's automatically converted to an MCP error response
// The error is logged to stderr and returned to the client with error code -32002Common Error Scenarios
Configuration Errors:
- Missing required fields (tools array, tool name, description, etc.)
- Invalid tool schemas (unsupported types, missing properties)
- Invalid serverInfo format
Tool Execution Errors:
- Tool function throws an exception → Returns MCP error code -32002
- Tool not found → Returns MCP error code -32001
- Invalid tool arguments → Returns JSON-RPC error code -32602 (Invalid Params)
Protocol Errors:
- Unsupported protocol version → Returns error code -32602 with clear message
- Invalid JSON-RPC request → Returns error code -32600 (Invalid Request)
- Unknown method → Returns error code -32601 (Method Not Found)
Error Message Format
All error messages are designed to be helpful for debugging:
- Include the context (tool name, parameter name, etc.)
- Provide suggestions when possible
- Never expose internal implementation details to clients
Example error messages:
"Tool 'getUser': property 'userId' must have a description""Unsupported protocol version: 2024-10-01. Supported version: 2025-11-25""Missing required parameter: userId"
Troubleshooting
Tools Not Executing
If tools are registered but not being called:
- Check Tool Registration: Verify tools appear in console log during initialization
- Tool Schema: Ensure
inputSchemamatches JSON Schema format - Tool Description: Make tool descriptions clear so LLM agents know when to use them
- MCP Client: Verify your MCP client (Claude Desktop, etc.) is properly configured
- Enable Debug Logging: Set
DEBUG=1environment variable to see detailed protocol messages
Build/Import Issues
If you encounter TypeScript or import errors:
- Peer Dependencies: Ensure all peer dependencies are installed (see Installation section)
- Type Exports: Verify you're importing from the main package:
import { EasyMCP } from 'easy-mcp-nest' - Build Output: Check that
dist/index.jsanddist/index.d.tsexist after building - Module Resolution: Ensure your
tsconfig.jsonhas proper module resolution settings
Protocol Version Errors
If you see "Unsupported protocol version" errors:
- Check Client Version: Ensure your MCP client uses protocol version
2025-11-25 - Update Client: Update your MCP client to the latest version
- Debug Mode: Set
DEBUG=1to see detailed protocol version information
Integration Issues
If your server doesn't work with MCP clients:
- Check Configuration: Verify your MCP client configuration is correct
- Test Server: Run your server script directly to verify it starts correctly
- Review Logs: Check stderr output for initialization and error messages
- Integration Guide: See Integration Testing Guide for detailed setup instructions
- Examples: Check examples/claude-desktop-integration and examples/cursor-integration for client-specific setup
Debug Mode
Enable debug logging to troubleshoot issues:
DEBUG=1 node your-server.js
# or
DEBUG=true node your-server.jsDebug mode provides detailed information about:
- Protocol message flow
- Tool execution details
- Protocol version validation
- Argument validation
Note: The DEBUG environment variable accepts either '1' or 'true' (case-sensitive) to enable debug logging.
High-Value Framework Features
EasyMCP provides production-ready features out of the box:
1. Declarative Tool Registration with @McpTool
Define tools using decorators for automatic discovery and registration:
import { McpTool, McpParam, McpContext } from 'easy-mcp-nest';
import { z } from 'zod';
const CreateBuildingSchema = z.object({
name: z.string(),
address: z.string(),
});
@McpTool({
name: 'create_building',
description: 'Creates a new building',
requiredScopes: ['building:write'],
rateLimit: { max: 10, window: '1m' },
retry: { maxAttempts: 3, backoff: 'exponential' }
})
async createBuilding(
@McpParam(CreateBuildingSchema) params: z.infer<typeof CreateBuildingSchema>,
@McpContext() context: McpContext
) {
return this.service.createBuilding(context.userId, params);
}2. Automatic Scope Checking
Declarative scope validation - framework handles security automatically:
@McpTool({
name: 'delete_building',
requiredScopes: ['building:write', 'building:delete']
})3. Progress Notifications
Report progress for long-running operations:
@McpTool({ name: 'analyze_debts' })
async analyzeDebts(
args: any,
cancellationToken?: CancellationToken,
context?: McpContext,
progress?: ProgressCallback
) {
progress?.({ progress: 0.1, message: 'Fetching payment data...' });
// ... work ...
progress?.({ progress: 0.5, message: 'Processing 5/10 apartments...' });
// ... work ...
progress?.({ progress: 1.0, message: 'Complete!' });
}4. Built-in Observability
Automatic metrics collection and Prometheus-compatible endpoints:
- Tool execution count
- Average execution time
- Error rate by tool
- Request tracing
- Performance monitoring
Access metrics at /metrics endpoint (Prometheus format).
5. Rate Limiting
Per-tool rate limiting with configurable limits:
@McpTool({
name: 'create_building',
rateLimit: { max: 10, window: '1m' } // 10 requests per minute
})6. Retry Logic
Automatic retry with exponential backoff:
@McpTool({
name: 'create_building',
retry: {
maxAttempts: 3,
backoff: 'exponential',
initialDelay: 100,
maxDelay: 10000
}
})7. Circuit Breaker
Automatic circuit breaking when error rate exceeds threshold:
- Prevents overwhelming system during outages
- Automatic recovery with half-open state
- Per-tool circuit breakers
8. Error Handling Hooks
Centralized error handling with decorators:
@McpErrorHandler((error, context) => {
logErrorSecurely('MCP error', error, context);
return sanitizeError(error);
})
export class MyErrorHandler {}9. Middleware System
Pre/post execution hooks for cross-cutting concerns:
@McpMiddleware(async (req, context, next) => {
const start = Date.now();
const result = await next();
logPerformance(req.method, Date.now() - start);
return result;
})
export class PerformanceMiddleware {}10. Batch Tool Execution
Execute multiple tools in parallel:
// JSON-RPC request
{
"jsonrpc": "2.0",
"method": "tools/batch",
"params": {
"tools": [
{ "name": "create_building", "arguments": {...} },
{ "name": "add_apartment", "arguments": {...} }
]
}
}11. Health Check Endpoints
Production-ready health checks:
GET /health- Basic health checkGET /health/ready- Readiness probeGET /health/live- Liveness probeGET /metrics- Prometheus metrics
12. Testing Utilities
Easy testing with test helpers:
import { createMcpTestApp, mockMcpContext } from 'easy-mcp-nest';
const app = await createMcpTestApp([BuildingTools]);
const context = mockMcpContext({ userId: '123', scopes: ['read', 'write'] });
const result = await app.callTool('create_building', params, context);
await app.close();New Features
Express Adapter
Use EasyMCP in Express applications without requiring NestJS for the HTTP layer:
import express from 'express';
import { createMcpExpressRouter } from 'easy-mcp-nest';
const app = express();
app.use(express.json());
const mcpRouter = createMcpExpressRouter({
tools: [BuildingTools, PaymentTools],
resources: [BuildingResources],
auth: mcpAuthMiddleware, // Optional
});
app.use('/mcp', mcpRouter);
app.listen(3000);Context Injection
Inject user context into tools using the @McpContext() decorator:
import { McpContext, McpContextDecorator } from 'easy-mcp-nest';
@McpTool({ name: 'create_building' })
async createBuilding(
@McpParam() params: CreateBuildingDto,
@McpContextDecorator() context: McpContext
) {
return this.service.createBuilding(context.userId, params);
}Factory Pattern Support
Use factory functions instead of dependency injection:
import { McpService } from 'easy-mcp-nest';
@McpTool({ name: 'create_building' })
export class BuildingTools {
constructor(
@McpService(() => getBuildingService())
private buildingService: BuildingService
) {}
}OAuth Integration
Standardize OAuth integration across MCP servers:
import { createOAuthMiddleware, OAuthProviderConfig } from 'easy-mcp-nest';
const oauthConfig: OAuthProviderConfig = {
provider: 'custom',
validateToken: async (token) => {
// Validate token and return payload
return jwt.verify(token, secret);
},
extractContext: (payload) => ({
userId: payload.sub,
scopes: payload.scopes,
buildingIds: payload.buildingIds,
}),
};
const oauthMiddleware = createOAuthMiddleware(oauthProviderService);
app.use('/mcp', oauthMiddleware, mcpRouter);TypeScript-First Validation with Zod
Use Zod schemas for type-safe validation:
import { z } from 'zod';
import { McpParam } from 'easy-mcp-nest';
const CreateBuildingSchema = z.object({
name: z.string(),
address: z.string(),
floors: z.number().int().min(1).max(100),
});
@McpTool({ name: 'create_building' })
async createBuilding(
@McpParam(CreateBuildingSchema) params: z.infer<typeof CreateBuildingSchema>
) {
// params is fully typed and validated
return this.service.createBuilding(params);
}Standalone Mode
Use EasyMCP without NestJS:
import { createMcpServer } from 'easy-mcp-nest';
const server = createMcpServer({
tools: [BuildingTools, PaymentTools],
resources: [BuildingResources],
transport: 'http', // or 'stdio'
auth: validateMcpToken, // Optional
port: 3000,
});
await server.start();Testing Utilities
Test MCP tools easily:
import { createMcpTestApp, mockMcpContext } from 'easy-mcp-nest';
const app = await createMcpTestApp([BuildingTools]);
const context = mockMcpContext({
userId: '123',
scopes: ['read', 'write'],
});
const result = await app.callTool('create_building', params, context);
await app.close();Examples
See the examples/ directory for complete working examples.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE file for details.
Support
For issues and questions, please open an issue on GitHub.
