Text-to-SQL Agent with Docker MCP and Model Runner | Smarter and Safer

MCP-TOOLKIt

A containerized approach to natural language database queries with built-in safety and auditability

Author: Vishesh Sharma and Karan Singh

Date: November 12th, 2025

Core

  • The original Text-to-SQL setup worked but wasn’t scalable or secure.
  • The new Model Context Protocol (MCP) introduces modular, isolated layers.
  • Each service has a single role , the API orchestrates, model generates, MCP executes safely.
  • All components run in Docker for consistent, reproducible deployment.
  • The result is a secure, scalable, and auditable Text-to-SQL system that translates natural language into reliable SQL.

Introduction

When this project started, the goal was to make it possible to ask a question in plain English and get an accurate, safe SQL query in return. The earliest version achieved exactly that; it could translate natural language into structured data retrieval using a large language model (LLM). The earlier design proved fast but fragile, efficient but limited. The introduction of the Model Context Protocol (MCP) reshaped the entire system into something modular, traceable, and secure. This post explores that update and what changed and why it matters.


The Earlier Setup

The first version of the Text-to-SQL pipeline worked beautifully on paper. It relied on a FastAPI service, a model runner, and a SQLite database, all wired together inside a double Docker setup. The user sent a question to the API, which built a structured prompt using table metadata and passed it to the model. The model returned an SQL statement, the system validated it for safety, and the database executed it in read-only mode.

Everything ran inside containers:

  • The API container handled requests, prompt construction, SQL validation, and query execution.
  • The model container ran Ollama with the mistral:latest model.
  • The database was there locally, mounted as a read-only file.

At first look , it felt efficient as one connected chain where each part did its job quickly. For smaller workloads or local demos, this setup worked perfectly. You could run everything with a single command and start asking questions instantly.

Figure 1 — Earlier System Architecture: The modular design with separate components for prompt building, LLM execution, SQL validation, query execution, and logging, all coordinated through Docker.

The Limitations of the Earlier Setup 

The architecture had no concept of concurrency, multi-user access, or proper isolation between components. Everything depended on one process staying healthy, and that process lived inside the API container.As soon as the system started handling real workloads like larger datasets, and repeated queries  its inefficiency struck out. What was good for a demo wasn’t ready for day-to-day use.

The core issue was responsibility overload. The API container had to do everything at once: build prompts, talk to the model, validate generated SQL, run queries on the database, and then log every result. It was a single brain trying to think, act, and remember all at the same time. When traffic grew or responses slowed, that central role appeared as a bottleneck. The API couldn’t scale independently because model calls and database access were hard-wired together

The architecture worked fine for controlled environments but was too fragile for anything resembling a real-world deployment. It needed to structure something that could separate logic, data, and model operations into clearly defined layers. Here comes the MCP !!

The Core Parts of MCP

After setting up the base system, three main parts make everything work smoothly and safely: the MCP Toolkit, the MCP Gateway, and the MCP Server. Each has a single job and they all connect like pieces of a simple chain. Together, they keep the system clean, reliable, and easy to follow.

MCP Toolkit

The Toolkit is the first stop when the main app needs to talk to the database. Instead of reaching out directly, the app sends its request to the Toolkit.
The Toolkit understands what the app is asking for and passes it along in the right way. It can ask for table details or run a read-only query. This makes the app simpler because it no longer has to deal with the database directly. The Toolkit acts like a helpful middle layer that keeps communication clear and safe.

MCP Gateway

The Gateway sits in the middle of the system. Its job is to check that every request follows the right steps and is sent in the right format.
You can think of the Gateway as a checkpoint. It doesn’t change the data or rewrite anything, but it makes sure that what passes through is correct and safe to send forward. This helps the whole setup stay organized, especially when there are many requests happening at the same time.

MCP Server

The Server is where the actual database work happens. It is the only part that talks directly to the database.
When a request reaches the Server, it carefully runs the query and sends the results back. It is designed to read data only, never to change or delete it. This rule makes the database secure and ensures that even if something goes wrong elsewhere, the stored information stays safe.

