Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

December 2025 Update: MCP is now supported by Claude, Cursor, Windsurf, and many other AI tools. This module includes production-ready server examples.

What is MCP?

Model Context Protocol (MCP) is an open standard for connecting AI models to external data sources and tools. Developed by Anthropic, it provides a unified way for LLMs to interact with:
  • Databases (PostgreSQL, MongoDB, SQLite)
  • APIs (GitHub, Slack, Notion)
  • File systems (local, S3, Google Drive)
  • Development tools (Git, Docker, Kubernetes)
  • Any external service
Think of MCP as USB for AI — a standard interface that lets any AI model connect to any tool, and any tool connect to any AI model.

Why MCP Matters in 2025

Before MCPWith MCP
Custom integration per toolStandard protocol for all
Tight coupling to one modelModel-agnostic
Rebuild for each projectReusable servers
Limited community sharingOpen ecosystem

Who’s Using MCP?

  • Claude Desktop - Native MCP support
  • Cursor - IDE with MCP integrations
  • Windsurf - AI coding assistant
  • Continue - Open-source AI coding
  • Zed - Next-gen code editor

Architecture

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   AI Model      │     │   MCP Client    │     │   MCP Server    │
│   (Claude,      │◄───►│   (in your      │◄───►│   (provides     │
│    GPT, etc)    │     │    app)         │     │    tools)       │
└─────────────────┘     └─────────────────┘     └─────────────────┘


                                                ┌─────────────────┐
                                                │  External       │
                                                │  Resources      │
                                                │  (DB, API, etc) │
                                                └─────────────────┘

Building an MCP Server

Basic Server Structure

# server.py
from mcp.server import Server
from mcp.types import Tool, TextContent
import mcp.server.stdio

# Create server
server = Server("my-tools-server")

# Define tools
@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="get_weather",
            description="Get current weather for a location",
            inputSchema={
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "City name"
                    }
                },
                "required": ["location"]
            }
        ),
        Tool(
            name="search_database",
            description="Search the product database",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "limit": {"type": "integer", "default": 10}
                },
                "required": ["query"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "get_weather":
        location = arguments["location"]
        # In production, call real weather API
        return [TextContent(
            type="text",
            text=f"Weather in {location}: 22°C, Sunny"
        )]
    
    elif name == "search_database":
        query = arguments["query"]
        limit = arguments.get("limit", 10)
        # In production, query real database
        return [TextContent(
            type="text",
            text=f"Found {limit} results for '{query}'"
        )]
    
    raise ValueError(f"Unknown tool: {name}")

# Run server
async def main():
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options()
        )

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

Database MCP Server

# db_server.py
from mcp.server import Server
from mcp.types import Tool, TextContent, Resource
import mcp.server.stdio
import psycopg2
import json

server = Server("postgres-server")

# Database connection
conn = psycopg2.connect("postgresql://user:pass@localhost/mydb")

