Skip to main content
Glama

penelope-mcp

An MCP server that lets Claude answer natural-language questions about NER car telemetry by writing SQL against the Penelope databases.

Status: working over stdio and as a hosted LAN server. Ten tools, read-only.

Install

python3 -m venv .venv && .venv/bin/pip install -e .

Local (stdio)

.mcp.json in this repo registers the server for this project. To use it from anywhere instead:

claude mcp add --scope user penelope /Users/chrispyle/NER/penelope-mcp/.venv/bin/penelope-mcp

Hosted (LAN)

One process serves both the MCP endpoint and the export files:

export PENELOPE_TOKENS="chris:$(openssl rand -hex 16),jack:$(openssl rand -hex 16)"
export PENELOPE_SIGNING_KEY="$(openssl rand -hex 32)"
export PENELOPE_PUBLIC_URL="http://penelope.local:8000"
.venv/bin/penelope-mcp-serve

Teammates then point a client at it — no repo clone, no database credentials:

{ "mcpServers": { "penelope": {
    "type": "http",
    "url": "http://penelope.local:8000/mcp",
    "headers": { "Authorization": "Bearer <their token>" } } } }
cp .env.example .env      # then fill in PENELOPE_TOKENS and PENELOPE_SIGNING_KEY
docker compose up -d --wait

--wait blocks until the container reports healthy, so it fails loudly instead of returning while the server is still broken.

Command

Does

docker compose up -d --wait

Start, block until healthy

docker compose down

Stop and remove the container (exports survive)

docker compose restart

Restart in place

docker compose ps

Status and host port

docker compose logs -f

Follow logs

docker compose up -d --build

Rebuild after a code change, then restart

docker compose down -v

Stop and delete all exported files

The container runs as a non-root user (uid 10001), binds 0.0.0.0:8000 internally, and keeps exports on a named volume so signed URLs issued before a restart keep working. Set PENELOPE_HOST_PORT to publish somewhere other than 3050.

Things that will bite you

You usually don't need PENELOPE_PUBLIC_URL. Leave it unset and each export link is built from that request's Host header — which already records exactly how the client reached you, published port included. Connect via 127.0.0.1:3050 and links come back on 127.0.0.1:3050; connect via 10.0.0.102:3050 and they come back on that. Set it only when clients reach the server by a name it cannot observe: behind a proxy that rewrites Host without setting X-Forwarded-Host. (X-Forwarded-Host and X-Forwarded-Proto are honored, so an ordinary TLS-terminating proxy needs no configuration either.)

If you do set it, it must be the URL clients use — never localhost:8000, which only resolves inside the container. Getting that wrong is the nastiest failure in this server: every tool call still succeeds and only the download fails, silently, from the client's side.

Set PENELOPE_SIGNING_KEY explicitly. Unset, the server generates a random one per start, so every outstanding export link breaks on restart — which containers do far more often than a terminal session does.

The container needs its own route to the database. It reaches server.finishlinebyner.com:59021 through Docker's NAT, not your host's network stack, so a split-tunnel VPN that covers your Mac may not cover the container. If host tools work and the container reports Connection refused, that's the cause, not a config error. Check with:

docker compose exec penelope-mcp python -c \
  "import socket; socket.create_connection(('server.finishlinebyner.com', 59021), 5)"

Use an IP or a real DNS name, not .local. Claude Code's MCP client does not resolve mDNS names — http://mymac.local:3050/mcp times out after 30 s even though curl resolves the same name in milliseconds. Register the server by LAN IP (http://10.0.0.102:3050/mcp) or a real DNS A-record. Note a DHCP lease will eventually change; a router reservation or a DNS entry saves re-registering everyone.

Misconfiguration exits with a single clear line rather than a traceback, so docker compose logs tells you what to fix even under a restart loop.

Auth is a static bearer token, deliberately — this is a LAN tool, not a public service. Tokens are named so one person can be revoked without rotating everyone, and GET /whoami confirms a token works without an MCP handshake. The server refuses to start without PENELOPE_TOKENS: defaulting to open would hand arbitrary SQL against the car database to anyone who can reach the port.

Related MCP server: Orders DB MCP Server

Tools

Three tiers, because the scarce resource is context, not database time.

1. Discovery — cheap, resolves a phrase into an exact topic name.

Tool

Purpose

car_info

Which database is being read, and its tables

search_topics(query, limit, offset)

Substring/fuzzy topic search; paginated, reports total matches + namespaces

browse_topics(prefix, limit)

