Skip to content

MCP Servers

Extend Claude Code with external capabilities using the Model Context Protocol.

What You'll Learn

  • What MCP is and why it matters
  • Installing MCP servers
  • Using MCP tools
  • Building custom MCP servers

What is MCP?

The Model Context Protocol (MCP) is an open standard that lets AI assistants connect to external services. With MCP, Claude can:

  • Access databases directly
  • Query APIs
  • Control applications
  • Use specialized tools

Think of MCP servers as plugins that give Claude new abilities.

How MCP Works

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ Claude Code │────▶│ MCP Server  │────▶│  External   │
│             │◀────│             │◀────│  Service    │
└─────────────┘     └─────────────┘     └─────────────┘
  1. Claude decides it needs an external capability
  2. It calls the MCP server's tool
  3. The server interacts with the external service
  4. Results flow back to Claude

Installing MCP Servers

Configuration Location

MCP servers are configured in: - User: ~/.claude/settings.json - Project: .claude/settings.json

Basic Configuration

{
  "mcpServers": {
    "server-name": {
      "command": "npx",
      "args": ["-y", "@example/mcp-server"]
    }
  }
}

With Environment Variables

{
  "mcpServers": {
    "database": {
      "command": "npx",
      "args": ["-y", "@example/mcp-postgres"],
      "env": {
        "DATABASE_URL": "postgresql://localhost/mydb"
      }
    }
  }
}

File System

Read and write files outside the working directory:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-fs", "/path/to/allow"]
    }
  }
}

PostgreSQL

Query and modify PostgreSQL databases:

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-postgres"],
      "env": {
        "POSTGRES_CONNECTION_STRING": "postgresql://user:pass@host/db"
      }
    }
  }
}

Web Browser

Fetch and interact with web pages:

{
  "mcpServers": {
    "browser": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-browser"]
    }
  }
}

GitHub

Access GitHub repos, issues, and PRs:

{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-github"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxxxx"
      }
    }
  }
}

Using MCP Tools

Once configured, MCP tools are available to Claude automatically. Tools appear with the mcp__ prefix:

# Claude can now use tools like:
mcp__postgres__query
mcp__github__list_issues
mcp__browser__fetch

Example Interactions

Database Query:

> Show me the top 10 customers by revenue

Claude uses mcp__postgres__query to run SQL and return results.

GitHub Issues:

> What are the open bugs in our repo?

Claude uses mcp__github__list_issues to fetch and summarize.

Web Research:

> What does the React documentation say about useEffect?

Claude uses browser tools to fetch and read the page.

Building Custom MCP Servers

You can create MCP servers for any service. Here's a simple example:

Basic Structure

// my-mcp-server/index.js
import { McpServer } from "@anthropic/mcp-sdk";

const server = new McpServer({
  name: "my-server",
  version: "1.0.0",
});

// Define a tool
server.tool({
  name: "get_weather",
  description: "Get current weather for a city",
  parameters: {
    type: "object",
    properties: {
      city: { type: "string", description: "City name" }
    },
    required: ["city"]
  },
  handler: async ({ city }) => {
    // Your implementation
    const weather = await fetchWeather(city);
    return { temperature: weather.temp, condition: weather.condition };
  }
});

server.run();

Package Configuration

{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "bin": "./index.js",
  "dependencies": {
    "@anthropic/mcp-sdk": "^0.1.0"
  }
}

Register Your Server

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/path/to/my-mcp-server/index.js"]
    }
  }
}

MCP Server Patterns

Read-Only Services

For safety, start with read-only tools:

server.tool({
  name: "get_user",
  description: "Get user info by ID (read-only)",
  handler: async ({ userId }) => {
    return await db.users.findById(userId);
  }
});

With Confirmation

For write operations, require confirmation:

server.tool({
  name: "delete_user",
  description: "Delete a user (requires confirmation)",
  parameters: {
    userId: { type: "string" },
    confirmed: { type: "boolean" }
  },
  handler: async ({ userId, confirmed }) => {
    if (!confirmed) {
      return { error: "Please confirm deletion by setting confirmed: true" };
    }
    return await db.users.delete(userId);
  }
});

With Rate Limiting

Protect against excessive calls:

const rateLimit = {};

server.tool({
  name: "send_email",
  handler: async ({ to, subject, body }) => {
    const now = Date.now();
    if (rateLimit[to] && now - rateLimit[to] < 60000) {
      return { error: "Rate limited: wait 1 minute between emails" };
    }
    rateLimit[to] = now;
    return await sendEmail(to, subject, body);
  }
});

Debugging MCP Servers

Enable Logging

Set environment variable:

export MCP_DEBUG=1
claude

Test Standalone

Run your server directly to test:

echo '{"tool": "get_weather", "args": {"city": "London"}}' | node my-server.js

Check Server Status

In Claude Code:

> /config

# Look for MCP server status

Security Considerations

Principle of Least Privilege

Only expose necessary capabilities:

// Don't do this
server.tool({
  name: "run_sql",
  handler: async ({ query }) => db.raw(query)  // Dangerous!
});