How They Work Together

  • The app sends a request to the Toolkit.
  • The Toolkit forwards the request to the Gateway.
  • The Gateway passes it along to the Server.
  • The Server runs the query on the database.
  • The results move back through the Server → Gateway → Toolkit → App, and finally reach the user.

Each part knows its own role, and nothing overlaps. This makes the setup easier to understand, easier to fix, and easier to expand when more users or databases are added later.

 The Model Context Protocol (MCP)

The next phase of the project introduces a new layer between the API and the database that is the Model Context Protocol. MCP redefined how components talked to each other. Instead of having one monolithic pipeline, the system was reorganized into small, well-defined services:

  • MCP Toolkit: The API’s local interface for schema retrieval and SQL execution.
  • MCP Gateway: A proxy layer that routes all requests safely and ensures standardization.
  • MCP Server: Handles actual database communication and enforces read-only execution.
  • Database: Still local or containerized, but now fully isolated behind the MCP chain.

This structure means the model runner and the API no longer touch the database directly. Every query passes through a managed route that provides logging, schema introspection, and validation.

The MCP chain acts like a compartment between the intelligent model and the sensitive data it needs. Each step has a clear responsibility, which keeps the system modular, auditable, and secure.

How the New Workflow Operates

When a user asks a question, the journey now looks like this:

  1. The user sends a natural-language question to the FastAPI service.
  2. The API requests the schema from the MCP Toolkit, which forwards it through the Gateway to the MCP Server.
  3. The MCP Server fetches metadata from the database and returns it along the same path.
  4. The API builds a prompt for the Model Runner (Mistral through Ollama).
  5. The model generates a SQL query based on schema context.
  6. The SQL Firewall checks the query for safety, structure, and table validation.
  7. The validated query goes back through the MCP Toolkit → Gateway → Server → Database, where it executes in read-only mode.
  8. The results follow the reverse path, reaching the user through the API.
  9. Every step — input, SQL, and output — is logged by the Artifact Logger.

This creates a smooth but strongly governed flow where no single container can break isolation. Each service can restart, scale, or update independently without affecting others.

MCP Workflow

Figure 2 — Text-to-SQL Request–Response Workflow (MCP Pipeline):
The new MCP-based workflow illustrating how the API, model runner, and MCP layer (Toolkit, Gateway, Server, Database) interact to securely process and validate user queries.

Docker Compose: The Heart of the MCP Setup

With the Model Context Protocol (MCP) in place, the system is no longer a single stack.It now consists of multiple independent yet connected services and each with a clearly defined purpose.This is where Docker Compose becomes the centerpiece.

Instead of manually launching every container, Docker Compose acts as the conductor that starts, manages, and connects all these services together.
It ensures they start in the right order, share the right environment variables, and talk to one another seamlessly.

When you run

docker-compose up –build

Everything from the FastAPI layer to the database now comes alive in the right sequence. Each part knows only what it needs to know and nothing more.

How the Setup Works

Refer to the Github Repo with MCP branch

In this architecture, each service represents a single responsibility in the Text-to-SQL pipeline.They all live inside their own containers but communicate over internal Docker networks using well-defined URLs.

Let’s break it down layer by layer.

1. API Service 

Purpose:

This is the main entry point where users interact with the system. It’s a FastAPI-based service that receives natural language questions, builds prompts for the model, validates the generated SQL, and sends it to the MCP layer for execution.

How it fits in:

  • Talks to the model runner (model-runner) to generate SQL.
  • Sends validated queries to the MCP Toolkit (mcp-toolkit) for safe execution.
  • Logs all requests and responses for traceability.

Port: 8000

In the earlier setup, the API handled everything from model calls, database queries, to logging. Now, it just coordinates and delegates those jobs, keeping things clean and secure.

2. Model Runner 

Purpose:
This container runs the large language model (LLM) locally using Ollama and the mistral:latest model. It’s the system’s “brain,” responsible for converting human questions into SQL statements.