ls for the topic tree: immediate children + descendant counts

describe_topic(topic)

Profile one topic: span, arity, value range, latest samples

list_runs(limit)

Recent logging sessions, newest first

describe_table(table)

Columns and types for one table

2. Export — returns a link and statistics, never rows. Use for anything destined for a chart.

Tool

Purpose

export_series(topics, start/end | run_id, bucket, index, format)

Bucketed or raw export to CSV/Parquet

export_query(sql, format)

Same, for arbitrary read-only SQL

3. Escape hatch — rows inline, capped at 1000, charged to context.

Tool

Purpose

get_series(topic, start, end, bucket, index)

time_bucket downsampling: avg and min/max

run_query(sql)

Arbitrary read-only SQL

Every tool takes an optional car. Both get_series and bucketed exports return min/max alongside avg on purpose: averaging alone hides the spikes and dropouts that matter most in telemetry.

How exports keep context flat

The problem: 211k rows is ~5M tokens inline. Even the 1000-row cap on get_series costs ~25k.

So the rows never enter context. export_series writes a file, and returns a signed URL plus a statistical profile — n, nulls, true min/max, p01/p50/p99, median sample interval, largest dropout gap. That profile is what lets a model choose axis limits and catch a millivolts-vs-volts error without reading a single row. The agent then reads the URL from its own sandbox:

df = pd.read_csv(URL, parse_dates=["bucket"])

Measured against BMS/Pack/SoC on 25A:

Window

Rows

File

Response

1 day, 1 s buckets

12,633

0.6 MB

314 tokens

120 days, 1 s buckets

170,152

7.8 MB

322 tokens

120 days, raw

211,001

11.1 MB

318 tokens

17× the data, the same context cost. Rows stream from a server-side cursor to disk, so an export is bounded by PENELOPE_EXPORT_MAX_ROWS, not by RAM.

Bucketed exports are wide (one row per bucket; __avg/__min/__max per topic). bucket=null gives long raw output — raw samples from different topics don't share timestamps, so they can't be aligned into columns.

Export URLs carry an HMAC signature and an expiry, and are served without the auth header: a sandbox running pd.read_csv(url) has no way to send one, so the signature is the credential. Files are swept on each export once they pass PENELOPE_EXPORT_TTL or the directory exceeds PENELOPE_EXPORT_MAX_BYTES.

The topic-naming rule

Telemetry is keyed by ~1500 hierarchical topic names, many confusingly similar (BMS/PerCell/Alpha/3/Volts/5 vs BMS/PerCell/Beta/3/S_Volts/5). A plausible but wrong topic is the main failure mode, so the server instructions require the model to:

  1. Always name the exact topic(s) it queried, verbatim, in its answer.

  2. Ask the user which topic they meant when the question is ambiguous — rather than guessing or silently averaging across a family of topics.

  3. Say a topic doesn't exist rather than substituting a different one.

search_topics supports this. It substring-matches, case-insensitively and in any order, and reports three things a model must read before trusting the list:

  • total_matches vs returned. Broad words match far more than one page: "volt" matches 332 of 25A's 1502 topics. Returning 40 of those while implying that was all of them is how a model confidently plots the wrong thing, so truncated is set and the note says so outright.

  • namespaces — a two-segment prefix breakdown of all matches, computed in SQL so a broad search doesn't transfer every name. For "volt": BMS/PerCell 280, VCU/eFuses 11, BMS/Cells 7, BMS/Segment_Volt 5. That turns an unusable list into an obvious next query.

  • mode — how the hits were found:

Mode

Meaning

all-terms

Every term appears in the name. A real hit.

any-term

Nothing contained all terms, so they're OR-ed. Near misses.

fuzzy

No substring matched at all; approximate per-segment matching.

"pack temp" finds nothing on 25A — pack temperature is BMS/Segment_Temp/0..N — so it falls back to any-term. A typo like "volatge" matches no substring at all and falls back to fuzzy, which still finds VCU/LV/voltage.

Broad searches paginate: pass offset, or the next_offset the response hands back. Ordering is (length(name), name) — total and stable — so pages never repeat or skip a topic between calls.

Fuzzy matching is stdlib difflib, scored against each /-separated segment rather than the whole name — whole-name similarity scores "volt" against BMS/Segment_Volt/0 near zero on length alone. It costs ~35 ms over 1502 names, after a one-time 350 ms vocabulary fetch that's cached for the client's lifetime. Postgres would do this better with pg_trgm, but that extension is available and not installed, and CREATE EXTENSION needs privileges the readonly account doesn't have.

