{"slug": "designing-safe-database-access-for-ai-agents-using-mcp", "title": "Designing Safe Database Access for AI Agents using MCP", "summary": "A developer designed a safe database access system for AI agents using the Model Context Protocol (MCP), implementing authentication, authorization, auditing, and a repository layer to prevent arbitrary SQL queries. The architecture treats the MCP server as a controlled boundary, exposing only domain-level operations like get_survival_rate() instead of direct database access.", "body_md": "I recently took part in a coding challenge with a deceptively simple task: **make data from a database available to AI agents**. The interesting part wasn’t making it work — it was deciding how to do it safely.\n\nAn AI agent needs to query company data, but it should never be free to execute arbitrary SQL. Give it direct database access and you’ve handed over far more than it needs: table layouts, implementation details, and the ability to ask questions you never intended it to ask. A database understands tables and permissions — not business boundaries.\n\nThat observation shaped my approach. Rather than treating the challenge as an integration problem, I treated it as an architecture problem:**What is the right boundary between an AI agent and sensitive data?**\n\nThe implementation was small. The architectural decisions behind it were not. This article walks through the decisions I made, why I built the system in the order I did, and the trade-offs behind each layer.\n\nFor me, the choice of using an **MCP server** was obvious. MCP gives AI agents a controlled set of tools instead of direct access to the underlying database. Rather than generating arbitrary SQL, an agent calls domain-level operations such as get_survival_rate(), leaving the server to decide how that request is fulfilled.\n\nThat shifts the problem from *how an agent queries a database* to *how the server exposes business capabilities safely*. Database permissions still matter, but they’re only the last line of defence. The more interesting questions were architectural:\n\nThose questions shaped every design decision that followed.\n\nBefore writing any code, I sketched the architecture that would answer those three questions.\n\nEvery request flows through the same pipeline. The caller is authenticated first, after which the server verifies whether the requested tool is authorized for that agent. Once those checks pass, the request is recorded by the audit middleware before the tool executes its business logic and retrieves data through the repository.\n\nEach layer exists to answer a single architectural question: **authentication** determines who is calling, **authorization** decides whether they’re allowed to use a tool, **auditing** records what happened, and the **repository** abstracts where the data comes from. Keeping those responsibilities separate became the guiding principle for the rest of the implementation.\n\nWith the architecture in place, I defined a simple four-step roadmap:\n\nEach step answered one architectural question while creating the foundation for the next. By the time I introduced authentication or auditing, the underlying layers were already stable enough that no redesign was necessary.\n\nThe first step wasn’t authentication, authorization, or even database access. It was simply getting an MCP client and server talking to each other.\n\nI built a minimal client and server using **FastMCP** over HTTP. At this point there were no real tools and no business logic — the goal was simply to establish the communication layer that everything else would build on.\n\nI deliberately chose HTTP over stdio. While stdio is great for local development, the target architecture was always multiple remote agents talking to a central service. Starting with HTTP meant the transport layer wouldn't need to change as the system grew.\n\nAuthentication came later. At this stage there was nothing worth protecting yet, so adding identity would only have introduced complexity without solving a real problem.\n\nWith the communication layer in place, the next question was how the server should access the data.\n\nMy rule was simple:\n\nIf the server contains the wordSELECT, the abstraction has already failed.\n\nInstead of talking directly to the database, the server depends on a repository interface that speaks the language of the business domain. The MCP server never knows whether the data comes from SQLite, PostgreSQL, or something entirely different — it only knows about domain operations such as get_survival_rate().\n\nFirst, that makes the server independent of the storage technology. Switching from SQLite to PostgreSQL becomes a configuration change, while moving to a completely different backend only requires another repository implementation — not changes to the server itself.\n\nSecond, it makes testing almost trivial. Because the server depends only on the interface, tests can inject a lightweight in-memory implementation without starting a real database.\n\nMost importantly, the interface exposes business operations rather than database operations. The server asks for get_survival_rate(), never execute_sql(). That preserves the boundary between the agent and the database.\n\nOnly after the application had something worth protecting did identity become relevant.\n\nAgents authenticate with **Keycloak**, which issues access tokens containing the scopes assigned to each agent. **FastMCP** uses those scopes to enforce tool-level authorization, so there was no need to build my own permission mapping. The best code I wrote here was the code I **didn’t** write.\n\nOne design decision did survive, though: authentication and authorization remain separate concerns. Authentication answers **who is calling?** and belongs at the edge of the system, where a gateway or identity provider can validate tokens. Authorization answers **what may this agent do?** and stays inside the MCP server, because only it understands its tools and the business rules behind them.\n\nKeeping those responsibilities separate means the deployment model can evolve without changing the authorization logic.\n\nAuditing was the final layer because it depends on everything that came before. An audit trail answers a simple question: **who did what, and when?** Until agents have identities and tools return real data, there’s nothing meaningful to record.\n\nThe audit layer lives in middleware, keeping the tools focused on business logic. One decision was deliberate: the log records **what was requested** — the agent, the tool, and its parameters — but never **what was returned**. Otherwise, the audit log gradually becomes another copy of the very data it’s meant to protect.\n\nThe challenge came with a constraint: roughly eight hours of implementation time. Rather than building a production-ready system, I focused on validating the architecture.\n\nThe result is a working end-to-end prototype running locally. Agents authenticate, permissions are enforced, data is accessed through a repository, and every request is audited. That was enough to validate the architectural boundaries without investing time in production infrastructure.\n\nEverything else — deployment, automated onboarding, and operational tooling — was intentionally left out because it wouldn’t have answered any new architectural questions.\n\nKnowing where to stop is part of engineering. A clearly defined boundary between **designed** and **implemented** is more valuable than a long list of half-finished features.\n\nBuilding an MCP server turned out to be the easy part. Deciding where the boundaries belong was the real challenge.\n\nGood architecture isn’t measured by the number of technologies you use, but by whether each layer has a single, well-defined responsibility. The protocol may change, the database may change, and even the identity provider may change. The architectural boundaries should not.\n\nI’m curious where you would draw those boundaries.\n\nFurther reading\n\n[Designing Safe Database Access for AI Agents using MCP](https://blog.devgenius.io/designing-safe-database-access-for-ai-agents-using-mcp-3eee4c6319c8) was originally published in [Dev Genius](https://blog.devgenius.io) on Medium, where people are continuing the conversation by highlighting and responding to this story.", "url": "https://wpnews.pro/news/designing-safe-database-access-for-ai-agents-using-mcp", "canonical_source": "https://blog.devgenius.io/designing-safe-database-access-for-ai-agents-using-mcp-3eee4c6319c8?source=rss----4e2c1156667e---4", "published_at": "2026-07-14 09:45:54+00:00", "updated_at": "2026-07-14 10:25:39.420872+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-infrastructure", "ai-tools"], "entities": ["FastMCP", "MCP"], "alternates": {"html": "https://wpnews.pro/news/designing-safe-database-access-for-ai-agents-using-mcp", "markdown": "https://wpnews.pro/news/designing-safe-database-access-for-ai-agents-using-mcp.md", "text": "https://wpnews.pro/news/designing-safe-database-access-for-ai-agents-using-mcp.txt", "jsonld": "https://wpnews.pro/news/designing-safe-database-access-for-ai-agents-using-mcp.jsonld"}}