mcp-server-suite
Provides a validated, row-capped, read-only SQL query tool for executing SELECT statements against PostgreSQL, with read-only transactions and a least-privilege role.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-server-suiteWhat are the current CPU and memory usage on this system?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-server-suite
A hardened Model Context Protocol (MCP) server that speaks JSON-RPC 2.0 over standard I/O and exposes three narrowly scoped tools:
Tool | Purpose |
| One validated, row-capped |
| Bounded text reads confined to a single directory tree |
| Host CPU, memory and disk utilisation |
The server is designed to be launched by an MCP client as a child process, inside an unprivileged container with a read-only root filesystem.
Design tenets
Fail closed. Every input crosses a strict schema (
extra="forbid", strict types, no NaN/Infinity). Anything the policy does not explicitly allow is rejected with a machine-readable RFC 9457 problem.Structured concurrency. One
asyncio.TaskGroupowns every task in a session. Eachtools/callis a child task that can be cancelled, deadline-bounded and drained on shutdown. No task outlives the session.No runtime network dependencies. The transport is stdio. The only outbound connection the server can make is to the PostgreSQL DSN you configure, and the shipped Compose profile has networking disabled.
Defence in depth. Each control assumes the one before it failed. The SQL AST policy is backed by read-only transactions and a least-privilege role. Path resolution is backed by a race-free
openatwalk. The container adds a non-root UID, no capabilities and an immutable root filesystem.
Related MCP server: rag-mcp-agent-demo
Contents
Quickstart
# Container (recommended)
docker compose build
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"cli","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' |
docker compose run --rm -T mcp-server
# From source (Python 3.12, uv)
uv sync
mkdir -p data
MCP_SAFE_DATA_DIR="$PWD/data" uv run mcp-server-suiteThe server reads requests from stdin until EOF or SIGINT/SIGTERM. Responses go to
stdout and logs go to stderr.
Architecture
flowchart TB
client["MCP client<br/>(parent process)"]
subgraph proc["mcp-server-suite process: UID 10001, read-only rootfs, no capabilities"]
direction TB
stdio["Framed stdio<br/>newline-delimited JSON, 1 MiB ceiling,<br/>oversized lines dropped while streaming"]
guard["Protocol guard<br/>strict UTF-8 JSON-RPC 2.0, no batches,<br/>no NaN, no duplicate keys, typed ids"]
dispatch["Strict dispatcher<br/>lifecycle state, method routing,<br/>tools/call envelope, tool lookup"]
subgraph rails["Guardrails: one TaskGroup task per tools/call"]
direction TB
san["Structural sanitizer<br/>identifier keys, depth 8, 1000 items,<br/>control, bidi and surrogate code points"]
lim["Concurrency limiter<br/>16 slots, FIFO, 5 s bounded wait"]
dl["Execution deadline<br/>30 s asyncio.timeout"]
schema["Strict argument schema<br/>Pydantic v2, extra=forbid"]
end
subgraph tools["Isolated tools"]
sql["read_only_sql<br/>sqlglot AST policy"]
fs["sandbox_fs_read<br/>openat + O_NOFOLLOW walk"]
met["system_metrics<br/>psutil in worker thread"]
end
end
pg[("PostgreSQL<br/>SELECT-only role")]
data[("/safe_data_dir<br/>read-only mount")]
host[("/proc")]
client -- "stdin" --> stdio --> guard --> dispatch
dispatch -- "initialize, ping, tools/list<br/>answered inline" --> client
dispatch -- "tools/call" --> san --> lim --> dl --> schema
schema --> sql
schema --> fs
schema --> met
sql -- "BEGIN READ ONLY, always rolled back" --> pg
fs --> data
met --> host
tools -- "stdout: ASCII-only JSON frames" --> clientLayer | Module | Guarantees |
Framed stdio |
| Memory bounded by frame limit + one 64 KiB chunk. An over-long line is discarded as it streams and reported once, then reading resumes at the next newline. Output is |
Protocol guard |
| Rejects invalid UTF-8, |
Dispatcher |
| Enforces the |
Guardrails |
| Sanitizer, then concurrency slot, then deadline. Every rejection is an in-band problem the model can act on. |
Argument schema |
| Arguments are validated in JSON mode against frozen, strict Pydantic models. Results are re-checked against the tool's output contract. |
Tools |
| Each tool carries its own isolation controls (see Tool contracts). |
Session lifecycle and concurrency
sequenceDiagram
autonumber
participant C as MCP client
participant L as Read loop
participant T as tools/call tasks
participant OS as Signals
C->>L: initialize (protocolVersion 2025-11-25)
L-->>C: result: protocolVersion, capabilities.tools, serverInfo
C-)L: notifications/initialized
C->>L: tools/list
L-->>C: result: tools with inputSchema, outputSchema, annotations
par Concurrent calls
C->>L: tools/call id=7 system_metrics
L->>T: spawn task 7 in the TaskGroup
Note over T: sanitize, acquire slot, run under deadline
T-->>C: result id=7: structuredContent, isError false
and
C->>L: tools/call id=8 read_only_sql
L->>T: spawn task 8
C-)L: notifications/cancelled requestId=8
L->>T: cancel task 8
Note over T,C: A cancelled request receives no response
end
OS->>L: SIGTERM or SIGINT
L->>L: stop reading stdin
L->>T: drain in-flight calls (5 s grace)
alt Second signal or grace period expires
L->>T: cancel remaining tasks
end
L-->>C: flush and close stdout, exit 0initialize,pingandtools/listare answered inline, in arrival order. Eachtools/callruns as its own task, so a slow tool never blocks the read loop and responses may arrive out of order (match them byid).notifications/cancelledcancels the matching task. Per the MCP specification, a cancelled request gets no response.EOF on stdin,
SIGINTandSIGTERMall start the same graceful drain. A second signal cancels in-flight work immediately. A write failure on stdout (client gone) also triggers shutdown.Blocking work (file reads, CPU sampling) runs in worker threads via
asyncio.to_thread, so it never stalls the event loop.
Wire specification
Transport. One JSON-RPC 2.0 message per line (\n-terminated, an optional trailing
\r is tolerated, blank lines are ignored). The maximum frame size is 1 MiB (1,048,576
bytes). Larger lines get a -32600 error with "data": {"max_frame_bytes": 1048576} and
the session continues.
Protocol versions. 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05. A client
requesting any other version is offered 2025-11-25, per the MCP negotiation rules.
Method | Kind | Before | Notes |
| request | yes | Exactly once per session |
| request | yes | Returns |
| request | no |
|
| request | no | Concurrent; see Error model |
| notification | n/a | Marks the client ready |
| notification | n/a | Cancels an in-flight |
Unknown notifications are ignored. Unknown methods return -32601. Responses sent by the
client are ignored, because the server issues no requests of its own.
Tool contracts
Every tool is advertised with readOnlyHint: true, destructiveHint: false,
idempotentHint: true and openWorldHint: false. Successful calls return the result as
both JSON structuredContent and an identical JSON text block.
read_only_sql
Registered only when MCP_SQL_DSN is set.
Argument | Type | Constraints | Default |
| string | 6–10,000 chars, no NUL, not blank | — |
| integer | 1–50 | 50 |
Result | Type | Meaning |
| string[] | Column names in result order |
| array[] (≤ 50) | Values normalised to JSON: |
| integer | Equals |
| boolean |
|
How a query is enforced:
Single statement. The query is parsed with sqlglot (PostgreSQL dialect). More than one statement is a
MCP_QUERY_VIOLATION.Root and tree policy. The root must be a
SELECTor a set operation (UNION/INTERSECT/EXCEPT). The entire AST is walked, so a write hidden in a CTE (WITH d AS (DELETE …) SELECT …) is rejected. Rejected nodes include DML, DDL,DROP,ALTER,TRUNCATE,GRANT/REVOKE,SET, transaction control,SELECT INTO, row locks (FOR UPDATE) and anything sqlglot models only as an opaque command (DO,CALL,COPY,VACUUM, …).Function denylist. Functions with effects that survive a rolled-back read-only transaction are rejected:
dblink*,lo_*, advisory locks,pg_sleep,pg_read_file,pg_ls_*,pg_terminate_backend,set_config,pg_notify, replication and WAL control, among others. Bind parameters are rejected (inline literals instead).Deterministic LIMIT. The outer
LIMITis rewritten tomin(literal, max_rows). A missing, non-literal,ALL,PERCENTorWITH TIESlimit becomesLIMIT max_rows. The SQL actually executed is regenerated from the validated AST (comments dropped) and validated a second time.Execution. The query runs inside
BEGIN READ ONLYwith a transaction-localstatement_timeout, and the transaction is always rolled back. Pooled connections also default todefault_transaction_read_only=on.
Input | Outcome |
| Executes |
| Executes |
| Executes |
|
|
|
|
|
|
PostgreSQL errors caused by the query are mapped in-band: 57014 (statement timeout) to
MCP_EXECUTION_TIMEOUT, 25006 (read-only violation) to MCP_QUERY_VIOLATION, 42501
to MCP_ACCESS_DENIED, and SQLSTATE classes 21, 22, 42, 0A and 54 to
MCP_VALIDATION_ERROR. Connection and server failures are infrastructure faults
(-32603).
sandbox_fs_read
Registered only when MCP_SAFE_DATA_DIR is set (the container image sets it to
/safe_data_dir).
Argument | Type | Constraints | Default |
| string | 1–4,096 chars, no NUL | — |
| string | A Python text codec ( |
|
| integer | 1–1,048,576 | 65,536 |
Result | Type | Meaning |
| string | Canonical path relative to the sandbox root |
| string | Strictly decoded text |
| integer | Bytes decoded. A multi-byte character cut by |
| boolean | The file is longer than |
How a read is enforced:
Canonical containment.
(root / relative_path).resolve()follows every symlink and the result must satisfyis_relative_to(root). Resolution is non-strict, so a path outside the sandbox is denied whether or not it exists (no existence oracle). Symlink loops are rejected.Race-free open. The resolved path is re-opened one component at a time with
openatrelative to a directory fd, usingO_NOFOLLOWon every component. A directory swapped for a symlink between steps 1 and 2 fails withELOOPand cannot escape. Symlinks that resolve inside the sandbox are followed by step 1, so the walk itself never meets one.File type. The leaf is opened
O_NONBLOCK | O_NOCTTYand checked withfstaton the open descriptor. Only regular files are read. FIFOs, sockets and character or block devices are denied, and a FIFO cannot hang the worker.Bounded read. At most
max_bytes + 1bytes are read, in a worker thread, then strictly decoded.No path leaks. Problems echo at most 256 characters of the caller's own
relative_pathand never an absolute host path.
system_metrics
Registered unless MCP_METRICS_ENABLED=false.
Argument | Type | Default |
| boolean |
|
Result | Type |
|
|
|
|
| 0–100 for |
| ISO 8601 timestamp with a zero UTC offset |
Readings are clamped into range, because containers and VMs occasionally report slightly out-of-range values. The disk path is configuration only and is never echoed back.
Error model
MCP distinguishes two failure channels, and this server uses them deliberately:
In-band tool errors (
result.isError: true). The tool ran (or was refused) because of what the caller asked for: invalid arguments, a policy violation, a denied path, a timeout or a full concurrency queue. The result carries one text block whose content is an RFC 9457 problem document. The calling model can read it, correct the arguments and retry.structuredContentis omitted because it would not match the tool'soutputSchema.Transport-level JSON-RPC errors (
error). The message is wrong rather than the tool call: unparsable JSON, an invalid envelope, an unknown method, a call beforeinitialize, an unknown tool name or a malformedtools/callenvelope. Unexpected crashes inside a tool also land here as-32603. The traceback goes to stderr and the client sees only a generic message.
| RFC 9457 | HTTP status | JSON-RPC code | Typical cause |
|
| 422 |
| Schema violation, SQL syntax, missing file, sanitizer rejection |
|
| 403 |
| Path outside the sandbox, special file, SQL |
|
| 403 |
| Write, multi-statement or denylisted function |
|
| 504 |
| Tool deadline or PostgreSQL |
|
| 500 |
| Concurrency queue full ( |
The JSON-RPC code is used when a problem travels as error.data, for example an unknown
tool name, which is reported as -32602 with the problem attached. type and status
are validated against the code, so the same code can never be emitted with conflicting
semantics. The other JSON-RPC codes are the standard ones: -32700 parse error,
-32600 invalid request, -32601 method not found.
In-band error (path traversal attempt):
{"jsonrpc":"2.0","id":3,"result":{"isError":true,"content":[{"type":"text",
"text":"{\"type\": \"urn:mcp-server-suite:problem:access-denied\", \"title\": \"Access to the requested resource is denied\", \"status\": 403, \"detail\": \"'../../etc/passwd' resolves outside the sandbox\", \"code\": \"MCP_ACCESS_DENIED\", \"invalid_params\": [\"relative_path\"], \"metadata\": {}}"}]}}Protocol error (unknown tool):
{"jsonrpc":"2.0","id":4,"error":{"code":-32602,"message":"Unknown tool: 'no_such_tool'",
"data":{"type":"urn:mcp-server-suite:problem:validation-error","title":"Invalid tool arguments",
"status":422,"detail":"Unknown tool: 'no_such_tool'","code":"MCP_VALIDATION_ERROR",
"invalid_params":["name"],"metadata":{}}}}Threat model (STRIDE)
Assets. Database contents and integrity, files on the host outside the sandbox, host availability, and the integrity of the published source.
Trust boundaries.
The MCP client and the model driving it, which supply all tool arguments. These are treated as untrusted, including prompt-injected input.
The process boundary, which the container hardens.
The PostgreSQL server.
The source supply chain (commits, dependencies, base images, CI actions).
MCP over stdio has no authentication of its own: whoever can spawn the process is the client. Access control is therefore the job of the launching environment, the mounted data and the database role.
Threat | Attack vector | Controls | Verified by |
Spoofing | Commits published under a forged or tool-attributed identity. | Sole-committer invariant ( |
|
Tampering | SQL injection or write smuggling: stacked statements, writes in CTEs, | sqlglot AST walk over the whole tree. Single-statement rule. Root must be |
|
Tampering | Argument poisoning: bidi overrides (Trojan Source), zero-width characters, control characters, dunder or non-identifier keys. | Structural sanitizer runs before schema validation. Strict Pydantic models with |
|
Repudiation | A call that cannot be traced afterwards. | Errors and lifecycle events are logged to stderr ( | — |
Information disclosure | Path traversal ( | Non-strict canonical resolution plus |
|
Information disclosure | Credentials or data leaking through the server. | The DSN is a |
|
Denial of service | Memory exhaustion via giant lines, crashing the session with lone UTF-16 surrogates, deeply nested JSON or SQL, call floods, slow tools, blocking I/O. | 1 MiB frame ceiling, enforced while streaming. Surrogates are rejected by the sanitizer and Pydantic, and output is ASCII-escaped. Nesting depth and size limits, with parser |
|
Elevation of privilege | A compromised tool or dependency gaining root, capabilities or persistence. | Non-root UID/GID |
|
Residual risks and limitations
The SQL policy is a denylist over a parser's model of SQL, so it is a filter, not an isolation boundary. The database role is the boundary: give it
SELECTon exactly the objects the model may see.A deadline cancels the awaiting task immediately, but Python cannot interrupt a worker thread. Thread work is kept short by design: reads are capped and CPU sampling is ≤ 2 s.
Content returned by tools is data, not instructions. Clients should present it to models accordingly.
Configuration
All settings are environment variables, validated at startup. Invalid values stop the server rather than being silently replaced.
Variable | Default | Constraint | Effect |
| unset |
| Enables |
|
| > 0, ≤ 60 s | Connect and pool-acquire timeout |
|
| > 0, ≤ 300 s | Client timeout and server |
|
| 1–32 | Maximum pooled connections (created lazily) |
|
| 1–63 chars |
|
| unset ( | absolute, existing directory | Enables |
|
| boolean | Enables |
|
| > 0, ≤ 2 s | CPU sampling interval |
|
| absolute path | Filesystem reported by |
|
| 1–256 | Concurrent |
|
| 0–60 s | Wait for a slot before rejecting ( |
|
| > 0, ≤ 600 s | Per-call execution deadline |
|
| 1–64 | Maximum nesting of tool arguments |
|
| 1–100,000 | Maximum items in any argument container |
|
| logging level name | stderr log verbosity |
Operations
Local development
Prerequisites: uv and, optionally,
direnv. The .envrc keeps every cache, the virtualenv and
temporary files under ./.cache/, and configures the git hooks and identity.
direnv allow # once; or export the variables from .envrc manually
uv sync --locked # Python 3.12 + locked dependencies into .cache/venv
ruff check . && ruff format --check .
mypy src/ tests/ --strict
bandit -c pyproject.toml -r src/
pytest # sockets disabled, branch coverage, >= 95% floor
scripts/coverage-gate.sh # 100% branch coverage on tools/sql.py, tools/fs.py, guardrails/The test suite is hermetic. pytest-socket blocks all IP sockets for the whole run
(Unix sockets stay available for the event loop), no test needs a live database, and
MCP_* variables are cleared around each test. The suite covers:
Unit: contracts and controls.
Property-based: Hypothesis fuzzing,
tests/unit/test_fuzz.py.End-to-end: the real server as a subprocess under
python -X dev,tests/e2e/. These cover handshakes, concurrency, malformed input, oversized frames, signals and file-descriptor/task leak checks.
HYPOTHESIS_PROFILE=ci pytest tests/unit/test_fuzz.py --no-cov # 1000 derandomised examples per property
pytest -m e2e --no-cov # subprocess protocol tests onlyMutation checks. The repository ships no mutation-testing script or framework, so mutation checks are run by hand. Disable one defence, run its test module, confirm it fails, then restore the file. For example:
# Drop O_NOFOLLOW from the directory walk: test_fs_tool.py must fail.
sed -i.bak 's/os.O_DIRECTORY | os.O_NOFOLLOW/os.O_DIRECTORY/' src/mcp_server_suite/tools/fs.py
pytest tests/unit/test_fs_tool.py --no-cov; mv src/mcp_server_suite/tools/fs.py.bak src/mcp_server_suite/tools/fs.py
# Skip the LIMIT rewrite: test_sql_tool.py must fail.
sed -i.bak 's/root.limit(effective, copy=False).sql/root.sql/' src/mcp_server_suite/tools/sql.py
pytest tests/unit/test_sql_tool.py --no-cov; mv src/mcp_server_suite/tools/sql.py.bak src/mcp_server_suite/tools/sql.py
git diff --exit-code src/ # confirm the tree is restoredContainer deployment
The image is a two-stage build:
Builder:
python:3.12-slimwith uv installs the locked dependencies and the package into/opt/venv.Runtime: the same digest-pinned base, with no pip, uv, compilers or package managers. It runs as
10001:10001withSTOPSIGNAL SIGTERM.
docker build -t mcp-server-suite:local .
docker run --rm -i \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges:true \
--network none \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=16m \
--pids-limit 64 --memory 256m --cpus 1 \
-v "$PWD/data:/safe_data_dir:ro" \
mcp-server-suite:localMount only the directory the model should read, read-only. Files must be readable by UID
10001(world-readable files in a world-traversable directory are enough).Keep
-iand omit-t: a TTY would rewrite the newline-delimited stream.To use
read_only_sql, replace--network nonewith a network that reaches only the database, and pass-e MCP_SQL_DSN. Prefer an env file or secret store to putting the DSN on the command line.
docker-compose.yml applies the same profile, with resource reservations and a
dedicated read-only safe-data volume:
docker compose build
docker compose run --rm -T mcp-server < requests.jsonlscripts/smoke-docker.sh builds the image and checks the result:
the UID is 10001;
the root filesystem and
/opt/venvreject writes while/tmpaccepts them;pip, package managers and setuid binaries are absent;
an MCP handshake over container stdio succeeds through both
docker runand Compose;docker stopexits cleanly.
--offline validates the Dockerfile and Compose file without a daemon.
An MCP client launches the container like any stdio server:
{
"mcpServers": {
"mcp-server-suite": {
"command": "docker",
"args": [
"run", "--rm", "-i", "--read-only", "--cap-drop", "ALL",
"--security-opt", "no-new-privileges:true", "--network", "none",
"--tmpfs", "/tmp:rw,noexec,nosuid,nodev,size=16m",
"-v", "mcp-safe-data:/safe_data_dir:ro",
"mcp-server-suite:local"
]
}
}
}PostgreSQL least-privilege role
The AST policy and read-only transactions reduce risk. The role is what actually bounds it. Create a dedicated login role that can only read the objects you intend to expose:
-- Dedicated login role: no inheritance, no special attributes.
CREATE ROLE mcp_reader LOGIN NOINHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE
NOREPLICATION NOBYPASSRLS CONNECTION LIMIT 8;
\password mcp_reader
-- Server-side defaults that hold even if a client-side layer is bypassed.
ALTER ROLE mcp_reader SET default_transaction_read_only = on;
ALTER ROLE mcp_reader SET statement_timeout = '10s';
ALTER ROLE mcp_reader SET lock_timeout = '1s';
ALTER ROLE mcp_reader SET idle_in_transaction_session_timeout = '15s';
-- Read access to one schema only; ideally a schema of curated views.
GRANT CONNECT ON DATABASE app TO mcp_reader;
GRANT USAGE ON SCHEMA reporting TO mcp_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA reporting TO mcp_reader;
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA reporting
GRANT SELECT ON TABLES TO mcp_reader;Keep
CONNECTION LIMITat or aboveMCP_SQL_POOL_MAX_SIZE.Never grant membership in
pg_read_all_data,pg_read_server_files,pg_execute_server_programorpg_signal_backend.Use row-level security or views to expose only the rows and columns the model needs.
Connect with TLS (
?sslmode=verify-full):export MCP_SQL_DSN='postgresql://mcp_reader@db.internal:5432/app?sslmode=verify-full'
Continuous integration
.github/workflows/ci.yml runs on pushes to main and on every pull request, with
contents: read permissions. Every action is pinned to a commit SHA, with its version
in a comment.
Job | Gates |
| Commit identity, subject and trailer audit of the PR or push range. Tracked-file hygiene audit. |
| ruff lint and format, |
| Buildx image build, |
Repository layout
src/mcp_server_suite/
__main__.py entry point: logging to stderr, stdio session
core/ framing, JSON-RPC parsing, dispatcher, tool registry, stdio transport
guardrails/ argument sanitizer, concurrency limiter, execution deadline
schemas/ strict Pydantic contracts and the RFC 9457 error taxonomy
tools/ read_only_sql, sandbox_fs_read, system_metrics
tests/
unit/ contracts, controls, fuzzing, hermeticity, hook/CI policy parity
e2e/ real-process stdio protocol tests
scripts/ attribution, hygiene, coverage and container smoke gates
.githooks/ pre-commit, commit-msg, pre-push (shared rules in _policy.sh)
Dockerfile, docker-compose.yml, .github/workflows/ci.ymlContributing
Commits use Conventional Commits (
type(scope): subject) with no generated trailers.Author and committer must match the configured maintainer identity. The hooks in
.githooks/enforce this locally (git config core.hooksPath .githooks, set automatically by.envrc) and CI enforces the same rules.Integrate changes with a fast-forward or rebase and push. Merging through the GitHub web UI records GitHub as the committer, which the audit rejects.
Keep caches and temporary files under
./.cache/. Never commit absolute machine paths; the hygiene gates reject them.
License
Released under the MIT License (SPDX: MIT).
Copyright (c) 2026 mikeZ98.
Available Tools
1 toolsystem_metricsSystem metricsARead-onlyIdempotent
Report host health: CPU utilisation, logical core count, 1-minute load average and memory usage; set include_disk for root-volume usage. Values are a point-in-time sample with a UTC timestamp.
| Name | Required | Description | Default |
|---|---|---|---|
| include_disk | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| cpu | Yes | |
| memory | Yes | |
| timestamp_utc | Yes | |
| disk_percent_used | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, non-destructive and closed-world, so the safety profile is covered. The description adds genuinely useful context beyond that: values are a "point-in-time sample with a UTC timestamp," which tells the agent the data is a snapshot rather than a time series and how to interpret the timestamp. Missing only things like sampling latency or permission requirements.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two tight sentences: the returned metrics are front-loaded, the optional parameter follows, and the sampling semantics close it out. No filler or redundant restatement of the name.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be fully documented, yet the description still names the key fields. Combined with the snapshot/timestamp semantics and the parameter explanation, this is nearly complete; only minor operational details (units, cost/latency) are absent.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description must carry the burden for the single optional boolean. It does so explicitly: setting include_disk adds root-volume usage. The only gap is that it doesn't restate the default (false), which the schema already supplies as default=false.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("Report host health") and enumerates exactly what is returned: CPU utilisation, logical core count, 1-minute load average, memory usage, plus optional root-volume usage. An agent knows precisely what this tool produces without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied — the framing "host health" and the point-in-time nature suggest quick inspection rather than historical trending, and include_disk is described as an opt-in extra. There are no sibling tools to route against, but the description never explicitly says when to reach for this versus another diagnostic approach, nor any exclusions.
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 tool update
v0.1.0- First observed
system_metrics
TDQS
Scored across 1 tool
There is only one tool, so there is no possibility of selecting the wrong one. Its purpose (host health snapshot) is stated clearly and unambiguously.
The single name system_metrics is snake_case, descriptive, and resource-oriented, with no competing conventions to clash with. Consistency cannot be violated with one tool, and the name reads predictably.
A one-tool server is too thin for a general 'mcp-server-suite' scope; everything is crammed into a single snapshot call. It is well-scoped for what it does, but the surface is minimal.
CPU, load, core count, memory, and optional disk cover the core host-health snapshot, but there is no history/time-series, network, process, uptime, or multi-host coverage. Agents needing anything beyond a point-in-time sample hit a dead end.
Maintenance
Related MCP Connectors
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Deterministic safety, correctness & cost gate that vets Postgres SQL before your AI agent runs it.
Query PostgreSQL databases in plain English — LLM-generated, safety-validated SQL.
Related MCP Servers
- FlicenseAqualityBmaintenanceEnables AI agents to safely interact with GitHub, query Postgres read-only, check deployment status, and search/read files within an allowlisted sandbox—all with dry-run writes and SQL/path safety gates.8-
- AlicenseAqualityCmaintenanceEnables read-only document retrieval and SQLite exploration through tools over stdio, enforcing a safety gate before SQL execution.3MIT
- AlicenseAqualityBmaintenanceEnables coding agents to safely investigate PostgreSQL runtime state through bounded, read-oriented MCP tools for row-and-foreign-key neighborhood inspection, schema and statistics lookup, and table listing, with truncation, redaction, timeouts, and audit logging.437 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables AI models to securely analyze SQLite datasets and read local text documents through a local stdio interface. It provides read-only querying, schema discovery, pagination, and timeout protections to prevent data leaks or destructive changes.MIT