Browsing the topic tree

Search finds; browse_topics explores. The two are not interchangeable, because of how the vocabulary is shaped on 25A:

Name depth

1

2

3

4

5

6 segments

Topics

1

4

213

252

111

921

Most names are six segments deep, so a namespace can hold hundreds of topics behind two or three children. BMS/PerCell is the extreme case — 970 of the 1502 topics, 65% of the whole vocabulary, behind exactly two children:

browse_topics()                    -> BMS(1105) VCU(190) SYS_tpu(58) DTI(45) …
browse_topics("BMS")               -> PerCell(970) Segment_Onboard_Temps(30) …
browse_topics("BMS/PerCell")       -> Alpha(485) Beta(485)
browse_topics("BMS/PerCell/Alpha") -> 0(97) 1(97) 2(97) 3(97) 4(97)
browse_topics("BMS/Segment_Volt")  -> 0* 1* 2* 3* 4*        (* = a real topic)

search_topics("BMS/PerCell") returns 40 of those 970 as flat leaf names (BMS/PerCell/Alpha/0/Burning/11 and 39 siblings) — the least useful possible view. Browsing returns two lines. That's the whole reason the tool exists.

Each child reports topics (count at or below it), so you can see where the mass is before descending, and is_topic — a path can be both a folder and a logged topic. Numeric segments sort naturally (0,1,2,3,10,11, not 0,1,10,11,2,3). It runs entirely against the cached topic list, so it costs no query.

Which car

Every tool takes an optional car argument. Unset, it reads the default: NER_DB_CAR if set, else LATEST_CAR in config.py (currently V25A). Bump that constant when a new car comes online. The server instructions tell the model to omit car unless the user names one, so year-over-year archaeology is available without every question paying for it.

Enum

Database

Topic table

Topics

Car.V22A

penelope22a

"dataType"

151

Car.V24A

penelope24a

data_type

1412

Car.V25A

penelope25a

data_type

1502

Schema (verified against penelope25a)

Four tables in public:

Table

Columns

data

values (float[]), "time", "dataTypeName", "runId"

data_type

name — topic names

run

id, "runId", "driverName", "locationName", notes, "time"

_prisma_migrations

ignore

Gotchas

  • data is a TimescaleDB hypertable (~434M rows on 25A), compressed with segmentby = "dataTypeName", orderby = time DESC. This is why filtering by topic is cheap at any time range — a per-topic count(*) over all 434M rows answers from compressed batch metadata in ~0.1 s.

  • The join key: data."runId" holds a UUID matching run.idnot run."runId", which is an unrelated small integer counter. Joining the same-named columns is the single easiest mistake to make here.

  • runId is not indexed or segmented, only "time" is. A bare WHERE "runId" = ... scans all 434M rows and hits the statement timeout; bounding it by "time" lets chunk exclusion work and takes it from >30 s to 0.2 s. This is why resolve_run_window looks only 24 h past a run's start.

  • Quoting: "dataTypeName", "runId", "time", "driverName" are camelCase and need double quotes. values and name do not.

  • Units are not in the database. units_22a.tsv is bundled and used to annotate topics opportunistically, but it was written for 22A: ~99/151 of its names still exist on 24A, 72/151 on 25A. No unit shown means unknown.

  • driverName / locationName are empty strings on all recent runs, so "who was driving" is generally unanswerable.

  • Data is batch-uploaded after test days. Nothing here is live; the newest rows can lag today by over a week.

  • 22A's data table has an extra id column that later cars dropped.

Guardrails

Model-authored SQL goes straight to run_query, so:

  • Queries run in a Postgres READ ONLY transaction — writes are rejected server-side, not by inspecting the SQL string. (Verified: CREATE TABLEcannot execute CREATE TABLE in a read-only transaction.)

  • statement_timeout is 30 s. The worst measured query — avg(values[1]) grouped over all 434M rows, forcing full decompression — took ~17 s.

  • Results cap at 1000 rows; the response sets truncated when rows were dropped. Exports get their own, much larger cap and a 120 s timeout, and error rather than truncate — a silently short chart is worse than a failed call.

  • DB errors come back as a one-line message, not a multi-page traceback.

There is deliberately no SQL string validation. The read-only transaction is the real boundary; a parser would only add false negatives.

Configuration

Variable

Default

NER_DB_SERVER

server.finishlinebyner.com:59021

NER_DB_USER

readonly

NER_DB_PASSWORD

(baked-in read-only password)

