Skip to main content
Glama
JimmyAlter

guarded-sql-mcp

by JimmyAlter

guarded-sql-mcp

CI

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"]
  1. Fixed catalog (src/catalog.ts). One MCP tool per entry. There is no generic query tool and no parameter named sql, query or statement.

  2. Input validation. Each entry has a z.strictObject schema. 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, limit is 1..100. Search text is matched with ILIKE ... ESCAPE '\' after escaping % and _, so the model cannot turn a search into a wildcard dump.

  3. 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.

  4. Read-only execution (src/executor.ts). Every call runs as BEGIN READ ONLY, a transaction-local statement_timeout (default 5 s), the statement, then COMMIT, or ROLLBACK on any error. Every statement must end in LIMIT, and at most 100 rows leave the server.

  5. 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.

  6. Database role (db/roles.sql). mcp_readonly has SELECT on the allowlisted tables only, column-level SELECT on people that leaves out password_hash and mfa_secret, nothing on api_tokens, and no CREATE or TEMP. 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

list_sites

none

code, name, city, count of non-retired devices

search_devices

site?, status? (online/offline/retired), os_contains? (1-40), limit

hostname, site, os, status, ip, last_seen_at

get_device

hostname

device, site, and the assigned person's name and email

find_people

name_or_email (2-80), limit

full_name, email, department, site

device_software

hostname, name_contains? (1-60), limit

name, version, installed_at

stale_devices

days (1-365, default 30), site?, limit

non-retired devices not seen for days or never

open_tickets

site?, priority? (low/medium/high/critical), limit

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 build

The 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

DATABASE_URL

required

e.g. postgres://mcp_readonly:mcp_readonly_dev@localhost:5432/inventory

STATEMENT_TIMEOUT_MS

5000

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.js

Claude 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

table-not-allowed

Declares or reads api_tokens or any table outside ALLOWED_TABLES

table-undeclared, table-unreferenced

SQL reads a table missing from tables, or tables lists one the SQL never reads

unsupported-table-ref

Comma joins, schema-qualified or quoted names, TABLE x, LATERAL, anything after FROM it cannot verify

sensitive-column, sensitive-identifier

A declared column, or any identifier in the SQL, matching /pass(word)?|hash|secret|token|api[_-]?key|salt|mfa|otp/i (so password_hash AS note fails too)

not-select, multi-statement

Anything that does not start with SELECT/WITH; any ;

write-keyword, forbidden-function

INSERT/UPDATE/DELETE in a CTE, SELECT INTO, FOR UPDATE/FOR SHARE, pg_sleep, set_config, query_to_xml and similar

wildcard-select

SELECT * or alias.* (count(*) is fine)

missing-limit

No trailing LIMIT, or a literal limit above the row cap

unsupported-syntax

Comments, dollar quoting, unbalanced quotes

placeholder-gap, param-count, param-undefined

$1, $3 without $2; params() length differs from the highest placeholder; an optional input mapped to undefined instead of null

non-strict-input, unbounded-input, forbidden-param-name

z.object instead of z.strictObject; a string without max, a number without both bounds, nested objects or arrays; a parameter named like sql, query, statement

invalid-example, duplicate-name, invalid-name, no-columns

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 otherwise

What 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, including FROM a JOIN b with no alias and a sensitive column behind an innocent alias.

  • Unit: inputs (test/unit/inputs.test.ts). Out-of-range limits, bad hostnames such as x'; DROP TABLE devices;--, overlong and control-character strings, unknown keys like sql are all rejected. % and _ are escaped, and every ILIKE has a matching ESCAPE '\'.

  • 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, PgExecutor issues BEGIN READ ONLY, the timeout, the query, COMMIT, and ROLLBACK on error.

  • Protocol (test/protocol/server.test.ts). A real SDK Client connects over InMemoryTransport. tools/list is 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 returns password_hash, mfa_secret and token_hash, none of them reaches the client. Database errors come back generic.

  • Integration (test/integration/database.test.ts). Runs against PostgreSQL loaded with db/*.sql, connected as mcp_readonly. Every tool returns rows with no sensitive keys and none of the seeded secret values. Writes through the executor fail with 25006 (read-only transaction), and the timeout cancels pg_sleep. The role gets 42501 on api_tokens, on people.password_hash, and on SELECT * or row_to_json(p) from people. 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 as SELECT 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 tools
device_softwareDevice softwareA
Read-onlyIdempotent

List software installed on a device, optionally filtered by package name.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum rows to return (1-100, default 25).
hostnameYesDevice hostname, e.g. "nb-lt-001". Case-insensitive.
name_containsNoSubstring of the software name, e.g. "Chrome". Matched literally: % and _ have no special meaning.

TDQS

A3.6/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 peopleA
Read-onlyIdempotent

Find people whose name or email contains the given text. Returns name, email, department and site.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum rows to return (1-100, default 25).
name_or_emailYesPart of a full name or email address. Matched literally: % and _ have no special meaning.

TDQS

A3.8/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 deviceA
Read-onlyIdempotent

Get one device by hostname, including its site and the name and email of the person it is assigned to.

ParametersJSON Schema
NameRequiredDescriptionDefault
hostnameYesDevice hostname, e.g. "nb-lt-001". Case-insensitive.

TDQS

A3.8/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 sitesA
Read-onlyIdempotent

List all sites with their code, city and number of non-retired devices.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A3.7/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters4/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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 ticketsA
Read-onlyIdempotent

List tickets that are not closed, highest priority first, optionally filtered by site and priority.

ParametersJSON Schema
NameRequiredDescriptionDefault
siteNoSite code as returned by list_sites, e.g. "north-branch".
limitNoMaximum rows to return (1-100, default 25).
priorityNoTicket priority.

TDQS

A4/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines3/5

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 devicesB
Read-onlyIdempotent

Search devices by site, status and operating system. Returns hostname, site, OS, status, IP and last check-in time.

ParametersJSON Schema
NameRequiredDescriptionDefault
siteNoSite code as returned by list_sites, e.g. "north-branch".
limitNoMaximum rows to return (1-100, default 25).
statusNoDevice status.
os_containsNoSubstring of the operating system name, e.g. "Windows". Matched literally: % and _ have no special meaning.

TDQS

B3.4/5.0
Behavior3/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines2/5

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 devicesA
Read-onlyIdempotent

List non-retired devices that have not checked in for at least the given number of days (or never), oldest first.

ParametersJSON Schema
NameRequiredDescriptionDefault
daysNoDays without check-in (1-365, default 30).
siteNoSite code as returned by list_sites, e.g. "north-branch".
limitNoMaximum rows to return (1-100, default 25).

TDQS

A3.8/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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.

  1. 7 tool updatesv0.1.0
    • First observeddevice_software
    • First observedfind_people
    • First observedget_device
    • First observedlist_sites
    • First observedopen_tickets
    • First observedsearch_devices
    • First observedstale_devices

TDQS

A3.7/5.0

Scored across 7 tools

Disambiguation4/5

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.

Naming Consistency3/5

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.

Tool Count5/5

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.

Completeness3/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    F
    maintenance
    Provides a secure, schema-aware PostgreSQL database agent for LLMs, enabling natural language queries and validated SQL execution with strong security guardrails.
    21 npm
    5
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides a read-only PostgreSQL SQL surface for LLM agents via MCP, with defense-in-depth security layers for safe database queries.
    135 PyPI
    4
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables LLM agents to explore and query a Postgres database through secure, read-only tools.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables 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
    -