A database MCP gateway turns database operations into model-callable tools without asking every application team to write a custom MCP server. Instead of hand-coding protocol handlers, teams declare data sources, SQL-backed tools, semantic-search tools, prompts, and toolsets in a configuration file. The gateway handles protocol transport, schema exposure, connection pooling, authentication, and client SDK integration.
Google’s MCP Toolbox for Databases illustrates this pattern. It can run prebuilt tools for common databases, but the more reusable mechanism is the custom tool framework: a tools.yaml file defines sources, tools, and toolsets, then the server exposes those capabilities to MCP-compatible clients and application SDKs.
The key design question is not “can the model execute SQL?” The production question is how to expose database actions with tight scope, stable parameter schemas, auditable descriptions, and authorization checks, while keeping dangerous operations explicit and reviewable.
A source describes a reachable database or service: type, host, port, database name, credentials, and provider-specific connection settings. Tools reference sources rather than embedding connection details directly, which keeps credentials separate from model-facing capability descriptions.
A tool describes one action an agent can take. For SQL tools, that usually means a name, description, typed parameters, source reference, and statement template. The description becomes part of the model’s decision surface, while the parameter schema constrains the arguments the client can send.
A toolset groups related tools so different agents can load only the capabilities they need. A reporting agent may receive read-only analytics tools, while a migration assistant may receive schema-inspection tools but no write operations.
A YAML-defined database tool is easier to review than arbitrary code. Reviewers can inspect the SQL, parameters, source, auth policy, and destructive-operation hints before an agent ever sees the tool.
A minimal gateway configuration separates the database source from the model-facing action. The source owns connectivity; the tool owns semantics and parameters.
kind: source
name: app-postgres
type: postgres
host: 127.0.0.1
port: 5432
database: appdb
user: readonly_app
password: ${DB_PASSWORD}
---
kind: tool
name: search_customers_by_email
type: postgres-sql
source: app-postgres
description: Search customers by email fragment. Use for support lookups only.
parameters:
- name: email
type: string
description: Email fragment supplied by the user.
statement: |
SELECT id, email, status, created_at
FROM customers
WHERE email ILIKE '%' || $1 || '%'
ORDER BY created_at DESC
LIMIT 20;
The client or application can load a named toolset instead of the whole gateway catalog. That matters because every exposed tool increases model choice complexity and security surface.
kind: toolset
name: support-readonly
tools:
- search_customers_by_email
- get_order_status
- list_recent_account_events
Client SDKs can fetch gateway tools and adapt them into framework-native tool definitions. The gateway remains the runtime authority for execution, while the application receives tool names, descriptions, and input schemas suitable for the model host.
A support agent can safely inspect account state using scoped lookup tools rather than free-form SQL. Parameterized statements prevent prompt-injected SQL from becoming executable text.
A coding assistant can inspect schemas, list tables, and run controlled exploratory queries while working on application code. This improves generated migrations and query code without handing the assistant broad database credentials.
For analytics agents, the gateway can expose curated query templates, semantic search over catalog metadata, and table-info tools. The model composes among reviewed operations instead of inventing every query from scratch.
Do not concatenate model text into SQL. Treat every model-supplied value as a parameter and keep identifiers, table names, and clauses inside reviewed templates unless you have a separate allowlist parser.
type ToolCall struct {
Name string
Arguments map[string]any
}
func ExecuteSearch(ctx context.Context, db *sql.DB, call ToolCall) ([]Customer, error) {
email, ok := call.Arguments["email"].(string)
if !ok || len(email) > 128 {
return nil, ErrInvalidArgument
}
rows, err := db.QueryContext(ctx, `
SELECT id, email, status, created_at
FROM customers
WHERE email ILIKE '%' || $1 || '%'
ORDER BY created_at DESC
LIMIT 20`, email)
if err != nil { return nil, err }
defer rows.Close()
return scanCustomers(rows)
}
Tool metadata should distinguish read-only, idempotent, and destructive actions. Hosts can use those annotations to require user approval before running writes or administrative operations.
A good description says when to use the tool, what it returns, and what it must not be used for. Vague descriptions increase accidental calls and make tool-selection traces harder to audit.