M
MeshWorld.
MCP Model Context Protocol Claude AI LLM API Integration Cursor Windsurf Tools 10 min read

MCP (Model Context Protocol): The Complete Developer's Guide

Vishnu
By Vishnu

The Model Context Protocol (MCP) is the USB-C for AI applications. It standardizes how Large Language Models connect to external tools, APIs, and data sources. Instead of writing custom integrations for every service, you build one MCP server — and any MCP-compatible client (Claude, Cursor, Windsurf, etc.) can use it.

This guide covers everything from protocol basics to building production-ready MCP servers.

:::note[TL;DR]

  • MCP is an open protocol for connecting LLMs to tools, APIs, and data sources
  • Build an MCP server once, use it with Claude, Cursor, Windsurf, and more
  • Three core primitives: Tools (actions), Resources (data), Prompts (templates)
  • Use the MCP SDK (TypeScript/Python) for rapid development
  • Deploy via stdio (local) or SSE (remote) transports :::

What Is MCP?

MCP (Model Context Protocol) solves a simple problem: every AI tool has its own way of calling external services. OpenAI has function calling. Claude has tool use. LangChain has its own abstractions. MCP unifies these into a standard protocol.

Think of it like HTTP for web services — but specifically designed for LLM context and tool interactions.

The Scenario: Your team uses a proprietary internal API for customer data. You want Claude Code, Cursor, and your custom AI dashboard to all access it. Without MCP, you write three different integrations. With MCP, you write one MCP server and all three clients connect automatically.

Core Concepts

The Three Primitives

PrimitivePurposeExample
ToolsActions the LLM can invokecreate_ticket, send_email, query_database
ResourcesData the LLM can readCustomer records, documentation, files
PromptsReusable prompt templates”Analyze this code for security issues”

Architecture

┌─────────────────┐         ┌─────────────────┐         ┌─────────────────┐
│   MCP Client    │◄───────►│   MCP Server    │◄───────►│   External API  │
│  (Claude/Cursor)│  JSON   │  (Your Bridge)  │  HTTP   │  (Your Service) │
│                 │  RPC    │                 │  /DB    │                 │
└─────────────────┘         └─────────────────┘         └─────────────────┘

Building Your First MCP Server

Setup

# Install MCP SDK
npm install @modelcontextprotocol/sdk

# Or for Python
pip install mcp

Basic Server (TypeScript)

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

// Define your tools
const TOOLS = [
  {
    name: 'get_weather',
    description: 'Get current weather for a location',
    inputSchema: {
      type: 'object',
      properties: {
        location: {
          type: 'string',
          description: 'City name or coordinates'
        }
      },
      required: ['location']
    }
  },
  {
    name: 'search_docs',
    description: 'Search internal documentation',
    inputSchema: {
      type: 'object',
      properties: {
        query: {
          type: 'string',
          description: 'Search query'
        }
      },
      required: ['query']
    }
  }
];

// Create server
const server = new Server(
  {
    name: 'my-mcp-server',
    version: '1.0.0'
  },
  {
    capabilities: {
      tools: {}
    }
  }
);

// Handle tool list requests
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return { tools: TOOLS };
});

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

// Tool implementations
async function handleGetWeather(location: string) {
  // Call your weather API
  const response = await fetch(`https://api.weather.com/v1/current?city=${location}`);
  const data = await response.json();
  
  return {
    content: [
      {
        type: 'text',
        text: `Weather in ${location}: ${data.temperature}°F, ${data.condition}`
      }
    ]
  };
}

async function handleSearchDocs(query: string) {
  // Search your documentation
  const results = await searchInternalDocs(query);
  
  return {
    content: [
      {
        type: 'text',
        text: results.map(r => `- ${r.title}: ${r.url}`).join('\n')
      }
    ]
  };
}

// Start server with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP Server running on stdio');

Running Your Server

# Direct execution
node server.js

# Or via npx (for distribution)
npx my-mcp-server

Advanced Patterns

1. Resources (Read-Only Data)

Resources expose data that LLMs can reference but not modify:

import { ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';

// Define available resources
const RESOURCES = [
  {
    uri: 'docs://api-reference',
    name: 'API Reference',
    mimeType: 'text/markdown',
    description: 'Complete API documentation'
  },
  {
    uri: 'config://app-settings',
    name: 'Application Settings',
    mimeType: 'application/json',
    description: 'Current app configuration'
  }
];

// List resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return { resources: RESOURCES };
});

// Read resource content
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;
  
  if (uri === 'docs://api-reference') {
    const content = await loadApiDocs();
    return {
      contents: [
        {
          uri,
          mimeType: 'text/markdown',
          text: content
        }
      ]
    };
  }
  
  throw new Error(`Resource not found: ${uri}`);
});

2. Prompts (Reusable Templates)

Prompts provide structured starting points for common tasks:

import { ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';

const PROMPTS = [
  {
    name: 'code_review',
    description: 'Review code for best practices',
    arguments: [
      {
        name: 'language',
        description: 'Programming language',
        required: true
      },
      {
        name: 'code',
        description: 'Code to review',
        required: true
      }
    ]
  }
];

server.setRequestHandler(ListPromptsRequestSchema, async () => {
  return { prompts: PROMPTS };
});

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
  
  if (name === 'code_review') {
    return {
      description: 'Code review prompt',
      messages: [
        {
          role: 'user',
          content: {
            type: 'text',
            text: `Review this ${args.language} code for best practices, bugs, and performance issues:\n\n${args.code}`
          }
        }
      ]
    };
  }
  
  throw new Error(`Prompt not found: ${name}`);
});

3. Streaming Progress

For long-running operations, stream progress updates:

async function handleLongTask(args: any) {
  const progressToken = args._meta?.progressToken;
  
  // Send progress updates
  for (let i = 0; i < 10; i++) {
    await server.notification({
      method: 'notifications/progress',
      params: {
        progressToken,
        progress: i * 10,
        total: 100
      }
    });
    
    await doSomeWork(i);
  }
  
  return {
    content: [{ type: 'text', text: 'Task completed!' }]
  };
}

Transport Options

stdio (Local)

Best for local development and CLI tools:

import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const transport = new StdioServerTransport();
await server.connect(transport);

SSE (Server-Sent Events)

For remote servers and web applications:

import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';

const app = express();

app.get('/sse', async (req, res) => {
  const transport = new SSEServerTransport('/messages', res);
  await server.connect(transport);
});

app.post('/messages', async (req, res) => {
  // Handle incoming messages
});

app.listen(3000);

Client Configuration

Claude Desktop

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/your/server.js"],
      "env": {
        "API_KEY": "your-api-key"
      }
    },
    "weather": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-weather"]
    }
  }
}

Cursor

Add to Cursor settings (~/.cursor/mcp.json):

{
  "servers": [
    {
      "name": "my-server",
      "command": "node /path/to/server.js",
      "type": "command"
    }
  ]
}

Windsurf

Add to Windsurf MCP config:

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

Real-World Example: Database MCP Server

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Client } from 'pg';

// PostgreSQL MCP Server
class DatabaseMCPServer {
  private server: Server;
  private db: Client;
  
  constructor(connectionString: string) {
    this.db = new Client({ connectionString });
    
    this.server = new Server(
      { name: 'postgres-mcp', version: '1.0.0' },
      { capabilities: { tools: {}, resources: {} } }
    );
    
    this.setupHandlers();
  }
  
