MCP Server Boilerplate

MCP

Quick 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 ListToolsRequestSchema handler 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


Last updated: May 2026

View on GitHub

Related Templates