Return to Home
Deep Research

Database Agents with MCP and LangChain

Architecting Production-Grade Database Agents

A synergistic approach using Model Context Protocol and LangGraph

The MCP Foundation

The Model Context Protocol (MCP) is an open standard that solves the M×N integration problem. Instead of building custom connectors for every model-to-tool pair, MCP creates a universal interface, turning the problem into a scalable M+N solution.

🔌 The "USB-C for AI"

MCP standardizes communication between AI applications and external systems, creating a universal connector that eliminates the need for proprietary adapters. Any compliant AI agent can discover, understand, and utilize external tools in a consistent manner.

🏗️ Client-Server Architecture

MCP clients (AI agents) initiate requests for information or tool execution. MCP servers expose APIs, tools, or data sources, processing requests and returning structured responses.

⚙️ Core Primitives

Tools (executable functions), Resources (structured data streams), and Prompts (reusable instruction templates) form the foundation of MCP interactions.

sql_server.py - MCP Database Server Implementation
# sql_server.py
import sqlite3
from mcp.server.fastmcp import FastMCP
from sqlalchemy import create_engine, text
from langchain_community.utilities import SQLDatabase

# Initialize the MCP server with a descriptive name
mcp = FastMCP("DatabaseQueryServer")

# --- Database Connection Setup ---
DB_FILE = "Chinook.db"
engine = create_engine(f"sqlite:///{DB_FILE}")
db = SQLDatabase(engine=engine)

@mcp.tool()
def list_tables() -> str:
    """
    Returns a comma-separated list of available table names in the database.
    Use this tool first to understand what tables you can query.
    """
    return ", ".join(db.get_usable_table_names())

@mcp.tool()
def get_schema_for_tables(tables: str) -> str:
    """
    Returns the schema (CREATE TABLE statement) for a comma-separated list of table names.
    Example input: "Album, Artist"
    """
    try:
        table_list = [table.strip() for table in tables.split(',')]
        return db.get_table_info(table_names=table_list)
    except Exception as e:
        return f"Error retrieving schema: {e}."

@mcp.tool()
def execute_safe_query(sql_query: str) -> str:
    """
    Executes a read-only SQL query and returns the result.
    IMPORTANT: This tool ONLY supports SELECT statements.
    """
    if not sql_query.strip().upper().startswith("SELECT"):
        return "Error: Only SELECT queries are allowed."
    
    try:
        with engine.connect() as connection:
            result = connection.execute(text(sql_query))
            return str(result.fetchall())
    except Exception as e:
        return f"Error executing query: {e}"

if __name__ == "__main__":
    mcp.run()

The LangGraph Brain

LangChain has evolved from simple chains to LangGraph, a framework for building stateful, cyclic agents. This gives developers full, transparent control over the agent's reasoning loop with explicit state management and error recovery.

🧠 Explicit State

Define exactly what information the agent carries between steps, ensuring transparency and debuggability.

🔄 Cyclic Workflows

Create complex, multi-step reasoning loops that can adapt and recover from errors dynamically.

🎯 Production Ready

Full control over the agent's decision-making process enables robust error handling and observability.

agent_graph.py - LangGraph Workflow Implementation
# agent_graph.py
from typing import List, TypedDict
from langchain_core.messages import BaseMessage
from langgraph.prebuilt import create_react_agent
from langgraph.graph import StateGraph, END
from langchain_community.agent_toolkits import SQLDatabaseToolkit

# 1. Define Agent State
class AgentState(TypedDict):
    question: str
    chat_history: List
    agent_scratchpad: List

# 2. Setup Toolkit and Agent Runnable
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
tools = toolkit.get_tools()
agent_runnable = create_react_agent(llm, tools)

# 3. Define Graph Nodes
def run_agent_reasoning(state: AgentState):
    agent_outcome = agent_runnable.invoke(state)
    return {"agent_scratchpad": agent_outcome.return_values["messages"]}

def execute_tools(state: AgentState):
    tool_calls = state["agent_scratchpad"][-1].tool_calls
    # ... logic to execute tool calls ...
    return {"agent_scratchpad": [tool_output]}

def should_continue(state: AgentState):
    if "final_answer" in state["agent_scratchpad"][-1].content:
        return "end"
    return "continue"

# 4. Build the Graph
workflow = StateGraph(AgentState)
workflow.add_node("agent", run_agent_reasoning)
workflow.add_node("action", execute_tools)
workflow.set_entry_point("agent")
workflow.add_conditional_edges(
    "agent",
    should_continue,
    {"continue": "action", "end": END},
)
workflow.add_edge("action", "agent")

# 5. Compile and run
app = workflow.compile()
inputs = {"question": "How many employees are there?", "chat_history": []}
for s in app.stream(inputs):
    print(s)

Mastering Context Provisioning

The quality of generated SQL is entirely dependent on the context provided to the LLM. A successful strategy requires a multi-stage pipeline to overcome challenges like model hallucination, context window limits, and the semantic gap between technical schemas and business concepts.

Dynamic Schema Selection

To avoid overwhelming the LLM, the agent first identifies the relevant tables for a user's query. It then retrieves the schema for only that small subset of tables, keeping the context focused and efficient.

Semantic Layer

Enrich raw database schemas with business context using techniques like database comments, curated views, or a RAG pipeline on enterprise documentation. This bridges the gap between cryptic column names and their real-world meaning.

Few-Shot Prompting