NER_DB_CAR

unset → LATEST_CAR

Hosted mode only:

Variable

Default

PENELOPE_TOKENS

requiredname:token,name:token

PENELOPE_SIGNING_KEY

random per start (export links die on restart)

PENELOPE_PUBLIC_URL

unset → derived per-request from Host (usually correct)

PENELOPE_HOST / PENELOPE_PORT

0.0.0.0 / 8000

PENELOPE_EXPORT_DIR

<tmp>/penelope-exports

PENELOPE_EXPORT_TTL

3600 s

PENELOPE_EXPORT_MAX_BYTES

5000000000

PENELOPE_EXPORT_MAX_ROWS

5000000 (errors rather than truncating)

PENELOPE_EXPORT_TIMEOUT_MS

120000

The server is reachable from NEU's network; off campus you'll need the VPN or a local mirror (~/NER/penelope has a compose file and a backup.sql dump).

Tests

.venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest -q          # unit only, offline
.venv/bin/python -m pytest -q -m e2e   # end to end, needs the database

77 unit tests, no database required: token signing (tamper, expiry, path traversal), token parsing and the auth middleware, the export writer's profiling, row cap, and retention sweep, and topic search's namespace grouping, fuzzy matching, pagination, and tree browsing.

19 end-to-end tests in tests/test_e2e.py that spawn a real penelope-mcp-serve process on a free port and speak the MCP wire protocol over HTTP: auth boundary, handshake, discovery, pagination, browsing, and the full export round trip including downloading the signed URL without a header. They skip automatically when the Penelope host is unreachable, so pytest stays green off the NEU network.

Two things worth knowing if you extend them: they talk raw httpx rather than using the SDK client, because the point is to exercise the deployed surface (middleware, transport, export route) exactly as a teammate's client hits it; and the fixture terminates the server by process handle, never by name — a pkill -f penelope-mcp-serve would also kill your own server on another port.

Next steps

  • An eval set of ~20 real questions, run end to end and checked by hand. The failure mode of NL-over-SQL isn't crashes, it's confident wrong answers. The one to watch here: does the model actually read export URLs from its sandbox, or does it try to fetch them into context?

  • TLS, if this ever leaves the LAN.

  • A server-side plot_series returning a PNG, for clients with no sandbox.

  • A values[] arity/meaning map for multi-element topics.

  • Units for 25A topic names, if the team has them anywhere.

Available Tools

10 tools
browse_topicsA

List the immediate children of a topic namespace, like ls.

Use this to explore, and search_topics to find. Topic names are deeply nested -- most are six segments -- so a namespace can hold hundreds of topics while having only a handful of children. BMS/PerCell holds 970 of 25A's 1502 topics but has exactly two children, Alpha and Beta. Searching that prefix returns an unreadable page of leaf names; browsing it returns two lines.

Call with no prefix for the top-level namespaces, then walk down. Each child reports topics (how many exist at or below it), so you can see where the mass is before descending, and is_topic (whether the path is itself a logged topic rather than only a folder).

Once you reach a specific topic, confirm it with describe_topic.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo
limitNo
prefixNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior4/5

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

With no annotations, the description carries the burden of behavioral disclosure. It explains that the tool lists only immediate children (not recursive), that namespaces can hold many topics but few children, and that each child reports `topics` and `is_topic`. It does not mention pagination or `limit` behavior, which is a minor gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core behavior and uses a concrete, vivid example (BMS/PerCell) to illustrate why browsing beats searching on nested namespaces. Every sentence earns its place by defining behavior, giving usage guidance, or explaining output semantics.

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?

The output schema exists, and the description covers the browsing workflow, result field semantics, and follow-up action. The main gap is the undocumented `car` and `limit` parameters; otherwise the tool is well contextualized for an agent.

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

Parameters2/5

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

Schema description coverage is 0%, so the description must compensate. It thoroughly explains `prefix` (no prefix means top-level, then walk down), but says nothing about `limit` or the opaque `car` parameter. With three undocumented parameters and one non-obvious parameter, this is insufficient.

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?

The opening sentence names a specific verb and resource ('List the immediate children of a topic namespace') and the `ls` analogy makes the behavior instantly recognizable. It is explicitly contrasted with `search_topics` and `describe_topic`, so an agent can tell it apart from its siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description states 'Use this to explore, and search_topics to find' and gives a concrete workflow: call with no prefix for top-level namespaces, walk down, then confirm with describe_topic. This is explicit when-to-use guidance with named alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

