MCP Server Boilerplate
MCPQuick start template for building custom MCP servers.
MCP Server Boilerplate
Overview
Quick start template for building custom MCP (Model Context Protocol) servers with TypeScript. This boilerplate provides a production-ready foundation for creating MCP servers that can connect AI models to your data, tools, and systems.
What is MCP?
MCP (Model Context Protocol) is an open protocol that standardizes how AI models connect to data sources, tools, and systems. It provides:
- Standardized interface: Consistent way for AI models to interact with external resources
- Type-safe communication: Strong typing for tools, resources, and prompts
- Transport flexibility: Support for stdio, SSE, and other transport mechanisms
- Resource management: Built-in support for reading and watching resources
- Tool invocation: Standardized tool calling with argument validation
Template Structure
mcp-boilerplate/
├── src/
│ ├── index.ts # Server entry point and configuration
│ ├── tools.ts # Tool definitions and handlers
│ ├── resources.ts # Resource definitions and handlers
│ ├── prompts.ts # Prompt templates and handlers
│ ├── types.ts # TypeScript type definitions
│ └── utils.ts # Utility functions
├── package.json
├── tsconfig.json
├── .env.example
└── README.md
Installation
# Create from official template
npx @modelcontextprotocol/create-server my-server
cd my-server
# Or clone and customize
git clone https://github.com/modelcontextprotocol/servers.git
cd servers/template
# Install dependencies
npm install
# Install dev dependencies
npm install -D typescript @types/node
Development Setup
1. Initialize Project
# Create package.json
npm init -y
# Install MCP SDK
npm install @modelcontextprotocol/sdk zod
# Install dev dependencies
npm install -D typescript @types/node tsx
2. Configure TypeScript
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
3. Package.json Scripts
{
"name": "mcp-server-my-server",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
}
}
Core Components
Server Entry Point (src/index.ts)
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { tools, handleToolCall } from './tools.js';
import { resources, handleResourceRead } from './resources.js';
// Create server instance
const server = new Server(
{
name: 'my-custom-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
// Call a tool
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = tools.find(t => t.name === request.params.name);
if (!tool) {
throw new Error(`Tool not found: ${request.params.name}`);
}
return await handleToolCall(tool, request.params.arguments);
});
// List available resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: resources.map(resource => ({
uri: resource.uri,
name: resource.name,
description: resource.description,
mimeType: resource.mimeType,
})),
};
});
// Read a resource
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const resource = resources.find(r => r.uri === request.params.uri);
if (!resource) {
throw new Error(`Resource not found: ${request.params.uri}`);
}
return await handleResourceRead(resource, request.params.uri);
});
// Start server with stdio transport
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP server running on stdio');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
Tool Definitions (src/tools.ts)
import { z } from 'zod';
import { Tool } from '@modelcontextprotocol/sdk/types.js';
// Define tool schemas with Zod for validation
export const SearchToolSchema = z.object({
query: z.string().describe('Search query'),
limit: z.number().int().min(1).max(50).default(10).describe('Maximum results'),
filters: z.record(z.string()).optional().describe('Search filters'),
});
export const ExecuteToolSchema = z.object({
command: z.string().describe('Command to execute'),
args: z.array(z.string()).optional().describe('Command arguments'),
timeout: z.number().int().min(1000).max(60000).default(30000).describe('Timeout in ms'),
});
export const tools: Array<{
name: string;
description: string;
inputSchema: object;
handler: (args: Record<string, unknown>) => Promise<{ content: Array<{ type: string; text: string }> }>;
}> = [
{
name: 'search',
description: 'Search for information across connected data sources',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
limit: { type: 'number', description: 'Maximum results', default: 10 },
filters: { type: 'object', description: 'Search filters' },
},
required: ['query'],
},
handler: async (args) => {
const { query, limit = 10, filters } = SearchToolSchema.parse(args);
// Implement search logic
const results = await performSearch(query, limit, filters);
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
},
},
{
name: 'execute',
description: 'Execute a command or script',
inputSchema: {
type: 'object',
properties: {
command: { type: 'string', description: 'Command to execute' },
args: { type: 'array', items: { type: 'string' }, description: 'Command arguments' },
timeout: { type: 'number', description: 'Timeout in ms', default: 30000 },
},
required: ['command'],
},
handler: async (args) => {
const { command, args: commandArgs = [], timeout = 30000 } = ExecuteToolSchema.parse(args);
// Implement command execution
const output = await executeCommand(command, commandArgs, timeout);
return {
content: [
{
type: 'text',
text: output,
},
],
};
},
},
];
// Helper function to handle tool calls
export async function handleToolCall(
tool: typeof tools[0],
args: Record<string, unknown>
): Promise<{ content: Array<{ type: string; text: string }> }> {
try {
return await tool.handler(args);
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error executing tool: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
}
Resource Definitions (src/resources.ts)
import { Resource } from '@modelcontextprotocol/sdk/types.js';
export interface CustomResource {
uri: string;
name: string;
description: string;
mimeType: string;
readHandler: (uri: string) => Promise<string>;
}
export const resources: CustomResource[] = [
{
uri: 'file://config/settings.json',
name: 'Configuration Settings',
description: 'Current application configuration',
mimeType: 'application/json',
readHandler: async (uri) => {
// Read and return configuration
const config = await readConfig();
return JSON.stringify(config, null, 2);
},
},
{
uri: 'file://logs/recent.log',
name: 'Recent Logs',
description: 'Last 100 log entries',
mimeType: 'text/plain',
readHandler: async (uri) => {
const logs = await readRecentLogs(100);
return logs.join('\n');
},
},
];
export async function handleResourceRead(
resource: CustomResource,
uri: string
): Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }> {
const text = await resource.readHandler(uri);
return {
contents: [
{
uri,
mimeType: resource.mimeType,
text,
},
],
};
}
Prompt Templates (src/prompts.ts)
import { GetPromptResultSchema, ListPromptsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
export const prompts = [
{
name: 'analyze-data',
description: 'Analyze a dataset and provide insights',
arguments: [
{
name: 'dataset',
description: 'Path or identifier for the dataset',
required: true,
},
{
name: 'analysis-type',
description: 'Type of analysis (summary, trends, outliers)',
required: false,
},
],
template: `
You are a data analysis assistant. Analyze the following dataset:
Dataset: {dataset}
Analysis Type: {analysis-type}
Please provide:
1. Executive summary
2. Key findings
3. Recommendations
`,
},
{
name: 'code-review',
description: 'Review code for quality and best practices',
arguments: [
{
name: 'code',
description: 'Code to review',
required: true,
},
{
name: 'language',
description: 'Programming language',
required: false,
},
],
template: `
Please review the following code for:
- Code quality and readability
- Security vulnerabilities
- Performance issues
- Best practices adherence
Code:
{code}
Language: {language}
Provide a structured review with specific suggestions.
`,
},
];
// Register prompt handlers
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: prompts.map(p => ({
name: p.name,
description: p.description,
arguments: p.arguments,
})),
};
});
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const prompt = prompts.find(p => p.name === request.params.name);
if (!prompt) {
throw new Error(`Prompt not found: ${request.params.name}`);
}
const rendered = prompt.template;
for (const [key, value] of Object.entries(request.params.arguments || {})) {
rendered.replace(`{${key}}`, String(value));
}
return {
description: prompt.description,
messages: [
{
role: 'user',
content: {
type: 'text',
text: rendered,
},
},
],
};
});
Transport Options
Stdio Transport (Default)
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const transport = new StdioServerTransport();
await server.connect(transport);
SSE Transport (HTTP Server-Sent Events)
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
const app = express();
app.post('/mcp', async (req, res) => {
const transport = new SSEServerTransport('/messages', res);
await server.connect(transport);
});
app.get('/messages', (req, res) => {
// Handle SSE connections
});
app.listen(3000);
WebSocket Transport
import { WebSocketServerTransport } from '@modelcontextprotocol/sdk/server/websocket.js';
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', async (ws) => {
const transport = new WebSocketServerTransport(ws);
await server.connect(transport);
});
Configuration
Environment Variables (.env)
# Server configuration
SERVER_NAME=my-custom-server
SERVER_VERSION=1.0.0
# API keys
BRAVE_API_KEY=your_brave_api_key
OPENAI_API_KEY=your_openai_api_key
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
# Logging
LOG_LEVEL=info
Claude Desktop Configuration
Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/my-server/dist/index.js"],
"env": {
"BRAVE_API_KEY": "your_api_key"
}
},
"my-server-sse": {
"command": "node",
"args": ["/path/to/my-server/dist/sse-server.js"],
"env": {
"SERVER_PORT": "3000"
}
}
}
}
Cursor Integration
Add to Cursor settings:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/my-server/dist/index.js"]
}
}
}
Windsurf Integration
Add to .windsurf/mcp.json:
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["/path/to/my-server/dist/index.js"]
}
}
}
Security Best Practices
Input Validation
import { z } from 'zod';
// Always validate inputs
const SafeInputSchema = z.object({
query: z.string().max(500).regex(/^[a-zA-Z0-9\s\-_]+$/),
limit: z.number().int().min(1).max(100),
});
// Reject invalid inputs
const parsed = SafeInputSchema.safeParse(args);
if (!parsed.success) {
throw new Error('Invalid input: ' + parsed.error.message);
}
Rate Limiting
import { RateLimiter } from 'limiter';
const rateLimiter = new RateLimiter({
tokensPerInterval: 10,
interval: 'minute',
});
async function rateLimitedHandler(args) {
const remaining = rateLimiter.getTokensRemaining();
if (remaining < 1) {
throw new Error('Rate limit exceeded. Try again later.');
}
rateLimiter.removeTokens(1);
return await actualHandler(args);
}
Authentication
// For SSE/WebSocket transports, add authentication
app.post('/mcp', async (req, res) => {
const authHeader = req.headers.authorization;
if (authHeader !== `Bearer ${process.env.MCP_SECRET}`) {
return res.status(401).send('Unauthorized');
}
// Continue with MCP handling
});
Resource Access Control
const allowedPaths = ['/data/public', '/config/read-only'];
async function handleResourceRead(uri: string) {
const path = uri.replace('file://', '');
// Validate path is within allowed directories
const normalized = path.normalize();
const isAllowed = allowedPaths.some(allowed =>
normalized.startsWith(allowed)
);
if (!isAllowed) {
throw new Error('Access denied to this resource');
}
return await readFile(path);
}
Testing
Unit Tests
// tests/tools.test.ts
import { describe, it, expect, vi } from 'vitest';
import { tools } from '../src/tools.js';
describe('search tool', () => {
it('should return results for valid query', async () => {
const searchTool = tools.find(t => t.name === 'search');
const result = await searchTool!.handler({ query: 'test', limit: 5 });
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
});
it('should reject invalid query', async () => {
const searchTool = tools.find(t => t.name === 'search');
// Test validation logic
});
});
Integration Tests
// tests/integration.test.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
describe('MCP Server Integration', () => {
it('should list tools correctly', async () => {
const server = createTestServer();
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
const result = await clientTransport.send({
method: 'tools/list',
});
expect(result.tools).toBeDefined();
});
});
Deployment
Docker Deployment
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
CMD ["node", "dist/index.js"]
Docker Compose
# docker-compose.yml
services:
mcp-server:
build: .
environment:
- BRAVE_API_KEY=${BRAVE_API_KEY}
- DATABASE_URL=${DATABASE_URL}
volumes:
- ./data:/app/data:ro
restart: unless-stopped
Kubernetes
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
spec:
replicas: 3
selector:
matchLabels:
app: mcp-server
template:
metadata:
labels:
app: mcp-server
spec:
containers:
- name: mcp-server
image: my-registry/mcp-server:latest
env:
- name: BRAVE_API_KEY
valueFrom:
secretKeyRef:
name: mcp-secrets
key: brave-api-key
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Troubleshooting
Common Issues
Server not starting:
- Check Node.js version (requires v18+)
- Verify all dependencies are installed
- Check for port conflicts (SSE mode)
Tools not appearing:
- Verify
ListToolsRequestSchemahandler is registered - Check tool names don't contain special characters
- Ensure inputSchema is valid JSON Schema
Resources not loading:
- Verify file paths are absolute
- Check file permissions
- Ensure URI format is correct (
file://path)
Connection errors:
- For stdio: ensure no extra output to stdout
- For SSE: check CORS headers
- For WebSocket: verify WebSocket server is running
Debug Mode
// Enable verbose logging
const server = new Server(
{ name: 'my-server', version: '1.0.0' },
{
capabilities: { tools: {}, resources: {} },
logger: {
log: (level, message, data) => {
console.log(`[${level}] ${message}`, data);
},
},
}
);
Resources
- MCP Specification: https://modelcontextprotocol.io/specification
- MCP SDK (TypeScript): https://github.com/modelcontextprotocol/typescript-sdk
- MCP SDK (Python): https://github.com/modelcontextprotocol/python-sdk
- Official Servers: https://github.com/modelcontextprotocol/servers
- MCP Documentation: https://modelcontextprotocol.io
Last updated: May 2026
