Tekko

Language

Get in Touch

Usually respond within 24 hours

Back to BlogArchitecture

MCP: Securely Connecting LLMs to Private SQL Databases

7 min read
MCPLLMSQLSecurityAI Infrastructure
MCP: Securely Connecting LLMs to Private SQL Databases

The "Context Gap" is the single greatest hurdle in moving LLM-powered applications from impressive demos to production-ready tools. While Retrieval-Augmented Generation (RAG) has become the industry standard for unstructured data, it often falls short when dealing with the structured, relational data stored in private SQL databases. Vectorizing an entire PostgreSQL database is frequently overkill, expensive, and leads to latency issues when real-time accuracy is required.

Enter the Model Context Protocol (MCP). Developed as an open standard, MCP provides a structured way for LLM orchestrators (like Claude Desktop or custom LangChain implementations) to communicate with external data sources. In this article, we will explore how to implement an MCP server that acts as a secure, real-time bridge between your private SQL infrastructure and an AI orchestrator.

The Architecture of an MCP Bridge

At its core, MCP follows a client-server architecture. The Client is the LLM orchestrator—the "brain" that decides which tool to call. The Server is a lightweight application that sits next to your data source, exposing specific capabilities to the client.

When we talk about a "bridge" for SQL databases, we aren't simply giving an LLM a raw connection string. That would be a security nightmare. Instead, the MCP server acts as a governance layer. It translates the LLM’s high-level intent into specific, sanitized SQL queries, and returns the results in a format the model can ingest.

Why MCP over Traditional API Hooks?

Before MCP, developers typically built bespoke API endpoints for every tool an LLM needed. This approach doesn't scale. MCP standardizes the discovery and execution phase. An MCP-compliant client can query a server to find out what "tools" (functions) or "resources" (data sets) are available without the developer having to write custom glue code for every new integration.

Designing the Secure SQL Server

When building an MCP server for a private SQL database, security is the first, second, and third priority. You are effectively creating a programmable interface to your most sensitive data.

1. Principle of Least Privilege

Your MCP server should never connect to the database as a superuser. Create a dedicated database user for the MCP server with CONNECT and SELECT permissions only on the specific tables required for the LLM's tasks.

-- Example: Setting up a restricted user in PostgreSQL CREATE USER mcp_bridge_user WITH PASSWORD 'secure_password'; GRANT CONNECT ON DATABASE production_db TO mcp_bridge_user; GRANT USAGE ON SCHEMA public TO mcp_bridge_user; GRANT SELECT ON TABLE customers, orders, products TO mcp_bridge_user;

2. Tools vs. Resources

MCP distinguishes between Resources (static or dynamic data the model can read) and Tools (functions the model can execute). For SQL bridges, I recommend using Tools for complex queries and Resources for schema definitions.

  • Resource: The database schema (so the LLM understands the table relationships).
  • Tool: get_customer_lifetime_value(customer_id: string)—a predefined function that executes a specific, parameterized query.

Implementing the Server with TypeScript

Using the official @modelcontextprotocol/sdk, we can build a robust server in Node.js. In this example, we’ll create a server that allows an LLM to query order status from a private PostgreSQL instance.

import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { Pool } from 'pg'; // Database configuration using environment variables const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); const server = new Server( { name: "inventory-bridge", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Define available tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "get_order_status", description: "Retrieves the current status and shipping date of an order.", inputSchema: { type: "object", properties: { orderId: { type: "string" }, }, required: ["orderId"], }, }, ], })); // Handle tool execution server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "get_order_status") { const orderId = String(request.params.arguments?.orderId); // Use parameterized queries to prevent SQL injection const result = await pool.query( 'SELECT status, shipped_at FROM orders WHERE id = $1', [orderId] ); if (result.rows.length === 0) { return { content: [{ type: "text", text: "Order not found." }] }; } return { content: [ { type: "text", text: JSON.stringify(result.rows[0]) } ], }; } throw new Error("Tool not found"); }); // Start the server using stdio transport const transport = new StdioServerTransport(); await server.connect(transport);

Guarding Against SQL Injection

It is tempting to create a tool called execute_raw_sql to give the LLM maximum flexibility. Do not do this.

LLMs are susceptible to "Prompt Injection," where a user might trick the model into generating a malicious SQL string (e.g., DROP TABLE users;). By defining specific tools with parameterized inputs, you ensure that the LLM can only interact with the database through the narrow paths you've defined. The database driver handles the sanitization, making the bridge resilient to injection attacks.

Real-Time Data vs. Latency

One of the primary benefits of MCP is the ability to provide "Live Context." Unlike a RAG pipeline that might rely on a vector index updated every hour, an MCP SQL bridge queries the source of truth directly.

However, this introduces a latency trade-off. Every tool call involves:

  1. The LLM deciding to use a tool.
  2. The orchestrator sending a request to your MCP server.
  3. Your server querying the database.
  4. The LLM processing the result.

To optimize this, ensure your MCP server is deployed geographically close to your database. If you are using a managed database like AWS RDS, host your MCP server in the same VPC/Subnet to minimize round-trip time (RTT).

Advanced Pattern: Schema Exposure for Dynamic Discovery

If you have a very large database, manually defining tools for every query is tedious. A more advanced pattern involves exposing the database schema as an MCP Resource. This allows the LLM to "browse" the metadata to understand what data is available before calling a more general (but still safe) query tool.

Exposing Schema Metadata

server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: "sql://inventory/schema", name: "Inventory Database Schema", mimeType: "application/json", description: "The table structure for customers, orders, and products." } ] }));

When the model reads this resource, it gains the context needed to formulate better arguments for your tools. For example, it will know that customer_id is a UUID and not an integer, reducing the number of failed tool calls.

Observability and Governance

In a production environment, you cannot treat the MCP bridge as a black box. You need to know exactly what queries the LLM is running and how the database is responding.

  1. Logging: Log every tool call, including the arguments provided by the LLM and the execution time. This is vital for debugging "hallucinations" where the model might be passing incorrect IDs.
  2. Rate Limiting: Implement rate limiting at the MCP server level. An LLM in a loop could inadvertently DDOS your database if not properly constrained.
  3. Audit Trails: Use the metadata field in MCP responses to pass back transaction IDs or tracking tokens that link the database query to the specific LLM session.

The Actionable Conclusion

Implementing the Model Context Protocol is the most effective way to turn an LLM from a generic chatbot into a specialized agent capable of navigating your private data landscape. By moving away from brittle, custom-coded integrations and toward a standardized protocol, you gain scalability and security.

To get started:

  1. Audit your data: Identify the top 3 SQL queries your users frequently ask for via manual reports.
  2. Build a prototype: Use the @modelcontextprotocol/sdk to create a simple Node.js server with restricted database permissions.
  3. Parameterize everything: Never allow raw SQL strings to pass from the LLM to your database driver.
  4. Connect to an Orchestrator: Use Claude Desktop or a LangChain MCP client to test the bridge in a real-world chat scenario.

By following these steps, you build more than just an integration; you build a secure, real-time foundation for the next generation of AI-native applications.