Skip to main content
โšก Calmops

MCP Server Development: Building Custom Model Context Protocol Servers

Introduction

The Model Context Protocol (MCP) has emerged as the standard for connecting AI assistants to external tools and data sources. While pre-built MCP servers exist, building custom servers allows you to integrate any tool, API, or data source with AI models.

This comprehensive guide teaches you how to build MCP servers from scratch, covering architecture, implementation patterns, and deployment strategies.


Understanding MCP Architecture

Protocol Overview

MCP uses a client-server architecture:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    MCP Host                          โ”‚
โ”‚  (Cursor, Claude, ChatGPT)                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                      โ”‚
                      โ”‚ JSON-RPC 2.0
                      โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   MCP Server                        โ”‚
โ”‚  - Resources                                        โ”‚
โ”‚  - Tools                                           โ”‚
โ”‚  - Prompts                                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Core Concepts

Component Purpose
Resources Data sources AI can read
Tools Actions AI can execute
Prompts Pre-defined prompt templates

Setting Up Your Environment

Installation

# Create project
mkdir my-mcp-server
cd my-mcp-server

# Initialize
npm init -y

# Install MCP SDK
npm install @modelcontextprotocol/sdk

# TypeScript support
npm install -D typescript @types/node

Project Structure

my-mcp-server/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ index.ts          # Main entry
โ”‚   โ”œโ”€โ”€ resources.ts      # Resource handlers
โ”‚   โ”œโ”€โ”€ tools.ts         # Tool handlers
โ”‚   โ””โ”€โ”€ prompts.ts       # Prompt templates
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ tsconfig.json

Building Your First MCP Server

Basic Server Implementation

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

class MyServer {
  constructor() {
    this.server = new Server(
      {
        name: 'my-mcp-server',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
          resources: {},
        },
      }
    );

    this.setupHandlers();
  }

  private setupHandlers() {
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [
          {
            name: 'get_weather',
            description: 'Get weather for a location',
            inputSchema: {
              type: 'object',
              properties: {
                location: {
                  type: 'string',
                  description: 'City name'
                }
              },
              required: ['location']
            }
          }
        ]
      };
    });

    // Handle tool calls
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;
      
      switch (name) {
        case 'get_weather':
          return await this.getWeather(args.location);
        default:
          throw new Error(`Unknown tool: ${name}`);
      }
    });
  }

  private async getWeather(location: string) {
    // Implement weather API call
    const response = await fetch(
      `https://api.weather.com/v3/wx/conditions/current?location=${location}`
    );
    const data = await response.json();
    
    return {
      content: [
        {
          type: 'text',
          text: `Weather in ${location}: ${data.temperature}ยฐF, ${data.description}`
        }
      ]
    };
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('MCP Server running on stdio');
  }
}

const server = new MyServer();
server.start();

Creating Tools

Tool Patterns

1. API Integration

// tools/api.ts
export const apiTools = [
  {
    name: 'search_github',
    description: 'Search GitHub repositories',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Search query' },
        language: { type: 'string', description: 'Programming language' }
      }
    }
  },
  {
    name: 'get_github_user',
    description: 'Get GitHub user info',
    inputSchema: {
      type: 'object',
      properties: {
        username: { type: 'string' }
      },
      required: ['username']
    }
  }
];

export async function handleGitHubTool(
  name: string,
  args: Record<string, any>
) {
  const headers = {
    'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
    'Accept': 'application/vnd.github.v3+json'
  };

  switch (name) {
    case 'search_github': {
      const response = await fetch(
        `https://api.github.com/search/repositories?q=${encodeURIComponent(args.query)}`,
        { headers }
      );
      const data = await response.json();
      return formatResults(data.items);
    }
    case 'get_github_user': {
      const response = await fetch(
        `https://api.github.com/users/${args.username}`,
        { headers }
      );
      const user = await response.json();
      return formatUser(user);
    }
  }
}

function formatResults(items: any[]) {
  return {
    content: [{
      type: 'text',
      text: items.slice(0, 10).map(item => 
        `- ${item.full_name}: ${item.description || 'No description'}`
      ).join('\n')
    }]
  };
}

2. Database Operations

// tools/database.ts
export const dbTools = [
  {
    name: 'query_users',
    description: 'Query users from database',
    inputSchema: {
      type: 'object',
      properties: {
        limit: { type: 'number', default: 10 },
        offset: { type: 'number', default: 0 }
      }
    }
  },
  {
    name: 'create_user',
    description: 'Create a new user',
    inputSchema: {
      type: 'object',
      properties: {
        name: { type: 'string' },
        email: { type: 'string' }
      },
      required: ['name', 'email']
    }
  }
];

