A production-ready foundation for developing custom Model Context Protocol (MCP) servers in TypeScript. Provides a complete layered architecture pattern, working example implementation, and comprehensive developer infrastructure to connect AI assistants with external APIs and data sources.
- Dual Transport Support: STDIO and Streamable HTTP transports with automatic fallback
- 5-Layer Architecture: Clean separation between CLI, tools, controllers, services, and utilities
- Type Safety: Full TypeScript implementation with Zod schema validation
- Complete IP Address Example: Tools, resources, and CLI commands for IP geolocation
- Comprehensive Testing: Unit and integration tests with coverage reporting
- Production Tooling: ESLint, Prettier, semantic-release, and MCP Inspector integration
- Error Handling: Structured error handling with contextual logging
Model Context Protocol (MCP) is an open standard for securely connecting AI systems to external tools and data sources. This boilerplate implements the MCP specification with a clean, layered architecture that can be extended to build custom MCP servers for any API or data source.
- Node.js (>=18.x): Download
- Git: For version control
# Clone the repository git clone https://github.com/aashari/boilerplate-mcp-server.git cd boilerplate-mcp-server # Install dependencies npm install # Build the project npm run build # Run in different modes: # 1. CLI Mode - Execute commands directly npm run cli -- get-ip-details 8.8.8.8 npm run cli -- get-ip-details # Get your current IP npm run cli -- get-ip-details 1.1.1.1 --include-extended-data # 2. STDIO Transport - For AI assistant integration (Claude Desktop, Cursor) npm run mcp:stdio # 3. HTTP Transport - For web-based integrations npm run mcp:http # 4. Development with MCP Inspector npm run mcp:inspect # Auto-opens browser with debugging UI
- JSON-RPC communication via stdin/stdout
- Used by Claude Desktop, Cursor AI, and other local AI assistants
- Run with:
TRANSPORT_MODE=stdio node dist/index.js
- HTTP-based transport with Server-Sent Events (SSE)
- Supports multiple concurrent connections and web integrations
- Runs on port 3000 by default (configurable via
PORT
env var) - MCP Endpoint:
http://localhost:3000/mcp
- Health Check:
http://localhost:3000/
→ Returns server version - Run with:
TRANSPORT_MODE=http node dist/index.js
Project Structure (Click to expand)
src/
├── cli/ # Command-line interfaces
│ ├── index.ts # CLI entry point with Commander setup
│ └── ipaddress.cli.ts # IP address CLI commands
├── controllers/ # Business logic orchestration
│ ├── ipaddress.controller.ts # IP lookup business logic
│ └── ipaddress.formatter.ts # Response formatting
├── services/ # External API interactions
│ ├── vendor.ip-api.com.service.ts # ip-api.com service
│ └── vendor.ip-api.com.types.ts # Service type definitions
├── tools/ # MCP tool definitions (AI interface)
│ ├── ipaddress.tool.ts # IP lookup tool for AI assistants
│ └── ipaddress.types.ts # Tool argument schemas
├── resources/ # MCP resource definitions
│ └── ipaddress.resource.ts # IP lookup resource (URI: ip://address)
├── types/ # Global type definitions
│ └── common.types.ts # Shared interfaces (ControllerResponse, etc.)
├── utils/ # Shared utilities
│ ├── logger.util.ts # Contextual logging system
│ ├── error.util.ts # MCP-specific error formatting
│ ├── error-handler.util.ts # Error handling utilities
│ ├── config.util.ts # Environment configuration
│ ├── constants.util.ts # Version and package constants
│ ├── formatter.util.ts # Markdown formatting
│ └── transport.util.ts # HTTP transport utilities
└── index.ts # Server entry point (dual transport)
The boilerplate follows a clean, layered architecture that promotes maintainability and clear separation of concerns:
- Purpose: Command-line interfaces for direct tool usage and testing
- Implementation: Commander-based argument parsing with contextual error handling
- Example:
get-ip-details [ipAddress] --include-extended-data --no-use-https
- Pattern: Register commands → Parse arguments → Call controllers → Handle errors
- Purpose: MCP tool definitions that AI assistants can invoke
- Implementation: Zod schema validation with structured responses
- Example:
ip_get_details
tool with optional IP address and configuration options - Pattern: Define schema → Validate args → Call controller → Format MCP response
- Purpose: MCP resources providing contextual data accessible via URIs
- Implementation: Resource handlers that respond to URI-based requests
- Example:
ip://8.8.8.8
resource providing IP geolocation data - Pattern: Register URI patterns → Parse requests → Return formatted content
- Purpose: Business logic orchestration with comprehensive error handling
- Implementation: Options validation, fallback logic, response formatting
- Example: IP lookup with HTTPS fallback, test environment detection, API token validation
- Pattern: Validate inputs → Apply defaults → Call services → Format responses
- Purpose: Direct external API interactions with minimal business logic
- Implementation: HTTP transport utilities with structured error handling
- Example: ip-api.com API calls with authentication and field selection
- Pattern: Build requests → Make API calls → Validate responses → Return raw data
- Purpose: Shared functionality across all layers
- Key Components:
logger.util.ts
: Contextual logging (file:method context)error.util.ts
: MCP-specific error formattingtransport.util.ts
: HTTP/API utilities with retry logicconfig.util.ts
: Environment configuration management
# Build and Clean npm run build # Build TypeScript to dist/ npm run clean # Remove dist/ and coverage/ npm run prepare # Build + ensure executable permissions (for npm publish) # CLI Testing npm run cli -- get-ip-details 8.8.8.8 # Test specific IP npm run cli -- get-ip-details --include-extended-data # Test with extended data npm run cli -- get-ip-details --no-use-https # Test with HTTP # MCP Server Modes npm run mcp:stdio # STDIO transport for AI assistants npm run mcp:http # HTTP transport on port 3000 npm run mcp:inspect # HTTP + auto-open MCP Inspector # Development with Debugging npm run dev:stdio # STDIO with MCP Inspector integration npm run dev:http # HTTP with debug logging enabled # Testing npm test # Run all tests (Jest) npm run test:coverage # Generate coverage report npm run test:cli # Run CLI-specific tests # Code Quality npm run lint # ESLint with TypeScript rules npm run format # Prettier formatting npm run update:deps # Update dependencies
TRANSPORT_MODE
: Transport mode (stdio
|http
, default:stdio
)PORT
: HTTP server port (default:3000
)DEBUG
: Enable debug logging (true
|false
, default:false
)
IPAPI_API_TOKEN
: API token for ip-api.com extended data (optional, free tier available)
# Basic configuration TRANSPORT_MODE=http PORT=3001 DEBUG=true # Extended data (requires ip-api.com account) IPAPI_API_TOKEN=your_token_here
-
MCP Inspector: Visual tool for testing your MCP tools
- Run server with
npm run mcp:inspect
- Open the URL shown in terminal
- Test your tools interactively
- Run server with
-
Debug Logging: Enable with
DEBUG=true
environment variable
Configuration (Click to expand)
Create ~/.mcp/configs.json
:
{ "boilerplate": { "environments": { "DEBUG": "true", "TRANSPORT_MODE": "http", "PORT": "3000" } } }
Step-by-Step Tool Implementation Guide (Click to expand)
Create a new service in src/services/
following the vendor-specific naming pattern:
// src/services/vendor.example-api.service.ts import { Logger } from '../utils/logger.util.js'; import { fetchApi } from '../utils/transport.util.js'; import { ExampleApiResponse, ExampleApiRequestOptions } from './vendor.example-api.types.js'; import { createApiError, McpError } from '../utils/error.util.js'; const serviceLogger = Logger.forContext('services/vendor.example-api.service.ts'); async function get( param?: string, options: ExampleApiRequestOptions = {} ): Promise<ExampleApiResponse> { const methodLogger = serviceLogger.forMethod('get'); methodLogger.debug(`Calling Example API with param: ${param}`); try { const url = `https://api.example.com/${param || 'default'}`; const rawData = await fetchApi<ExampleApiResponse>(url, { headers: options.apiKey ? { 'Authorization': `Bearer ${options.apiKey}` } : {} }); methodLogger.debug('Received successful response from Example API'); return rawData; } catch (error) { methodLogger.error('Service error fetching data', error); if (error instanceof McpError) { throw error; } throw createApiError( 'Unexpected service error while fetching data', undefined, error ); } } export default { get };
Add a controller in src/controllers/
to handle business logic with error context:
// src/controllers/example.controller.ts import { Logger } from '../utils/logger.util.js'; import exampleService from '../services/vendor.example-api.service.js'; import { formatExample } from './example.formatter.js'; import { handleControllerError, buildErrorContext } from '../utils/error-handler.util.js'; import { ControllerResponse } from '../types/common.types.js'; import { config } from '../utils/config.util.js'; const logger = Logger.forContext('controllers/example.controller.ts'); export interface GetDataOptions { param?: string; includeMetadata?: boolean; } async function getData( options: GetDataOptions = {} ): Promise<ControllerResponse> { const methodLogger = logger.forMethod('getData'); methodLogger.debug(`Getting data for param: ${options.param || 'default'}`, options); try { // Apply business logic and defaults const apiKey = config.get('EXAMPLE_API_TOKEN'); // Call service layer const data = await exampleService.get(options.param, { apiKey, includeMetadata: options.includeMetadata ?? false }); // Format response const formattedContent = formatExample(data); return { content: formattedContent }; } catch (error) { throw handleControllerError( error, buildErrorContext( 'ExampleData', 'getData', 'controllers/example.controller.ts@getData', options.param || 'default', { options } ) ); } } export default { getData };
Create a tool definition in src/tools/
following the registration pattern:
// src/tools/example.tool.ts import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { Logger } from '../utils/logger.util.js'; import { formatErrorForMcpTool } from '../utils/error.util.js'; import exampleController from '../controllers/example.controller.js'; const logger = Logger.forContext('tools/example.tool.ts'); // Define Zod schema for tool arguments const GetDataSchema = z.object({ param: z.string().optional().describe('Optional parameter for the API call'), includeMetadata: z.boolean().optional().default(false) .describe('Whether to include additional metadata in the response') }); async function handleGetData(args: Record<string, unknown>) { const methodLogger = logger.forMethod('handleGetData'); try { methodLogger.debug('Tool example_get_data called', args); // Validate arguments with Zod const validatedArgs = GetDataSchema.parse(args); // Call controller const result = await exampleController.getData({ param: validatedArgs.param, includeMetadata: validatedArgs.includeMetadata }); // Return MCP-formatted response return { content: [ { type: 'text' as const, text: result.content } ] }; } catch (error) { methodLogger.error('Tool example_get_data failed', error); return formatErrorForMcpTool(error); } } // Registration function following the pattern used by existing tools function registerTools(server: McpServer) { const registerLogger = logger.forMethod('registerTools'); registerLogger.debug('Registering example tools...'); server.tool( 'example_get_data', `Gets data from the Example API with optional parameter. Use this tool to fetch example data. Returns formatted data as Markdown.`, GetDataSchema.shape, handleGetData ); registerLogger.debug('Example tools registered successfully'); } export default { registerTools };
Create a CLI command in src/cli/
following the Commander pattern:
// src/cli/example.cli.ts import { Command } from 'commander'; import { Logger } from '../utils/logger.util.js'; import exampleController from '../controllers/example.controller.js'; import { handleCliError } from '../utils/error.util.js'; const logger = Logger.forContext('cli/example.cli.ts'); function register(program: Command) { const methodLogger = logger.forMethod('register'); methodLogger.debug('Registering example CLI commands...'); program .command('get-data') .description('Gets data from the Example API') .argument('[param]', 'Optional parameter for the API call') .option('-m, --include-metadata', 'Include additional metadata in response') .action(async (param, options) => { const actionLogger = logger.forMethod('action:get-data'); try { actionLogger.debug('CLI get-data called', { param, options }); const result = await exampleController.getData({ param, includeMetadata: options.includeMetadata || false }); console.log(result.content); } catch (error) { handleCliError(error); } }); methodLogger.debug('Example CLI commands registered successfully'); } export default { register };
Update the entry points to register your new components:
// 1. Register CLI in src/cli/index.ts import exampleCli from './example.cli.js'; export async function runCli(args: string[]) { // ... existing setup code ... // Register CLI commands exampleCli.register(program); // Add this line // ... rest of function } // 2. Register Tools in src/index.ts import exampleTools from './tools/example.tool.js'; // In the startServer function, after existing registrations: exampleTools.registerTools(serverInstance);
The boilerplate includes a complete IP address geolocation example demonstrating all layers:
CLI Commands:
npm run cli -- get-ip-details # Get current public IP npm run cli -- get-ip-details 8.8.8.8 # Get details for specific IP npm run cli -- get-ip-details 1.1.1.1 --include-extended-data # With extended data npm run cli -- get-ip-details 8.8.8.8 --no-use-https # Force HTTP (for free tier)
MCP Tools:
ip_get_details
- IP geolocation lookup for AI assistants
MCP Resources:
ip://
- Current IP detailsip://8.8.8.8
- Specific IP details
- Fallback Logic: HTTPS → HTTP fallback for free tier users
- Environment Detection: Different behavior in test vs production
- API Token Support: Optional token for extended data (ASN, mobile detection, etc.)
- Error Handling: Structured errors for private/reserved IP addresses
- Response Formatting: Clean Markdown output with geolocation data
# Optional - for extended data features IPAPI_API_TOKEN=your_token_from_ip-api.com # Development DEBUG=true # Enable detailed logging TRANSPORT_MODE=http # Use HTTP transport PORT=3001 # Custom port
-
Customize Package Details:
{ "name": "your-mcp-server-name", "version": "1.0.0", "description": "Your custom MCP server", "author": "Your Name", "keywords": ["mcp", "your-domain", "ai-integration"] }
-
Update Documentation: Replace IP address examples with your use case
-
Test Thoroughly:
npm run build && npm test npm run cli -- your-command npm run mcp:stdio # Test with MCP Inspector
-
Publish:
npm publish
(requires npm login)
The boilerplate includes comprehensive testing infrastructure:
tests/ # Not present - tests are in src/
src/
├── **/*.test.ts # Co-located with source files
├── utils/ # Utility function tests
├── controllers/ # Business logic tests
├── services/ # API integration tests
└── cli/ # CLI command tests
- Unit Tests: Test utilities and pure functions (
*.util.test.ts
) - Controller Tests: Test business logic with mocked service calls
- Service Tests: Test API integration with real/mocked HTTP calls
- CLI Tests: Test command parsing and execution
- Test Environment Detection: Automatic test mode handling in controllers
npm test # Run all tests npm run test:coverage # Generate coverage report npm run test:cli # CLI-specific tests only
- Target: >80% test coverage
- Focus on business logic (controllers) and utilities
- Mock external services appropriately
- MCP Specification
- MCP SDK Documentation
- MCP Inspector - Visual debugging tool
- Anthropic MCP Announcement
- Awesome MCP Servers - Community examples
- TypeScript Documentation
- All @aashari MCP Servers - NPM packages
- GitHub Repositories - Source code