How it fits in:

  • Receives prompt data from the API.
  • Returns a generated SQL query.
  • Stays isolated and it doesn’t access any database directly.

Port: 11434 and  Image: ollama/ollama:latest

To make startup smooth, the model runner uses a health check, Docker waits until the model is fully loaded before letting the API start.That way, the queries never fail due to “model not ready” errors.

3. MCP Toolkit 

Purpose:
The MCP Toolkit is the middleman between the API and the rest of the MCP chain.
It takes API requests and translates them into MCP-compliant messages for the Gateway and Server layers.

How it fits in:

  • The API never talks to the database directly; it only talks to this toolkit.
  • Fetches schema information.
  • Sends SQL queries for execution.
  • Simplifies communication by exposing simple routes like /schema and /query.

Port: 8002

Think of it as a behaving as a translator  it speaks both “API language” and “MCP language.”

4. MCP Gateway 

Purpose:
The Gateway controls how the Toolkit communicates with the actual database layer.
It ensures every request passes through proper validation, logging, and protocol checks.

How it fits in:

  • Routes schema and query requests to the MCP Server.
  • Keeps the communication standardized and structured.

Port: 8001

You can imagine this as a custom traffic controller and nothing reaches the database unless the Gateway approves it.

5. MCP Server 

Purpose:
This is the final stop before the database.The MCP Server executes validated SQL queries in a read-only mode and returns results safely.

How it fits in:

  • Talks only to the database, not the API or model directly.
  • Uses environment variables for credentials (DB_URL).
  • Ensures no unsafe SQL (like DROP, UPDATE, or DELETE) can be executed.

Port:9000

By isolating this layer, even if something goes wrong in the API or model, the data itself remains protected.

6. Database 

Purpose:
The heart of the system’s information.In this case, it’s a PostgreSQL database initialized with a simple dataset.It can be replaced with  other SQL engine .

How it fits in:

  • Only the MCP Server can talk to it.
  • Initializes with init_db.sql and runs a health check.
  • Fully containerized for consistency across environments.

Port: 5432 and  Image: postgres:15-alpine

The Startup Order

Docker Compose ensures everything starts in a chain of dependencies:

Figure 3 — Docker Compose Service Chain:
Overview of all containers in the MCP architecture, including startup order and interdependencies between API, model runner, MCP Toolkit, Gateway, Server, and Database

The system waits for each service to report healthy status before moving to the next one.This guarantees that when the API finally goes live, the model, MCP layers, and database are all ready.

What Makes This Setup Different

The biggest shift from the earlier setup is separation and safety. Before, the API was overloaded and  it did everything.Now, every container has a single focus:

  • The API coordinates.
  • The model runner generates.
  • The MCP chain protects and executes.
  • The database stores data safely behind multiple layers.
ServicePurpose/RolePortDepends On
APIUser entry, orchestration and validation8000model-runner,mcp-toolkit
Model RunnerRuns the LLM(curently Mistral via Ollama)11434N/A
MCP ToolkitBridges API with MCP layers8002mcp-gateway
MCP GatewayRoutes and validates requests8001mcp-server
MCP ServerExecutes SQL safely on DB9000database
DatabaseStores and provides structured data5432N/A
Table 1 — Service Overview and Dependencies:
Summary of all Docker Compose services in the MCP-based Text-to-SQL architecture, outlining each component’s purpose, exposed port, and dependency chain for system orchestration.

Inside the MCP Layer 

The MCP layer is where most of the magic happens. It forms the secure bridge between the application logic (API + Model) and the actual database.
Instead of letting the model or API run SQL directly, everything now passes through three well-defined components the Toolkit, Gateway, and Server.

Together, they act like a controlled relay system:the Toolkit asks, the Gateway routes, and the Server executes, always in read-only mode.

1. The MCP Toolkit 

The Toolkit is the API’s assistant inside the MCP layer.Whenever the FastAPI service needs to fetch a schema or execute a SQL query, it doesn’t reach the database, it sends the request here first.

Here’s how it looks in code:

