Building a Database MCP Tool with PostgreSQL
Let AI agents explore your database safely. This tutorial builds a production-ready PostgreSQL MCP tool.
What You'll Build
list_tables — discover database schema
describe_table — get column details
run_query — execute read-only SQL queries
- Safety guardrails against writes and dangerous operations
Step 1: Setup
<span class="hljs-built_in">mkdir</span> pg-mcp && <span class="hljs-built_in">cd</span> pg-mcp
npm init -y
npm install @modelcontextprotocol/sdk pg
Step 2: Database Connection
<span class="hljs-keyword">import</span> pg <span class="hljs-keyword">from</span> <span class="hljs-string">"pg"</span>;
<span class="hljs-keyword">const</span> pool = <span class="hljs-keyword">new</span> pg.<span class="hljs-title class_">Pool</span>({
<span class="hljs-attr">connectionString</span>: process.<span class="hljs-property">env</span>.<span class="hljs-property">DATABASE_URL</span>,
<span class="hljs-attr">max</span>: <span class="hljs-number">5</span>, <span class="hljs-comment">// Limit connections</span>
<span class="hljs-attr">idleTimeoutMillis</span>: <span class="hljs-number">30000</span>
});
<span class="hljs-comment">// Test connection on startup</span>
pool.<span class="hljs-title function_">query</span>(<span class="hljs-string">"SELECT 1"</span>).<span class="hljs-title function_">then</span>(<span class="hljs-function">() =></span> {
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"✅ Database connected"</span>);
}).<span class="hljs-title function_">catch</span>(<span class="hljs-function"><span class="hljs-params">err</span> =></span> {
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"❌ Database connection failed:"</span>, err.<span class="hljs-property">message</span>);
process.<span class="hljs-title function_">exit</span>(<span class="hljs-number">1</span>);
});
Step 3: Define Tools
server.<span class="hljs-title function_">setRequestHandler</span>(<span class="hljs-string">"tools/list"</span>, <span class="hljs-title function_">async</span> () => ({
<span class="hljs-attr">tools</span>: [
{
<span class="hljs-attr">name</span>: <span class="hljs-string">"list_tables"</span>,
<span class="hljs-attr">description</span>: <span class="hljs-string">"List all tables in the database with row counts"</span>,
<span class="hljs-attr">inputSchema</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"object"</span>,
<span class="hljs-attr">properties</span>: {
<span class="hljs-attr">schema</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
<span class="hljs-attr">description</span>: <span class="hljs-string">"Schema name (default: public)"</span>
}
}
}
},
{
<span class="hljs-attr">name</span>: <span class="hljs-string">"describe_table"</span>,
<span class="hljs-attr">description</span>: <span class="hljs-string">"Get column names, types, and constraints for a table"</span>,
<span class="hljs-attr">inputSchema</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"object"</span>,
<span class="hljs-attr">properties</span>: {
<span class="hljs-attr">table</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
<span class="hljs-attr">description</span>: <span class="hljs-string">"Table name"</span>
}
},
<span class="hljs-attr">required</span>: [<span class="hljs-string">"table"</span>]
}
},
{
<span class="hljs-attr">name</span>: <span class="hljs-string">"run_query"</span>,
<span class="hljs-attr">description</span>: <span class="hljs-string">"Execute a read-only SQL query (SELECT only)"</span>,
<span class="hljs-attr">inputSchema</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"object"</span>,
<span class="hljs-attr">properties</span>: {
<span class="hljs-attr">query</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>,
<span class="hljs-attr">description</span>: <span class="hljs-string">"SQL SELECT query to execute"</span>
},
<span class="hljs-attr">limit</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-string">"number"</span>,
<span class="hljs-attr">description</span>: <span class="hljs-string">"Maximum rows to return (default: 100, max: 1000)"</span>
}
},
<span class="hljs-attr">required</span>: [<span class="hljs-string">"query"</span>]
}
}
]
}));
Step 4: Implement Safety Guards
<span class="hljs-keyword">function</span> <span class="hljs-title function_">isReadOnly</span>(<span class="hljs-params">query</span>) {
<span class="hljs-keyword">const</span> trimmed = query.<span class="hljs-title function_">trim</span>().<span class="hljs-title function_">toUpperCase</span>();
<span class="hljs-keyword">const</span> dangerous = [<span class="hljs-string">"INSERT"</span>, <span class="hljs-string">"UPDATE"</span>, <span class="hljs-string">"DELETE"</span>, <span class="hljs-string">"DROP"</span>, <span class="hljs-string">"ALTER"</span>,
<span class="hljs-string">"CREATE"</span>, <span class="hljs-string">"TRUNCATE"</span>, <span class="hljs-string">"GRANT"</span>, <span class="hljs-string">"REVOKE"</span>];
<span class="hljs-keyword">return</span> !dangerous.<span class="hljs-title function_">some</span>(<span class="hljs-function"><span class="hljs-params">cmd</span> =></span> trimmed.<span class="hljs-title function_">startsWith</span>(cmd));
}
<span class="hljs-keyword">function</span> <span class="hljs-title function_">sanitizeIdentifier</span>(<span class="hljs-params">name</span>) {
<span class="hljs-comment">// Only allow alphanumeric and underscore</span>
<span class="hljs-keyword">return</span> name.<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/[^a-zA-Z0-9_]/g</span>, <span class="hljs-string">""</span>);
}
Step 5: Handle Tool Calls
<span class="hljs-keyword">case</span> <span class="hljs-string">"run_query"</span>: {
<span class="hljs-keyword">const</span> { query, limit = <span class="hljs-number">100</span> } = args;
<span class="hljs-comment">// Safety check</span>
<span class="hljs-keyword">if</span> (!<span class="hljs-title function_">isReadOnly</span>(query)) {
<span class="hljs-keyword">return</span> {
<span class="hljs-attr">content</span>: [{
<span class="hljs-attr">type</span>: <span class="hljs-string">"text"</span>,
<span class="hljs-attr">text</span>: <span class="hljs-string">"❌ Only SELECT queries are allowed for safety."</span>
}]
};
}
<span class="hljs-comment">
<span class="hljs-keyword">const</span> finalQuery = query.<span class="hljs-title function_">toUpperCase</span>().<span class="hljs-title function_">includes</span>(<span class="hljs-string">"LIMIT"</span>)
? query
: <span class="hljs-string">`<span class="hljs-subst">${query}</span> LIMIT <span class="hljs-subst">${<span class="hljs-built_in">Math</span>.min(limit, <span class="hljs-number">1000</span>)}</span>`</span>;
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> pool.<span class="hljs-title function_">query</span>(finalQuery);
<span class="hljs-keyword">const</span> columns = result.<span class="hljs-property">fields</span>.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">f</span> =></span> f.<span class="hljs-property">name</span>);
<span class="hljs-keyword">const</span> rows = result.<span class="hljs-property">rows</span>.<span class="hljs-title function_">slice</span>(<span class="hljs-number">0</span>, limit);
<span class="hljs-comment">// Format as markdown table</span>
<span class="hljs-keyword">let</span> output = <span class="hljs-string">`📊 Query returned <span class="hljs-subst">${result.rowCount}</span> rows\n\n`</span>;
output += <span class="hljs-string">`| <span class="hljs-subst">${columns.join(<span class="hljs-string">" | "</span>)}</span> |\n`</span>;
output += <span class="hljs-string">`| <span class="hljs-subst">${columns.map(() => <span class="hljs-string">"---"</span>).join(<span class="hljs-string">" | "</span>)}</span> |\n`</span>;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> row <span class="hljs-keyword">of</span> rows) {
output += <span class="hljs-string">`| <span class="hljs-subst">${columns.map(c => <span class="hljs-built_in">String</span>(row[c] ?? <span class="hljs-string">"NULL"</span>).slice(<span class="hljs-number">0</span>, <span class="hljs-number">50</span>)).join(<span class="hljs-string">" | "</span>)}</span> |\n`</span>;
}
<span class="hljs-keyword">if</span> (result.<span class="hljs-property">rowCount</span> > limit) {
output += <span class="hljs-string">`\n*Showing first <span class="hljs-subst">${limit}</span> of <span class="hljs-subst">${result.rowCount}</span> rows*`</span>;
}
<span class="hljs-keyword">return</span> { <span class="hljs-attr">content</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">"text"</span>, <span class="hljs-attr">text</span>: output }] };
} <span class="hljs-keyword">catch</span> (err) {
<span class="hljs-keyword">return</span> {
<span class="hljs-attr">content</span>: [{
<span class="hljs-attr">type</span>: <span class="hljs-string">"text"</span>,
<span class="hljs-attr">text</span>: <span class="hljs-string">`❌ Query error: <span class="hljs-subst">${err.message}</span>`</span>
}]
};
}
}
Step 6: Deploy and Publish
<span class="hljs-comment"># Set your database URL</span>
<span class="hljs-built_in">export</span> DATABASE_URL=<span class="hljs-string">"postgresql://user:pass@localhost:5432/mydb"</span>
<span class="hljs-comment"># Test</span>
node index.js
<span class="hljs-comment"># Publish</span>
mcpm-dev publish
Configuration for Users
In their Claude/Cursor/Windsurf config:
<span class="hljs-punctuation">{</span>
<span class="hljs-attr">"mcpServers"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">"pg-mcp"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">"command"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"npx"</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"args"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">"-y"</span><span class="hljs-punctuation">,</span> <span class="hljs-string">"pg-mcp"</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"env"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">"DATABASE_URL"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"postgresql:
<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
Security Checklist
- ✅ Read-only queries enforced
- ✅ Connection pooling with limits
- ✅ Row limits on all queries
- ✅ Identifier sanitization
- ✅ No raw error exposure
- ✅ Timeout on long queries
Your AI agents can now safely explore your database. 🎉
#MCP #Tutorial #PostgreSQL #Database #mcpm
Building a Database MCP Tool with PostgreSQL
Let AI agents explore your database safely. This tutorial builds a production-ready PostgreSQL MCP tool.
What You'll Build
list_tables— discover database schemadescribe_table— get column detailsrun_query— execute read-only SQL queriesStep 1: Setup
<span class="hljs-built_in">mkdir</span> pg-mcp && <span class="hljs-built_in">cd</span> pg-mcp npm init -y npm install @modelcontextprotocol/sdk pgStep 2: Database Connection
<span class="hljs-keyword">import</span> pg <span class="hljs-keyword">from</span> <span class="hljs-string">"pg"</span>; <span class="hljs-keyword">const</span> pool = <span class="hljs-keyword">new</span> pg.<span class="hljs-title class_">Pool</span>({ <span class="hljs-attr">connectionString</span>: process.<span class="hljs-property">env</span>.<span class="hljs-property">DATABASE_URL</span>, <span class="hljs-attr">max</span>: <span class="hljs-number">5</span>, <span class="hljs-comment">// Limit connections</span> <span class="hljs-attr">idleTimeoutMillis</span>: <span class="hljs-number">30000</span> }); <span class="hljs-comment">// Test connection on startup</span> pool.<span class="hljs-title function_">query</span>(<span class="hljs-string">"SELECT 1"</span>).<span class="hljs-title function_">then</span>(<span class="hljs-function">() =></span> { <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"✅ Database connected"</span>); }).<span class="hljs-title function_">catch</span>(<span class="hljs-function"><span class="hljs-params">err</span> =></span> { <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">"❌ Database connection failed:"</span>, err.<span class="hljs-property">message</span>); process.<span class="hljs-title function_">exit</span>(<span class="hljs-number">1</span>); });Step 3: Define Tools
server.<span class="hljs-title function_">setRequestHandler</span>(<span class="hljs-string">"tools/list"</span>, <span class="hljs-title function_">async</span> () => ({ <span class="hljs-attr">tools</span>: [ { <span class="hljs-attr">name</span>: <span class="hljs-string">"list_tables"</span>, <span class="hljs-attr">description</span>: <span class="hljs-string">"List all tables in the database with row counts"</span>, <span class="hljs-attr">inputSchema</span>: { <span class="hljs-attr">type</span>: <span class="hljs-string">"object"</span>, <span class="hljs-attr">properties</span>: { <span class="hljs-attr">schema</span>: { <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>, <span class="hljs-attr">description</span>: <span class="hljs-string">"Schema name (default: public)"</span> } } } }, { <span class="hljs-attr">name</span>: <span class="hljs-string">"describe_table"</span>, <span class="hljs-attr">description</span>: <span class="hljs-string">"Get column names, types, and constraints for a table"</span>, <span class="hljs-attr">inputSchema</span>: { <span class="hljs-attr">type</span>: <span class="hljs-string">"object"</span>, <span class="hljs-attr">properties</span>: { <span class="hljs-attr">table</span>: { <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>, <span class="hljs-attr">description</span>: <span class="hljs-string">"Table name"</span> } }, <span class="hljs-attr">required</span>: [<span class="hljs-string">"table"</span>] } }, { <span class="hljs-attr">name</span>: <span class="hljs-string">"run_query"</span>, <span class="hljs-attr">description</span>: <span class="hljs-string">"Execute a read-only SQL query (SELECT only)"</span>, <span class="hljs-attr">inputSchema</span>: { <span class="hljs-attr">type</span>: <span class="hljs-string">"object"</span>, <span class="hljs-attr">properties</span>: { <span class="hljs-attr">query</span>: { <span class="hljs-attr">type</span>: <span class="hljs-string">"string"</span>, <span class="hljs-attr">description</span>: <span class="hljs-string">"SQL SELECT query to execute"</span> }, <span class="hljs-attr">limit</span>: { <span class="hljs-attr">type</span>: <span class="hljs-string">"number"</span>, <span class="hljs-attr">description</span>: <span class="hljs-string">"Maximum rows to return (default: 100, max: 1000)"</span> } }, <span class="hljs-attr">required</span>: [<span class="hljs-string">"query"</span>] } } ] }));Step 4: Implement Safety Guards
<span class="hljs-keyword">function</span> <span class="hljs-title function_">isReadOnly</span>(<span class="hljs-params">query</span>) { <span class="hljs-keyword">const</span> trimmed = query.<span class="hljs-title function_">trim</span>().<span class="hljs-title function_">toUpperCase</span>(); <span class="hljs-keyword">const</span> dangerous = [<span class="hljs-string">"INSERT"</span>, <span class="hljs-string">"UPDATE"</span>, <span class="hljs-string">"DELETE"</span>, <span class="hljs-string">"DROP"</span>, <span class="hljs-string">"ALTER"</span>, <span class="hljs-string">"CREATE"</span>, <span class="hljs-string">"TRUNCATE"</span>, <span class="hljs-string">"GRANT"</span>, <span class="hljs-string">"REVOKE"</span>]; <span class="hljs-keyword">return</span> !dangerous.<span class="hljs-title function_">some</span>(<span class="hljs-function"><span class="hljs-params">cmd</span> =></span> trimmed.<span class="hljs-title function_">startsWith</span>(cmd)); } <span class="hljs-keyword">function</span> <span class="hljs-title function_">sanitizeIdentifier</span>(<span class="hljs-params">name</span>) { <span class="hljs-comment">// Only allow alphanumeric and underscore</span> <span class="hljs-keyword">return</span> name.<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/[^a-zA-Z0-9_]/g</span>, <span class="hljs-string">""</span>); }Step 5: Handle Tool Calls
<span class="hljs-keyword">case</span> <span class="hljs-string">"run_query"</span>: { <span class="hljs-keyword">const</span> { query, limit = <span class="hljs-number">100</span> } = args; <span class="hljs-comment">// Safety check</span> <span class="hljs-keyword">if</span> (!<span class="hljs-title function_">isReadOnly</span>(query)) { <span class="hljs-keyword">return</span> { <span class="hljs-attr">content</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">"text"</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">"❌ Only SELECT queries are allowed for safety."</span> }] }; } <span class="hljs-comment">// Add limit if not present</span> <span class="hljs-keyword">const</span> finalQuery = query.<span class="hljs-title function_">toUpperCase</span>().<span class="hljs-title function_">includes</span>(<span class="hljs-string">"LIMIT"</span>) ? query : <span class="hljs-string">`<span class="hljs-subst">${query}</span> LIMIT <span class="hljs-subst">${<span class="hljs-built_in">Math</span>.min(limit, <span class="hljs-number">1000</span>)}</span>`</span>; <span class="hljs-keyword">try</span> { <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> pool.<span class="hljs-title function_">query</span>(finalQuery); <span class="hljs-keyword">const</span> columns = result.<span class="hljs-property">fields</span>.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">f</span> =></span> f.<span class="hljs-property">name</span>); <span class="hljs-keyword">const</span> rows = result.<span class="hljs-property">rows</span>.<span class="hljs-title function_">slice</span>(<span class="hljs-number">0</span>, limit); <span class="hljs-comment">// Format as markdown table</span> <span class="hljs-keyword">let</span> output = <span class="hljs-string">`📊 Query returned <span class="hljs-subst">${result.rowCount}</span> rows\n\n`</span>; output += <span class="hljs-string">`| <span class="hljs-subst">${columns.join(<span class="hljs-string">" | "</span>)}</span> |\n`</span>; output += <span class="hljs-string">`| <span class="hljs-subst">${columns.map(() => <span class="hljs-string">"---"</span>).join(<span class="hljs-string">" | "</span>)}</span> |\n`</span>; <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> row <span class="hljs-keyword">of</span> rows) { output += <span class="hljs-string">`| <span class="hljs-subst">${columns.map(c => <span class="hljs-built_in">String</span>(row[c] ?? <span class="hljs-string">"NULL"</span>).slice(<span class="hljs-number">0</span>, <span class="hljs-number">50</span>)).join(<span class="hljs-string">" | "</span>)}</span> |\n`</span>; } <span class="hljs-keyword">if</span> (result.<span class="hljs-property">rowCount</span> > limit) { output += <span class="hljs-string">`\n*Showing first <span class="hljs-subst">${limit}</span> of <span class="hljs-subst">${result.rowCount}</span> rows*`</span>; } <span class="hljs-keyword">return</span> { <span class="hljs-attr">content</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">"text"</span>, <span class="hljs-attr">text</span>: output }] }; } <span class="hljs-keyword">catch</span> (err) { <span class="hljs-keyword">return</span> { <span class="hljs-attr">content</span>: [{ <span class="hljs-attr">type</span>: <span class="hljs-string">"text"</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">`❌ Query error: <span class="hljs-subst">${err.message}</span>`</span> }] }; } }Step 6: Deploy and Publish
<span class="hljs-comment"># Set your database URL</span> <span class="hljs-built_in">export</span> DATABASE_URL=<span class="hljs-string">"postgresql://user:pass@localhost:5432/mydb"</span> <span class="hljs-comment"># Test</span> node index.js <span class="hljs-comment"># Publish</span> mcpm-dev publishConfiguration for Users
In their Claude/Cursor/Windsurf config:
<span class="hljs-punctuation">{</span> <span class="hljs-attr">"mcpServers"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"pg-mcp"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"command"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"npx"</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">"args"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">"-y"</span><span class="hljs-punctuation">,</span> <span class="hljs-string">"pg-mcp"</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">"env"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"DATABASE_URL"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"postgresql://user:pass@host:5432/db"</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span>Security Checklist
Your AI agents can now safely explore your database. 🎉
#MCP #Tutorial #PostgreSQL #Database #mcpm