  private setupHandlers() {
    // Tool: Execute query
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [
          {
            name: 'query',
            description: 'Execute a SQL SELECT query',
            inputSchema: {
              type: 'object',
              properties: {
                sql: { type: 'string', description: 'SQL query to execute' }
              },
              required: ['sql']
            }
          },
          {
            name: 'get_tables',
            description: 'List all tables in the database',
            inputSchema: { type: 'object', properties: {} }
          },
          {
            name: 'get_table_schema',
            description: 'Get schema for a specific table',
            inputSchema: {
              type: 'object',
              properties: {
                table: { type: 'string', description: 'Table name' }
              },
              required: ['table']
            }
          }
        ]
      };
    });
    
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;
      
      try {
        switch (name) {
          case 'query':
            const result = await this.db.query(args.sql);
            return {
              content: [{
                type: 'text',
                text: JSON.stringify(result.rows, null, 2)
              }]
            };
            
          case 'get_tables':
            const tables = await this.db.query(`
              SELECT table_name 
              FROM information_schema.tables 
              WHERE table_schema = 'public'
            `);
            return {
              content: [{
                type: 'text',
                text: tables.rows.map(r => r.table_name).join('\n')
              }]
            };
            
          case 'get_table_schema':
            const schema = await this.db.query(`
              SELECT column_name, data_type, is_nullable
              FROM information_schema.columns
              WHERE table_name = $1
            `, [args.table]);
            return {
              content: [{
                type: 'text',
                text: JSON.stringify(schema.rows, null, 2)
              }]
            };
            
          default:
            throw new Error(`Unknown tool: ${name}`);
        }
      } catch (error) {
        return {
          content: [{
            type: 'text',
            text: `Error: ${error.message}`
          }],
          isError: true
        };
      }
    });
    
    // Resource: Database schema
    this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
      const tables = await this.db.query(`
        SELECT table_name
        FROM information_schema.tables
        WHERE table_schema = 'public'
      `);
      
      return {
        resources: tables.rows.map(row => ({
          uri: `table://${row.table_name}`,
          name: row.table_name,
          mimeType: 'application/json',
          description: `Schema and data for ${row.table_name}`
        }))
      };
    });
  }
  
  async start() {
    await this.db.connect();
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('PostgreSQL MCP Server running');
  }
}

// Start server
const server = new DatabaseMCPServer(process.env.DATABASE_URL);
server.start();

Security Best Practices

1. Input Validation

import { z } from 'zod';

const QuerySchema = z.object({
  sql: z.string().max(1000),
  limit: z.number().max(1000).default(100)
});

async function handleQuery(rawArgs: unknown) {
  const args = QuerySchema.parse(rawArgs);
  // Now safe to use args.sql
}

2. Read-Only Mode

// Reject dangerous operations
const FORBIDDEN_KEYWORDS = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'ALTER'];

function validateReadOnly(sql: string) {
  const upper = sql.toUpperCase();
  for (const keyword of FORBIDDEN_KEYWORDS) {
    if (upper.includes(keyword)) {
      throw new Error(`Forbidden operation: ${keyword}`);
    }
  }
}

3. Rate Limiting

import { RateLimiter } from 'limiter';

const limiter = new RateLimiter({ tokensPerInterval: 10, interval: 'minute' });

async function handleRequest(req: Request) {
  if (!await limiter.removeTokens(1)) {
    throw new Error('Rate limit exceeded');
  }
  // Process request
}

Testing Your MCP Server

Manual Testing

# Test with mcp-cli
npx @modelcontextprotocol/inspector node server.js

# Or use the MCP Inspector UI
open http://localhost:5173

Automated Tests

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';

describe('Database MCP Server', () => {
  let client: Client;
  
  beforeEach(async () => {
    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    
    const server = new DatabaseMCPServer('postgres://localhost/test');
    await server.connect(serverTransport);
    
    client = new Client({ name: 'test-client', version: '1.0.0' });
    await client.connect(clientTransport);
  });
  
  test('lists available tools', async () => {
    const tools = await client.listTools();
    expect(tools.tools).toContainEqual(
      expect.objectContaining({ name: 'query' })
    );
  });
  
  test('executes query', async () => {
    const result = await client.callTool('query', { sql: 'SELECT 1 as num' });
    expect(result.content[0].text).toContain('1');
  });
});

Deployment Options

Local Development

{
  "mcpServers": {
    "dev-server": {
      "command": "node",
      "args": ["./server.js"],
      "env": { "NODE_ENV": "development" }
    }
  }
}

Production (Docker)

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# docker-compose.yml
version: '3.8'
services:
  mcp-server:
    build: .
    environment:
      - DATABASE_URL=${DATABASE_URL}
    ports:
      - "3000:3000"

Summary

  • MCP standardizes how LLMs connect to tools and data
  • Three primitives: Tools (actions), Resources (data), Prompts (templates)
  • Build once, use everywhere: Claude, Cursor, Windsurf, and more
  • Transports: stdio for local, SSE for remote
  • Security: Validate inputs, use read-only modes, implement rate limiting

MCP turns your custom integrations into reusable, composable AI infrastructure.