>Custom MCP Server for Database Queries
Getting Claude Code to understand your database schema and answer questions about your data is a game-changer for development workflows. Instead of constantly switching between your code editor and database client, you can ask Claude to explore tables, analyze data patterns, and even suggest optimizations—all while staying in your coding flow.
The secret is building a custom MCP (Model Context Protocol) server that gives Claude direct access to your database through read-only queries. Here's how to build one that's both powerful and secure.
Setting Up Your MCP Server Foundation
First, create a new TypeScript project for your MCP server. You'll need the MCP SDK and a database client library:
mkdir database-mcp-server
cd database-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk/server pg sqlite3
npm install -D @types/node @types/pg typescript
Create the basic server structure:
// src/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
export class DatabaseMCPServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "database-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
}
private setupToolHandlers() {
// Tool handlers will go here
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Database MCP server running on stdio");
}
}
Creating Database Query Tools
Now add the core functionality—a tool that lets Claude run read-only SQL queries. This example uses PostgreSQL, but you can adapt it for any database:
import { Pool } from 'pg';
export class DatabaseMCPServer {
private pool: Pool;
constructor() {
// Initialize database connection
this.pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 5, // Limit concurrent connections
});
this.server = new Server(
{
name: "database-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
}
private setupToolHandlers() {
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "query_database":
return this.handleDatabaseQuery(request.params.arguments);
case "describe_table":
return this.handleDescribeTable(request.params.arguments);
case "list_tables":
return this.handleListTables();
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
});
}
private async handleDatabaseQuery(args: any) {
const { query } = args;
// Security: Only allow SELECT statements
const normalizedQuery = query.trim().toLowerCase();
if (!normalizedQuery.startsWith('select')) {
throw new Error('Only SELECT queries are allowed for security reasons');
}
try {
const result = await this.pool.query(query);
return {
content: [
{
type: "text",
text: JSON.stringify({
rows: result.rows,
rowCount: result.rowCount,
fields: result.fields?.map(f => ({ name: f.name, dataTypeID: f.dataTypeID }))
}, null, 2)
}
]
};
} catch (error) {
throw new Error(`Database query failed: ${error.message}`);
}
}
}
Adding Schema Exploration Tools
Claude works better when it understands your database structure. Add tools that expose schema information:
private async handleDescribeTable(args: any) {
const { tableName } = args;
const schemaQuery = `
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length
FROM information_schema.columns
WHERE table_name = $1
ORDER BY ordinal_position;
`;
try {
const result = await this.pool.query(schemaQuery, [tableName]);
return {
content: [
{
type: "text",
text: `Table: ${tableName}\n\n` +
result.rows.map(row =>
`${row.column_name}: ${row.data_type}${row.is_nullable === 'NO' ? ' NOT NULL' : ''}`
).join('\n')
}
]
};
} catch (error) {
throw new Error(`Failed to describe table: ${error.message}`);
}
}
private async handleListTables() {
const tablesQuery = `
SELECT table_name, table_type
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
`;
try {
const result = await this.pool.query(tablesQuery);
return {
content: [
{
type: "text",
text: "Available tables:\n\n" +
result.rows.map(row =>
`• ${row.table_name} (${row.table_type})`
).join('\n')
}
]
};
} catch (error) {
throw new Error(`Failed to list tables: ${error.message}`);
}
}
Registering Tools with Proper Metadata
Register your tools so Claude knows what's available:
private setupToolHandlers() {
// Register tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query_database",
description: "Execute a read-only SQL SELECT query against the database",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "SQL SELECT query to execute"
}
},
required: ["query"]
}
},
{
name: "describe_table",
description: "Get detailed schema information for a specific table",
inputSchema: {
type: "object",
properties: {
tableName: {
type: "string",
description: "Name of the table to describe"
}
},
required: ["tableName"]
}
},
{
name: "list_tables",
description: "List all available tables in the database",
inputSchema: {
type: "object",
properties: {}
}
}
]
};
});
// Tool execution handler (previous code here)
}
Configuring Claude Code Integration
Create a startup script and configure Claude Code to use your MCP server:
// src/index.ts
import { DatabaseMCPServer } from './server.js';
const server = new DatabaseMCPServer();
server.run().catch(console.error);
Add this to your Claude Code configuration file:
{
"mcpServers": {
"database": {
"command": "node",
"args": ["./dist/index.js"],
"cwd": "/path/to/your/database-mcp-server",
"env": {
"DATABASE_URL": "postgresql://user:password@localhost:5432/mydb"
}
}
}
}
Pro Tips for Production Use
Query Limits: Add query timeouts and row limits to prevent runaway queries:
const result = await this.pool.query({
text: query + ' LIMIT 1000',
timeout: 30000 // 30 second timeout
});
Connection Management: Always clean up connections and handle errors gracefully. Consider using connection pooling with reasonable limits.
Query Logging: Log all queries for debugging and monitoring:
console.error(`Executing query: ${query.substring(0, 100)}...`);
Environment-Specific Access: Use different database credentials for your MCP server—preferably read-only credentials with limited table access.
Common Pitfalls to Avoid
Don't expose write operations, even if they seem safe. Claude might generate UPDATE or DELETE statements that could affect your data unexpectedly.
Watch out for complex joins that might overwhelm your database. Consider adding query complexity analysis or execution plan checking for production use.
Be careful with sensitive data. Your MCP server has access to everything Claude can query, so ensure proper access controls are in place.
What's Next
Once your basic database MCP server is running, you can extend it with more sophisticated features like query result caching, support for multiple databases, or integration with data visualization tools. You might also want to explore building MCP servers for other external systems like APIs, file systems, or cloud services to create a comprehensive development assistant ecosystem.
The power of MCP servers lies in their composability—each server you build makes Claude more capable at understanding and working with your specific development environment.