export async function handleDbTool(name: string, args: any) {
  const db = getDbConnection();
  
  switch (name) {
    case 'query_users': {
      const users = await db.user.findMany({
        take: args.limit,
        skip: args.offset
      });
      return { content: [{ type: 'text', text: JSON.stringify(users) }] };
    }
    case 'create_user': {
      const user = await db.user.create({
        data: { name: args.name, email: args.email }
      });
      return { content: [{ type: 'text', text: JSON.stringify(user) }] };
    }
  }
}

Resources

File System Resources

// resources/filesystem.ts
export const fileResources = [
  {
    uri: 'file://config',
    name: 'Configuration',
    description: 'App configuration file',
    mimeType: 'application/json'
  },
  {
    uri: 'file://logs',
    name: 'Application Logs',
    description: 'Recent application logs',
    mimeType: 'text/plain'
  }
];

export async function handleResourceRequest(uri: string) {
  const path = uri.replace('file://', '');
  
  switch (uri) {
    case 'file://config':
      const config = await fs.readFile('./config.json', 'utf-8');
      return {
        contents: [{
          uri,
          mimeType: 'application/json',
          text: config
        }]
      };
      
    case 'file://logs':
      const logs = await fs.readFile('./logs/app.log', 'utf-8');
      const recent = logs.split('\n').slice(-100).join('\n');
      return {
        contents: [{
          uri,
          mimeType: 'text/plain',
          text: recent
        }]
      };
  }
}

Prompts

Template Prompts

// prompts.ts
export const prompts = [
  {
    name: 'analyze_code',
    description: 'Analyze code for issues',
    arguments: [
      {
        name: 'code',
        description: 'Code to analyze',
        required: true
      },
      {
        name: 'language',
        description: 'Programming language',
        required: false
      }
    ]
  },
  {
    name: 'review_pr',
    description: 'Review a pull request',
    arguments: [
      {
        name: 'pr_url',
        description: 'URL of the PR',
        required: true
      }
    ]
  }
];

export function getPrompt(name: string, args: Record<string, string>) {
  switch (name) {
    case 'analyze_code':
      return `Analyze the following ${args.language || 'code'} for issues, bugs, and improvements:\n\n${args.code}`;
      
    case 'review_pr':
      return `Review the pull request at ${args.pr_url} for:\n- Code quality\n- Potential bugs\n- Security issues\n- Performance concerns`;
  }
}

Testing MCP Servers

Using the MCP Inspector

# Install inspector
npm install -g @modelcontextprotocol/inspector

# Run inspector
mcp-inspector node ./dist/index.js

Unit Testing

import { describe, it, expect, vi } from 'vitest';

describe('MCP Server Tools', () => {
  it('should return weather data', async () => {
    // Mock fetch
    global.fetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({
        temperature: 72,
        description: 'Sunny'
      })
    });

    const result = await handleGitHubTool('get_weather', { 
      location: 'New York' 
    });
    
    expect(result.content[0].text).toContain('72');
  });
});

Best Practices

Security

// Always validate inputs
function validateToolInput(args: any, schema: any) {
  if (schema.required) {
    for (const field of schema.required) {
      if (!args[field]) {
        throw new Error(`Missing required field: ${field}`);
      }
    }
  }
  
  // Validate types
  for (const [key, value] of Object.entries(args)) {
    const expectedType = schema.properties[key]?.type;
    if (expectedType && typeof value !== expectedType) {
      throw new Error(`Invalid type for ${key}: expected ${expectedType}`);
    }
  }
}

// Use environment variables for secrets
const apiKey = process.env.API_KEY;
if (!apiKey) {
  throw new Error('API_KEY environment variable required');
}

Error Handling

async function safeToolHandler(name: string, args: any) {
  try {
    return await handleTool(name, args);
  } catch (error) {
    return {
      content: [{
        type: 'text',
        text: `Error: ${error.message}`
      }],
      isError: true
    };
  }
}

External Resources

Documentation

Examples


Conclusion

Building MCP servers enables powerful integrations between AI models and your tools, data, and services. By following this guide, you can create custom servers that expose any capability to AI assistants.

Key takeaways:

  1. Start with the SDK - Simplifies protocol handling
  2. Define clear tools - Well-documented tools work best
  3. Handle errors gracefully - Return meaningful error messages
  4. Test thoroughly - Use inspector and unit tests
  5. Secure your server - Validate inputs, protect secrets

Comments