// Do this instead
server.tool({
  name: "get_user_count",
  handler: async () => db.users.count()  // Specific, safe
});

Validate Inputs

Always validate tool inputs:

server.tool({
  name: "get_file",
  handler: async ({ path }) => {
    // Validate path is within allowed directory
    const resolved = resolve(path);
    if (!resolved.startsWith(ALLOWED_DIR)) {
      throw new Error("Access denied");
    }
    return await readFile(resolved);
  }
});

Environment Variables

Keep secrets in environment variables, not code:

{
  "mcpServers": {
    "my-api": {
      "command": "node",
      "args": ["server.js"],
      "env": {
        "API_KEY": "${MY_API_KEY}"
      }
    }
  }
}

Try It Yourself

Exercise: Add a GitHub MCP Server

  1. Get a GitHub token from https://github.com/settings/tokens
  2. Add to your settings:
    {
      "mcpServers": {
        "github": {
          "command": "npx",
          "args": ["-y", "@anthropic/mcp-server-github"],
          "env": {
            "GITHUB_TOKEN": "your-token-here"
          }
        }
      }
    }
    
  3. Restart Claude Code
  4. Try:
    > What are my most recent GitHub notifications?
    
    > List open issues in my favorite repo
    

Exercise: Build a Simple MCP Server

Create an MCP server that provides the current time in different timezones:

  1. Create the server structure
  2. Add a get_time tool that accepts a timezone
  3. Register it in your settings
  4. Test it with Claude

Expert MCP Patterns

Multi-Server Architecture

Configure multiple MCP servers for comprehensive coverage:

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-postgres"],
      "env": { "POSTGRES_CONNECTION_STRING": "${DATABASE_URL}" }
    },
    "redis": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-redis"],
      "env": { "REDIS_URL": "${REDIS_URL}" }
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-github"],
      "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
    },
    "slack": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-slack"],
      "env": { "SLACK_TOKEN": "${SLACK_TOKEN}" }
    },
    "n8n": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-n8n"],
      "env": {
        "N8N_API_URL": "${N8N_URL}",
        "N8N_API_KEY": "${N8N_API_KEY}"
      }
    }
  }
}

Development vs Production Config

// .claude/settings.json (project - development)
{
  "mcpServers": {
    "database": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-postgres"],
      "env": {
        "POSTGRES_CONNECTION_STRING": "postgresql://localhost/myapp_dev"
      }
    }
  }
}
// ~/.claude/settings.json (user - production read-only)
{
  "mcpServers": {
    "prod-database-readonly": {
      "command": "npx",
      "args": ["-y", "@anthropic/mcp-server-postgres", "--read-only"],
      "env": {
        "POSTGRES_CONNECTION_STRING": "${PROD_DATABASE_URL}"
      }
    }
  }
}

Building Production-Grade MCP Servers

// production-mcp-server/index.js
import { McpServer } from "@anthropic/mcp-sdk";
import { createLogger } from './logger.js';
import { validateInput, sanitizeOutput } from './security.js';
import { metrics } from './metrics.js';

const logger = createLogger('mcp-server');
const server = new McpServer({
  name: "production-server",
  version: "1.0.0",
});

// Middleware pattern for all tools
const withMiddleware = (handler) => async (params) => {
  const startTime = Date.now();
  const requestId = crypto.randomUUID();

  logger.info({ requestId, params }, 'Tool invoked');

  try {
    // Validate inputs
    const validated = validateInput(params);

    // Execute handler
    const result = await handler(validated);

    // Sanitize outputs (remove sensitive data)
    const sanitized = sanitizeOutput(result);

    // Record metrics
    metrics.recordSuccess(handler.name, Date.now() - startTime);

    logger.info({ requestId, duration: Date.now() - startTime }, 'Tool completed');
    return sanitized;

  } catch (error) {
    metrics.recordError(handler.name, error);
    logger.error({ requestId, error }, 'Tool failed');
    throw error;
  }
};

// Production-ready tool with full error handling
server.tool({
  name: "get_user",
  description: "Get user by ID with full profile",
  parameters: {
    type: "object",
    properties: {
      userId: {
        type: "string",
        description: "User UUID",
        pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
      },
      includeProfile: {
        type: "boolean",
        description: "Include extended profile data",
        default: false
      }
    },
    required: ["userId"]
  },
  handler: withMiddleware(async ({ userId, includeProfile }) => {
    const user = await db.users.findById(userId);

    if (!user) {
      return { error: "User not found", code: "USER_NOT_FOUND" };
    }

    // Remove sensitive fields
    const { passwordHash, twoFactorSecret, ...safeUser } = user;

    if (includeProfile) {
      safeUser.profile = await db.profiles.findByUserId(userId);
    }

    return safeUser;
  })
});

// Health check tool
server.tool({
  name: "health_check",
  description: "Check server health and connectivity",
  handler: async () => {
    const checks = {
      database: await checkDatabase(),
      redis: await checkRedis(),
      timestamp: new Date().toISOString()
    };
    return checks;
  }
});

server.run();

MCP Server with Caching

import NodeCache from 'node-cache';