car_infoD

Which car's database these tools read, and its table names.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

D1.5/5.0
Behavior1/5

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

No annotations are provided, so the description carries the full burden of behavioral disclosure. It does not mention side effects, permissions, data scope, or return format. It only vaguely states the tool reads 'which car's database' — unclear if this is a query or a metadata lookup. Absolutely no behavioral transparency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely short (one clause), so it is concise in length, but this is under-specification rather than good conciseness. It fails to convey the tool's purpose or usage. Front-loading is irrelevant when the content is nearly meaningless.

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

Completeness2/5

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

For a low-complexity tool with one optional parameter and an output schema, the description still must clarify what the tool returns. It vaguely mentions 'database these tools read' and 'table names,' but doesn't explain the output structure or how it relates to the parameter. The output schema might help, but the description alone is insufficient for correct invocation.

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

Parameters1/5

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

The schema has one parameter 'car' with no description (0% coverage). The description does not add any meaning about the parameter's type, allowed values, or its effect on the output. The agent gets zero help understanding what to pass for 'car'.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose2/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description is a noun phrase ('Which car's database these tools read, and its table names') rather than a clear verb+resource statement. It hints at the tool providing database/schema context, but doesn't explicitly state what action the tool performs or what the returned data represents. This is more of a label than a purpose statement.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines1/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use this tool vs the siblings (export_query, get_series, etc.). No mention of prerequisites, conditions, or alternatives. The agent is left entirely without direction on selecting this tool.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

describe_tableC

Columns, types, and nullability for one table in public.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo
tableYes

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

C2.9/5.0
Behavior1/5

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

The description does not disclose any side effects, permissions, or data modification potential. Although it seems to be a read-only operation, this is not confirmed, and there are no annotations to fill the gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, concise sentence with no redundancy or filler. It efficiently conveys the core purpose.

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

Completeness2/5

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

The description is missing critical context: it does not explain the 'car' parameter, nor does it mention the output schema or any expected return format. An agent may not know how to correctly invoke the tool or interpret results.

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

Parameters2/5

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

The description indirectly suggests that the 'table' parameter is the table name, but it does not explicitly explain it. The 'car' parameter is entirely unexplained, leaving ambiguity. Schema coverage is low, and the description does not clarify parameter purposes.

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?

The description clearly states that the tool provides columns, types, and nullability for a single table in the public schema. The verb is implied by the name 'describe_table', and it is easily distinguished from sibling tools like run_query or describe_topic.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No explicit guidance is given on when to use this tool over alternatives. The mention of the 'public' schema hints at scope, but it does not reference any sibling tools or provide criteria for selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

describe_topicA

Profile one topic: sample count, time span, array arity, value range.

Use this to confirm a topic is the right one -- and to learn whether its values array holds more than one element -- before querying it in bulk. Pass the exact name from search_topics.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo
topicYes

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4/5.0
Behavior3/5

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

The description implies a read-only profiling operation, but it does not explicitly state side effects, whether it modifies data, or any operational constraints such as rate limits. Since no annotations are provided, the description carries the burden of disclosing behavioral traits and does so only implicitly.

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?

The description is concise, front-loaded with the core purpose, and uses no unnecessary words. It efficiently communicates the tool's function and usage in two sentences.

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

Completeness3/5

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

The description provides enough context for the main use case and key output aspects, but it omits explanation for the optional car parameter and does not address error cases or output schema details. Given the presence of an output schema and a second parameter, the description is not fully complete.

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

Parameters2/5

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

The required topic parameter is explained well, but the optional car parameter is completely undocumented. With only 50% of parameters described, the description does not sufficiently compensate for the schema's lack of detail, leaving ambiguity about car's purpose and usage.

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?

The description clearly states the tool's purpose: profiling a single topic with specific outputs like sample count, time span, array arity, and value range. It also differentiates itself from siblings by framing it as a pre-query confirmation step using names from search_topics.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly says when to use this tool: to confirm a topic is correct and to check whether its values array has more than one element before querying in bulk. It also directs the user to pass the exact name from search_topics, providing clear usage guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

export_queryA

Run arbitrary read-only SQL straight to a file; return a link, not rows.

The export escape hatch, for shapes export_series does not cover (joins against run, multi-index array columns, custom aggregates). Same contract as export_series: read the returned URL from your code execution environment rather than into context.

Runs in a READ ONLY transaction. Prefer run_query when you only need to look at a handful of rows yourself.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo
sqlYes
formatNocsv

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.2/5.0
Behavior4/5

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

