guarded-sql-mcp
Provides read-only access to a PostgreSQL database through a fixed catalog of parameterized queries. An LLM agent can query an IT asset inventory to list sites, search devices, find people, inspect installed software, identify stale devices, and view open tickets, without writing SQL or accessing restricted tables and columns.
Click on "Deploy 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., "@guarded-sql-mcpshow me devices that haven't checked in for 90 days"
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.
guarded-sql-mcp
An MCP server that gives an LLM agent read access to a PostgreSQL database through a fixed catalog of parameterized queries. The model can pick a query and fill in bounded parameters. It cannot write SQL, and it cannot reach tables or columns that the catalog and the database role do not allow.
The schema is a fictional IT asset inventory (sites, devices, people, installed software, tickets).
Threat model
Treat everything the model sends as untrusted input. It may have read a malicious ticket title, a poisoned web page or a crafted email, and its tool arguments can carry whatever that content asked for. A system prompt that says "never read the password column" is a request, not an access control. So every restriction here is enforced in code or in the database, and each one is covered by a test:
No free-form SQL is reachable by the model. Every tool maps to one static, reviewed statement. Arguments are only ever bind parameters.
Password and secret columns are excluded by construction. They are rejected in the catalog at startup, stripped from results at runtime, and not granted to the database role.
The table allowlist is checked in code, not described in a prompt. A catalog entry that touches a table outside the allowlist stops the server from starting.
Related MCP server: django-mcp-sql
Defense layers
flowchart TD
S["Startup: validateCatalog()"] -.->|must pass before the server is built| T
M["Model: tools/call"] --> T{"Tool in the catalog?"}
T -- no --> R1["Refused"]
T -- yes --> V{"Strict zod schema:<br/>bounded fields, no unknown keys"}
V -- invalid --> R2["Refused, executor never called"]
V -- valid --> P["params() builds $1..$n bind values"]
P --> X["PgExecutor: BEGIN READ ONLY,<br/>SET LOCAL statement_timeout,<br/>static SQL ending in LIMIT, COMMIT"]
X --> DB[("PostgreSQL as mcp_readonly:<br/>table and column grants")]
DB --> C["Row cap, then projection onto declared<br/>columns; sensitive keys dropped"]
C --> OUT["JSON text to the model"]
X -- error --> E["Generic error to the model,<br/>details to the audit log"]Fixed catalog (src/catalog.ts). One MCP tool per entry. There is no generic query tool and no parameter named
sql,queryorstatement.Input validation. Each entry has a
z.strictObjectschema. Unknown keys are rejected. Every string has a maximum length and usually a pattern (hostnames:^[A-Za-z0-9-]{1,63}$), every number has explicit bounds,limitis 1..100. Search text is matched withILIKE ... ESCAPE '\'after escaping%and_, so the model cannot turn a search into a wildcard dump.Startup catalog validation (src/validateCatalog.ts). Before the server is created, every entry is checked (see What the validator rejects). One bad entry means the process exits.
Read-only execution (src/executor.ts). Every call runs as
BEGIN READ ONLY, a transaction-localstatement_timeout(default 5 s), the statement, thenCOMMIT, orROLLBACKon any error. Every statement must end inLIMIT, and at most 100 rows leave the server.Output projection. Each row is rebuilt from the entry's declared column list. Undeclared keys are dropped. Keys matching the sensitive pattern are dropped at any depth, including inside JSON values, even if declared. Drops are recorded in the audit log.
Database role (db/roles.sql).
mcp_readonlyhasSELECTon the allowlisted tables only, column-levelSELECTonpeoplethat leaves outpassword_hashandmfa_secret, nothing onapi_tokens, and noCREATEorTEMP. The code layers do not rely on this, and the integration tests check it separately.
Database errors reach the model as a generic message. The SQLSTATE and message go to the audit log: JSON lines on stderr, because stdout is the MCP stdio channel.
{"ts":"2026-09-25T16:30:15.850Z","event":"tool_call","tool":"find_people","args":{"name_or_email":"rivera","limit":25},"rowCount":1,"durationMs":3.1,"outcome":"ok","truncated":false}Tools
Tool | Parameters | Returns |
| none | code, name, city, count of non-retired devices |
|
| hostname, site, os, status, ip, last_seen_at |
|
| device, site, and the assigned person's name and email |
|
| full_name, email, department, site |
|
| name, version, installed_at |
|
| non-retired devices not seen for |
|
| ticket_id, title, priority, status, site, hostname, opened_at |
site is a site code such as north-branch. limit is 1-100, default 25.
All tools are annotated readOnlyHint: true.
Quick start
Requirements: Node.js 20.19 or later, and Docker for the local database.
docker compose up -d # postgres:16 with db/schema.sql, roles.sql, seed.sql
npm ci
npm run buildThe server reads its settings from environment variables and does not load
.env files. .env.example lists them, and the MCP client
passes them (see below). Always connect as mcp_readonly, never as the owner.
At startup the server logs a privilege_warning if the role is a superuser or
can write.
Variable | Default | |
| required | e.g. |
|
| Per-statement timeout inside each transaction |
Claude Code:
claude mcp add --transport stdio guarded-sql \
--env DATABASE_URL=postgres://mcp_readonly:mcp_readonly_dev@localhost:5432/inventory \
-- node /absolute/path/to/guarded-sql-mcp/dist/index.jsClaude Desktop (or any client that takes an mcpServers block):
{
"mcpServers": {
"guarded-sql": {
"command": "node",
"args": ["/absolute/path/to/guarded-sql-mcp/dist/index.js"],
"env": {
"DATABASE_URL": "postgres://mcp_readonly:mcp_readonly_dev@localhost:5432/inventory"
}
}
}
}The password in db/roles.sql and the compose file is for local development.
Anywhere else, set a real one with ALTER ROLE mcp_readonly PASSWORD '...'.
Adding a query
Add an entry with defineQuery in src/catalog.ts and append
it to CATALOG:
export const devicesByPerson = defineQuery({
name: 'devices_by_person',
title: 'Devices by person',
description: 'List devices assigned to a person, by exact email address.',
input: z.strictObject({
email: z.string().max(120).regex(/^[^\s@]+@[^\s@]+$/),
limit,
}),
sql: `
SELECT d.hostname, d.os, d.status
FROM devices d
JOIN people p ON p.id = d.assigned_person_id
WHERE lower(p.email) = lower($1)
ORDER BY d.hostname
LIMIT $2`,
params: (i) => [i.email, i.limit],
tables: ['devices', 'people'],
columns: ['hostname', 'os', 'status'],
example: { email: 'sam.rivera@example.com' },
});If the query needs a table or column the role cannot read, update
db/roles.sql as well. The integration suite runs every entry's example
against the seeded database and expects rows back.
What the validator rejects
Rule | Example |
| Declares or reads |
| SQL reads a table missing from |
| Comma joins, schema-qualified or quoted names, |
| A declared column, or any identifier in the SQL, matching |
| Anything that does not start with |
|
|
|
|
| No trailing |
| Comments, dollar quoting, unbalanced quotes |
|
|
|
|
| Self-explanatory |
Testing
npm run typecheck # tsc --noEmit, strict
npm test # unit + protocol tests, no database needed
npm run build && npm run smoke # start dist/index.js over stdio and check tools/list
npm run test:integration # needs DATABASE_URL (as mcp_readonly); skipped otherwiseWhat each suite shows:
Unit: validator (
test/unit/validateCatalog.test.ts). The real catalog passes. For each rule above, a small inline catalog that breaks it is rejected, includingFROM a JOIN bwith no alias and a sensitive column behind an innocent alias.Unit: inputs (
test/unit/inputs.test.ts). Out-of-range limits, bad hostnames such asx'; DROP TABLE devices;--, overlong and control-character strings, unknown keys likesqlare all rejected.%and_are escaped, and everyILIKEhas a matchingESCAPE '\'.Unit: executor (
test/unit/executor.test.ts). Projection keeps only declared columns and drops sensitive keys at any depth. The row cap holds when the executor returns 500 rows. Arguments reach the executor only as bind parameters. Against a recording fake pool,PgExecutorissuesBEGIN READ ONLY, the timeout, the query,COMMIT, andROLLBACKon error.Protocol (
test/protocol/server.test.ts). A real SDKClientconnects overInMemoryTransport.tools/listis exactly the catalog, and every schema is closed and bounded. Invalid arguments and unknown tools are refused before the executor runs. When a fake executor returnspassword_hash,mfa_secretandtoken_hash, none of them reaches the client. Database errors come back generic.Integration (
test/integration/database.test.ts). Runs against PostgreSQL loaded withdb/*.sql, connected asmcp_readonly. Every tool returns rows with no sensitive keys and none of the seeded secret values. Writes through the executor fail with25006(read-only transaction), and the timeout cancelspg_sleep. The role gets42501onapi_tokens, onpeople.password_hash, and onSELECT *orrow_to_json(p)frompeople. Injection-looking inputs return zero rows, not errors.
CI (.github/workflows/ci.yml) runs typecheck, tests,
build and the stdio smoke test on Node 20 and 22. It also runs the integration
suite against a postgres:16 service, with REQUIRE_INTEGRATION=1 so a
missing database fails the job instead of skipping it.
Limitations and non-goals
Not a general SQL tool. If a question is not covered by the catalog, the answer is a new reviewed entry, not a more flexible tool.
PostgreSQL only. The pattern works for SQL Server too, but this repository does not implement it.
The validator is a guard, not a parser. It uses regular expressions over a small catalog that humans review, and it fails closed on constructs it cannot verify (
EXTRACT(x FROM y),IS DISTINCT FROM, comma joins). Never use it to vet SQL from a user or a model. It also cannot see whole-row references such asSELECT p FROM people p. The column grants catch those, and the integration tests show it.Projection works on names, not content. It removes keys that look sensitive. It cannot know whether an innocently named column holds a secret.
Tool output is untrusted too. Names, emails and ticket titles are returned to the model by design, and a ticket title can itself contain a prompt injection. This server does not sanitize content. The client must treat tool results as data.
Refused calls are not audited. The SDK answers calls with invalid arguments or unknown tool names before the handler runs, so they do not appear in the audit log. Calls that pass validation are always logged.
No per-user authorization. It is a local stdio server. Whoever can start it gets the database role's access.
Provenance
This is a public reimplementation of a pattern I use in five internal, closed-source MCP servers that expose PostgreSQL and SQL Server to LLM agents. None of that code is here. This repository was written from scratch with a fictional schema and data, so that the approach and the tests behind it can be read in full.
License
MIT © Thiago Langone
Available Tools
7 toolsdevice_softwareDevice softwareARead-onlyIdempotent
List software installed on a device, optionally filtered by package name.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum rows to return (1-100, default 25). | |
| hostname | Yes | Device hostname, e.g. "nb-lt-001". Case-insensitive. | |
| name_contains | No | Substring of the software name, e.g. "Chrome". Matched literally: % and _ have no special meaning. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, non-destructive, and closed-world, so the safety profile is covered. The description adds only the optional-filter behavior and says nothing about result volume, the limit cap, or ordering, which would have been useful on top of the 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?
A single front-loaded sentence with the action, target resource, and optional narrowing stated in order. No filler or 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?
For a simple read-only, non-nested tool with a fully documented schema and no output schema, the description covers what the tool returns at a high level. It omits any mention of the 25-default/100-max result cap behavior, which is the only real gap.
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 100%, so all three parameters are already documented in the schema, including the limit range and the literal-substring matching semantics for name_contains. The description's phrase 'package name' is slightly looser than the schema's 'software name' and adds no new detail, so the baseline 3 applies.
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 a specific verb (List) and resource (software installed on a device) plus the optional filter, which is enough to distinguish it from siblings like get_device or search_devices. It does not name a sibling or explicitly contrast its scope with get_device, so it stops short of a 5.
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?
No explicit when-to-use or when-not-to-use guidance is given. Usage is only implied by the resource (software inventory for a known hostname), so an agent must infer that hostname is required and that this is the inventory view rather than the device-detail view.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
find_peopleFind peopleARead-onlyIdempotent
Find people whose name or email contains the given text. Returns name, email, department and site.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum rows to return (1-100, default 25). | |
| name_or_email | Yes | Part of a full name or email address. Matched literally: % and _ have no special meaning. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, openWorldHint=false and destructiveHint=false, so the safety profile is covered. The description adds genuinely new behavioral context by disclosing the result shape (name, email, department, site) in the absence of an output schema, plus the substring-match behavior. It stops short of covering pagination or empty-result behavior.
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?
Two short sentences with zero filler, and the core purpose is front-loaded before the return-value note.
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?
For a two-parameter read-only search with a fully documented schema and no output schema, the description supplies the missing piece: the fields returned. Only pagination/result-count behavior is left unaddressed.
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 100%, so both parameters (name_or_email, limit) are already documented in the schema, including the literal-match rule and the 1-100 range. The description's 'contains the given text' restates rather than extends that, and it says nothing about the limit parameter. Baseline 3 applies.
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 names a specific verb (find) and resource (people), and specifies the matching field (name or email contains the given text). It also lists the returned fields. It does not explicitly contrast with siblings, but no sibling tool operates on people, so the resource itself is distinguishing.
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?
Usage is only implied by the verb 'find' and the match semantics; there is no statement of when to prefer this over other lookups or any exclusion. No guidance on limit/pagination or what to do when many people match.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_deviceGet deviceARead-onlyIdempotent
Get one device by hostname, including its site and the name and email of the person it is assigned to.
| Name | Required | Description | Default |
|---|---|---|---|
| hostname | Yes | Device hostname, e.g. "nb-lt-001". Case-insensitive. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already carry readOnlyHint, idempotentHint, openWorldHint=false and destructiveHint=false, and with no output schema the description is the only source of return-shape information — it discloses that the site and assignee name/email are included. It stops short of describing missing/not-found behavior or the full field set.
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?
A single sentence that front-loads the action (get one device by hostname) and then appends the return contents. No filler, no repetition of the title.
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?
For a simple one-parameter read with no output schema, the description covers purpose and the key returned fields adequately. Slightly more on not-found handling or the remaining device fields would make it fully self-sufficient.
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?
There is only one parameter and schema coverage is 100%, with the pattern, maxLength and case-insensitivity all documented in the schema. The description adds nothing to the hostname semantics beyond naming it, so the baseline 3 applies.
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?
Specific verb+resource: fetch a single device keyed by hostname. The description also names what comes back (site, assignee name/email), which implicitly separates it from the plural search_devices/list_sites siblings, but it never names an alternative explicitly.
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?
Usage is only implied: the required hostname parameter signals 'use when you already know the hostname.' There is no statement of when to prefer this over search_devices, nor any prerequisite or exclusion.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_sitesList sitesARead-onlyIdempotent
List all sites with their code, city and number of non-retired devices.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, destructiveHint=false, and openWorldHint=false, so the safety profile is covered. The description adds one behavioral detail — that only non-retired devices are counted — but says nothing about pagination, ordering, or result size.
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?
A single front-loaded sentence that names the resource and its returned fields with no wasted words.
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?
For a zero-parameter, read-only list tool with no output schema, the description adequately conveys what comes back and the key filtering nuance (non-retired devices). Pagination or ordering behavior is unaddressed but not critical here.
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?
There are no parameters, so per the baseline the schema carries no semantics to explain. The description usefully clarifies the shape of the returned data (code, city, device count) even though no input semantics are needed.
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 a specific verb (list) and resource (sites) and specifies the returned fields: code, city, and count of non-retired devices. It is easy to distinguish from siblings like search_devices or open_tickets, though it doesn't explicitly contrast with them.
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 implies usage (enumerate all sites) but gives no explicit when-to-use guidance, prerequisites, or alternatives. With zero parameters the intent is fairly obvious, so this is adequate but not helpful guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
open_ticketsOpen ticketsARead-onlyIdempotent
List tickets that are not closed, highest priority first, optionally filtered by site and priority.
| Name | Required | Description | Default |
|---|---|---|---|
| site | No | Site code as returned by list_sites, e.g. "north-branch". | |
| limit | No | Maximum rows to return (1-100, default 25). | |
| priority | No | Ticket priority. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, non-destructive, closed-world behavior, so safety is covered. The description adds genuinely new behavioral context: the default exclusion of closed tickets and the priority-descending ordering, both of which affect result interpretation. It stops short of describing pagination/total behavior, keeping it out of the top score.
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?
A single 15-word sentence that front-loads the resource and scope, then the ordering, then the optional filters. Every clause carries information; nothing is redundant or padded.
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?
For a zero-required-parameter, three-parameter list tool with annotations covering the safety profile and a fully documented schema, the description gives enough to call it correctly. With no output schema, a brief note on the returned fields or result count would be the only missing piece.
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 100% and each parameter (site, limit, priority) is documented in the schema itself, including the enum values and default. The description adds only a loose mention that site and priority are optional filters, which is already implied by the zero-required-parameter schema. Baseline 3 applies.
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 a specific verb (List) and resource (tickets) with the default filter (not closed), the sort order (highest priority first), and the optional filters. An agent can tell immediately what this returns without opening the schema, and no sibling tool overlaps with tickets.
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 phrase 'not closed' implicitly scopes usage and the filters imply the narrowing options, but there is no explicit when-to-use or when-not guidance and no alternative named (e.g., a closed-tickets or single-ticket lookup tool). Usage is inferable rather than stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_devicesSearch devicesBRead-onlyIdempotent
Search devices by site, status and operating system. Returns hostname, site, OS, status, IP and last check-in time.
| Name | Required | Description | Default |
|---|---|---|---|
| site | No | Site code as returned by list_sites, e.g. "north-branch". | |
| limit | No | Maximum rows to return (1-100, default 25). | |
| status | No | Device status. | |
| os_contains | No | Substring of the operating system name, e.g. "Windows". Matched literally: % and _ have no special meaning. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, destructiveHint=false, and openWorldHint=false, so the safety profile is covered elsewhere. The description usefully discloses the return payload, but says nothing about pagination, default result size, or empty-result behavior.
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?
Two tightly written sentences with zero filler; purpose comes first, return shape second. Every clause carries information.
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?
With no output schema, describing the returned fields is genuinely valuable and it does so. The gaps, no mention of pagination/limit behavior or how to handle zero matches, are minor for a simple read-only search tool.
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 100%, so all parameters (site, status, os_contains, limit) are already documented, including the literal-substring semantics of os_contains and the 1-100 range on limit. The description only restates the filter dimensions, adding no syntax or format detail beyond the schema.
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 a specific verb (search) and resource (devices) plus the filter dimensions (site, status, OS) and the returned fields. It is clearly distinct from list_sites, get_device, device_software, and stale_devices, though it never explicitly names an alternative to route against.
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?
No guidance on when to use this versus siblings: stale_devices for stale inventory, get_device for a single device, or device_software for software detail. The filter list implies usage but no condition or exclusion is stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
stale_devicesStale devicesARead-onlyIdempotent
List non-retired devices that have not checked in for at least the given number of days (or never), oldest first.
| Name | Required | Description | Default |
|---|---|---|---|
| days | No | Days without check-in (1-365, default 30). | |
| site | No | Site code as returned by list_sites, e.g. "north-branch". | |
| limit | No | Maximum rows to return (1-100, default 25). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the safety profile (readOnly, idempotent, non-destructive), so the bar is lower. The description adds genuinely new context: the result set excludes retired devices, includes devices that never checked in, and is ordered oldest first. It does not mention pagination behavior, which is the only notable gap.
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?
A single well-formed sentence that front-loads the resource and the filter condition with zero 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?
For a read-only listing tool with full schema coverage and safety annotations, the description covers scope, filter semantics, and ordering. Nothing essential is missing, though noting result pagination would round it out.
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 100%, so all three parameters (days, site, limit) are already fully documented with ranges and defaults. The description only restates the days semantics implicitly; baseline 3 is appropriate when the schema does the heavy lifting.
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 a specific verb (List) and resource (non-retired devices) plus a precise filter condition (no check-in for at least N days, or never). It is distinguishable from search_devices by its staleness criterion, but it does not explicitly name or contrast with that sibling.
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?
Usage is implied by the filter definition (find stale devices), but there is no explicit when-to-use vs when-not guidance and no mention of alternatives such as search_devices, despite the tool returning a list of devices that overlaps with it.
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.
7 tool updates
v0.1.0- First observed
device_software - First observed
find_people - First observed
get_device - First observed
list_sites - First observed
open_tickets - First observed
search_devices - First observed
stale_devices
TDQS
Scored across 7 tools
search_devices and get_device have a clear plural-vs-single boundary, and stale_devices is a distinct specialized filter. device_software, find_people, and open_tickets each target clearly different resources. Minor risk of overlap between search_devices and stale_devices since both return device lists.
list_sites, search_devices, get_device, and find_people follow a verb_noun pattern, but device_software, stale_devices, and open_tickets are noun-only phrases. The mix of verb-led and noun-led names breaks predictability, and search vs find for similar lookups is inconsistent.
Seven tools is a well-scoped set that cleanly covers the main entities (sites, devices, people, software, tickets). Each tool earns its place with no redundancy or bloat.
The read-only surface covers broad listing and lookup but lacks single-record detail for sites, people, and tickets (e.g. get_site, get_ticket), and there is no way to fetch a ticket's details or a person's assigned devices. Core workflows are usable but leave dead ends.
Maintenance
Related MCP Connectors
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
Query PostgreSQL databases in plain English — LLM-generated, safety-validated SQL.
Deterministic safety, correctness & cost gate that vets Postgres SQL before your AI agent runs it.
Query 40 databases from Claude, ChatGPT, or Cursor — on any device. Read-only, encrypted, audited.
Related MCP Servers
- FlicenseNot gradedqualityFmaintenanceProvides a secure, schema-aware PostgreSQL database agent for LLMs, enabling natural language queries and validated SQL execution with strong security guardrails.21 npm5-
- AlicenseNot gradedqualityCmaintenanceProvides a read-only PostgreSQL SQL surface for LLM agents via MCP, with defense-in-depth security layers for safe database queries.135 PyPI4MIT
- FlicenseNot gradedqualityCmaintenanceEnables LLM agents to explore and query a Postgres database through secure, read-only tools.-
- FlicenseNot gradedqualityCmaintenanceEnables AI agents to query a Postgres data warehouse through a governed, read-only SQL interface with policy enforcement, row limits, schema-level PII isolation, and a full audit trail.1-