Skip to main content
Glama
fabianguiliani

warehouse-mcp-server

warehouse-mcp-server

A Model Context Protocol server that gives an LLM safe, read-only access to a PostgreSQL warehouse — schema introspection, guarded SQL, and result caps that tell the caller when they truncated.

npm install && npm test   # 33 tests, including a real MCP client↔server round-trip

The suite runs against PGlite (PostgreSQL compiled to WebAssembly), so the introspection queries, the read-only transaction and the row caps are exercised for real rather than mocked into agreement — no Docker, no service to start.


Why the safety model is layered

Handing a language model a database connection is the most useful and most dangerous thing in this repo. A model can be talked into things; a prompt injection buried in a row of data is a real attack, not a thought experiment. So there are three layers, ordered by how much each deserves to be trusted:

1. A read-only database role. The actual boundary. The connection has SELECT on the mart schema and nothing else, so even a total bypass of everything below can only read what a dashboard could read. If you deploy this with a superuser connection string, nothing else here saves you.

2. A read-only transaction and a statement timeout. Every query runs inside BEGIN; SET TRANSACTION READ ONLY; SET LOCAL statement_timeout = …. Postgres refuses writes and kills runaway scans, independently of anything this process believes. There is a test that bypasses the static checks entirely to prove this layer holds on its own.

3. Static SQL checks. Reject bad queries before they are sent, so the model gets only SELECT queries are allowed instead of an opaque permission error it will start trying to work around.

Layer 3 is defence in depth and a UX feature. It is deliberately not the thing standing between a prompt injection and your data — a string-matching filter never should be.


Related MCP server: psql-mcp

What the static checks actually catch

The parser strips comments and string literals first, replacing them with spaces, and every check runs on that. Without it the checks are trivially defeated and simultaneously produce false positives:

Input

Verdict

Why

SELECT * FROM mart.audit WHERE action = 'delete from ledger'

allowed

The word is inside a string. A naive filter blocks this.

SELECT 1; DROP TABLE core.fct_order_line

rejected

Second statement — the classic way a "read-only" tool executes a DELETE.

SELECT 1; /* nothing to see */ DROP TABLE x

rejected

Comments cannot hide a statement separator.

WITH gone AS (DELETE FROM t RETURNING *) SELECT * FROM gone

rejected

A writable CTE. It starts with WITH, and it writes.

SELECT * INTO new_table FROM mart.sales

rejected

SELECT … INTO quietly creates a table.

SELECT pg_read_file('/etc/passwd')

rejected

Nothing an analytics question needs reaches outside the database.

SET statement_timeout = 0

rejected

No session tampering.

/* nested /* deeper */ still comment */ DROP TABLE x

rejected

Postgres block comments nest; the stripper does too.

Row caps are applied by wrapping, not appending: SELECT * FROM (⟨query⟩) AS _guarded LIMIT n. Appending LIMIT to a query that already has one is a syntax error, and appending to a UNION silently limits the last branch only.


Designing tools for a model rather than a human

The server fetches one row more than the cap so it can report truncated: true with a note. A model that does not know it saw a partial result will reason as if it saw everything — and then confidently tell someone the top location is whichever one happened to sort first.

Errors are written for the caller deciding what to do next:

schema "core" is not exposed. This connection serves: mart. Call list_tables to see what is available.

rather than permission denied for table dim_item, which sends a model looking for a way around instead of somewhere useful.

Failures come back as tool results with isError: true, not protocol errors. A protocol error aborts the call; a readable tool result lets the model correct itself. Unexpected exceptions are logged for us and generalised for the caller, because a raw Postgres error can carry a connection string or an internal path.


Tools

Tool

Purpose

list_tables

Every table, view and materialized view in the exposed schemas, with descriptions. Cheap; call it first.

describe_table

Columns, types, nullability and column comments for one relation.

run_query

One read-only SELECT, capped, timed out, inside a read-only transaction.

Table and column comments are surfaced in the output, which is the cheapest schema-documentation win available: a COMMENT ON COLUMN written once stops a model guessing what net_amount means for the rest of the model's life.


Running it

DATABASE_URL='postgres://warehouse_reader:…@host/db' \
EXPOSED_SCHEMAS=mart \
DEFAULT_ROW_LIMIT=200 \
npx warehouse-mcp-server

In an MCP client's config:

{
  "mcpServers": {
    "warehouse": {
      "command": "npx",
      "args": ["-y", "warehouse-mcp-server"],
      "env": {
        "DATABASE_URL": "postgres://warehouse_reader:…@host/db",
        "EXPOSED_SCHEMAS": "mart"
      }
    }
  }
}

Use a role that can only read the schemas you name. That role is the security boundary; everything in this process is the second and third lines of defence.

Variable

Default

DATABASE_URL

required; point it at a read-only role

EXPOSED_SCHEMAS

mart

comma-separated

DEFAULT_ROW_LIMIT

200

MAX_ROW_LIMIT

5000

hard ceiling the caller cannot raise

STATEMENT_TIMEOUT_MS

15000

Note that stdout is the protocol channel — all logging goes to stderr, because a stray console.log corrupts the stream and produces a failure that is genuinely baffling to debug.


Layout

src/
  sql-guard.js  literal/comment-aware stripper + read-only policy + row-limit wrapper
  tools.js      the three tools, transport-agnostic and independently testable
  server.js     MCP wiring — the thinnest file here, on purpose
test/
  guard.test.js     16 tests, every bypass above
  tools.test.js     12 tests against real Postgres via PGlite
  protocol.test.js  5 tests over a real MCP client↔server transport pair

Requires Node 20+. MIT licensed.

Available Tools

