Do you want to know how complicated it is to create an MCP server in python using the official MCP package?

It’s complicated because:

  • the MCP server should run over stdio
  • transport belongs outside
  • tools live inside
  • and HTTP is just a gateway layer

This blog explains how I rewired my architecture.


The Architecture

mcparchitecture

The application is composed of:

  • MCP Server

  • HTTP Gateway

Layer 1: The MCP Server

This is where:

  • tables are queried
  • tools are registered
  • SQLite is accessed
  • results are returned

For example:

import sqlite3
from contextlib import contextmanager
from typing import List, Dict, Any

from mcp.server import Server
from mcp.types import TextContent, Tool, ListToolsResult, ListToolsRequest

DB_PATH = "data_lake_config.db"

@contextmanager
def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row
    try:
        yield conn
    finally:
        conn.close()


# ----- Core table logic -----

def get_last_import_date(table_name: str, business_line: str) -> Dict[str, Any]:
    with get_db() as conn:
        cursor = conn.cursor()
        cursor.execute(
            f"SELECT {environment} FROM table_config WHERE table_name=? AND business_line=?",
            (table_name, business_line),
        )
        row = cursor.fetchone()
        if not row or row[0] is None:
            return {"exists": False, "last_imported": None}
        return {"exists": True, "last_imported": row[0]}

def get_table_status(table_name: str, business_line: str) -> Dict[str, Any]:
    with get_db() as conn:
        cursor = conn.cursor()
        cursor.execute(
            "SELECT table_name, business_line, dev, st, uat, prod "
            "FROM table_config WHERE table_name=? AND business_line=?",
            (table_name, business_line),
        )
        row = cursor.fetchone()
        if not row:
            return {"found": False}
        return {"found": True}


def list_tables(business_line: str = None) -> List[Dict[str, Any]]:
    with get_db() as conn:
        cursor = conn.cursor()
        cursor.execute(
            "SELECT table_name, business_line FROM table_config"
        )
        rows = cursor.fetchall()
        tables = []
        for r in rows:
            tables.append({
                "table_name": r["table_name"],
                "business_line": r["business_line"],
            })
        return tables


# ----- MCP Server Setup -----

server = Server(name="demo-mcp", version="0.1.0")


# ----- Tool wrapper functions -----

async def tool_get_last_import_date(table_name: str, business_line: str, environment: str):
    return [
        TextContent(type="text", text=str(get_last_import_date(table_name, business_line, environment)))
    ]

tool_get_last_import_date.description = "Get last import date"
tool_get_last_import_date.inputSchema = tool_check_table_exists.inputSchema

async def tool_get_table_status(table_name: str, business_line: str):
    return [
        TextContent(type="text", text=str(get_table_status(table_name, business_line)))
    ]

tool_get_table_status.description = "Get complete table status"
tool_get_table_status.input_schema = {
    "type": "object",
    "properties": {
        "table_name": {"type": "string"},
        "business_line": {"type": "string"}
    },
    "required": ["table_name", "business_line"]
}

async def tool_list_tables(business_line: str = None, environment: str = None):
    return [
        TextContent(type="text", text=str(list_tables(business_line, environment)))
    ]

tool_list_tables.description = "List all tables"
tool_list_tables.input_schema = {
    "type": "object",
    "properties": {
        "business_line": {"type": "string"},
        "environment": {"type": "string", "enum": ["dev", "st", "uat", "prod"]}
    },
    "required": ["business_line", "environment"]
}

# ----- Register tools -----

server.call_tool()(tool_get_last_import_date)
server.call_tool()(tool_get_table_status)
server.call_tool()(tool_list_tables)


# ----- ListTools handler -----

@server.list_tools()
async def list_all_tools(_req: ListToolsRequest = None):
    """
    Always return all available tools in the proper MCP format.
    """
    return ListToolsResult(tools=[
        Tool(
            name=func.__name__,
            title=func.__name__.replace("_", " ").title(),
            description=getattr(func, "description", ""),
            inputSchema=getattr(func, "inputSchema", {})
        )
        for func in [
            tool_get_last_import_date,
            tool_get_table_status,
            tool_list_tables
        ]
    ])

The magic is in the Tool wrapper functions that describe the functions, input, output, etc.

Other piece of magic is the list_all_tools function that will be called from the HTTP Gateway to register the functions in the MCP Server

Layer 2: The HTTP Gateway

he gateway is thin by design.

It only:

  • receives HTTP POST MCP messages
  • forwards them to the stdio server
  • sends back results

That’s it.

It never contains:

  • database code
  • tool logic (sort off)
  • schemas
  • MCP state

It is a stateless transport wrapper that Azure or Docker can run.

For example:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from mcp import types
import uvicorn
from mcp_server import server

app = FastAPI(title="Demo MCP Gateway")


@app.post("/mcp")
async def mcp_proxy(request: Request):
    body = await request.json()
    method = body.get("method")
    request_id = body.get("id")

    # ----- Initialize -----
    if method == "initialize":
        return JSONResponse({
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {
                "protocolVersion": "2024-11-05",
                "capabilities": {"tools": {}},
                "serverInfo": {"name": server.name, "version": server.version}
            }
        })

    # ----- List tools -----
    if method == "tools/list":
        # Look up the registered handler
        handler = server.request_handlers.get(types.ListToolsRequest)
        if handler is None:
            return JSONResponse({"jsonrpc": "2.0", "id": request_id, "result": {"tools": []}})

        # Call the MCP server list_tools handler
        server_result = await handler(None)
        list_result = server_result.root
        tools_list = [
            {"name": t.name, "description": t.description, "inputSchema": t.inputSchema}
            for t in list_result.tools
        ]
        print("Registered tools:", tools_list)
        return JSONResponse({
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {"tools": tools_list}
        })

    # ----- Call a tool -----
    if method == "tools/call":
        params = body.get("params", {})
        tool_name = params.get("name")
        arguments = params.get("arguments", {})

        # Dispatch to MCP server call_tool handler
        handler = server.request_handlers.get(types.CallToolRequest)
        if handler is None:
            return JSONResponse({
                "jsonrpc": "2.0", "id": request_id,
                "error": {"code": -32601, "message": "tool handler not registered"}
            })

        server_result = await handler(types.CallToolRequest(params=types.CallToolParams(name=tool_name, arguments=arguments)))
        result = server_result.result

        # Build standard MCP response
        return JSONResponse({
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {
                "content": [{"type": c.type, "text": c.text} for c in result.content]
            }
        })

    # ----- Unknown method -----
    return JSONResponse({
        "jsonrpc": "2.0",
        "id": request_id,
        "error": {"code": -32601, "message": f"Unknown method: {method}"}
    })

if __name__ == "__main__":
    # Run server
    uvicorn.run(app, host="0.0.0.0", port=8000)

Testing it all out

First, we start the server.

The we register the server in VS Code as MCP Server.

If everything is ok then the logs should look like:

[info] Starting server from LocalProcess extension host
[info] Connection state: Running
[info] Discovered 3 tools

And now it’s ready to use in VS Code.