MCP (Model Context Protocol) Tutorial: Complete Guide for Developers - Nayaka Yoga Pradipta
ID | EN

MCP (Model Context Protocol) Tutorial: Complete Guide for Developers

Monday, Dec 23, 2024

If you’re a developer who has started working with AI and LLMs (Large Language Models), you’ve probably experienced this frustration: “How can I get this AI to access my data or tools?”

This is where Model Context Protocol (MCP) comes in as a game-changing solution. MCP is an open standard developed by Anthropic to seamlessly connect AI with various data sources and tools.

In this tutorial, I’ll share a complete guide about MCP—from basic concepts, architecture, to creating your own MCP server with code examples you can practice right away.

What Is Model Context Protocol (MCP)?

Model Context Protocol (MCP) is an open-source protocol that provides a standard way to connect AI applications with external data sources and tools. Think of MCP like a “USB port” for AI—a universal interface that allows various AI models to connect with different applications.

Simple Analogy

Before MCP existed, every developer had to create custom integrations for each AI + data source combination. It was like the old days when every phone had a different charger—super inconvenient!

MCP is like USB-C becoming the universal standard. With MCP:

  • One protocol for all connections
  • Reusable components that can be used across various AI clients
  • Standardization that simplifies development and maintenance

Why Is MCP Important for Developers?

  1. Eliminates Boilerplate Code: No more writing repetitive integration code for each AI platform
  2. Interoperability: MCP servers you create can be used in Claude, VS Code, and other clients that support MCP
  3. Security First: MCP is designed with security as a priority—data stays on the server side, AI can only access through defined interfaces
  4. Scalability: Modular architecture makes scaling and maintenance easier
  5. Community Driven: As an open standard, there are many MCP servers already created by the community that you can use directly

MCP Architecture: Host, Client, and Server

To understand MCP well, you need to know its three main components:

1. MCP Host

Host is the main application users interact with AI through. Examples include:

  • Claude Desktop
  • Cursor IDE
  • VS Code with MCP-supporting extensions
  • Custom applications you build

The host is responsible for:

  • Managing MCP client lifecycle
  • Managing permissions and security
  • Providing UI for users

2. MCP Client

Client is a component within the host that is responsible for:

  • Maintaining connections to MCP servers (1 client : 1 server)
  • Sending requests to servers
  • Receiving and processing responses from servers

Each MCP client can only connect to one server, but a single host can have multiple clients connected to different servers.

3. MCP Server

Server is where the magic happens! Servers expose:

  • Tools: Functions that AI can call to perform actions
  • Resources: Data or content that AI can read
  • Prompts: Prompt templates that can be used

Servers can run as:

  • Local process: Running on the same machine as the host
  • Remote service: Running in the cloud or a separate server

Architecture Diagram

┌─────────────────────────────────────────────────────┐
│                    MCP HOST                          │
│  (Claude Desktop / Cursor / VS Code)                │
│                                                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐          │
│  │  Client  │  │  Client  │  │  Client  │          │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘          │
└───────┼─────────────┼─────────────┼─────────────────┘
        │             │             │
        ▼             ▼             ▼
   ┌─────────┐   ┌─────────┐   ┌─────────┐
   │ Server  │   │ Server  │   │ Server  │
   │(GitHub) │   │  (DB)   │   │(Custom) │
   └─────────┘   └─────────┘   └─────────┘

How MCP Works: JSON-RPC, Tools, Resources, and Prompts

MCP uses JSON-RPC 2.0 as its communication format. This is a lightweight and well-established protocol in the industry.

Transport Layer

MCP supports two types of transport:

1. stdio (Standard Input/Output)

  • Server runs as a subprocess
  • Communication via stdin/stdout
  • Suitable for local development and tools like Claude Desktop
{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["path/to/server.js"]
    }
  }
}

2. HTTP with SSE (Server-Sent Events)

  • Server runs as an HTTP service
  • Uses SSE for streaming responses
  • Suitable for remote deployment

Three Main MCP Primitives

1. Tools

Tools are functions that AI can call to perform specific actions. Examples:

  • search_database: Search data in a database
  • send_email: Send an email
  • create_file: Create a new file
// Tool definition example
{
  name: "get_weather",
  description: "Get current weather for a city",
  inputSchema: {
    type: "object",
    properties: {
      city: {
        type: "string",
        description: "City name"
      }
    },
    required: ["city"]
  }
}

2. Resources

Resources provide data or content that AI can read. Unlike tools, resources are more read-only and contextual.

// Resource example
{
  uri: "file:///docs/readme.md",
  name: "Project README",
  mimeType: "text/markdown"
}

Resources are useful for:

  • Giving document context to AI
  • Providing reference data
  • Exposing file system or database content

3. Prompts

Prompts are templates that can be used to guide AI interactions. This helps standardize how users interact with specific capabilities.

// Prompt example
{
  name: "code_review",
  description: "Review code for best practices",
  arguments: [
    {
      name: "language",
      description: "Programming language",
      required: true
    }
  ]
}

MCP Communication Lifecycle

  1. Initialization: Client and server exchange capability and versioning info
  2. Discovery: Client requests the list of available tools, resources, and prompts
  3. Execution: Client calls tools or requests resources as needed
  4. Shutdown: Graceful termination when no longer needed

Environment Setup for MCP Development

Now let’s set up the environment to start developing your own MCP server.

Prerequisites

Make sure you have:

  • Node.js 18+ or Python 3.10+
  • Claude Desktop or another MCP client for testing
  • Code editor (VS Code recommended)

Install MCP SDK

For TypeScript/JavaScript:

# Create new project
mkdir my-mcp-server
cd my-mcp-server
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

Setup TypeScript config:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Update package.json:

{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx src/index.ts"
  }
}

Simple MCP Server Example

Let’s create a simple but functional MCP server—a server that can provide weather information.

Project Structure

my-mcp-server/
├── src/
│   └── index.ts
├── package.json
└── tsconfig.json

MCP Server Code

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

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

// Define input schema
const GetWeatherSchema = z.object({
  city: z.string().describe("City name to check weather"),
});

// List available tools
server.tool(
  "get_weather",
  "Get weather information for a specific city",
  GetWeatherSchema.shape,
  async ({ city }) => {
    // Simulated weather data (in production, you would call a weather API)
    const weatherData = {
      "New York": { temp: 15, condition: "Partly cloudy", humidity: 65 },
      "Los Angeles": { temp: 24, condition: "Sunny", humidity: 45 },
      "Chicago": { temp: 8, condition: "Cloudy", humidity: 70 },
      "Miami": { temp: 28, condition: "Sunny", humidity: 75 },
    };

    const weather = weatherData[city as keyof typeof weatherData];

    if (!weather) {
      return {
        content: [
          {
            type: "text" as const,
            text: `Sorry, weather data for ${city} is not available. Available cities: New York, Los Angeles, Chicago, Miami`,
          },
        ],
      };
    }

    return {
      content: [
        {
          type: "text" as const,
          text: `Weather in ${city}:\n- Temperature: ${weather.temp}°C\n- Condition: ${weather.condition}\n- Humidity: ${weather.humidity}%`,
        },
      ],
    };
  }
);

// Add resource for documentation
server.resource(
  "weather://docs",
  "Weather server usage documentation",
  async () => ({
    contents: [
      {
        uri: "weather://docs",
        mimeType: "text/plain",
        text: `
Weather MCP Server
==================
This server provides weather information for cities.

Available tools:
- get_weather: Get weather information for a specific city

Supported cities:
- New York
- Los Angeles
- Chicago
- Miami
        `.trim(),
      },
    ],
  })
);

