Build Your First MCP Server: A Developer's Guide to the Model Context Protocol

AI, Devops & Infrastructure, Tutorials, and What Is

Build Your First MCP Server: A Developer's Guide to the Model Context Protocol

The Model Context Protocol (MCP) is a standardized way to connect AI coding assistants to external tools, data sources, and services. Instead of every AI tool building custom integrations for every service, MCP provides a single protocol that works across all of them — build one server, and Claude Code, GitHub Copilot, Cursor, and any other MCP-compatible client can use it.

Anthropic introduced MCP in November 2024. Within months, OpenAI, Microsoft, Google, and GitHub adopted it. The reason is practical: without a standard, connecting 10 AI tools to 10 services requires 100 custom integrations. With MCP, you build 10 servers and 10 clients — each works with all the others.

This guide covers what MCP servers are, how the protocol works, and walks you through building your first server from scratch.

Why MCP Matters for Web Developers

If you deploy web applications, you already use multiple tools daily: GitHub for code, Slack for communication, databases for application data, and deployment platforms like DeployHQ for shipping code to production. MCP lets AI assistants interact with all of these through a single protocol.

In practice, this means:

  • Ask AI about your deployments: Instead of checking logs manually, ask Claude What failed in yesterday's production deployment? and get a detailed answer pulled from your deployment pipeline
  • Automate routine tasks: An AI assistant can trigger deployments, check server status, or analyze deployment patterns — all through natural language
  • Context-aware coding: Your AI tools understand your full development environment, from code in GitHub to deployment configurations to server logs

If you've used any of the AI CLI coding assistants we've covered in this series, you've already used MCP — Claude Code, Codex CLI, and Gemini CLI all support MCP servers as tool providers.

How MCP Works: The Architecture

MCP uses a client-server architecture with three components:

flowchart LR
    A[MCP Host<br/>Claude Desktop, VS Code, Cursor] --> B[MCP Client<br/>Built into the host]
    B <-->|JSON-RPC 2.0| C[MCP Server<br/>Your custom server]
    C --> D[External Service<br/>API, Database, Tool]

MCP Servers

Lightweight programs that expose specific capabilities through standardized interfaces. A server can provide:

  • Resources: Data the AI can read (files, database records, API responses, documentation)
  • Tools: Functions the AI can call (deploy code, query databases, send notifications, create tickets)
  • Prompts: Predefined templates for common workflows (code review checklist, deployment runbook)

For example, a deployment platform's MCP server could expose tools like list_deployments, trigger_deploy, and get_deployment_logs — letting AI assistants manage your deployment pipeline through natural language.

MCP Clients

Built into AI-powered applications (Claude Desktop, VS Code with Copilot, Cursor IDE), clients handle communication with servers. They present available tools to the AI model and execute requests on the user's behalf.

MCP Hosts

The runtime environment that manages everything. Claude Desktop, VS Code, and Cursor are all MCP hosts — they run the clients, connect to your servers, and route tool calls between the AI model and your services.

The protocol uses JSON-RPC 2.0 for message passing and supports two transport mechanisms:

  • stdio: For local servers (the server runs as a subprocess)
  • Streamable HTTP: For remote servers (the server runs over the network)

Building Your First MCP Server

Let's build something more useful than a weather demo. We'll create an MCP server that checks the status of a website — something directly relevant to deployment workflows.

Prerequisites

  • Python 3.10 or higher
  • Basic familiarity with async programming
  • An MCP-compatible client (Claude Desktop is the easiest to start with)

Setting Up

# Install uv package manager (if you don't have it)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create a new project
mkdir site-checker-mcp
cd site-checker-mcp
uv init
uv add mcp httpx

The Server Code

# server.py
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
import httpx
import time

app = Server("site-checker")

@app.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="check_site_status",
            description="Check if a website is up and measure response time",
            inputSchema={
                "type": "object",
                "properties": {
                    "url": {
                        "type": "string",
                        "description": "The URL to check (e.g., https://deployhq.com)"
                    }
                },
                "required": ["url"]
            }
        ),
        types.Tool(
            name="check_ssl_expiry",
            description="Check when a site's SSL certificate expires",
            inputSchema={
                "type": "object",
                "properties": {
                    "hostname": {
                        "type": "string",
                        "description": "The hostname to check (e.g., deployhq.com)"
                    }
                },
                "required": ["hostname"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    if name == "check_site_status":
        url = arguments["url"]
        if not url.startswith("http"):
            url = f"https://{url}"

        try:
            async with httpx.AsyncClient(follow_redirects=True) as client:
                start = time.monotonic()
                response = await client.get(url, timeout=10)
                elapsed = (time.monotonic() - start) * 1000

            result = f"Site: {url}\n"
            result += f"Status: {response.status_code}\n"
            result += f"Response time: {elapsed:.0f}ms\n"
            result += f"Final URL: {response.url}\n"

            if response.status_code == 200:
                result += "Verdict: ✓ Site is up"
            elif response.status_code >= 500:
                result += "Verdict: ✗ Server error"
            elif response.status_code >= 400:
                result += "Verdict: ⚠ Client error (check URL)"
            else:
                result += f"Verdict: Responded with {response.status_code}"

        except httpx.TimeoutException:
            result = f"Site: {url}\nVerdict: ✗ Timed out after 10s"
        except httpx.ConnectError:
            result = f"Site: {url}\nVerdict: ✗ Connection refused"
        except Exception as e:
            result = f"Site: {url}\nVerdict: ✗ Error: {str(e)}"

        return [types.TextContent(type="text", text=result)]

    elif name == "check_ssl_expiry":
        import ssl
        import socket
        from datetime import datetime

        hostname = arguments["hostname"]
        try:
            ctx = ssl.create_default_context()
            with ctx.wrap_socket(
                socket.socket(), server_hostname=hostname
            ) as s:
                s.settimeout(5)
                s.connect((hostname, 443))
                cert = s.getpeercert()

            expiry = datetime.strptime(
                cert["notAfter"], "%b %d %H:%M:%S %Y %Z"
            )
            days_left = (expiry - datetime.utcnow()).days

            result = f"Host: {hostname}\n"
            result += f"Expires: {expiry.strftime('%Y-%m-%d')}\n"
            result += f"Days remaining: {days_left}\n"

            if days_left < 7:
                result += "Verdict: ✗ CRITICAL — renew immediately"
            elif days_left < 30:
                result += "Verdict: ⚠ Expiring soon — renew this month"
            else:
                result += "Verdict: ✓ Certificate is valid"

        except Exception as e:
            result = f"Host: {hostname}\nVerdict: ✗ Error: {str(e)}"

        return [types.TextContent(type="text", text=result)]

    raise ValueError(f"Unknown tool: {name}")

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

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

This server exposes two tools: one that checks whether a URL is responding (and how fast), and another that checks SSL certificate expiry. Both are immediately useful for anyone managing deployed web applications.

Connecting to Claude Desktop

Edit (or create) the config file:

  • Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "site-checker": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/site-checker-mcp", "server.py"]
    }
  }
}