@server.list_tools()
async def list_tools():
    return [
        Tool(
            name="query_database",
            description="Execute a read-only SQL query",
            inputSchema={
                "type": "object",
                "properties": {
                    "sql": {
                        "type": "string",
                        "description": "SQL SELECT query"
                    }
                },
                "required": ["sql"]
            }
        ),
        Tool(
            name="list_tables",
            description="List all tables in the database",
            inputSchema={"type": "object", "properties": {}}
        ),
        Tool(
            name="describe_table",
            description="Get schema of a table",
            inputSchema={
                "type": "object",
                "properties": {
                    "table_name": {"type": "string"}
                },
                "required": ["table_name"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "query_database":
        sql = arguments["sql"]
        
        # Security: Only allow SELECT
        if not sql.strip().upper().startswith("SELECT"):
            return [TextContent(type="text", text="Error: Only SELECT queries allowed")]
        
        with conn.cursor() as cur:
            cur.execute(sql)
            columns = [desc[0] for desc in cur.description]
            rows = cur.fetchall()
            
            result = [dict(zip(columns, row)) for row in rows]
            return [TextContent(type="text", text=json.dumps(result, indent=2))]
    
    elif name == "list_tables":
        with conn.cursor() as cur:
            cur.execute("""
                SELECT table_name FROM information_schema.tables
                WHERE table_schema = 'public'
            """)
            tables = [row[0] for row in cur.fetchall()]
            return [TextContent(type="text", text=json.dumps(tables))]
    
    elif name == "describe_table":
        table = arguments["table_name"]
        with conn.cursor() as cur:
            cur.execute("""
                SELECT column_name, data_type, is_nullable
                FROM information_schema.columns
                WHERE table_name = %s
            """, (table,))
            
            columns = [
                {"name": row[0], "type": row[1], "nullable": row[2]}
                for row in cur.fetchall()
            ]
            return [TextContent(type="text", text=json.dumps(columns, indent=2))]

# Resources: Expose data as readable resources
@server.list_resources()
async def list_resources():
    return [
        Resource(
            uri="db://tables",
            name="Database Tables",
            description="List of all tables"
        )
    ]

@server.read_resource()
async def read_resource(uri: str):
    if uri == "db://tables":
        with conn.cursor() as cur:
            cur.execute("""
                SELECT table_name FROM information_schema.tables
                WHERE table_schema = 'public'
            """)
            return json.dumps([row[0] for row in cur.fetchall()])

Using MCP with Claude Desktop

Configure in claude_desktop_config.json:
{
  "mcpServers": {
    "my-database": {
      "command": "python",
      "args": ["path/to/db_server.py"],
      "env": {
        "DATABASE_URL": "postgresql://user:pass@localhost/mydb"
      }
    },
    "my-tools": {
      "command": "python", 
      "args": ["path/to/server.py"]
    }
  }
}

Building an MCP Client

# client.py
from mcp import Client
from mcp.client.stdio import stdio_client
import asyncio

async def main():
    # Connect to MCP server
    async with stdio_client(
        command="python",
        args=["server.py"]
    ) as (read, write):
        async with Client(read, write) as client:
            # Initialize
            await client.initialize()
            
            # List available tools
            tools = await client.list_tools()
            print("Available tools:", [t.name for t in tools.tools])
            
            # Call a tool
            result = await client.call_tool(
                name="get_weather",
                arguments={"location": "Tokyo"}
            )
            print("Result:", result.content[0].text)

asyncio.run(main())

Integrating MCP with LangChain

from langchain_core.tools import StructuredTool
from mcp import Client

class MCPToolkit:
    def __init__(self, client: Client):
        self.client = client
    
    async def get_langchain_tools(self) -> list[StructuredTool]:
        """Convert MCP tools to LangChain tools"""
        mcp_tools = await self.client.list_tools()
        
        langchain_tools = []
        for tool in mcp_tools.tools:
            async def call_mcp(client=self.client, name=tool.name, **kwargs):
                result = await client.call_tool(name=name, arguments=kwargs)
                return result.content[0].text
            
            langchain_tools.append(StructuredTool(
                name=tool.name,
                description=tool.description,
                func=call_mcp,
                args_schema=tool.inputSchema
            ))
        
        return langchain_tools

Common MCP Servers

The MCP ecosystem has grown rapidly. Here are the most popular servers:

Filesystem

Read/write files, list directories. npx @anthropic-ai/mcp-server-filesystem

PostgreSQL

Query databases, list tables. npx @anthropic-ai/mcp-server-postgres

GitHub

Manage repos, issues, PRs. npx @anthropic-ai/mcp-server-github

Slack

Send messages, read channels. npx @anthropic-ai/mcp-server-slack

Google Drive

Read/write documents. npx @anthropic-ai/mcp-server-gdrive

Puppeteer

Web scraping, automation. npx @anthropic-ai/mcp-server-puppeteer

Memory

Persistent knowledge graph. npx @anthropic-ai/mcp-server-memory

Brave Search

Web search integration. npx @anthropic-ai/mcp-server-brave-search

Quick Install for Claude Desktop

// claude_desktop_config.json (MacOS: ~/Library/Application Support/Claude/)
// Windows: %APPDATA%\Claude\
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@anthropic-ai/mcp-server-filesystem", "/path/to/allowed/dir"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@anthropic-ai/mcp-server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "your-token"
      }
    }
  }
}

Best Practices

  • Validate all inputs
  • Use read-only database connections when possible
  • Implement rate limiting
  • Log all tool calls
