An MCP server for internal schema inspection: security, limits, and lessons

In early 2026 we built an internal Model Context Protocol (MCP) server allowing engineering assistants to inspect production-replica database schemas, table relationships, and index definitions. The goal was to stop pasting table declarations into chat windows. Within three weeks, the server proved indispensable for migrations and query optimization, and exposed four distinct security and resource boundaries that standard REST APIs never have to consider.

The symptom

// What an agent requested via MCP tool "describe_table":
{
  "table": "users"
}

// What the initial tool returned:
{
  "columns": [
    {"name": "id", "type": "bigint unsigned"},
    {"name": "email", "type": "varchar(255)"},
    {"name": "password_hash", "type": "varchar(255)"}, // Leaked column name!
    {"name": "totp_secret", "type": "varchar(255)"},   // Leaked sensitive identifier!
    {"name": "created_at", "type": "datetime"}
  ]
}

// 2 days later: an agent attempted an unbounded table search:
{
  "query": "SELECT * FROM information_schema.columns"
}
// Payload size: 48MB. Agent context window exhausted. Process OOM.

The failure modes were specific to agent interactions. First, revealing column names that identify authentication secrets invites prompt injections to attempt data exfiltration. Second, language models do not respect pagination unless strictly enforced at the schema validation layer. An agent will gladly request fifty megabytes of schema metadata in one call.

Why it happens

Standard developer tools assume human judgment: an engineer running DESCRIBE users in a terminal knows to ignore the totp_secret column. But an agent ingests the entire tool output into its working memory. If sensitive column names are present, they become available context for subsequent speculative prompts. Furthermore, agents treat tools as authoritative oracles; if a tool offers no pagination limits, the agent assumes the output will fit into its context.

The fix

Column masking and hard context caps

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";

const SENSITIVE_COLUMN_PATTERNS = [
  /password/i,
  /secret/i,
  /token/i,
  /auth/i,
  /api_key/i
];

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "describe_table") {
    const tableName = sanitizeTableName(request.params.arguments?.table);
    const columns = await getTableColumns(tableName);

    // Redact sensitive schema existence entirely:
    const safeColumns = columns
      .filter(col => !SENSITIVE_COLUMN_PATTERNS.some(p => p.test(col.name)))
      .slice(0, 100); // Hard cap at 100 columns per tool invocation

    return {
      content: [{
        type: "text",
        text: JSON.stringify(safeColumns, null, 2)
      }]
    };
  }
  throw new Error("Unknown tool");
});

Every tool output is capped at four kilobytes of JSON. If a table has more than one hundred columns or indexes, the tool returns a cursor and an explicit notice: "Truncated at 100 entries. Provide cursor for next page." This forces the agent to loop intentionally rather than blowing up its context window.

Verifying it worked

$ ./bin/test-mcp-security --suite=leak-detection
  [PASS] Masked columns check: 18 sensitive patterns verified.
  [PASS] Max payload size assertion: 3.8KB (under 4KB limit).
  [PASS] SQL injection in table name parameter rejected.
  [PASS] Read-only replica role enforced (DDL/DML rejected).

The security test suite runs against the MCP server in CI. The server connects exclusively to a read-only database user stripped of all permissions except SELECT on information_schema and application table structures.

What this costs

Maintenance of the sensitive pattern deny-list. Whenever a new credential column name is introduced that does not match existing regexes, it must be added to the security filter before exposing the table to the agent server.

And developer frustration when debugging auth tables. When an engineer actually wants to discuss an auth schema with an assistant, the columns are masked. The compromise is allowing explicit opt-in with a short-lived elevated token, logged to an internal audit trail.