const cache = new NodeCache({
  stdTTL: 300,      // 5 minute default TTL
  checkperiod: 60   // Check for expired keys every 60s
});

server.tool({
  name: "get_analytics",
  description: "Get analytics data (cached for 5 minutes)",
  parameters: {
    type: "object",
    properties: {
      metric: { type: "string" },
      startDate: { type: "string" },
      endDate: { type: "string" }
    }
  },
  handler: async ({ metric, startDate, endDate }) => {
    const cacheKey = `analytics:${metric}:${startDate}:${endDate}`;

    // Check cache first
    const cached = cache.get(cacheKey);
    if (cached) {
      return { ...cached, fromCache: true };
    }

    // Fetch from source
    const data = await analyticsService.query(metric, startDate, endDate);

    // Cache result
    cache.set(cacheKey, data);

    return { ...data, fromCache: false };
  }
});

MCP for Internal APIs

// internal-api-mcp/index.js
// Connect Claude to your company's internal APIs

server.tool({
  name: "jira_create_issue",
  description: "Create a Jira issue",
  parameters: {
    type: "object",
    properties: {
      project: { type: "string", description: "Project key (e.g., PROJ)" },
      summary: { type: "string", description: "Issue title" },
      description: { type: "string", description: "Issue description" },
      issueType: { type: "string", enum: ["Bug", "Task", "Story"] },
      priority: { type: "string", enum: ["Low", "Medium", "High", "Critical"] }
    },
    required: ["project", "summary", "issueType"]
  },
  handler: async (params) => {
    const response = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
      method: 'POST',
      headers: {
        'Authorization': `Basic ${JIRA_AUTH}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        fields: {
          project: { key: params.project },
          summary: params.summary,
          description: params.description,
          issuetype: { name: params.issueType },
          priority: { name: params.priority || 'Medium' }
        }
      })
    });

    const issue = await response.json();
    return {
      key: issue.key,
      url: `${JIRA_URL}/browse/${issue.key}`
    };
  }
});

server.tool({
  name: "confluence_search",
  description: "Search Confluence documentation",
  parameters: {
    type: "object",
    properties: {
      query: { type: "string", description: "Search query" },
      space: { type: "string", description: "Space key to search in" }
    },
    required: ["query"]
  },
  handler: async ({ query, space }) => {
    const cql = space
      ? `text ~ "${query}" AND space = "${space}"`
      : `text ~ "${query}"`;

    const response = await fetch(
      `${CONFLUENCE_URL}/rest/api/content/search?cql=${encodeURIComponent(cql)}`,
      { headers: { 'Authorization': `Basic ${CONFLUENCE_AUTH}` }}
    );

    const results = await response.json();
    return results.results.map(r => ({
      title: r.title,
      url: `${CONFLUENCE_URL}${r._links.webui}`,
      excerpt: r.excerpt
    }));
  }
});

Testing MCP Servers

// mcp-server.test.js
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { spawn } from 'child_process';

describe('MCP Server', () => {
  let serverProcess;

  beforeAll(async () => {
    serverProcess = spawn('node', ['index.js'], {
      env: { ...process.env, TEST_MODE: 'true' }
    });
    await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for startup
  });

  afterAll(() => {
    serverProcess.kill();
  });

  it('should return user data for valid ID', async () => {
    const response = await callMcpTool('get_user', {
      userId: 'test-user-123'
    });

    expect(response.email).toBeDefined();
    expect(response.passwordHash).toBeUndefined(); // Should be sanitized
  });

  it('should handle invalid user ID', async () => {
    const response = await callMcpTool('get_user', {
      userId: 'invalid-id'
    });

    expect(response.error).toBe('User not found');
  });

  it('should respect rate limits', async () => {
    // Make rapid requests
    const promises = Array(10).fill().map(() =>
      callMcpTool('send_notification', { message: 'test' })
    );

    const results = await Promise.all(promises);
    const rateLimited = results.filter(r => r.error?.includes('rate'));

    expect(rateLimited.length).toBeGreaterThan(0);
  });
});

What's Next?

Learn about multi-agent workflows in 04-multi-agent.

For even deeper MCP integration with workflow automation, see the expert tutorial on n8n Integration.


Summary: - MCP extends Claude CLI with external service access - Configure servers in .claude/settings.json - Popular servers: filesystem, database, GitHub, browser - Build custom servers with the MCP SDK - Production patterns: caching, logging, metrics, validation - Security: validate inputs, limit privileges, protect secrets - Test MCP servers like any other code


Learning Resources

Fireship: Model Context Protocol Explained (3M+ subscribers, 500K+ views)

Comprehensive MCP tutorial - extend Claude with databases, APIs, and external services.

Additional Resources

Type Resource Description
🎬 Video MCP Workflows Deep Dive All About AI - Advanced MCP patterns
📚 Official Docs MCP Specification Official protocol documentation
📖 Tutorial Hugging Face MCP Course Free MCP course (Anthropic partnership)
🎓 Free Course MCP Servers Repo Official reference implementations
💼 Commercial Cursor MCP Course MCP integration patterns