| 3 min read

Building an MCP Server for Claude Desktop Integration

MCP Claude Desktop Model Context Protocol AI tools Python integration

What Is MCP and Why Build a Server?

The Model Context Protocol (MCP) is an open standard that lets AI assistants like Claude Desktop connect to external tools and data sources. By building an MCP server, you give Claude the ability to query your databases, call your APIs, read your files, and execute your custom functions, all through a standardized interface.

I built an MCP server that connects Claude Desktop to my project management tools, code repositories, and deployment systems. Here is the complete walkthrough.

MCP Architecture Overview

An MCP server exposes three types of capabilities to the client:

  • Tools: Functions that Claude can call (like querying a database or deploying code)
  • Resources: Data sources that Claude can read (like files or API endpoints)
  • Prompts: Reusable prompt templates for common workflows

The server communicates over stdio or HTTP using JSON-RPC. Claude Desktop handles the client side automatically once you configure the server.

Project Setup

# Install the MCP SDK
pip install mcp

# Project structure
mcp-server/
  server.py
  tools/
    database.py
    deployment.py
    github.py
  resources/
    projects.py
  config.json

Building the Server

from mcp.server import Server
from mcp.types import Tool, TextContent, Resource
import json

server = Server("my-tools")

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="query_database",
            description="Run a read-only SQL query against the project database",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "SQL SELECT query to execute"
                    },
                    "database": {
                        "type": "string",
                        "enum": ["projects", "analytics", "logs"],
                        "description": "Which database to query"
                    }
                },
                "required": ["query", "database"]
            }
        ),
        Tool(
            name="deploy_service",
            description="Deploy a service to staging or production",
            inputSchema={
                "type": "object",
                "properties": {
                    "service": {"type": "string"},
                    "environment": {
                        "type": "string",
                        "enum": ["staging", "production"]
                    },
                    "version": {"type": "string"}
                },
                "required": ["service", "environment"]
            }
        )
    ]

Implementing Tool Handlers

import sqlite3

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "query_database":
        return await handle_database_query(arguments)
    elif name == "deploy_service":
        return await handle_deployment(arguments)
    else:
        raise ValueError(f"Unknown tool: {name}")

async def handle_database_query(args: dict) -> list[TextContent]:
    query = args["query"].strip()
    
    # Safety check: only allow SELECT queries
    if not query.upper().startswith("SELECT"):
        return [TextContent(
            type="text",
            text="Error: Only SELECT queries are allowed"
        )]
    
    db_path = DATABASE_PATHS[args["database"]]
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    
    try:
        cursor = conn.execute(query)
        rows = [dict(row) for row in cursor.fetchall()]
        return [TextContent(
            type="text",
            text=json.dumps(rows, indent=2, default=str)
        )]
    except Exception as e:
        return [TextContent(type="text", text=f"Query error: {str(e)}")]
    finally:
        conn.close()

Adding Resources

Resources let Claude read data without executing a tool. I expose project documentation and configuration files as resources.

@server.list_resources()
async def list_resources() -> list[Resource]:
    return [
        Resource(
            uri="projects://active",
            name="Active Projects",
            description="List of currently active projects with status",
            mimeType="application/json"
        ),
        Resource(
            uri="config://deployment",
            name="Deployment Configuration",
            description="Current deployment configuration for all services",
            mimeType="application/json"
        )
    ]

@server.read_resource()
async def read_resource(uri: str) -> str:
    if uri == "projects://active":
        projects = load_active_projects()
        return json.dumps(projects, indent=2)
    elif uri == "config://deployment":
        config = load_deployment_config()
        return json.dumps(config, indent=2)
    raise ValueError(f"Unknown resource: {uri}")

Configuring Claude Desktop

To connect your MCP server to Claude Desktop, add it to the configuration file:

// ~/Library/Application Support/Claude/claude_desktop_config.json
// On Linux: ~/.config/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "my-tools": {
      "command": "python",
      "args": ["/path/to/mcp-server/server.py"],
      "env": {
        "DATABASE_PATH": "/path/to/databases"
      }
    }
  }
}

Running the Server

import asyncio
from mcp.server.stdio import stdio_server

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

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

Security Considerations

MCP servers execute code on your machine with your permissions. Security is critical:

  • Input validation: Never pass raw user input to shell commands or SQL queries without sanitization
  • Read-only by default: Start with read-only tools and add write capabilities only where necessary
  • Scope limiting: Restrict database access to specific tables and file access to specific directories
  • Audit logging: Log every tool invocation with timestamps and arguments
  • Rate limiting: Prevent runaway loops that could call your APIs thousands of times

What I Use It For Daily

My MCP server has become an essential part of my workflow. I ask Claude to check deployment status, query project metrics, look up error logs, and even trigger staging deployments. It saves me from constant context switching between terminal windows and dashboards.

The beauty of MCP is that once you build the server, Claude figures out how and when to use your tools based on the descriptions you provide. Write clear, specific tool descriptions, and Claude will use them effectively.