With no annotations provided, the description carries the full behavioral burden. It discloses that the query runs in a READ ONLY transaction, that the result is a URL rather than rows, and that the URL must be fetched from the code execution environment rather than pulled into context. These are meaningful behavioral details, though it does not mention file lifetime, cleanup, or access constraints.

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?

The description is tight and well-structured: the main behavior is front-loaded, followed by the escape-hatch rationale, the URL-fetching contract, and the alternative tool preference. Every sentence adds distinct value with no filler.

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

Completeness3/5

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

The description covers purpose, alternatives, and key runtime behavior, and an output schema exists to document return values. However, with zero schema descriptions and no explanation of the car parameter or format options, an agent cannot fully determine how to invoke the tool correctly in all cases.

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

Parameters2/5

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

Schema description coverage is 0%, so the description must compensate for missing parameter explanations. It gives context for sql as arbitrary read-only SQL, but the car parameter is entirely unexplained, and format is only implied by 'straight to a file'. This leaves a required part of the contract opaque.

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?

The description opens with a specific, unambiguous statement: run arbitrary read-only SQL to a file and return a link rather than rows. It also explicitly frames the tool as an escape hatch for cases export_series does not cover, clearly distinguishing it from sibling tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit when-to-use guidance: use this tool for joins against run, multi-index array columns, and custom aggregates that export_series cannot handle. It also tells the agent to prefer run_query when only a handful of rows are needed, providing a clear alternative and exclusion.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

export_seriesA

Export one or more topics to a file and return a link plus a profile.

Use this for anything you plan to plot or analyze in code. It returns no rows: read the returned URL (or path) from your code execution environment, e.g. pd.read_csv(URL, parse_dates=["bucket"]). Do not fetch it into context.

Give a time range either as start/end (ISO-8601) or as a run_id from list_runs, which expands to that run's full span.

With a bucket (a Postgres interval like '100 milliseconds' or '1 second') the file is wide: one row per bucket, with <topic>__avg, __min, and __max columns per topic, plus a samples count. Pass bucket=null for long raw output (time, dataTypeName, value) -- raw samples from different topics do not share timestamps, so they cannot be aligned into columns.

index picks the element of the values array (1-based). format is 'csv' or 'parquet'. Report the topic names you exported in your answer.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo
endNo
indexNo
startNo
bucketNo100 milliseconds
formatNocsv
run_idNo
topicsYes

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4/5.0
Behavior4/5

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

With no annotations, the description carries the full burden. It discloses the return behavior (no rows, read URL from code environment), explains the wide/long format distinctions, and mentions the index and format parameters. It does not cover error cases or permissions, but it provides enough behavioral context for an agent to understand how to consume the result.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but well-structured with bold headers and clear paragraphs. Each section adds necessary detail for a complex tool with 8 parameters. It front-loads the purpose and usage, then explains time range and output formats. It is not redundant, though it could be slightly tightened.

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

Completeness3/5

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

The description covers most parameters and explains the output usage, but it omits the 'car' parameter and does not clarify what 'profile' means (though output schema exists). It also lacks explicit mention of error handling or limits. For a tool with 8 parameters and no schema descriptions, this is a notable gap, making it incomplete for fully correct invocation.

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 0%, so the description must explain all parameters. It thoroughly covers start/end, run_id, bucket, index, and format, and implicitly topics. However, the 'car' parameter is entirely missing from the description, leaving a gap. This prevents full compensation for the schema's lack of property descriptions.

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?

The description states a specific verb+resource: 'Export one or more topics to a file and return a link plus a profile.' It clearly distinguishes from siblings by emphasizing that it returns no rows and is intended for code-based analysis, differentiating it from tools like get_series that return data rows.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It provides an explicit directive: 'Use this for anything you plan to plot or analyze in code.' It also warns 'Do not fetch it into context' and explains the wide vs long output depending on bucket. However, it does not name alternative tools or explicitly state when not to use it, though the context implies that other tools like get_series would be used when in-context rows are needed.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_seriesA

Downsampled time series returned INLINE: avg, min, max, count per bucket.

Capped at 1000 rows and charged to your context. Use it when you need to read the numbers to answer a question. For plotting or any bulk analysis, use export_series instead.

start and end are ISO-8601; bucket is a Postgres interval such as '100 milliseconds', '1 second', or '5 minutes'. Choose a bucket that keeps the result under a few hundred rows. index picks the element of the values array (1-based).

