pg-schema-scout
Provides read-only access to a PostgreSQL database, enabling schema search, table description, query plan analysis, and execution of SELECT queries.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@pg-schema-scoutWhich tables should I query to find total sales by territory?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
pg-schema-scout
Schema access for an agent is a retrieval problem, not a dump.
An MCP server that hands a model only the tables relevant to its question, and enforces read-only in three layers rather than annotating it.
The number
Measured against the seeded AdventureWorks database in this repo — 68 tables across five
schemas — over the 26 questions in evals/questions.yaml:
Tokens | |
Full schema DDL, dumped in one blob | 10,927 |
Retrieved slice, mean over 26 questions (k=8) | 1,816 |
6.0× less schema context (83% fewer tokens) |
Median slice 1,858; range 1,071–2,368. Tokenizer: tiktoken/cl100k_base. Backend: BM25.
Reproduce with python evals/measure_context.py. Every figure in this README comes from a
real run against the seeded database; none is estimated. CI re-measures all of them on every
push from a clean machine, and reproduces them exactly — so they are properties of the
schema, not of the laptop they were first taken on.
That ratio is the boring half of the argument. The interesting half is what happens on a
schema too wide to dump at all: a dump-everything server does not degrade gracefully, it
truncates, and if the relevant table falls outside the cutoff it is gone and the model has
no way to ask for it. This server's answer to that is describe_table, below.
Related MCP server: AgenticMCP
Quickstart
docker compose up -d --waitThat builds Postgres, downloads the AdventureWorks OLTP sample, seeds 68 tables across five schemas, and creates both roles. First run takes a few minutes; afterwards it is instant.
pip install -e .Then point your MCP client at it:
{
"mcpServers": {
"pg-schema-scout": {
"command": "pg-schema-scout",
"env": {
"PGSS_DSN": "postgresql://scout_ro:scout_ro_pw@localhost:55432/adventureworks"
}
}
}
}stdio by default. pg-schema-scout --http serves Streamable HTTP on port 3000 instead.
The credentials above are for a local throwaway container holding public sample data. There are no real credentials in this repo.
The four tools
Four, not fourteen. Every tool description is paid for on every request, so mirroring a REST surface into tools is a tax on each call. These four match the loop an agent actually runs.
Tool | Signature | Annotations |
|
|
|
|
| same |
|
| same |
|
| same |
All four return Pydantic models, so outputSchema and structuredContent are generated
rather than hand-written.
Annotation defaults in the spec are pessimistic — omitting them declares a tool destructive,
open-world and non-idempotent — so all four are set explicitly. They are returned in a fixed
order, because the SDK preserves registration order and the spec cites prompt-cache hit rate
as the reason to keep it stable. tools/list carries ttlMs: 300000 and
cacheScope: "public": the tool list is derived from this server's own code, so it is
identical for every caller and stable for the life of the process, but a redeploy can change
it, so it is not cached longer.
Safety model
Three layers, weakest first.
Layer | What it does | Why it is not enough alone |
| Tells the client the tool is safe to auto-approve | An untrusted hint. Decorative |
AST policy ( | Parses with sqlglot and rejects anything that is not a single read-only SELECT | A parser bug is a bypass |
| Holds | This is the layer that actually holds |
The annotation is the weakest layer, and deliberately so. The specification states that
clients must treat tool annotations from an untrusted server as untrusted — the SDK's own
ToolAnnotations docstring says clients should never make tool-use decisions based on
annotations received from untrusted servers. A malicious server can set readOnlyHint=true
and delete your data. It is documentation, not a control. Anyone who claims their database
MCP server is read-only because of the annotation has misread the spec.
The query a regex guard misses
WITH gone AS (DELETE FROM customer RETURNING *) SELECT * FROM gone;It begins with WITH. It contains SELECT. It empties the table. A regex allowlist that
checks the statement's prefix passes it straight through.
sqlglot parses it to a top-level Select whose tree contains a Delete node, so walking the
AST rejects it. The same argument runs the other way — a text denylist wrongly rejects
SELECT 'DROP TABLE customer' AS note FROM person.person;which is an ordinary read-only query. Both directions are in the fault matrix.
The guard fails closed: unparseable input, empty input, statement stacking, and anything
sqlglot can only represent as an opaque Command are all refused. It also blocks SELECT … FOR UPDATE (row locks are not read-only), SELECT … INTO (DDL wearing a SELECT costume),
and a denylist of filesystem, network, administrative and denial-of-service functions checked
against every function node in the tree rather than against the raw string.
Blocked is a tool error, not a protocol error
A rejected query returns HTTP 200 with a JSON-RPC result carrying isError: true and a
message the model can act on. Only genuine protocol faults become JSON-RPC errors. Verified
on the wire:
{"jsonrpc":"2.0","id":1,"result":{
"content":[{"type":"text","text":"Error executing tool run_query: Blocked: the statement contains a DELETE nested inside the query (in a CTE or subquery), which a prefix check would miss. This server is read-only. Rewrite the request as a plain SELECT."}],
"isError":true,"resultType":"complete"}}Below the parser
The server authenticates as scout_ro, which is granted SELECT and nothing else, with
TEMPORARY revoked on the database and no CREATE on any schema. Sessions run with
default_transaction_read_only=on, statement_timeout and
idle_in_transaction_session_timeout, and every query runs in an explicit read-only
transaction with a row cap.
tests/test_readonly_role.py sends DELETE, UPDATE,
INSERT, TRUNCATE, DROP, ALTER, CREATE TABLE, CREATE TEMP TABLE, CREATE INDEX
and GRANT straight to Postgres, bypassing the guard entirely, and asserts the database
refuses each one. That test is the proof the bottom layer holds independently of the parser.
Evals
Both figures below are from python evals/run_evals.py against the seeded database. It
exits non-zero on any regression and runs in CI.
Fault matrix — 12/12 blocked, 9/9 allowed, 9/9 executed.
Two-sided on purpose. The must_allow half is the half people skip, and it is the half that
proves the guard is a guard rather than a policy that refuses everything. Every must_allow
query is executed against the database, not merely parsed, so "allowed" means it actually
runs.
Retrieval — recall@8 0.955, precision@8 0.260, 24/26 questions with complete recall.
precision@8 is structurally capped: a question needs 1–4 tables but 8 slots come back, so
the ceiling is around 0.5. Recall is the number that matters here — a missing table breaks
the query, a spare one costs a few tokens.
Foreign-key propagation, and what it bought
The first eval run failed. Purely lexical ranking scored recall@8 0.840, with nine questions
missing a required table, and the misses had a shape: production.product missed four times,
person.person twice. These are join hubs. A slice containing salesorderdetail without
product names a productid the model cannot resolve, so it is not a usable slice.
So ranking adds one damped round of score propagation across foreign keys: a table adjacent to a strong match is likely to be needed, precisely because you cannot join to what you have not been shown. Measured over the 26 questions:
neighbour weight | recall@8 | complete recall |
0.00 (off) | 0.840 | 17/26 |
0.25 | 0.955 | 24/26 |
0.35 (shipped) | 0.955 | 24/26 |
0.50 | 0.965 | 24/26 |
0.35 takes nearly all the gain; past 0.5 the curve flattens because the graph term starts swamping the lexical signal, which is how you end up returning the whole schema again.
Two honest caveats. The weight was chosen on the same 26 questions the recall is measured on, so 0.955 is optimistic — it is not a held-out score. And better recall costs tokens: the mean slice grew from 1,684 to 1,816, moving the headline ratio from 6.5× to 6.0×. Recall was judged worth more than the ratio.
BM25 vs Titan embeddings: the lexical backend wins
Both backends were measured against the same 26 questions. Titan Text Embeddings V2 ran against the real Bedrock service — 94 invocations, 68 table documents plus 26 queries.
Backend | FK propagation | recall@8 | precision@8 | complete recall |
BM25 (default) | 0.35 | 0.955 | 0.260 | 24/26 |
Titan V2 | 0.35 | 0.926 | 0.255 | 22/26 |
BM25 | off | 0.840 | 0.226 | 17/26 |
Titan V2 | off | 0.833 | 0.226 | 17/26 |
The embeddings lose. Before propagation the two are indistinguishable (0.840 vs 0.833), so the embeddings were not finding anything the lexical path missed; after propagation BM25 pulls ahead. This is a reasonable outcome rather than a surprising one: a table's document here is mostly identifiers and a one-line comment, not prose, which is the regime BM25 is built for. Foreign-key structure turned out to matter far more than semantic similarity.
Two caveats that cut against the winner. Titan was handed the same identifier-heavy document text as BM25, and a prose rendering of each table might suit it better. And the lexicon and stemmer were written with BM25 in mind, so the comparison is not perfectly even-handed.
BM25 stays the default — which also means the repo runs offline, with no credentials and no
per-query cost. That was already the plan; it is now the plan and the better-measuring
option. Reproduce with PGSS_RETRIEVAL_BACKEND=titan.
Known limits
Retrieval has a recall ceiling, and this is the design's real failure mode. At k=8 it misses a required table on 2 of 26 questions:
revenue-by-category(needs a four-table join through the category hierarchy) andreseller-locations(the store→address path runs through a cross-reference table that matches nothing in the question's wording). When it misses, the table is simply absent and the slice alone cannot recover.describe_tableis the escape hatch for exactly that. It does not use the ranking, so a model that knows or guesses a table name can always reach it. EverySchemaSlicecarries a note telling the model to do this, and the failure is named in thesearch_schemadocstring the model reads.The lexical backend cannot do irregular morphology. Stemming reduces
employeestoemployee, butpaiddoes not reduce topay. There is a test asserting the limit rather than hiding it. Embeddings were the obvious fix and were measured; they did not help overall (see above), so this ceiling stands.The identifier lexicon is schema-specific. AdventureWorks names are lowercase concatenations (
salesorderheader), and splitting them needs a word list. The one inretrieval.pycovers this schema's vocabulary; another database would want its own.The Titan/Bedrock backend is measured but not the default, because it measured worse. It has been run against the real service, so the numbers above are real; it is not a code path to trust blindly at scale, since it has only ever been exercised over 68 documents and 26 queries with no retry, batching or rate-limit handling.
Not implemented, deliberately: no prompts, no resources, no write path of any kind, no
EXPLAIN ANALYZE(it would execute the query), no cross-database or multi-tenant support, no auth — the server is expected to run locally over stdio, and the security boundary is the database role, not the transport.
Conformance: does not pass, and here is why
npx @modelcontextprotocol/conformance@0.1.16 server --url http://localhost:3000/mcp reports
11 passed, 21 failed. That is not a green check, and it is not a defect in this server:
The tool has no knowledge of protocol revision 2026-07-28.
--spec-versionaccepts only2025-03-26,2025-06-18,2025-11-25,draftandextension. It therefore exercised the legacy handshake path that the SDK still supports for older clients, which is whyserver-initializeandpingappear in the passing column despite both being removed in the revision this server targets.Its scenarios assert against a reference fixture server. The failures are things like
Unknown resource: test://static-text,Unknown prompt: test_simple_promptandNo image content found— a server with four tools and no prompts or resources cannot satisfy them.logging-set-levelfails withMethod not found, which is correct behaviour for this revision.
Every scenario that tests protocol behaviour rather than fixture presence passes:
tools-list, tools-call-simple-text, tools-call-error, resources-list, prompts-list,
ping, server-initialize, server-sse-multiple-streams, dns-rebinding-protection.
It is not wired into CI, because doing so would mean checking in a baseline of 21 expected failures, and a baseline that large hides regressions rather than catching them.
Spec version
Built against MCP protocol revision 2026-07-28, on the official Python SDK mcp==2.0.0
(pinned exactly; the v1 line continues in parallel and the APIs are not compatible).
That revision made the protocol stateless: the initialize handshake,
notifications/initialized, sessions and Mcp-Session-Id are gone, replaced by
server/discover plus a per-request _meta envelope carrying the protocol version and the
client's capabilities. There is no session handling anywhere in this codebase for that
reason. Every result now carries a resultType, cacheable methods carry ttlMs/cacheScope,
HTTP+SSE is deprecated in favour of stdio and Streamable HTTP, and sampling, roots and
logging are deprecated — none of which this server builds on.
The v2 SDK also removed FastMCP: from mcp.server.fastmcp import FastMCP raises
ModuleNotFoundError, and the entry point is MCPServer from mcp.server. (The fastmcp
package on PyPI is a separate, independent project.)
Two notes for anyone reading the code against the docs:
mcp.typesis now a mirror of a standalonemcp-typespackage. Both spellings work.ToolAnnotationsfields are snake_case in Python (read_only_hint) and camelCase on the wire (readOnlyHint). Both are accepted at runtime, but only snake_case type-checks under mypy, so the source uses snake_case and this README quotes the wire form.
Design decisions not dictated by the brief
The five shorthand view schemas are excluded from the index. The upstream loader also creates
pe/hr/pr/pu/sa, 68 convenience views over the same 68 tables. Indexing them would double every table and inflate the full-schema baseline this README compares against. Excluding them makes the headline ratio harder to hit, not easier.Foreign-key edges leaving the slice are dropped from
SchemaSlice.foreign_keys. The model cannot join to a table whose columns it has not been shown, so listing those edges spends tokens on an unusable hint.DDL is rendered, not
pg_dumped. No storage parameters, tablespaces or ownership: they cost tokens and answer no question a query author has.Decimalis returned as a string, not a float. These are money and quantity columns, and a silent rounding is worse than a visible type change.Sample data is downloaded at image build time, not vendored, so no 40 MB blob lives in git. Both sources are pinned — the loader scripts by commit SHA, the data to Microsoft's immutable release asset.
server.pyuses absolute imports while the rest of the package uses relative ones.mcp devloads the file by path rather than as a package member, and relative imports fail under that loader.
Development
pip install -e ".[dev,bedrock]"
docker compose up -d --wait
pytest # 100 tests
pytest -m "not db" # skip anything needing Postgres
python evals/run_evals.py # fault matrix + retrieval, non-zero exit on regression
python evals/measure_context.py
ruff check . && mypy
mcp dev src/pg_schema_scout/server.py # InspectorCI runs ruff, ruff format, mypy, pytest and the evals, with the seeded database as a compose service.
License
MIT. The AdventureWorks sample data is Microsoft's, downloaded at build time and not redistributed here.
Available Tools
4 toolsdescribe_tableARead-onlyIdempotent
Return full detail for one table by name.
Use this when you already know the table name: either search_schema returned it and you need the complete column list, or search_schema did not return it and you believe it exists anyway. This does not use the ranking, so it is the reliable way to reach a table that retrieval scored poorly.
Accepts either a qualified name ("sales.salesorderheader") or a bare one ("salesorderheader"). Returns columns with types and comments, the primary key, foreign keys out, and the foreign keys pointing in, which is how you find the tables that join to this one.
On failure:
"No table named ..." means it does not exist under that name. The error lists close matches; try one of those. Do not retry the same name, and do not guess a third spelling. Fall back to search_schema with a description of what the table should contain.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| ddl | Yes | |
| name | Yes | |
| columns | Yes | |
| comment | No | |
| primary_key | No | |
| schema_name | Yes | |
| foreign_keys | No | Edges from this table to others. |
| row_estimate | No | |
| referenced_by | No | Edges from other tables into this one. |
| qualified_name | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, and non-destructive, but the description adds valuable behavioral details: accepted name formats (qualified vs bare), return contents (columns, types, comments, PK, FKs), and failure semantics with close-match suggestions and explicit 'do not retry/guess' instructions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the core purpose, then organized into usage context, accepted inputs, return values, and failure handling. Every sentence adds operational value and there is no redundancy or filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Considering the tool's moderate complexity and the presence of an output schema, the description is complete: it covers when to use, input formats, what the output contains (including how to find joining tables), and exact failure recovery steps. The sibling context (search_schema) is also directly addressed.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema only says 'name' is a required string (0% coverage), so the description fully compensates by explaining that the parameter accepts qualified or bare names, provides concrete examples, and clarifies the failure-response behavior associated with the name.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and resource: 'Return full detail for one table by name.' It explicitly distinguishes itself from search_schema by noting it does not use ranking, making it the reliable way to reach a table with poor retrieval scores.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit when-to-use scenarios: when the table name is already known from search_schema or when the table is believed to exist despite search_schema not returning it. It also provides fallback guidance on failure, telling the agent to try close matches and then fall back to search_schema with a description.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
explain_queryARead-onlyIdempotent
Plan a SELECT without executing it.
Use this before run_query when the query touches a large table, has no WHERE clause, or joins several tables, so you can see the cost before committing to it. The query is planned only; no rows are read and nothing is executed. EXPLAIN ANALYZE is deliberately not offered, because it would run the statement.
Read total_cost as a relative number, useful for comparing two phrasings of the same query rather than as a time.
On failure:
"Blocked: ..." means the same read-only policy as run_query rejected it. Rewrite as a plain SELECT.
A "column does not exist" error means the schema assumption was wrong. Call describe_table and fix the column names.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| sql | Yes | |
| note | No | |
| plan | Yes | EXPLAIN output, one line per plan node. |
| plan_rows | No | Planner's estimated output row count. |
| total_cost | No | Planner's estimated total cost of the top node. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare read-only and idempotent, but the description goes further: 'no rows are read and nothing is executed', explains why EXPLAIN ANALYZE is omitted, and clarifies total_cost as relative. It also discloses failure modes, adding significant context beyond annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact, front-loaded with the primary action, and uses a bulleted list for failure cases. Every sentence adds value—no fluff or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Despite having only one parameter and no param descriptions, the description covers usage context, safety, failure handling, and relative output interpretation. An output schema exists, so return values need not be described. It is fully complete for an agent to use correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% and there is one parameter (sql). The description compensates by implying sql must be a plain SELECT (via 'Rewrite as a plain SELECT') and explains column-error behavior. It does not explicitly label the parameter but the meaning is clear from context.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description starts with a specific verb+resource: 'Plan a SELECT without executing it.' This clearly distinguishes the tool from run_query (which executes) and explains its core function. It also scopes it to SELECT statements, avoiding ambiguity.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly instructs to 'Use this before run_query' under conditions (large table, no WHERE, joins), and names the alternative run_query. On failure, it directs the user to call describe_table for schema errors, providing clear when-to-use and when-not-to-use guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
run_queryARead-onlyIdempotent
Execute a read-only SELECT and return the rows.
Use this once you know which tables and columns you need. If you do not yet know, call search_schema first rather than guessing table names; a query against a table that does not exist wastes a round trip.
Only a single SELECT is accepted. WITH is fine as long as the whole statement is read-only. Anything else, including INSERT/UPDATE/DELETE, DDL, statement stacking, and DML hidden inside a CTE, is rejected before it reaches the database.
On failure:
"Blocked: ..." means the statement violated the read-only policy. The message names the offending construct. Rewrite as a plain SELECT; do not retry the same statement.
A syntax or "column does not exist" error means the schema assumption was wrong. Call describe_table on the table in question and correct the column names rather than guessing again.
A statement timeout means the query was too expensive. Call explain_query to see the plan, then add a WHERE clause or aggregate.
Results are capped. When truncated is true the rows shown are a prefix, not the answer; narrow the query instead of treating them as complete.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | ||
| max_rows | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | Set when truncated, explaining how to narrow the query. |
| rows | Yes | Row-major values, aligned with columns. |
| columns | Yes | |
| row_count | Yes | Number of rows returned, after any truncation. |
| truncated | Yes | True when the result hit max_rows and more rows exist. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Discloses read-only policy enforcement details: only single SELECT accepted, rejects DML/DDL/stacking (including DML in CTEs), with error prefix 'Blocked: ...' explained. Also discloses result capping and the meaning of the truncated flag—all beyond the readOnlyHint and idempotentHint annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Well-structured with a clear opening purpose and organized failure/recovery sections. Every sentence adds operational value; no filler or redundancy. Length is justified by the number of constraints and failure modes.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Covers purpose, usage timing, policy restrictions, failure remediation, and output limits. With an output schema present, return values need no further description. The description is sufficiently complete for the agent to select and invoke correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema provides no descriptions (0% coverage). The description thoroughly explains the sql parameter: must be a read-only SELECT, single statement, WITH allowed. However, max_rows is only indirectly addressed via result capping/truncation; it doesn't explicitly state that max_rows controls the cap. Still, it adds substantial meaning for the required parameter.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States specifically: 'Execute a read-only SELECT and return the rows.' This clearly conveys the verb (execute), resource (SQL SELECT), and output (rows). It differentiates from sibling search_schema (schema discovery), describe_table (schema details), and explain_query (plan analysis).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly instructs: 'Use this once you know which tables and columns you need. If you do not yet know, call search_schema first rather than guessing table names.' Also provides fallback guidance to call describe_table after schema errors and explain_query after timeouts, making alternatives and exclusions explicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_schemaARead-onlyIdempotent
Find the tables relevant to a question and return their DDL.
Prefer this over trying to read the whole schema. The schema is far larger than the part of it any one question needs, and sending all of it costs context you will want for the answer. Pass the user's question in natural language, not a table name guess: this ranks on column names, types and the schema's own comments.
Returns the highest-scoring tables with their columns and keys, plus the foreign-key edges between the returned tables, which is what you need to write the joins.
On failure or a disappointing result:
If a table you know you need is missing from the results, call describe_table with its name. Retrieval ranks, and a needed table can fall below the cutoff; describe_table is the escape hatch and does not depend on the ranking.
If nothing looks relevant, re-ask with the domain words the user used (for example "reseller", "purchase order", "pay history") before widening limit. Raising limit costs tokens and rarely fixes a vocabulary mismatch.
Do not invent table names. If you cannot find it here, say so.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| question | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | Guidance for the model, including what to do if the table it needs is absent. |
| tables | Yes | |
| backend | Yes | Retrieval backend that ranked these tables: 'bm25' or 'titan'. |
| question | Yes | |
| tokenizer | Yes | Which tokenizer produced the counts. |
| foreign_keys | No | Foreign-key edges *between tables in this slice*, so joins can be written without fetching more schema. |
| estimated_tokens | Yes | Token count of this slice's DDL, by the tokenizer named in tokenizer. |
| full_schema_tokens | Yes | Token count of the entire schema's DDL, for comparison with estimated_tokens. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false. The description adds valuable behavioral context beyond this: ranking based on column names/types/comments, returning highest-scoring tables with columns and keys, foreign-key edges between returned tables, and failure-mode behavior. This significantly enhances the agent's understanding of how the tool operates.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is longer than average but well-structured and front-loaded with the core purpose. Every sentence serves a purpose: explaining the tool's behavior, providing usage context, or detailing failure handling. The bullet-point structure for failure cases improves scannability without redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description is complete for the tool's complexity. It covers the tool's output (highest-scoring tables, columns, keys, foreign-key edges), input semantics, usage strategy, failure handling, and cost considerations. The presence of an output schema reduces the need to describe return details, and the description goes beyond the minimum.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must carry the burden. It explains that 'question' should be natural language ('Pass the user's question in natural language, not a table name guess') and clarifies the 'limit' parameter's cost/benefit ('Raising limit costs tokens and rarely fixes a vocabulary mismatch'). This fully compensates for missing schema descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence explicitly states the tool's purpose: 'Find the tables relevant to a question and return their DDL.' This uses a specific verb and resource, clearly distinguishing it from siblings like describe_table (which retrieves a single table) and run_query (which executes SQL).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides explicit usage guidance: 'Prefer this over trying to read the whole schema' and outlines specific fallback instructions, including when to call describe_table ('If a table you know you need is missing'), how to rephrase the question with domain words, and a caution against inventing table names. This creates a clear decision tree for when to use this tool vs alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
4 tool updates
v0.1.0- First observed
describe_table - First observed
explain_query - First observed
run_query - First observed
search_schema
TDQS
Each tool has a clear, distinct role: search_schema for discovery, describe_table for known tables, explain_query for planning, and run_query for execution. There is no overlap between them.
All four tools follow a consistent verb_noun pattern (search_schema, describe_table, explain_query, run_query), making the set predictable and easy to navigate.
Four tools is well within the ideal range for a focused schema exploration and read-only query server. Each tool earns its place and covers a distinct step in the workflow.
The tool surface fully covers the stated purpose: discover relevant tables, inspect a specific table, plan a query, and execute it. The descriptions even include escape hatches (describe_table when search_schema misses, explain_query for costly queries), leaving no obvious gaps.
Maintenance
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
Analytical memory for AI agents: a real Postgres queried in plain English over MCP. One command.
- XataOAuthio.github.xataio
Xata MCP server lets AI agents interact with your Xata projects, and Postgres database branches.
The Grafbase MCP server sits in front of a GraphQL API and exposes an MCP protocol-compliant interface that allows AI agents and LLMs to explore and query GraphQL APIs using natural language. It provides tools to search schemas, introspect types and fields, and execute GraphQL queries while minimizing context bloat by returning only relevant schema subsets, with built-in support for authentication, authorization, and configurable access control.
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceAn MCP server that gives an AI agent scoped, safe access to your Postgres databases with per-connection access control, row caps, timeouts, and defense-in-depth read-only enforcement.-
- AlicenseNot gradedqualityDmaintenanceA Model Context Protocol (MCP) server that provides secure, role-based access to PostgreSQL databases for AI agents.MIT
- AlicenseAqualityDmaintenanceA production-grade MCP server that gives AI agents safe, authenticated access to a PostgreSQL database.3MIT
- FlicenseNot gradedqualityFmaintenanceA read-only MCP server that enables AI agents to explore database schemas and execute safe queries on PostgreSQL and MySQL.-
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/AkshaySwami14/pg-schema-scout'
If you have feedback or need assistance with the MCP directory API, please join our Discord server