MP-301d · Module 1
Parameterized Queries
4 min read
Every database query executed by an MCP server must use parameterized statements. This is not a suggestion — it is the single most important security control in your database integration. When an AI model provides a customer ID to look up, that value flows from the model through the MCP client to the server as a URI parameter. If the server interpolates that value directly into a SQL string, it creates a classic SQL injection vector. Parameterized queries separate the query structure from the data values, making injection structurally impossible regardless of what the model sends.
The implementation differs by database driver but the principle is universal. In Node.js with pg, you use $1, $2 placeholders and pass values as an array. In Python with psycopg, you use %s placeholders. In Go with database/sql, you use ? placeholders. The driver handles escaping, type coercion, and quoting — you never construct SQL strings manually. This also improves performance: parameterized queries can be prepared once and executed many times with different values, leveraging the database query plan cache.
Template parameters from MCP URIs deserve special attention. When a resource template like db://customers/{customerId} is resolved, the customerId value comes from the AI model. Even though MCP URI templates look clean and typed, the actual values are arbitrary strings until validated. Your server must validate the type (is it a UUID? an integer?), check the range (is the ID within expected bounds?), and use it exclusively through parameterized queries. Never trust URI template parameters as safe inputs.
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// WRONG — string interpolation, vulnerable to injection
async function unsafeRead(tableName: string, id: string) {
return pool.query(`SELECT * FROM ${tableName} WHERE id = '${id}'`);
}
// RIGHT — parameterized query with validated table name
const ALLOWED_TABLES = ["customers", "orders", "products"] as const;
type AllowedTable = (typeof ALLOWED_TABLES)[number];
async function safeRead(tableName: string, id: string) {
// Step 1: Validate table against allowlist (not parameterizable)
if (!ALLOWED_TABLES.includes(tableName as AllowedTable)) {
throw new Error(`Table "${tableName}" is not exposed`);
}
// Step 2: Parameterize the value
return pool.query(
`SELECT * FROM "${tableName}" WHERE id = $1 LIMIT 500`,
[id]
);
}
// Template resource using parameterized query
server.resource(
"customer-detail",
new ResourceTemplate("db://customers/{customerId}", { list: undefined }),
async (uri, { customerId }) => {
// Validate UUID format before querying
if (!/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(customerId)) {
throw new Error("Invalid customer ID format");
}
const result = await pool.query(
"SELECT id, name, email, status FROM customers WHERE id = $1",
[customerId]
);
return {
contents: [{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(result.rows[0] ?? null),
}],
};
}
);
- Audit every query Search your MCP server codebase for string interpolation in SQL statements. Every backtick template literal or string concatenation involving user input is a vulnerability. Replace with parameterized placeholders.
- Validate URI parameters Add type validation for every template parameter — UUID regex for IDs, integer parsing for numeric keys, enum checks for status fields. Reject malformed values before they reach the query.
- Enable prepared statements Configure your database driver to use prepared statements by default. In pg, set prepareThreshold to a low number. In mysql2, use execute() instead of query() to force preparation.