Dynamically select a few high-quality examples of similar questions and their corresponding correct SQL queries. This guides the model on structure, dialect specifics, and formatting, improving accuracy.

Error Correction Tools

Equip the agent with retriever tools to correct common errors, like user misspellings of names in high-cardinality columns, before the SQL query is even generated.

📋 Context Provisioning Techniques Comparison

Each approach to context provisioning has distinct trade-offs in terms of implementation complexity, maintenance overhead, and effectiveness. Choose the right combination based on your database size, team capabilities, and accuracy requirements.

TechniqueDescriptionImplementationProsCons
Prompt EngineeringManually embedding descriptions of tables, columns, business rules, and value mappings directly into the agent's system prompt.The system_message string in the prompt template is augmented with detailed, human-readable explanations of the schema.Simple to implement for small, stable schemas; offers fine-grained control over the context provided.Becomes unmanageable and quickly exceeds token limits for large or evolving databases; context is hardcoded and difficult to maintain.
Database CommentsStoring metadata directly within the database using standard SQL COMMENT ON TABLE or COMMENT ON COLUMN statements.The database schema is modified to include these comments. The application must then include a function to extract these comments and inject them into the table_info string provided to the LLM.Metadata lives with the data, ensuring it is version-controlled and maintained alongside the schema itself.Requires database modification permissions; may not be expressive enough for complex business rules or multi-step logic.
RAG on DocumentationCreating a vector store from existing enterprise documentation, such as data dictionaries, business glossaries, or internal wikis. The agent is given a retriever tool to query this knowledge base.An embedding model (e.g., from Hugging Face) and a vector store (e.g., FAISS, Chroma) are used to index the documentation. A retriever tool is created that the agent can call to 'look up' the business meaning of a table or column before generating SQL.Can handle vast amounts of unstructured context; effectively decouples documentation from the agent's core prompt, making both easier to maintain.Adds architectural complexity (vector store, embedding model); introduces the possibility of retrieval errors or irrelevant context being returned.
Curated ViewsCreating simplified, denormalized database views with clean, human-readable column names (e.g., customer_status instead of cust_stat_cd) specifically for the LLM to query.This is a database administration task. The agent is then configured to only see and query these curated views, hiding the complexity of the underlying base tables.Drastically simplifies the schema the LLM has to reason about, which significantly improves accuracy and query generation performance.High initial setup and ongoing maintenance effort; may not cover all possible ad-hoc query needs, limiting flexibility.

💡 Key Strategy

Context provisioning is not just about providing more information—it's about providing the right information at the right time. The best agents use a multi-stage approach: schema discovery → semantic enrichment → few-shot guidance → error correction.

The Integrated Architecture

Fusing LangGraph and MCP creates a powerful, decoupled system. LangGraph acts as the orchestration "brain," managing the agent's state and reasoning. MCP provides the standardized "nervous system," allowing the brain to communicate with tool "limbs" via a universal protocol.

End-to-End Flow

User Interface
▼ HTTP Request
LangGraph Agent (Brain)
▼ MCP Tool Call (via Adapter)
MCP Client
▼ HTTP / stdio
Custom MCP Server (Limb)
▼ DB Operation
Database

🧠 Separation of Concerns

The orchestration logic (LangGraph) is completely separate from the tool implementation (MCP servers), enabling independent scaling and updates.

🔄 Tool Interoperability

Any MCP-compliant tool can be plugged into any LangGraph agent, fostering a modular ecosystem of AI-ready microservices.

Productionization & Security

Executing LLM-generated code against a database is inherently risky. A production-grade system requires a rigorous, defense-in-depth security posture, applying constraints at every layer of the stack. Relying on prompt instructions alone is not enough.

⚠️ Defense-in-Depth Strategy

Security cannot be an afterthought. Every layer—from database permissions to application logic to LLM prompts—must implement appropriate controls to prevent unauthorized access and malicious queries.

LayerControlImplementation DetailsRationale
DatabaseStrict Read-Only PermissionsCreate a dedicated DB user for the agent with SELECT privileges only. Deny all DML/DDL permissions.The most critical line of defense. Prevents any destructive changes to data or schema at the source.
DatabaseRow-Level Security / ViewsGrant the agent access only to database views that pre-filter data based on the user's role.Enforces data segregation and 'need-to-know' access policies at the most secure level.
ApplicationKeyword FilteringHardcode a check to reject any query containing forbidden keywords like DELETE, DROP, UPDATE.A fast, deterministic failsafe that is not susceptible to prompt injection attacks.
ApplicationPre-Execution ValidationUse a tool like LangChain's QuerySQLCheckerTool to have an LLM 'lint' the query for errors before execution.A cost-effective circuit breaker to catch syntax errors and reduce failed database calls.
LLMPrompt-Level GuardrailsFirmly instruct the agent in its system prompt to never generate DML/DDL statements.Hardens the model's default behavior, reducing the likelihood of generating harmful code.
AccessUser-Based ContextDynamically filter the schema and examples provided to the LLM based on the authenticated user's permissions.Prevents the LLM from even becoming aware of data structures the user is not authorized to see.

Database Layer

Read-only permissions, row-level security, and dedicated agent users form the foundation of database security.

Application Layer

Keyword filtering, query validation, and pre-execution checks provide additional safety nets.

LLM Layer

Prompt guardrails and user-based context filtering ensure the model operates within intended boundaries.