@server.call_tool()
async def call_tool(name: str, arguments: dict):
    try:
        result = await execute_tool(name, arguments)
        return [TextContent(type="text", text=result)]
    except ValueError as e:
        return [TextContent(type="text", text=f"Invalid input: {e}")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error: {e}")]
  • Use connection pooling for databases
  • Implement timeouts for external calls
  • Clean up resources on shutdown
  • Write clear tool descriptions
  • Document expected inputs and outputs
  • Include examples in descriptions

MCP vs Function Calling

AspectMCPOpenAI Function Calling
StandardOpen protocolProprietary
ReusabilityHigh (server-based)Per-application
Multi-modelYesOpenAI only
ComplexityHigher initial setupSimpler
EcosystemGrowingMature

Next Steps

Agentic Architecture

Design patterns for building multi-agent systems

Interview Deep-Dive

Strong Answer:
  • MCP solves the N-times-M integration problem. Before MCP, if you had N AI applications and M external tools, you needed N times M custom integrations. Every application had its own way of connecting to GitHub, its own database query tool, its own file system access layer. MCP introduces a standard protocol layer so that any MCP client (your AI application) can connect to any MCP server (a tool provider) through a single interface. Think of it like USB for AI — before USB, every peripheral needed its own proprietary cable.
  • Function calling, as implemented by OpenAI and Anthropic, solves a different problem: it lets an LLM express intent to call a function during generation. But the function itself must be implemented and hosted by your application. Function calling says “the model wants to call get_weather(city='Tokyo')” — but you still have to write the get_weather function, handle authentication, manage connections, and deal with errors. MCP encapsulates all of that on the server side. The MCP server for weather handles the API key, the HTTP calls, the error handling, and exposes a clean tool interface.
  • The architectural distinction is the client-server separation over a transport layer. MCP servers run as separate processes communicating via stdio or SSE. This means a single MCP server can be shared across multiple AI applications. If you build a PostgreSQL MCP server once, every MCP-compatible client — Claude Desktop, Cursor, your custom agent — can use it without modification. With function calling, you would reimplement the database integration in each application.
  • The practical limitation of MCP today is ecosystem maturity. The protocol is well-designed but the tooling is still evolving. Error handling semantics, authentication standards, and streaming behavior are not fully standardized across implementations. In production, you need to add your own retry logic, timeout handling, and input validation on top of what MCP provides.
Follow-up: What are the security implications of giving an LLM access to a database through an MCP server, and how would you mitigate them?This is the question that separates demo-quality MCP deployments from production-grade ones. The fundamental risk is that the LLM generates the SQL query, and if your MCP server executes arbitrary SQL, you have given an AI model direct database access with no guardrails. The first mitigation is read-only connections — the database user the MCP server connects with should only have SELECT privileges. Second, query allowlisting: instead of executing arbitrary SQL, the MCP server should expose specific parameterized queries as tools — search_customers(name='...') rather than run_sql(query='...'). Third, input validation: every argument the LLM passes to a tool must be validated and sanitized. The LLM might generate a SQL injection payload not out of malice but because it saw one in its training data. Fourth, rate limiting per user session to prevent the LLM from accidentally running a full table scan in a loop. Fifth, comprehensive audit logging of every tool call, its arguments, and its results. In a regulated environment, you also need approval flows where certain tool calls require human confirmation before execution.
Strong Answer:
  • First, I would scope the tool surface area. The biggest mistake in MCP server design is exposing too many tools. LLMs perform worse when given 50 tools to choose from compared to 5 well-designed ones. I would start by identifying the 5-8 most common operations the coding assistant needs: search the codebase, read file contents, query the CI/CD pipeline status, look up internal documentation, and create JIRA tickets. Each tool gets a precise name, a detailed description (this is the model’s only guide for when to use the tool), and a strict input schema.
  • Second, I would design the tool descriptions as if I were writing documentation for a junior developer. The model uses the description to decide when and how to use the tool. A description like “Search code” is insufficient. A description like “Search the codebase for files matching a pattern or containing specific text. Use this when the user asks about where something is defined, how something is implemented, or wants to find examples of a pattern. Returns file paths and matching line numbers” gives the model the context it needs to use the tool appropriately.
  • Third, I would implement robust error handling. MCP tool calls will fail — API timeouts, auth token expiry, malformed inputs. Each error case should return a clear, actionable error message as a TextContent response, not throw an exception. The model needs to understand what went wrong so it can decide whether to retry, adjust its approach, or inform the user. A raw stack trace is useless to the model.
  • Fourth, authentication. The MCP server needs to authenticate against your internal APIs. I would pass API tokens via environment variables in the MCP server configuration, never hardcoded. For per-user authentication (the assistant acts on behalf of the logged-in developer), you need a token delegation pattern where the client passes the user’s auth token as part of the session initialization.
  • Fifth, I would implement response truncation. Internal APIs can return enormous payloads — a codebase search returning 500 results, a CI log with 10,000 lines. The MCP server must truncate these to a reasonable size (say, 2,000 tokens) and indicate that results were truncated, so the model can request more specific queries.
Follow-up: How would you test this MCP server, and how do you handle the fact that it depends on internal APIs that might not be available in CI?I would build three test layers. First, unit tests with mocked API responses: every tool handler gets tested with representative inputs, edge cases (empty results, very large results, Unicode, special characters), and error cases (API timeout, 401, 500). These use pytest fixtures that return predefined responses and run in CI without any network access. Second, contract tests that validate the MCP protocol behavior: does the server correctly respond to list_tools, does it handle unknown tool names gracefully, does it respect the MCP message format? These are protocol-level tests that do not depend on the internal APIs. Third, integration tests that run against a staging environment of the internal APIs, triggered manually or nightly. These verify that the actual API contracts have not changed — field names, response formats, pagination behavior. I would also add a health check tool to the MCP server itself that verifies connectivity to all dependent APIs, so the client can check if the server is fully operational before exposing tools to the model.
Strong Answer:
  • Choose function calling when you are building a single application, using one provider, and your tools are tightly coupled to your application logic. For example, a customer support chatbot that needs to look up orders, issue refunds, and update tickets — these tools are specific to your business domain, they need access to your application’s database and authentication context, and they will never be reused by another application. Function calling is simpler to implement (just define the schema and handle the call), has no infrastructure overhead (no separate server process), and is deeply integrated with the provider’s streaming and tool-use capabilities.
  • Choose MCP when you need tool reuse across multiple AI applications, provider portability, or when the tools represent capabilities that are independent of your application logic. Database access, file system operations, API integrations to third-party services like GitHub or Slack — these are the sweet spot for MCP. Build the server once, use it from Claude Desktop during development, from your production agent in deployment, and from Cursor for coding assistance.
  • The hybrid approach is what most production systems end up with. Application-specific tools (order lookup, business logic) are implemented as function calls within your application. Infrastructure and third-party integration tools are implemented as MCP servers. This gives you the simplicity of function calling where it matters and the reusability of MCP where it matters.
  • One often-overlooked consideration is latency. Function calling happens in-process — the tool execution is a direct function call with microsecond overhead. MCP involves inter-process communication (stdio or HTTP), which adds milliseconds per tool call. For an agent that makes 10-15 tool calls per task, those milliseconds compound. If latency is critical, keep hot-path tools as in-process function calls and use MCP for less frequent, heavier operations.
  • Another consideration is model compatibility. Function calling schemas differ between OpenAI and Anthropic — parameter naming, required fields, how you return results. If you switch providers, you rewrite your function calling integration. MCP is provider-agnostic by design. If provider portability is a requirement (and in enterprise it usually is), MCP reduces switching costs significantly.
Follow-up: MCP servers run as separate processes. What are the operational challenges of running MCP servers in a production Kubernetes environment?The main challenges are lifecycle management, resource allocation, and multi-tenancy. Each MCP server is a separate process, and in Kubernetes that typically means a separate container or sidecar. If your agent uses 5 MCP servers, that is 5 sidecars per pod, each consuming CPU and memory. The resource allocation needs to be tuned per server — a database MCP server needs more memory for connection pooling than a simple API wrapper. For scaling, you need to decide: do you co-locate MCP servers as sidecars (simpler networking, higher per-pod resource cost) or run them as separate services (independent scaling, but now tool calls go over the network)? My preference for production is separate services behind a service mesh, with each MCP server deployed as a standard microservice. This lets you scale the database server independently of the GitHub server, apply different security policies, and monitor each server’s health independently. The transport shifts from stdio to HTTP/SSE, which is better suited for networked environments anyway. The trade-off is added latency from network hops, which you mitigate with connection pooling and keep-alive connections.