Report the topic name you passed in your answer to the user.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo
endYes
indexNo
startYes
topicYes
bucketNo1 second

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.8/5.0
Behavior5/5

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

With no annotations, the description carries the full burden, and it delivers: it discloses inline return behavior, a 1000-row cap, context charging, bucket interval semantics, 1-based indexing, and even instructs the agent to report the topic name back to the user. This goes well beyond a basic operation description.

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?

The description is compact and well-structured: the headline result is first, followed by usage trade-offs, parameter formats, and a final agent-facing instruction. Every sentence adds value, and code spans keep it scannable.

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?

The description covers the tool's purpose, alternatives, parameter formats, result shape, and operational constraints, so an agent can invoke it correctly. The only notable gap is the undocumented 'car' parameter, which prevents it from being fully complete.

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?

Schema description coverage is 0%, so the description must compensate. It explains start/end as ISO-8601, bucket as a Postgres interval with examples, and index as 1-based. However, the 'car' parameter is not explained at all, and 'topic' is only indirectly referenced via the reporting instruction.

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?

The description clearly states the tool returns a downsampled time series inline with avg, min, max, and count per bucket. It uses a specific verb and resource, and explicitly distinguishes itself from export_series by positioning this tool for reading numbers versus bulk/plotting analysis.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit when-to-use guidance: use when you need to read the numbers to answer a question, and use export_series instead for plotting or bulk analysis. It also provides bucket sizing advice to keep results manageable, which is actionable and context-rich.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_runsA

List recent logging sessions (runs), newest first.

A run is one recording session. Use run.id (the UUID) to filter data via data."runId"; the integer "runId" column is not the join key. Use this to resolve vague time references like "the last test" into a concrete run id, which export_series accepts directly.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo
limitNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.8/5.0
Behavior4/5

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

With no annotations, the description carries the burden and it does disclose non-obvious behavior: results are newest first, run.id is the UUID join key, and the integer runId column is deliberately not the join key. It does not cover pagination or auth, but an output schema is present for return shape.

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?

Three sentences, all information-bearing: core action, data-model caveat, and concrete use case. No filler and the key statement is front-loaded.

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

Completeness3/5

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

The definition is strong for a default call and the output schema covers returns, but the undocumented 'car' filter and the lack of explicit limit semantics leave a clear gap. An agent can call it with defaults, but not fully understand optional filtering.

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

Parameters1/5

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

Schema description coverage is 0% and the description does not explain either parameter. 'car' is completely opaque and 'limit' is only implicitly related to 'recent'; this is a significant gap for correct invocation.

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?

The first sentence states a specific operation and resource: 'List recent logging sessions (runs), newest first.' It defines the key entity and includes ordering, which is enough to distinguish list_runs from the query/export/browse siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives a concrete use case: resolve vague time references such as 'the last test' into a run id that export_series accepts directly. It does not, however, explicitly state when not to use this tool or name alternative list-like tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

run_queryA

Run arbitrary read-only SQL and return rows INLINE, capped at 1000.

The escape hatch for questions the other tools do not cover. Runs in a Postgres READ ONLY transaction with a 30 s statement timeout. TimescaleDB functions like time_bucket are available. Remember the camelCase columns need double quotes.

If you are collecting data to plot, use export_query instead -- this tool spends context on every row it returns.

Name any topic you filtered on in your answer to the user.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo
sqlYes

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.3/5.0
Behavior5/5

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

With no annotations provided, the description carries full responsibility for behavioral disclosure. It states the operation is read-only, runs in a Postgres READ ONLY transaction with a 30s timeout, returns a maximum of 1000 rows, and requires double-quoting camelCase columns. It also notes that every row consumes context, which is a critical cost warning. This is thorough and transparent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded with purpose and key constraints. It includes a necessary warning about context usage and a usage directive, all in a compact format. While slightly long, every sentence serves a function; it could be trimmed but remains efficient.

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

Completeness3/5

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

For an arbitrary SQL tool with an output schema, the description covers the main behavioral aspects: read-only, timeout, row cap, quoting, and context cost. However, it omits any explanation of the 'car' parameter and does not describe the exact return format or error behavior beyond the cap. Given the complexity of arbitrary SQL, this leaves some gaps, but the core essentials are present.

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

Parameters2/5

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

Schema description coverage is 0%, so the description must explain parameters. It implies the 'sql' parameter is the SQL query but does not name it or describe its format. The 'car' parameter is completely unmentioned, leaving the agent to guess its purpose. The description adds little beyond what the schema type hints suggest, and fails to compensate for the lack of schema documentation.

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?