Restart Claude Desktop. You should see the site-checker tools available. Try asking: Is deployhq.com up? And when does the SSL certificate expire?

Connecting to Claude Code

For Claude Code, add the server to your project's .mcp.json:

{
  "mcpServers": {
    "site-checker": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/site-checker-mcp", "server.py"]
    }
  }
}

The same server works in both Claude Desktop and Claude Code — that's the point of a standard protocol.

Real-World Use Cases for Deployment Workflows

The site-checker example is a starting point. Here's what production MCP servers look like:

Deployment Automation

An MCP server for your deployment platform can let AI assistants:

  • Trigger zero-downtime deployments based on natural language requests
  • Roll back problematic releases with one-click rollback
  • Schedule deployments for specific times
  • Compare configurations between staging and production environments

Log Analysis and Debugging

Instead of manually sifting through deployment logs:

  • Why did the last deployment fail?
  • Show me all warnings from production deployments this week
  • Analyze error patterns in staging deployments over the last month

Database and Infrastructure Monitoring

MCP servers for your databases and monitoring tools let you:

  • Check server health with natural language queries
  • Get alerts about unusual patterns
  • Compare performance metrics across deployments
  • Run read-only queries without switching tools

CI/CD Pipeline Integration

MCP servers can bridge your AI coding assistants with your entire CI/CD pipeline. An agent running in your terminal can check deployment status, review build logs, and trigger rollbacks — all without leaving the conversation. The 6 Must-Have MCP Servers for Web Developers covers production-ready servers worth adding to your setup, and our guide to agentic workflows in CI/CD shows how these tools fit into automated pipelines.

Security Considerations

MCP servers can read data and execute actions on your behalf. Take security seriously:

  1. Principle of least privilege: Only expose the capabilities the AI actually needs. A read-only deployment status server doesn't need write access
  2. Input validation: Sanitize all inputs. An AI might pass unexpected values — validate URLs, sanitize query parameters, reject malformed requests
  3. Authentication: For servers that access sensitive systems, require API tokens and verify them on every request
  4. Rate limiting: Prevent abuse — especially for servers that trigger expensive operations like deployments
  5. Audit logging: Track every tool call. When an AI triggers a deployment, you need to know who asked for it and when

Best Practices

For stdio servers (local, run as a subprocess):

  • Never write to stdout — it corrupts JSON-RPC messages. Use stderr for logging
  • Test with the MCP Inspector tool before connecting to a real client
  • Handle process shutdown gracefully (SIGTERM, SIGINT)

For HTTP servers (remote, run over the network):

  • Use HTTPS in production
  • Implement proper CORS headers if clients connect from browsers
  • Consider Streamable HTTP transport for long-running operations

General guidelines:

  • Keep tool names descriptive: check_site_status is better than check
  • Write clear descriptions — the AI uses them to decide when to call your tool
  • Use JSON Schema for input validation — catch errors before they reach your code
  • Return structured, parseable responses — the AI works better with consistent output formats
  • Start with one or two tools and expand. A server with 50 tools is harder to maintain and harder for the AI to use effectively

Building MCP Servers in Other Languages

Python is the quickest way to prototype, but MCP has official SDKs for:

  • TypeScript/JavaScript: @modelcontextprotocol/sdk — the most mature SDK, used by most production servers
  • Python: mcp — great for prototyping and data-heavy servers
  • C#/.NET: For enterprise environments
  • Java/Kotlin: For JVM-based infrastructure
  • Go: Community SDK, gaining traction for infrastructure tooling

We've covered building production MCP servers in detail:


MCP is the infrastructure layer that makes AI coding assistants genuinely useful beyond code generation. Instead of an AI that can only read and write files, you get one that can check deployment status, query databases, manage content, and trigger workflows — all through a protocol that works across every major AI tool.

Start with the site-checker example above, connect it to Claude Desktop, and see how it feels to ask your AI assistant about your infrastructure instead of switching to a dashboard. Once you see the pattern, you'll find dozens of services in your workflow that benefit from an MCP server.

Ready to streamline your deployment workflow? DeployHQ deploys to any server you own — VPS, cloud, or bare metal — with zero-downtime deployments, build pipelines, and a CLI built for automation. Get started for free.

For questions or feedback, reach out at support@deployhq.com or on Twitter/X.