// Add prompt template
server.prompt(
  "weather_check",
  "Template for checking weather",
  [
    {
      name: "city",
      description: "City name",
      required: true,
    },
  ],
  async ({ city }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Please check the weather in ${city} and give recommendations for suitable outdoor activities.`,
        },
      },
    ],
  })
);

// Run server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Weather MCP Server running on stdio");
}

main().catch(console.error);

Build and Test

# Build project
npm run build

# Test by running directly
npm run dev

Configure in Claude Desktop

To use the MCP server in Claude Desktop, add the following configuration:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Windows: %APPDATA%\Claude\claude_desktop_config.json

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

Restart Claude Desktop, and now you can ask about the weather!

One of MCP’s strengths is its growing ecosystem. Here are some popular MCP servers you can use directly:

1. Filesystem Server

Server for local file system access. Useful for:

  • Reading and writing files
  • Browsing directories
  • File manipulation
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/path/to/allowed/directory"
      ]
    }
  }
}

2. GitHub Server

Integration with GitHub for:

  • Reading repositories
  • Creating and managing issues
  • Reviewing pull requests

3. Database Servers

Various MCP servers for databases:

  • PostgreSQL: Query and manipulate PostgreSQL data
  • SQLite: Access local SQLite database
  • MongoDB: Integration with MongoDB

4. DataForSEO Server

For SEO and marketing needs:

  • Keyword research
  • SERP analysis
  • Backlink analysis

5. Web Browser Server

Enables AI to:

  • Read web page content
  • Screenshot pages
  • Interact with web elements

Finding MCP Servers

You can find MCP servers at:

  1. Official MCP Servers: github.com/modelcontextprotocol/servers
  2. Community Collections: Various lists on GitHub
  3. npm/PyPI: Search with keyword “mcp-server”

MCP Development Best Practices

From experience developing various MCP servers, here are best practices you should follow:

1. Security First

// ❌ Don't expose all files
server.tool("read_file", async ({ path }) => {
  return fs.readFileSync(path);
});

// ✅ Validate and restrict access
const ALLOWED_DIRS = ["/app/data", "/app/public"];

server.tool("read_file", async ({ path }) => {
  const resolvedPath = path.resolve(path);
  const isAllowed = ALLOWED_DIRS.some((dir) =>
    resolvedPath.startsWith(dir)
  );
  
  if (!isAllowed) {
    throw new Error("Access denied");
  }
  
  return fs.readFileSync(resolvedPath);
});

2. Descriptive Tool Definitions

// ❌ Not descriptive enough
server.tool("search", async ({ q }) => {
  // ...
});

// ✅ Clear and informative
server.tool(
  "search_products",
  "Search products in the catalog. Returns up to 10 results sorted by relevance. Supports filtering by category and price range.",
  {
    query: z.string().describe("Search keywords"),
    category: z.string().optional().describe("Product category filter"),
    minPrice: z.number().optional().describe("Minimum price in USD"),
    maxPrice: z.number().optional().describe("Maximum price in USD"),
  },
  async (params) => {
    // implementation
  }
);

3. Good Error Handling

server.tool("api_call", async ({ endpoint }) => {
  try {
    const response = await fetch(endpoint);
    
    if (!response.ok) {
      return {
        content: [{
          type: "text",
          text: `API error: ${response.status} - ${response.statusText}`,
        }],
        isError: true,
      };
    }
    
    const data = await response.json();
    return {
      content: [{
        type: "text",
        text: JSON.stringify(data, null, 2),
      }],
    };
  } catch (error) {
    return {
      content: [{
        type: "text",
        text: `Failed to call API: ${error.message}`,
      }],
      isError: true,
    };
  }
});

4. Logging and Debugging

// Use stderr for logging (stdout reserved for MCP protocol)
console.error("[INFO] Server started");
console.error("[DEBUG] Processing request:", requestId);
console.error("[ERROR] Failed to connect:", error.message);

5. Rate Limiting and Caching

import { LRUCache } from "lru-cache";

const cache = new LRUCache<string, any>({
  max: 100,
  ttl: 1000 * 60 * 5, // 5 minutes
});

server.tool("expensive_operation", async ({ query }) => {
  const cacheKey = `query:${query}`;
  
  // Check cache first
  const cached = cache.get(cacheKey);
  if (cached) {
    console.error("[CACHE] Hit for:", query);
    return cached;
  }
  
  // Perform expensive operation
  const result = await performExpensiveOperation(query);
  
  // Store in cache
  cache.set(cacheKey, result);
  
  return result;
});

6. Graceful Shutdown

process.on("SIGINT", async () => {
  console.error("[INFO] Shutting down gracefully...");
  // Cleanup connections, save state, etc.
  await cleanup();
  process.exit(0);
});

process.on("SIGTERM", async () => {
  console.error("[INFO] Received SIGTERM, shutting down...");
  await cleanup();
  process.exit(0);
});

Testing MCP Servers

Testing is an important part of development. Here’s how to test MCP servers:

1. Manual Testing with MCP Inspector

# Install MCP Inspector
npx @modelcontextprotocol/inspector node dist/index.js

MCP Inspector provides a UI for:

  • Viewing lists of tools, resources, and prompts
  • Running tools with custom input
  • Debugging responses

2. Automated Testing

// test/server.test.ts
import { describe, it, expect } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";

describe("Weather Server", () => {
  it("should return weather for valid city", async () => {
    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    
    // Setup client and server...
    
    const result = await client.callTool({
      name: "get_weather",
      arguments: { city: "New York" },
    });
    
    expect(result.content[0].text).toContain("New York");
    expect(result.content[0].text).toContain("°C");
  });
});

MCP with Python

If you’re more comfortable with Python, MCP also supports the Python SDK:

# Install: pip install mcp

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

server = Server("weather-server")

@server.tool()
async def get_weather(city: str) -> list[TextContent]:
    """Get weather information for a city."""
    weather_data = {
        "New York": {"temp": 15, "condition": "Cloudy"},
        "Los Angeles": {"temp": 24, "condition": "Sunny"},
    }
    
    weather = weather_data.get(city)
    if not weather:
        return [TextContent(
            type="text",
            text=f"Weather data for {city} not available"
        )]
    
    return [TextContent(
        type="text", 
        text=f"Weather in {city}: {weather['temp']}°C, {weather['condition']}"
    )]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Debugging Tips

1. Check Logs

MCP logs can be found at:

  • Claude Desktop: ~/Library/Logs/Claude/ (macOS)
  • Custom apps: Depends on logging implementation

2. Common Issues

Server doesn’t appear in Claude:

  • Make sure the path to the server is correct
  • Check JSON config syntax
  • Restart Claude Desktop

Tool not being called:

  • Make sure the tool description is clear enough
  • Check if AI understands when to use the tool

“Connection refused” error:

  • Server might have crashed on startup
  • Check stderr output for error messages

3. Development Mode

Use tsx for development with hot reload:

# Install tsx globally
npm install -g tsx

# Run with watch mode
tsx watch src/index.ts

Real-World Use Cases

1. Personal Knowledge Base

Create an MCP server that connects to Notion or Obsidian for:

  • Searching notes
  • Creating new notes
  • Updating existing notes

2. Development Workflow

MCP server for:

  • Running tests
  • Deploying applications
  • Managing Git operations

3. Business Intelligence

Connect AI with:

  • Company databases
  • Analytics platforms
  • Reporting tools

4. Content Creation

Like what I use on this blog:

  • Keyword research with DataForSEO
  • Image generation
  • Content publishing

The Future of MCP

MCP is still very new (launched November 2024), but adoption is very fast:

  • IDE Integration: Cursor, VS Code, and other IDEs are starting to support MCP
  • Enterprise Adoption: Large companies are starting to develop internal MCP servers
  • Community Growth: Thousands of MCP servers have been created by the community
  • Standardization: MCP is in the process of joining the Linux Foundation

As a developer, now is the right time to:

  1. Learn and experiment with MCP
  2. Contribute to the ecosystem by creating MCP servers
  3. Integrate MCP into your development workflow

Conclusion and Next Steps

MCP (Model Context Protocol) is a breakthrough in how we integrate AI with tools and data. By understanding the Host-Client-Server concept, as well as the Tools-Resources-Prompts primitives, you now have a strong foundation to start developing your own MCP servers.

Next Steps

  1. Install Claude Desktop and try some existing MCP servers
  2. Create a simple MCP server following the examples in this tutorial
  3. Explore MCP servers from the community on GitHub
  4. Join the MCP community for discussions and sharing experiences

Additional Resources

Happy experimenting with MCP! If you have questions or want to discuss, feel free to reach out. Happy coding! 🚀