The description clearly states the tool runs arbitrary read-only SQL and returns rows inline, capped at 1000. It explicitly labels itself as 'the escape hatch for questions the other tools do not cover', which distinguishes it from siblings like export_query and get_series. The verb 'run' and resource 'SQL' are specific and unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit guidance on when not to use it: 'If you are collecting data to plot, use export_query instead – this tool spends context on every row it returns.' It also frames itself as the fallback for uncovered questions, and instructs the agent to name any topic filtered on in the answer. This clearly routes the agent to alternatives and provides actionable usage context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_topicsA

Find telemetry topics by name. Start here for any data question.

Substring match, case-insensitive, order-independent: "volt" finds every topic containing "volt", and "pack temp" finds "BMS/Pack Temp". Each hit reports how many samples exist over the topic's whole lifetime (0 means declared but never logged) and its unit when known.

Read three fields before trusting the list:

  • total_matches vs returned -- broad words match hundreds of topics. When truncated is true you are seeing a page, not the answer. Pass offset (or the next_offset from the last response) to page through the rest; ordering is stable, so pages never repeat or skip a topic.

  • namespaces -- where the matches live, e.g. most "volt" hits are per-cell topics under BMS/PerCell. Search a longer prefix to narrow.

  • mode -- all-terms is a real hit. any-term and fuzzy mean nothing matched properly and these are near misses to offer the user.

If more than one hit could plausibly answer the user's question, ask the user to choose rather than picking one yourself.

ParametersJSON Schema
NameRequiredDescriptionDefault
carNo
limitNo
queryYes
offsetNo

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.2/5.0
Behavior4/5

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

With no annotations, the description reveals return fields (total_matches, truncated, namespaces, mode) and their importance, including subtle behaviors like near misses. It does not mention side effects or rate limits, but for a search operation this is acceptable.

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?

The description is well-organized with a clear lead sentence followed by a bulleted list of important caveats. It is detailed without being redundant, and the structure makes it easy to scan.

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?

The description gives an agent enough context to invoke the tool correctly and interpret results, including when to disambiguate with the user. It lacks explicit mention of the 'car' parameter, but the overall usage pattern is clear.

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?

The description explains query and offset semantics (substring matching, paging) but leaves car and limit unexplained. Since the schema has no property descriptions, these two parameters remain underspecified, giving roughly 50% parameter coverage.

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?

The description clearly states the tool finds telemetry topics by name, with a specific verb and object. It also positions it as a starting point for data questions, making its primary use obvious.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It instructs to start here for any data question and advises asking the user when multiple plausible hits exist. It also provides guidance on handling pagination and interpreting near misses, though it does not explicitly contrast with sibling tools.

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. 10 tool updatesv0.1.0
    • First observedbrowse_topics
    • First observedcar_info
    • First observeddescribe_table
    • First observeddescribe_topic
    • First observedexport_query
    • First observedexport_series
    • First observedget_series
    • First observedlist_runs
    • First observedrun_query
    • First observedsearch_topics

TDQS

A3.5/5.0

Scored across 10 tools

Disambiguation5/5

Each tool targets a distinct operation: search, browse, and describe cover topic discovery at different levels; run_query/export_query and get_series/export_series are clearly separated by inline-vs-file delivery. The descriptions explicitly reinforce these boundaries, so an agent should rarely confuse one tool for another.

Naming Consistency4/5

Nine of ten tools follow a consistent verb_noun pattern like export_query, browse_topics, describe_topic, and list_runs. The noun-only 'car_info' is a minor outlier, preventing a perfect score, but the overall convention is predictable.

Tool Count5/5

Ten tools is well-scoped for a telemetry exploration server: discovery, profiling, inline querying, bulk export, and schema/run metadata each have a dedicated tool. There are no redundant or filler tools.

Completeness5/5

The surface covers the full read-only workflow: find topics, confirm them, query small results inline, export bulk data, and resolve runs or table schemas. The arbitrary read-only SQL escape hatches cover any remaining analytical shapes, leaving no dead ends.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables natural language querying of a customer and orders database through read-only MCP tools for finding customers, listing orders, and generating revenue summaries.
    -
  • F
    license
    A
    quality
    C
    maintenance
    Provides read-only SQL Server access via MCP tools and a chat interface for natural-language database queries.
    4
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides read-only, guarded access to business databases via MCP. Enables natural language querying with built-in security barriers like table allowlists, PII masking, and audit logging.
    MIT