# mcp_toolkit.py
import httpx
from typing import Dict, Any
 class MCPToolkit:
    “””Toolkit for interacting with the MCP Gateway.”””
    def __init__(self, base_url: str = “http://mcp-gateway:8001”):
        self.base_url = base_url
 
    async def get_schema(self) -> Dict[str, Any]:
        “””Fetch database schema through the MCP Gateway.”””
        async with httpx.AsyncClient() as client:
            resp = await client.post(f”{self.base_url}/schema”, json={})
            resp.raise_for_status()
            return resp.json()
 
    async def run_query(self, sql: str) -> Dict[str, Any]:
        “””Execute SQL query through the MCP Gateway.”””
        async with httpx.AsyncClient() as client:
            resp = await client.post(f”{self.base_url}/query”, json={“sql”: sql})
            resp.raise_for_status()
            return resp.json()
})            resp.raise_for_status()            return resp.json()

What this does:

  • The Toolkit exposes two async methods, one for schema retrieval, one for query execution.
  • Both requests go to the Gateway (http://mcp-gateway:8001), never the database.
  • It returns clean, structured JSON responses that the API can consume.

Essentially, this file gives the FastAPI layer a simple, safe interface to interact with the database world indirectly.

2. The MCP Gateway 

Unlike a typical API router, the MCP Gateway doesn’t interpret or modify SQL.
Its only job is to securely pass messages between the outer world (Toolkit or API) and the inner world (MCP Server and database).Think of it as a custom traffic controller, every request passes through it, but it never opens the payload or touches the data directly.

Here’s the core logic:

from fastapi import FastAPI, Request
import httpx, os
app = FastAPI(title=”Local MCP Gateway”)
MCP_SERVER_URL = os.getenv(“MCP_SERVER_URL”, “http://mcp-server:9000”)
 
@app.post(“/{path:path}”)
async def proxy_post(path: str, request: Request):
    “””Forward POST requests to MCP server (e.g., /query, /schema).”””
    print(f”Proxying POST to MCP server: {path}”)
    try:
        data = await request.json()
    except Exception:
        data = {}
 
    try:
        async with httpx.AsyncClient() as client:
            resp = await client.post(f”{MCP_SERVER_URL}/{path}”, json=data)
            resp.raise_for_status()
            return resp.json()
    except Exception as e:
        return {“error”: str(e)}
 
@app.get(“/health”)
def health():
    “””Health check endpoint.”””
    return {“status”: “ok”}

What’s happening here

  • The route @app.post(“/{path:path}”) dynamically captures any incoming path (/schema, /query, or even /metadata) and forwards it to the MCP Server.
  • It acts like a universal POST proxy, using httpx for async forwarding.
  • If the body isn’t valid JSON, it safely defaults to {} instead of crashing.
  • Any error raised downstream is caught and returned in a simple, consistent JSON format.
  • The /health endpoint makes it easy for Docker or Compose to check that the gateway is alive.

3. The MCP Server 

The Server is the only component that talks directly to the database. It runs in a read-only mode, executes validated SQL, and returns the result as structured JSON

read-only mode, executes validated SQL, and returns the result as structured JSON.

import psycopg2, os 
# Database connection (Postgres)
DB_URI = os.getenv(“DB_URI”, “postgresql://postgres:password@db:5432/text2sqldb”)
 
# Create a single global connection but keep autocommit to avoid transaction issues
conn = psycopg2.connect(DB_URI)
conn.autocommit = True  #  prevents “current transaction is aborted” errors
@app.post(“/query”)
def run_query(request: SQLQuery):
    “””Execute a SQL query and return rows.”””
    sql = request.sql.strip()
    if not sql:
        return {“error”: “Empty SQL query.”}
 
    try:
        with conn.cursor() as cur:
            cur.execute(sql)
            try:
                rows = cur.fetchall()
            except psycopg2.ProgrammingError:
                # e.g. for INSERT/UPDATE/DELETE that have no results
                rows = []
        return {“rows”: rows}
    except Exception as e:
        conn.rollback()
        return {“error”: str(e)}

What this does:

  • Defines strict rules: only SELECT queries are permitted.
  • Uses SQLAlchemy to execute SQL safely against the database.
  • Converts query results into a list of JSON rows.
  • Returns structured output to the Gateway → Toolkit → API.

This ensures no matter what happens at the top layers, the database remains secure and untouched by unsafe operations.

4. The Chain in Action

Refer to the Github Repo with MCP branch

Here’s how a single query moves through the MCP layer in real time:

  1. API → Toolkit: “Fetch schema or run this SQL.”
  2. Toolkit → Gateway: “Forwarding structured request.”
  3. Gateway → Server: “Executing validated command.”
  4. Server → Gateway: “Here are your results.”
  5. Gateway → Toolkit → API → User: “Final answer in JSON.”

Each part does one job and nothing more. This keeps logs clean, failures contained, and security intact.

Comparing the Earlier Setup with the MCP Pipeline

The difference between the two architectures is not just structural but it also reflects a shift.

AspectEarlier ArchitectureMCP-Based Architecture
System DesignSingle linear pipeline where the API, model, and database were all directly connected.Layered and modular setup where the API, Model Runner, and MCP components (Toolkit, Gateway, Server) work independently.
Database AccessThe API had full access to the database and its credentials.Database access is limited to the MCP Server, and the API interacts only through controlled MCP routes.
ScalabilityScaling meant duplicating the entire system, including the model and database.Each service can scale separately, allowing independent scaling of the API or model without affecting others.
Failure HandlingA crash or timeout in one process could stop the entire system.Failures are contained within each service, which can restart or recover without disturbing others.
Concurrency and Multi-User SupportBuilt for a single user or demo use, without concurrency management.Supports multiple users and parallel queries through containerized services.
Deployment ModeCompact and quick to launch but only suitable for local testing or demos.Designed for production environments, secure, reproducible, and easy to deploy in the cloud.
MaintenanceDifficult to debug or update since one issue could disrupt the whole setup.Easier to maintain with clear separation of responsibilities between components.
Data SafetyThe risk of accidental data modification due to open access.Read-only operation and strict validation ensure data safety through the MCP chain.
Table 2 — Comparison Between Earlier Architecture and MCP-Based Pipeline:
Summary of how the MCP architecture replaces a tightly connected system with a structured, secure, and scalable setup that supports multi-user access and improves reliability.

In practice, this means the system can now grow, adapt, and recover gracefully. It’s the difference between a quick demo and a real, production-ready data service.

The Future of MCP and AI-Driven Data Systems

MCP Architecture
Figure 4 — Future Evolution of MCP Architecture:
A distributed multi-chain Model Context Protocol setup connected through a unified API, with adaptive governance and feedback loops for performance, security, and auditability.



Looking ahead, this approach opens several exciting possibilities. Multiple databases can be connected through independent MCP Servers, all working under a unified API. Logs and context traces could evolve into a learning system that improves model performance automatically. Governance and compliance features, like role-based access and audit trails, could make MCP-based pipelines ready for enterprise environments without extra complexity.

As AI becomes part of critical workflows, trust will matter more than speed. The MCP framework provides that trust by turning intelligence into something accountable and traceable. It allows innovation to scale without losing control striking a rare balance in modern AI development.

Conclusion

What began as a simple Text-to-SQL project has evolved into a model for how AI systems should think and behave. The earlier setup proved that natural-language querying could work, but the MCP-based version showed how to make it safe, traceable, and sustainable.

Each part of the system now plays a clear role , the model interprets, the API orchestrates, the firewall safeguards, and the MCP chain executes with precision. Together, they form a connected yet contained ecosystem that builds confidence into every interaction.

This evolution isn’t just technical progress; it’s a reflection of how AI infrastructure is maturing. Intelligence alone isn’t enough anymore and context and control define reliability. The Model Context Protocol gives us that framework, showing how to combine innovation with discipline.

It’s a small but important step toward the kind of systems that don’t just think for us they think responsibly, within boundaries we can trust.

References

Authors

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *