Skip to main content
Glama
JimmyAlter

guarded-sql-mcp

by JimmyAlter

guarded-sql-mcp

CI Release

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. 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. It was written from scratch, with a fictional schema and data, so that the approach and the tests behind it can be read in full.

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.

  • A single call cannot flood the model's context. Rows are capped at 100, every string cell at 1000 characters, and the whole JSON response at 64 KiB. Without the byte cap, one call over a few wide rows could return megabytes. Anything cut is flagged truncated: true in the result and recorded in the audit log.

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 --> SZ["Size cap: long cells cut,<br/>trailing rows dropped to fit 64 KiB"]
    SZ --> OUT["structuredContent + 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. The LIMIT is bound to one row more than requested, so a result can say hasMore: true when more rows matched than were returned.

  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. Response size cap (src/response.ts). Every cell becomes a JSON scalar (dates as ISO strings, JSON values serialized). String cells longer than MAX_CELL_CHARS (default 1000) are cut and end with …[truncated N chars]. Trailing rows are then dropped until the serialized body fits MAX_RESPONSE_BYTES (default 64 KiB). The result carries truncated: true and the audit record says how many cells were cut and rows dropped.

  7. 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. Its sessions default to default_transaction_read_only = on, statement_timeout = 5s and idle_in_transaction_session_timeout = 10s, so even a session opened outside the server (psql with the same credentials) is read-only and time-bounded. 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","hasMore":false,"truncated":false,"responseBytes":157}

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.

Every tool declares an outputSchema derived from its column list, and a successful result carries the same body twice: as structuredContent (validated against that schema by the SDK) and as JSON text for clients that do not read structured output:

{"rowCount":1,"hasMore":false,"truncated":false,"rows":[{"full_name":"Sam Rivera","email":"sam.rivera@example.com","department":"IT","site":"north-branch"}]}

hasMore: true means more rows matched than were returned. truncated: true means the size limits cut a value or dropped rows (layer 6 above). Cells are JSON scalars: strings, numbers, booleans or null; timestamps are ISO 8601 strings.

Quick start

Requirements: Node.js 22 or later, and Docker for the local database.

The package is not published to npm yet, so run it from a clone:

git clone https://github.com/JimmyAlter/guarded-sql-mcp.git
cd guarded-sql-mcp
docker compose up -d          # postgres:16 with db/schema.sql, roles.sql, seed.sql
npm ci
npm run build                 # produces dist/index.js

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 (src/roleCheck.ts) if the role is a superuser, has BYPASSRLS, is a member of pg_read_all_data or pg_write_all_data, can CREATE in a schema or create temporary tables, can write to any table in public, has any privilege on a table outside the allowlist (such as api_tokens), or can SELECT a column whose name looks sensitive (such as people.password_hash). The integration tests check that mcp_readonly gets no warnings and that the owner gets all of them.

Variable

Default

DATABASE_URL

required

A postgres:// or postgresql:// URL, e.g. postgres://mcp_readonly:mcp_readonly_dev@localhost:5432/inventory. Anything else stops the server at startup

STATEMENT_TIMEOUT_MS

5000

Per-statement timeout inside each transaction (1-600000)

MAX_RESPONSE_BYTES

65536

Upper bound on one tool result's JSON body (4096-1048576)

MAX_CELL_CHARS

1000

Longest string a cell may carry before it is cut (100-100000)

AUDIT_REDACT_ARGS

off

1/true/on: log string arguments as "[redacted]" in the audit log

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"
      }
    }
  }
}

Once the package is published to npm, npx -y guarded-sql-mcp will replace node /absolute/path/to/guarded-sql-mcp/dist/index.js in both snippets. Until then that command does not work.

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

Demo

scripts/demo.mjs connects a real MCP SDK client to the built server over an in-memory transport and makes five calls. The only stand-in is the database: a fake executor answers every query with one fixed row copied from db/seed.sql and records whether it was called, so the output is the same on every run and needs no PostgreSQL. This is not an LLM session; it shows exactly what any MCP client, model-driven or not, gets back.

npm run build && npm run demo

The output below was produced by that command, unedited (audit records are printed without their ts and durationMs fields, which change every run). CI runs node scripts/demo.mjs --check and fails if it drifts:

tools/list -> 7 tools: list_sites, search_devices, get_device, find_people, device_software, stale_devices, open_tickets

## A normal call
tools/call find_people {"name_or_email":"rivera"}
isError: false
text: {"rowCount":1,"hasMore":false,"truncated":false,"rows":[{"full_name":"Sam Rivera","email":"sam.rivera@example.com","department":"IT","site":"north-branch"}]}
structuredContent equals text: true
database reached: yes
audit: {"event":"tool_call","tool":"find_people","args":{"name_or_email":"rivera","limit":25},"rowCount":1,"outcome":"ok","hasMore":false,"truncated":false,"responseBytes":157}

## Refused: unknown key `sql`
tools/call get_device {"hostname":"nb-lt-001","sql":"SELECT token_hash FROM api_tokens"}
isError: true
text: MCP error -32602: Input validation error: Invalid arguments for tool get_device: Unrecognized key: "sql"
database reached: no
audit: (none: refused before the handler ran)

## Refused: limit above the cap
tools/call search_devices {"limit":1000}
isError: true
text: MCP error -32602: Input validation error: Invalid arguments for tool search_devices: Too big: expected number to be <=100 at limit
database reached: no
audit: (none: refused before the handler ran)

## Refused: injection-looking hostname
tools/call get_device {"hostname":"x'; DROP TABLE devices;--"}
isError: true
text: MCP error -32602: Input validation error: Invalid arguments for tool get_device: hostname: 1-63 letters, digits or hyphens at hostname
database reached: no
audit: (none: refused before the handler ran)

## Refused: a tool that does not exist
tools/call run_sql {"sql":"SELECT 1"}
isError: true
text: MCP error -32602: Tool run_sql not found
database reached: no
audit: (none: refused before the handler ran)

Invalid arguments are refused by the SDK against the tool's strict schema before the handler runs, so they never reach the database and are not in the audit log (see Limitations).

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, fetchLimit(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

limit-lookahead

A trailing LIMIT $n not bound to fetchLimit(limit) (limit + 1), which would make hasMore always false

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 lint               # typescript-eslint, strict-type-checked
npm run build && npm run smoke   # start dist/index.js over stdio and check tools/list
npm run demo               # the transcript in the Demo section (after build)
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.

  • Unit: response size (test/unit/response.test.ts, test/unit/config.test.ts). Long cells are cut without splitting a surrogate pair, trailing rows are dropped until the body fits, escaped characters are counted at their serialized size, and one over-wide row yields zero rows rather than an oversized response. Out-of-range limits in the environment stop the server from starting.

  • 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. Each tool's outputSchema lists exactly its columns, and structuredContent equals the JSON text. An executor that returns 101 rows of ~1 MB each produces a response under 64 KiB, flagged truncated and audited.

  • 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. A fresh session as mcp_readonly shows default_transaction_read_only = on, and writes still get 42501 inside an explicit READ WRITE transaction. 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, lint, tests, build and the stdio smoke test on Node 22 and 24. 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.

  • The byte cap is per body, and the body is sent twice. MAX_RESPONSE_BYTES bounds the JSON body; the same body goes out as structuredContent and as text, so a result on the wire is at most about twice the cap plus the JSON-RPC envelope.

  • The audit log keeps arguments verbatim by default. Search text such as a person's name or email in find_people ends up in the log as typed. Set AUDIT_REDACT_ARGS=1 to log every string argument as "[redacted]" (numbers such as limit are kept). Results are never logged, only row counts and sizes.

  • No per-user authorization. It is a local stdio server. Whoever can start it gets the database role's access.

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.

Output Schema

ParametersJSON Schema
NameRequiredDescription
rowsYes
hasMoreYesMore rows matched than were returned.
rowCountYesRows in this response.
truncatedYesLong values were cut or rows dropped to fit the response size limit.

TDQS

A3.6/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, fully covering the safety profile. The description adds that filtering is optional and by package name, but does not discuss pagination, sorting, or limits. With annotations handling safety, a 3 is appropriate.

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?

Single sentence, front-loaded with the core action, and no wasted words. It efficiently conveys the purpose and optional filter.

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

Completeness4/5

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

Given the rich schema (100% coverage) and annotations, and the absence of an output schema, the description is nearly complete. The only missing element is any indication of return volume or that results are paginated, but as a simple list tool, this is a minor 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 well-documented in the schema (limit, hostname, name_contains with patterns and examples). The description adds the concept of optional filtering by package name, which is already in the schema. Baseline 3 is correct when 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 ('software installed on a device') and adds the filtering capability. It is clear what the tool does, though it does not differentiate itself from siblings like get_device, which might also return software info.

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 description: use this to see installed software. However, there is no explicit guidance on when to choose this over get_device or other alternatives, and no exclusions or prerequisites are mentioned.

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.

Output Schema

ParametersJSON Schema
NameRequiredDescription
rowsYes
hasMoreYesMore rows matched than were returned.
rowCountYesRows in this response.
truncatedYesLong values were cut or rows dropped to fit the response size limit.

TDQS

A3.8/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 fully covered. The description adds the literal-match rule (also present in the schema) and lists return fields (covered by the output schema), so it contributes little genuinely new behavioral context, e.g. no case-sensitivity 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, zero filler, with the core purpose and match rule front-loaded ahead of the return summary. Every clause earns its place.

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 lookup with a full output schema and 100% parameter coverage, the description supplies everything needed to invoke it correctly. It is slightly redundant in restating return fields and omits minor details like case sensitivity, keeping it just short of fully complete.

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 both parameters (name_or_email, limit) are already documented with pattern, length and default details. The description's 'contains the given text' merely restates the parameter without adding format or edge-case semantics beyond the 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 (find) and resource (people) plus the exact matching semantics ('name or email contains the given text'), which is more than a restatement of the name. No sibling tool operates on people, so an agent can distinguish it immediately from list_sites, search_devices, and the other device-oriented siblings.

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 name and the matching description, but there is no explicit when-to-use, no prerequisites, and no named alternative for related lookups. Adequate for a simple, single-purpose lookup tool, but it leaves the agent to infer context.

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.

Output Schema

ParametersJSON Schema
NameRequiredDescription
rowsYes
hasMoreYesMore rows matched than were returned.
rowCountYesRows in this response.
truncatedYesLong values were cut or rows dropped to fit the response size limit.

TDQS

A3.7/5.0
Behavior3/5

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

Annotations already cover the safety profile (readOnly, idempotent, non-destructive, not open-world), so the bar for additional disclosure is lower. The description adds return-content context (site and assigned person), but does not describe permissions, rate limits, not-found behavior, or other operational traits.

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 states the action, scope, and returned details with no wasted words. It is appropriately sized for a simple read-only lookup tool.

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

Completeness5/5

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

Given the simple read-only operation, complete parameter schema, annotations, and the presence of an output schema, the description is sufficient. It summarizes the single-record lookup and its notable return fields without needing to duplicate the output schema.

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 the hostname parameter's pattern, length limit, example, and case-insensitivity are already documented in the schema. The description only repeats 'by hostname' and adds no syntax or format details beyond the schema.

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 ('Get') and resource ('device') with singular scope ('one device by hostname'), which implicitly distinguishes it from search_devices and list-style siblings. It also names the returned entity context (site and assignee), so an agent knows this is a single-record lookup.

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?

The description gives no explicit guidance on when to use this tool versus search_devices or other siblings, nor any exclusions. The only usage cue is the required hostname, which is already fully specified in the schema.

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

Output Schema

ParametersJSON Schema
NameRequiredDescription
rowsYes
hasMoreYesMore rows matched than were returned.
rowCountYesRows in this response.
truncatedYesLong values were cut or rows dropped to fit the response size limit.

TDQS

A3.5/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 fully covered by structured data. The description's only added behavior detail is the 'non-retired devices' scoping nuance, which is useful but modest; with an output schema present, describing return fields adds little.

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 action, the resource, and the returned fields with zero filler. Nothing is repeated from the schema or annotations.

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?

This is a zero-parameter, read-only listing tool with annotations covering safety and an output schema covering the return shape, so the description does not need to explain results. It is complete for the call, though it says nothing about volume, ordering, or whether the list is paginated.

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

Parameters4/5

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

The tool takes zero parameters, so the baseline of 4 applies. The description correctly avoids inventing parameter semantics for a parameterless call.

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 even enumerates the shape of each result (code, city, count of non-retired devices). No sibling among search_devices, get_device, find_people, device_software, stale_devices, or open_tickets overlaps with site listing, so there is nothing to differentiate against; the definition is clear as written.

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?

There is no statement of when to reach for this tool versus any other, nor any prerequisite or exclusion. The phrase 'all sites' implicitly signals there is no filtering or pagination option, but that is inference rather than guidance. No alternative site-lookup tool exists among the siblings, which softens but does not remove the gap.

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.

Output Schema

ParametersJSON Schema
NameRequiredDescription
rowsYes
hasMoreYesMore rows matched than were returned.
rowCountYesRows in this response.
truncatedYesLong values were cut or rows dropped to fit the response size limit.

TDQS

A3.8/5.0
Behavior4/5

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

Annotations already declare readOnly, idempotent, non-destructive, and closed-world. The description adds genuine value beyond them by disclosing the implicit filter ('not closed') and the sort order ('highest priority first'), which the annotations do not cover. It stops short of describing pagination or result caps.

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 carrying filter, ordering, and scope with zero filler. Every clause earns its place.

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?

An output schema exists and annotations are rich, so return values and safety profile need not be explained. The description covers scope, ordering, and filters adequately. Only minor gaps (e.g. the limit cap) remain, and those are already in the schema.

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 all three parameters including the limit default (25) are fully documented in the schema. The description names only site and priority, adding no format or constraint detail beyond what the schema already provides. Baseline 3 is appropriate.

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 (tickets) with precise scope: 'not closed, highest priority first'. The filters and ordering make the purpose unambiguous. No sibling tool lists tickets, so explicit differentiation isn't needed, keeping this just 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?

'Optionally filtered by site and priority' hints at when the filters apply, but there is no guidance on when to use this versus alternatives, nor any exclusion or prerequisite. Usage is implied 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 devicesA
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.

Output Schema

ParametersJSON Schema
NameRequiredDescription
rowsYes
hasMoreYesMore rows matched than were returned.
rowCountYesRows in this response.
truncatedYesLong values were cut or rows dropped to fit the response size limit.

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 and openWorldHint=false, so the safety profile is covered. The description adds the returned field set (hostname, site, OS, status, IP, last check-in), which is useful context, but says nothing about ordering, total counts, or paging behavior beyond the schema's limit.

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 sentences, filters first and return shape second, with zero filler. Every clause carries information an agent can act on.

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 a 100%-documented schema, full annotations and an output schema, the description needs only to state purpose and filters, which it does. It is slightly incomplete in not routing the agent away from the overlapping stale_devices sibling.

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 site, status, os_contains and limit are all documented with formats and constraints in the schema. The description only restates the filter names and adds no syntax or matching nuance the schema does not already carry.

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. An agent can distinguish it from get_device (single device) and list_sites, though it never explicitly contrasts itself with the sibling stale_devices, which is also a device-listing tool.

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 filter list implies when this tool is useful (you have a site/status/OS to narrow by), but there is no explicit guidance on when to prefer it over stale_devices or get_device, and no statement of prerequisites or result limits beyond what the schema says.

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

Output Schema

ParametersJSON Schema
NameRequiredDescription
rowsYes
hasMoreYesMore rows matched than were returned.
rowCountYesRows in this response.
truncatedYesLong values were cut or rows dropped to fit the response size limit.

TDQS

A3.8/5.0
Behavior4/5

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

Annotations already declare readOnly, idempotent, non-destructive and closed-world, so safety is covered. The description adds genuine behavioral context beyond that: the 'non-retired' exclusion rule, the 'never checked in' edge case, and the 'oldest first' ordering — details an agent would otherwise have to guess.

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 filter, the exclusion, and the sort order, and no filler. Every clause carries information an agent needs.

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 an output schema present, return values need no explanation, and annotations cover the safety profile. The description supplies the domain semantics (staleness window, retired exclusion, ordering); only explicit sibling routing is missing for a 5.

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 the schema already documents days, site and limit, including ranges and defaults. The description only echoes the 'days' semantics ('given number of days (or never)') and adds nothing for site or limit, 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 a precisely scoped resource ('non-retired devices that have not checked in for at least the given number of days (or never)'), including sort order. The scope is clearly distinct from siblings like search_devices, but the description never names or contrasts with them, so an agent must infer the boundary.

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 filter conditions imply when to use it (auditing stale/inactive hardware), and 'non-retired' quietly tells the agent retired devices are out of scope. However, there is no explicit statement of when to prefer this over search_devices or any other alternative, nor any prerequisite guidance.

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.3.0
    • Changeddevice_software1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "hasMore": {
        +      "description": "More rows matched than were returned.",
        +      "type": "boolean"
        +    },
        +    "rowCount": {
        +      "description": "Rows in this response.",
        +      "maximum": 9007199254740991,
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "rows": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "installed_at": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "name": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "version": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          }
        +        },
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "truncated": {
        +      "description": "Long values were cut or rows dropped to fit the response size limit.",
        +      "type": "boolean"
        +    }
        +  },
        +  "required": [
        +    "rowCount",
        +    "hasMore",
        +    "truncated",
        +    "rows"
        +  ],
        +  "type": "object"
        +}
    • Changedfind_people1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "hasMore": {
        +      "description": "More rows matched than were returned.",
        +      "type": "boolean"
        +    },
        +    "rowCount": {
        +      "description": "Rows in this response.",
        +      "maximum": 9007199254740991,
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "rows": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "department": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "email": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "full_name": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "site": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          }
        +        },
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "truncated": {
        +      "description": "Long values were cut or rows dropped to fit the response size limit.",
        +      "type": "boolean"
        +    }
        +  },
        +  "required": [
        +    "rowCount",
        +    "hasMore",
        +    "truncated",
        +    "rows"
        +  ],
        +  "type": "object"
        +}
    • Changedget_device1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "hasMore": {
        +      "description": "More rows matched than were returned.",
        +      "type": "boolean"
        +    },
        +    "rowCount": {
        +      "description": "Rows in this response.",
        +      "maximum": 9007199254740991,
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "rows": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "assigned_email": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "assigned_to": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "hostname": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "ip": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "last_seen_at": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "os": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "site": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "site_name": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "status": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          }
        +        },
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "truncated": {
        +      "description": "Long values were cut or rows dropped to fit the response size limit.",
        +      "type": "boolean"
        +    }
        +  },
        +  "required": [
        +    "rowCount",
        +    "hasMore",
        +    "truncated",
        +    "rows"
        +  ],
        +  "type": "object"
        +}
    • Changedlist_sites1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "hasMore": {
        +      "description": "More rows matched than were returned.",
        +      "type": "boolean"
        +    },
        +    "rowCount": {
        +      "description": "Rows in this response.",
        +      "maximum": 9007199254740991,
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "rows": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "active_devices": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "city": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "code": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "name": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          }
        +        },
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "truncated": {
        +      "description": "Long values were cut or rows dropped to fit the response size limit.",
        +      "type": "boolean"
        +    }
        +  },
        +  "required": [
        +    "rowCount",
        +    "hasMore",
        +    "truncated",
        +    "rows"
        +  ],
        +  "type": "object"
        +}
    • Changedopen_tickets1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "hasMore": {
        +      "description": "More rows matched than were returned.",
        +      "type": "boolean"
        +    },
        +    "rowCount": {
        +      "description": "Rows in this response.",
        +      "maximum": 9007199254740991,
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "rows": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "hostname": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "opened_at": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "priority": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "site": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "status": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "ticket_id": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "title": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          }
        +        },
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "truncated": {
        +      "description": "Long values were cut or rows dropped to fit the response size limit.",
        +      "type": "boolean"
        +    }
        +  },
        +  "required": [
        +    "rowCount",
        +    "hasMore",
        +    "truncated",
        +    "rows"
        +  ],
        +  "type": "object"
        +}
    • Changedsearch_devices1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "hasMore": {
        +      "description": "More rows matched than were returned.",
        +      "type": "boolean"
        +    },
        +    "rowCount": {
        +      "description": "Rows in this response.",
        +      "maximum": 9007199254740991,
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "rows": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "hostname": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "ip": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "last_seen_at": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "os": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "site": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "status": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          }
        +        },
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "truncated": {
        +      "description": "Long values were cut or rows dropped to fit the response size limit.",
        +      "type": "boolean"
        +    }
        +  },
        +  "required": [
        +    "rowCount",
        +    "hasMore",
        +    "truncated",
        +    "rows"
        +  ],
        +  "type": "object"
        +}
    • Changedstale_devices1 field changed
      • changedOutput schema / (root)
        Previous value: -nullNew value: +{
        +  "$schema": "http://json-schema.org/draft-07/schema#",
        +  "additionalProperties": false,
        +  "properties": {
        +    "hasMore": {
        +      "description": "More rows matched than were returned.",
        +      "type": "boolean"
        +    },
        +    "rowCount": {
        +      "description": "Rows in this response.",
        +      "maximum": 9007199254740991,
        +      "minimum": 0,
        +      "type": "integer"
        +    },
        +    "rows": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "hostname": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "last_seen_at": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "os": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "site": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          },
        +          "status": {
        +            "type": [
        +              "string",
        +              "number",
        +              "boolean",
        +              "null"
        +            ]
        +          }
        +        },
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "truncated": {
        +      "description": "Long values were cut or rows dropped to fit the response size limit.",
        +      "type": "boolean"
        +    }
        +  },
        +  "required": [
        +    "rowCount",
        +    "hasMore",
        +    "truncated",
        +    "rows"
        +  ],
        +  "type": "object"
        +}
  2. 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.8/5.0

Scored across 7 tools

Disambiguation5/5

Each tool targets a distinct resource or query need: sites, device search, single device, people search, software, stale devices, and open tickets. The only potential overlap between search_devices and stale_devices is resolved by stale_devices' unique time-since-check-in criterion.

Naming Consistency3/5

Four tools follow a clear verb_noun pattern (list_sites, search_devices, get_device, find_people), but device_software, stale_devices, and open_tickets are noun phrases with no leading verb. Snake_case is consistent, but the overall convention is mixed.

Tool Count5/5

Seven tools is well-scoped for a read-only IT asset and ticketing query server. Each tool addresses a distinct query dimension without redundancy or bloat.

Completeness4/5

The surface covers core read queries across sites, devices, people, software, and tickets, which is strong for a guarded SQL query server. Minor gaps include no ticket detail/closed-ticket query, no person detail beyond search, and no write operations.

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.
    9 npm
    5
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides a read-only PostgreSQL SQL surface for LLM agents via MCP, with defense-in-depth security layers for safe database queries.
    138 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
    -