3 tools
describe_tableA

Show the columns, types, nullability and column comments for one table or view. Call this before writing a query against a relation you have not queried before.

ParametersJSON Schema
NameRequiredDescriptionDefault
tableYesTable name, optionally schema-qualified, e.g. "mart.daily_location_sales".

TDQS

A4.1/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description discloses the core behavior (what metadata it returns) but goes little beyond that. No annotations are provided, and it does not mention error handling, schema-qualified name resolution, or whether it accesses system catalogs. Adequate but minimal for a simple read-only tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences, front-loaded with the purpose and followed by a usage directive. Every word earns its place; there is no redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with one parameter, no annotations, and no output schema, the description is complete. It states exactly what the tool returns (columns, types, nullability, comments) and when to use it, which is sufficient for an agent to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already describes the 'table' param fully with an example. The description adds only that it applies to 'table or view', which is a minor clarification. Since schema coverage is 100%, the description does not need to compensate, but it also does not add significant semantic value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with a specific verb ('Show') and resource ('columns, types, nullability and column comments for one table or view'). This distinguishes it from siblings like list_tables and run_query, which serve different functions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit when-to-use guidance: 'Call this before writing a query against a relation you have not queried before.' It does not mention alternatives or exclusions, but the context is clear enough for an agent to decide when to invoke it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_tablesA

List every table, view and materialized view available in the mart schema(s), with its description. Call this first — it is cheap and tells you what exists.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It adds that the tool is 'cheap' and 'tells you what exists,' which are useful traits. However, it does not mention potential limits (e.g., pagination, schema coverage), auth requirements, or any side effects. For a read-only listing tool, the disclosed behavior is adequate but not comprehensive.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two concise sentences. The first sentence states the action and included objects, and the second provides a clear usage directive. Every word contributes value, and the structure is front-loaded with the primary function.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (no parameters, no output schema), the description is sufficiently complete. It explains what the tool returns (tables with descriptions), when to use it (first), and why (cheap). It does not describe the return format in detail, but that is less critical for a discovery tool whose primary purpose is inventorying available data assets.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, so the baseline is 4. The description does not need to explain parameters, and it correctly focuses on the tool's purpose and output scope. No additional parameter detail is needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: listing every table, view, and materialized view in the mart schema(s) along with descriptions. The phrase 'Call this first' distinguishes it from siblings by positioning it as an initial discovery step, contrasting with describe_table (for specific tables) and run_query (for arbitrary queries).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly advises to call this tool first because it is cheap and reveals what exists. This provides clear usage context, though it does not explicitly mention alternatives or when not to use it. The sibling tool names imply that describe_table is for deeper details and run_query for specific queries, but this is not stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

run_queryA

Run a read-only SQL SELECT against the warehouse and return rows as JSON. Only SELECT (and WITH ... SELECT) is permitted; a single statement per call. Results are capped at 200 rows by default and 5000 at most — aggregate in SQL rather than pulling rows and counting them yourself.

ParametersJSON Schema
NameRequiredDescriptionDefault
sqlYesA single SELECT statement.
limitNoRow cap (default 200).

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses read-only behavior, allowed statement types, single-statement restriction, default and maximum row caps, and advises against pulling rows for counting. This is rich behavioral disclosure far beyond simple 'runs a query'.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, front-loaded with the core purpose, followed by constraints and best-practice advice. No filler or redundancy; every sentence adds necessary information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (SQL query execution), the description covers return format (JSON), allowed syntax, single-statement rule, and row limits. Sibling tools are metadata-oriented, so this is complete for selecting and invoking the tool. No output schema or annotations exist, but the description sufficiently covers what the agent needs to know.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds value by explaining the default row cap (200), the absolute max (5000), and the rationale for using SQL-side aggregation, which complements the schema's limit parameter and sql description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states a specific verb ('Run') and resource ('a read-only SQL SELECT against the warehouse') with a concrete outcome ('return rows as JSON'). It distinguishes itself from siblings (list_tables/describe_table) by being the data-query tool rather than a metadata tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit constraints: only SELECT/WITH...SELECT, single statement, row caps, and guidance to aggregate in SQL instead of pulling raw rows. This clearly tells the agent when to use the tool and what to avoid, effectively differentiating from metadata-only sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

TDQS

A4.5/5.0
Disambiguation5/5

Each tool targets a distinct concern: discovering relations, inspecting their schema, and executing queries. There is no functional overlap between listing, describing, or querying.

Naming Consistency5/5

All three tool names follow a consistent verb_noun pattern (list_tables, describe_table, run_query), with clear verbs and direct objects. The naming style is uniform across the set.

Tool Count5/5

Three tools is a well-scoped size for a read-only warehouse server. Each tool is essential for the workflow of exploring and querying data, with no redundancy or excessive surface area.

Completeness5/5

The toolset fully covers the core lifecycle of read-only data access: discover what exists, understand its schema, and query it. There are no obvious gaps for the stated purpose, and the server's read-only constraint makes additional write/update tools unnecessary.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A read-only MCP server for PostgreSQL that enables safe database introspection and querying via natural language.
    751
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A hardened, read-only Postgres MCP server that enables LLMs to safely query databases without write, DDL, shell, or credential exposure.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Read-only MCP server for SQL databases (SQLite/PostgreSQL) that enables listing tables, describing schemas, and executing SELECT queries with safety guardrails.
    MIT
  • F
    license
    A
    quality
    B
    maintenance
    A security-hardened Postgres MCP server that enables LLM agents to run safe, read-only SQL queries with enforcement via SQL-AST inspection and read-only transactions.
    1

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/fabianguiliani/warehouse-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server