penelope-mcp
OfficialClick 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., "@penelope-mcpWhat was the max engine RPM and speed during the last lap?"
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.
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-mcpHosted (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-serveTeammates 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>" } } } }Docker (recommended for anything long-lived)
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 |
| Start, block until healthy |
| Stop and remove the container (exports survive) |
| Restart in place |
| Status and host port |
| Follow logs |
| Rebuild after a code change, then restart |
| 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 |
| Which database is being read, and its tables |
| Substring/fuzzy topic search; paginated, reports total matches + namespaces |
|
|
| Profile one topic: span, arity, value range, latest samples |
| Recent logging sessions, newest first |
| Columns and types for one table |
2. Export — returns a link and statistics, never rows. Use for anything destined for a chart.
Tool | Purpose |
| Bucketed or raw export to CSV/Parquet |
| Same, for arbitrary read-only SQL |
3. Escape hatch — rows inline, capped at 1000, charged to context.
Tool | Purpose |
|
|
| 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:
Always name the exact topic(s) it queried, verbatim, in its answer.
Ask the user which topic they meant when the question is ambiguous — rather than guessing or silently averaging across a family of topics.
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_matchesvsreturned. 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, sotruncatedis 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/PerCell280,VCU/eFuses11,BMS/Cells7,BMS/Segment_Volt5. That turns an unusable list into an obvious next query.mode— how the hits were found:
Mode | Meaning |
| Every term appears in the name. A real hit. |
| Nothing contained all terms, so they're OR-ed. Near misses. |
| 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 |
|
|
| 151 |
|
|
| 1412 |
|
|
| 1502 |
Schema (verified against penelope25a)
Four tables in public:
Table | Columns |
|
|
|
|
|
|
| ignore |
Gotchas
datais a TimescaleDB hypertable (~434M rows on 25A), compressed withsegmentby = "dataTypeName",orderby = time DESC. This is why filtering by topic is cheap at any time range — a per-topiccount(*)over all 434M rows answers from compressed batch metadata in ~0.1 s.The join key:
data."runId"holds a UUID matchingrun.id— notrun."runId", which is an unrelated small integer counter. Joining the same-named columns is the single easiest mistake to make here.runIdis not indexed or segmented, only"time"is. A bareWHERE "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 whyresolve_run_windowlooks only 24 h past a run's start.Quoting:
"dataTypeName","runId","time","driverName"are camelCase and need double quotes.valuesandnamedo not.Units are not in the database.
units_22a.tsvis 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/locationNameare 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
datatable has an extraidcolumn that later cars dropped.
Guardrails
Model-authored SQL goes straight to run_query, so:
Queries run in a Postgres
READ ONLYtransaction — writes are rejected server-side, not by inspecting the SQL string. (Verified:CREATE TABLE→cannot execute CREATE TABLE in a read-only transaction.)statement_timeoutis 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
truncatedwhen 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 |
|
|
|
|
| (baked-in read-only password) |
| unset → |
Hosted mode only:
Variable | Default |
| required — |
| random per start (export links die on restart) |
| unset → derived per-request from |
|
|
|
|
|
|
|
|
|
|
|
|
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 database77 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_seriesreturning 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 toolsbrowse_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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No | ||
| limit | No | ||
| prefix | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No | ||
| table | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No | ||
| topic | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No | ||
| sql | Yes | ||
| format | No | csv |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No | ||
| end | No | ||
| index | No | ||
| start | No | ||
| bucket | No | 100 milliseconds | |
| format | No | csv | |
| run_id | No | ||
| topics | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No | ||
| end | Yes | ||
| index | No | ||
| start | Yes | ||
| topic | Yes | ||
| bucket | No | 1 second |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No | ||
| limit | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No | ||
| sql | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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_matchesvsreturned-- broad words match hundreds of topics. Whentruncatedis true you are seeing a page, not the answer. Passoffset(or thenext_offsetfrom 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 underBMS/PerCell. Search a longer prefix to narrow.mode--all-termsis a real hit.any-termandfuzzymean 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.
| Name | Required | Description | Default |
|---|---|---|---|
| car | No | ||
| limit | No | ||
| query | Yes | ||
| offset | No |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
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.
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.
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.
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.
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.
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.
10 tool updates
v0.1.0- First observed
browse_topics - First observed
car_info - First observed
describe_table - First observed
describe_topic - First observed
export_query - First observed
export_series - First observed
get_series - First observed
list_runs - First observed
run_query - First observed
search_topics
TDQS
Scored across 10 tools
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.
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.
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.
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
Related MCP Connectors
Query your org's data in natural language — read-only MCP access to SQL, NoSQL, files & warehouses.
Governed data discovery, exact queries, decisions, simulations, and runtime utilities over MCP.
Official Microsoft MCP Server to query Microsoft Entra data using natural language
Query your warehouse or a CSV with Claude/ChatGPT over MCP, governed by table-level ACL + audit.
Related MCP Servers
- FlicenseNot gradedqualityBmaintenanceEnables natural-language Q&A, human-approved actions, and dashboard generation over a data ontology via MCP.-
- FlicenseNot gradedqualityCmaintenanceEnables natural language querying of a customer and orders database through read-only MCP tools for finding customers, listing orders, and generating revenue summaries.-
- FlicenseAqualityCmaintenanceProvides read-only SQL Server access via MCP tools and a chat interface for natural-language database queries.4-
- AlicenseNot gradedqualityBmaintenanceProvides 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