goodreads-mcp
The server answers statistical questions about a Goodreads BigQuery dataset through twelve read-only aggregate MCP tools, with enforced data-quality guards and per-figure caveats.
Explore the dataset:
dataset_overviewreports live row counts, column coverage, and all known defects (unrated books, edition overcounts, placeholder publish dates).Analyze book ratings:
rating_distributiongives a histogram of per-book mean ratings and pooled 1–5 star shares;top_books_by_ratinglists highest/lowest-rated books above a ratings threshold.Group ratings by dimension:
stats_by_language,stats_by_year,stats_by_publisher, andstats_by_authorcompute rating aggregates per group, with optionalorder_by,direction, filters, and aunitchoice between editions and works.Study publication patterns:
page_count_statscompares book length against rating;publish_month_seasonalityshows monthly publication seasonality (with January flagged as inflated).Analyze the user panel:
user_ratings_overviewdescribes the 4,154-user ratings panel;top_titles_by_user_ratingsranks best/worst-liked titles within that panel.Cross-reference panel vs. Goodreads:
compare_user_vs_book_ratingsjoins user ratings to book ratings by normalized title and highlights where the panel disagrees with the wider rating.Every result is guarded: rankings enforce
min_ratings≥ 1, output is capped, queries are cost-capped, banned columns are rejected before reaching BigQuery, and each result includesn,excluded,filters, caveats, and query metadata.
Provides statistical analysis of Goodreads book data, including rating distributions, top books by ratings, stats by language/year/publisher/author, page-count vs rating relationships, publication month seasonality, and comparisons of user ratings against overall book ratings.
Click on "Install 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., "@goodreads-mcpShow me the best-rated books with at least 500 ratings."
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.
goodreads-mcp
An MCP server answering statistical questions about the Goodreads dataset in
BigQuery (<project>.goodreads), built on FastMCP and
google-cloud-bigquery with Application Default Credentials.
The dataset has defects that produce confident wrong numbers. This server is
built so that a caller who never reads DATA_NOTES.md still cannot get one:
the rules are enforced in code, and the relevant caveat travels with every
figure returned.
Setup
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'
gcloud auth application-default login # if ADC is not already set upRegister with Claude Code:
claude mcp add goodreads -- /home/safilo/projects/goodreads-mcp/.venv/bin/python \
-m goodreads_mcpEnvironment overrides: GOODREADS_BQ_PROJECT, GOODREADS_BQ_DATASET,
GOODREADS_BQ_LOCATION, GOODREADS_MAX_BYTES_BILLED (default 20 GiB).
Related MCP server: bq-readonly-mcp
Tools
Twelve tools, all aggregate. There is no row browser.
tool | answers |
| shape, live column coverage, every known defect |
| histogram of book ratings + pooled star share |
| best/worst books above a ratings threshold — |
| ratings grouped by |
| ratings and volume per publication year — |
| ratings grouped by publisher string — |
| ratings grouped by author string — |
| book length against rating |
| coarse monthly seasonality |
| shape of the 4,154-user panel |
| best/worst liked titles in the panel |
| where the panel disagrees with Goodreads |
Every tool returns the same envelope:
{"data": ..., "n": {...}, "excluded": {...}, "filters": {...},
"caveats": ["[measured] ...", "[DATA_NOTES.md #7] ..."], "query_meta": {...}}n is mandatory and keyword-only in queries.envelope() — no average leaves
this server without the count it rests on, and excluded reports what the
threshold removed to get there.
Editions or works: the unit parameter
A row in books is an edition, and the five tools marked unit above take
unit="editions" (default, except stats_by_author) or unit="works".
Which is right depends on the question. Most prolific publisher wants
editions — issuing five editions is five editions of work. Most-read author
wants works — a novel should not count five times because it has five
editions. stats_by_author therefore defaults to "works"; everything else
defaults to "editions", which preserves the numbers those tools returned
before this option existed.
Under "works", editions sharing a normalised title collapse to one
representative row — the edition carrying the most ratings — before
aggregating. Three things to know about that:
n_ratingsis a floor, not an exact work total. Editions repeat most of their work's rating pool but not exactly: of the 68,921 works with more than one edition at ≥100 ratings, 62,794 (91%) have editions whose totals differ, mean relative spread 8.6%. Summing would overcount and taking the maximum undercounts. The maximum is used.Deduplication is within a group, not across groups. A work issued by two publishers survives once in each, so group totals still do not sum to a corpus total.
Works are identified by title text alone, so the collapse still errs in both directions — see Two title keys and Why
authorsis not in the key below. No work key merges two distinct series ranges any more, but 7,749 keys (11.2%) span more than one author string, and editions titled differently stay split (Calvin And Hobbes: It's a Magical WorldvsCalvin & Hobbes: ...).
Both branches emit identical column names, so order_by and the envelope are
unaffected by the choice. Grouped results add n_edition_rows under "works"
so the size of the collapse is visible; n_books counts works.
Two title keys
queries.py has two title normalisers, and they are not interchangeable:
used for | series suffix | |
| the | stripped entirely |
| all work-level dedup and | range kept, volume number dropped |
title_norm() must reproduce the cleaning script's book_title_normalised
exactly or the documented 52,016-title join coverage breaks, so it cannot
change. But stripping the whole suffix is wrong as a work identity: it
deletes the only thing separating one boxed set from another, so
Harry Potter Boxed Set (Harry Potter, #1-5) and (#1-7) collapsed together
and the most-rated of the merged group displaced all four from a top-rated
list.
work_key() keeps a range (#1-5) and drops a single volume number
(#2). Only ranges denote a different product; a volume number is series
metadata about the same work, and keeping it would split an edition carrying
the suffix from one without it — a regression title_norm() did not have.
The Lion, the Witch and the Wardrobe is numbered both #1 and #2
depending on publication vs chronological order, and must stay merged.
Effect at ≥100 ratings: work keys merging two distinct ranges went 3 → 0, and a further 92 keys that mixed a ranged title with a bare one now split.
Why authors is not in the key
7,749 of the 68,921 multi-edition work keys (11.2%) span more than one author
string, which looks like an argument for adding authors to the key. It was
measured and rejected. Two kinds of case are mixed together and cannot be
separated from these fields:
example | splitting on author is | |
different books, same title |
| correct |
one work, adapter credited |
| wrong |
Two heuristics were tried and both failed. Author-token overlap classifies
92.9% as "disjoint authors", but its own highest-rated members are
Pride and Prejudice, 1984, Animal Farm and The Lion, the Witch and the Wardrobe — all the adapter case, all misclassified. Rating dominance does no
better: the top author's share of a key's ratings spreads smoothly across the
range (14.1% of keys ≥95%, 36.8% below 60%), with no threshold separating the
two kinds.
Adding authors would therefore split adaptations of single works, and those
are concentrated in exactly the heavily-rated classics that dominate rankings.
Against that, the benefit is small and shrinking:
stats_by_authoris structurally immune — it groups by author before collapsing, so a collision can never cross authors there. This is the tool theworksunit was added for.Grouping absorbs most of the rest: only 756 (work key, publisher) groups and 1,913 (work key, year) groups merge more than one author string.
top_books_by_ratingis the only tool partitioning onwork_keyalone, and exposure falls with the threshold — 11.2% of keys atmin_ratings=100, 4.0% at 5,000, and 0 of the top 50 works at 5,000.
Enforced rules
These are guards and validators, not conventions. tests/test_guards.py
covers each one.
publish_dayis never read.bq.guard()rejects any query mentioning it before the query reaches BigQuery, and a test asserts no SQL string in the codebase contains it.Grouping is on
language_normalised. The guard rejects the barelanguagecolumn; the regex leaveslanguage_normalisedalone because\bdoes not match betweenlanguageand_normalised.SELECT *is rejected on a 1.85M-row table.Every ranking takes
min_ratings, floored at 1, not 0 — see below.order_byanddirectionare whitelisted, never interpolated. All filter values are BigQuery named parameters.Row output is capped (100, or 200 for per-year series) and every query is cost-capped via
maximum_bytes_billed.
Caveats are code, not prose
caveats.py is a registry keyed by id. Tools name the ids belonging to the
code path they took; they never write caveat text inline. An unknown id raises
rather than silently omitting a warning. Each caveat is tagged with its
source — [DATA_NOTES.md #n] where the notes document it, [measured] where
this project found it by profiling the loaded tables.
Four defects found by profiling that DATA_NOTES.md does not mention
451,777 books (24.4%) have no ratings and are stored as
rating = 0.0. Not books rated zero. A naiveAVG(rating)is dragged toward zero by a quarter of the table. This is whymin_ratingsis floored at 1 rather than 0 — the floor excludes them structurally, andrequire_min_ratingsexplains why when it rejects 0.language_normalisedis populated for only 13.6% of rows, and 83% of that labelled slice is English. Any language grouping is a statement about a small, English-dominated subsample.A row is an edition, and each edition repeats most of its work's rating total. Crichton's The Lost World is five rows each holding ~117,000 ratings and an identical 3.78. The repetition is near-total but not exact — 62,794 of 68,921 multi-edition works (91%) have editions whose totals differ, mean spread 8.6%, so no exact work total is recoverable. So
SUM(rating_dist_total)overcounts — 7,529,817,002 against 3,148,039,676 deduplicated by work, a 2.4× overcount — andpooled_ratingover-weights works with many editions. Every grouped result reportsn_distinct_titlesandeditions_per_titleso the size of the effect is visible for that specific group. (It is usually mild within a publisher, ~1.05–1.22, and larger across the corpus.)publish_day = 1for 48.25% of rows (892,696 of 1,850,115), and the column has no NULLs. DATA_NOTES.md #1 previously said 73.6%; that was a denominator error, now corrected — 73.60% is the placeholder rate within the 1,212,960-row ambiguous subset of caveat 3, not within the whole table. Nothing to do with the transposition fix. The column stays banned either way.
Two averages, always
Every rating aggregate returns both, because they answer different questions and diverge whenever a group mixes blockbusters with long-tail titles:
avg_book_rating— mean of each book's own mean; every book counts once.pooled_rating— total stars ÷ total ratings; popular books dominate.
rating_dist_1..5 sums exactly to rating_dist_total on all 1,850,115 rows,
so the pooled figure is exact rather than reconstructed.
The one cross-table tool
user_ratings has no book ID, so compare_user_vs_book_ratings joins on
normalised title text, reproducing the cleaning script's normalise_title in
SQL. It uses [^\p{L}\p{N}_\s] rather than [^\w\s] because RE2's \w is
ASCII-only while Python's is Unicode-aware — with the Unicode classes it
reproduces the documented coverage of 52,016 of 98,686 titles exactly; with
\w it loses 922 matches. Editions are pooled per title before joining.
Confining the join to one tool keeps its 52.7% coverage caveat from leaking into eleven tools that would otherwise look authoritative.
Telemetry
Every tool is wrapped by @telemetry.instrument, sitting beneath @mcp.tool.
A test fails if a tool is added without it — the same structural enforcement the
query guards use. One JSON object per call is appended to
logs/telemetry.jsonl (gitignored):
{"ts":"...","tool":"stats_by_author","params":{"min_ratings":100,"unit":"works"},
"outcome":"ok","n_rows":5,"n_queries":2,"bytes_billed":232783872,
"bytes_processed":231847973,"cache_hit":false,"job_ids":["..."],
"duration_ms":4198.1,"bq_ms":4197.3,"overhead_ms":0.8,"queries":[...]}Query results are never recorded, and a guard rejection logs guard_rule and
guard_column — never the SQL that tripped it.
stdout is the MCP protocol channel. This server speaks JSON-RPC over stdio,
so a stray byte on stdout corrupts framing and kills the connection silently.
Telemetry writes to a file by path; its only fallback is an explicit
file=sys.stderr. test_no_server_module_can_reach_stdout walks every package
module's AST and fails on a stdout reference, a print() without an explicit
stderr target, or any logging.basicConfig call — whose default is stderr, but
whose stream= kwarg is one edit from stdout.
Configuration: GOODREADS_TELEMETRY_PATH moves the log,
GOODREADS_TELEMETRY=0 disables it entirely.
goodreads-telemetry # summary: calls, error rate, p50/p95,
# bytes billed, guard rules, params used
goodreads-telemetry --tool stats_by_author --jsonDeploying to Cloud Run
stdio remains the default and is unchanged — python -m goodreads_mcp with no
flag behaves exactly as before, and an existing local Claude Code registration
needs no edit. HTTP is a second transport, selected by --transport http
or GOODREADS_TRANSPORT=http.
./deploy.sh # project <project>, region us-central1
MIN_INSTANCES=0 ./deploy.sh # override the warm-instance knob (see below)IAM: what the service account gets, and why
goodreads-mcp-run@<project>.iam.gserviceaccount.com — read-only,
no key files. Credentials come from the Cloud Run metadata server.
role | scope | why |
| project |
|
| dataset |
|
| project | Container stdout → Cloud Logging, where telemetry goes in HTTP mode. Without it a custom runtime SA has its logs silently dropped. |
Deliberately not granted: roles/bigquery.user (carries
datasets.create, reservations.use and four cloudkms.* permissions) and
roles/bigquery.dataEditor (write access). Keeping the SA read-only is also
why telemetry goes to Cloud Logging rather than a BigQuery table.
Auth: the endpoint is not public
Deployed --no-allow-unauthenticated. Unauthenticated requests get 403 at
Google's edge before reaching the container. Connect through the authenticated
local proxy:
./proxy.sh # keep running
claude mcp add --transport http goodreads-remote http://127.0.0.1:8080/mcpNo header, no token, nothing in ~/.claude.json — proxy.sh injects
credentials and refreshes them itself. It must be running for the server to
connect; without it Claude Code reports ConnectionRefused.
The proxy needs the standalone Cloud SDK. gcloud components install cloud-run-proxy fails on a distro-packaged gcloud:
ERROR: You cannot perform this action because this Google Cloud CLI
installation is managed by an external package manager.Install from https://cloud.google.com/sdk/ — it coexists with the distro
package and shares ~/.config/gcloud, so authentication carries over with no
re-login. proxy.sh uses ~/google-cloud-sdk/bin/gcloud and clears
CLOUDSDK_ROOT_DIR, which a distro install exports and which would otherwise
point the standalone gcloud at the wrong root.
claude mcp add --transport http goodreads-remote \
https://goodreads-mcp-552178111715.us-central1.run.app/mcp \
--header 'Authorization: Bearer ${GOODREADS_ID_TOKEN}'
export GOODREADS_ID_TOKEN=$(gcloud auth print-identity-token)The ${VAR} form keeps the token out of ~/.claude.json. Measured: the token
lasts 60 minutes and its aud is the gcloud OAuth client ID, not the
service URL — the replay weakness Google documents. Re-export and restart
Claude Code when it expires. This is strictly worse than the proxy on both
ergonomics and security.
Trade-off: an extra local process and a gcloud dependency, and it only
works where you are gcloud-authenticated — not Claude.ai web, not a teammate
without a roles/run.invoker binding. In exchange there is no token in any
config file, nothing to expire, and revocation is one binding removal.
A static Authorization: Bearer $(gcloud auth print-identity-token) header
also works with Claude Code, but those tokens last about an hour and, per
Google's docs, lack an audience claim — worse on both ergonomics and security.
Note --ingress all is intentional: "not public" here means IAM returns 403,
not network unreachability. --ingress internal would break the proxy, which
reaches the public URL with a token.
The min-instances knob
deploy.sh sets MIN_INSTANCES=1. It is a knob, not a decision:
first-call latency | idle cost | |
| no cold start | one always-on instance, billed at the idle CPU rate |
| +3–5 s on the first call after scale-to-zero | nothing |
The cold-start figure comes from measuring this app's startup locally:
1,089 ms to import goodreads_mcp.server and a further 1,545 ms to
construct the BigQuery client — 2,634 ms of Python before a query starts, plus
container pull and start on top. --cpu-boost attacks that directly, and the
client is warmed at startup (HTTP mode only) so the first real call does not
pay the 1,545 ms.
Reverting is one variable: MIN_INSTANCES=0 ./deploy.sh. Idle cost is on the
order of tens of dollars a month for one small instance — verify against the
current Cloud Run pricing before
committing; that figure is an estimate, not a measurement.
Latency figures measured before deployment are cache-inflated
Do not quote the pre-deployment p50 as a production baseline.
The p50 of 3,557 ms and p95 of 6,457 ms in this repo's telemetry were measured locally on 2026-08-28 with a 95% BigQuery cache hit rate, produced by a smoke script issuing the same calls repeatedly. They understate real latency, for two compounding reasons:
The cache is per-identity. Google's docs: "Temporary, cached results tables are maintained per-user, per-project." Cross-user caching needs Enterprise edition. The Cloud Run service account is a different identity from your local ADC, so it starts with an empty cache and builds its own.
Real traffic varies parameters. Cache keys include parameter values. Model-driven calls varying
min_ratings,limitandunitwill miss far more often than a smoke script replaying identical calls.
The cache does work across Cloud Run instances — every instance runs as the same service account, so scaling out does not fragment it. Entries expire after about 24 hours.
Re-measure from production telemetry before anyone quotes a latency number. A cache hit bills 0 bytes, so the existing tooling already reports the real rate:
gcloud logging read \
'resource.type=cloud_run_revision AND resource.labels.service_name=goodreads-mcp AND jsonPayload.tool!=""' \
--project <project> --limit 1000 --format json \
| goodreads-telemetry --path -See RULES.md §6 — this is the same discipline the dataset figures are held to.
Telemetry in Cloud Run
HTTP mode switches the sink from the local file to structured JSON on
stdout, which Cloud Logging parses into jsonPayload and levels by the
severity field (ok → INFO, guard_rejected → WARNING, errors → ERROR).
stdout must stay purely structured for that to work, which is why uvicorn's
access log is disabled — it writes plain-text INFO: ... 200 OK lines to
stdout that would land as unstructured entries. Nothing is lost: Cloud Run
logs every request itself, with more structure. A test fails if a non-JSON
line appears on stdout in HTTP mode.
Retention is the _Default bucket's 30 days. If SQL access over telemetry is
ever wanted, add a Logging sink to BigQuery — that writes as a
Google-managed identity and keeps the runtime SA read-only.
Health check
GET /health returns status, transport and the active
max_bytes_billed. Note the path is /health, not /healthz — Google's
frontend intercepts /healthz before it reaches Cloud Run, so that path 404s
and never appears in the request log. It deliberately does not touch BigQuery: a probe that
queried would bill on every check and would fail the service during a
BigQuery incident the container could otherwise ride out.
GOODREADS_MAX_BYTES_BILLED survives the transport change
Read from the environment in bq.py and applied per job as
QueryJobConfig(maximum_bytes_billed=...). Nothing in either transport path
touches it. It is set explicitly in the Dockerfile and the deploy script rather
than relying on the 20 GiB default, surfaced by /healthz, and pinned by a
test.
The web console (webchat/)
A browser interface for the same twelve tools, deployed as a second Cloud
Run service, goodreads-chat. It exists to make the server's central property
visible: figures arrive with their limits attached.
The console is four views on a left rail. Overview is the default: one
paragraph, the live row counts from dataset_overview as tiles, the starting
points as cards, and the chat thread with its composer at the foot. Tools
is the explorer: the tool list in the rail, the selected tool's form at the
top of the main area, results below. Defects is dataset_overview laid out
by defect -- the unrated editions, the edition overcount and the placeholder
day as hero tiles, then every caveat with the live figures that quantify it.
Telemetry summarises the local log through the goodreads-telemetry
command's own functions, and says so: it is labelled local-session, because a
deployed server writes to Cloud Logging, which the console cannot read.
Every result card carries a closed query disclosure with each statement
behind the figure and the values bound to its named parameters, carried in
query_meta.statements by merge_meta().
Two paths reach the tools -- a model from a question, or you from the form -- and the path is the only difference between them:
chat | tools | |
what picks the tool | a model, from a question | you, from a list |
what sets the parameters | the model | a form built from the tool's JSON Schema |
needs | yes | no |
prose around the card | the model's, numeral-checked | none |
the card, caveats, charts, refusals | identical | identical |
The tool mode is the console with the model taken out. Everything downstream of
the tool call is the same code — the same envelope rendering, the same caveats
attached to the same fields, the same n/unit/threshold block, the same charts,
the same refusal cards, the same guard probe under its bff badge.
.venv/bin/pip install -e '.[web]'
./run-local.sh # starts proxy.sh if needed, then the console; prints the URL
./deploy-chat.sh # Cloud Run; prints the ?k= URLrun-local.sh is the whole local path in one command. It reads
ANTHROPIC_API_KEY from a gitignored .env if there is one — creating the file
with a comment when it is absent, and saying which modes the run will offer
rather than refusing to start — generates a
CHAT_ACCESS_TOKEN on first run and saves it back to .env so the ?k= URL
stays stable between runs, starts proxy.sh only if nothing is already serving
the MCP endpoint, waits for both to answer their health checks, and prints the
ready-to-click URL.
Ctrl+C shuts down what it started, and only that. proxy.sh execs gcloud,
which spawns cloud-run-proxy as a child, so the proxy is started with setsid
and the whole process group is signalled — killing the script's own pid would
leave the tunnel behind. A proxy that was already running when the script
started is deliberately left up, because something else is using it.
Neither script requires the key. deploy-chat.sh mounts the Anthropic secret
only when it exists, because --set-secrets naming an absent secret fails the
deploy and mounting an empty one would leave the console advertising a chat mode
that 500s on every turn.
Env overrides: PORT (default 8081), MCP_PORT (8080), ENV_FILE, LOG_DIR.
.env is parsed rather than sourced — it holds secrets, and . would execute
whatever is in it — so only KEY=value lines are read, and an already-exported
value wins over the file.
The no-model mode, and why its forms are generated
The form is built in the browser from each tool's inputSchema, served by
/api/tools — which is MCPBridge.catalogue(), the same cached tools/list
output the model is given, reshaped and nothing else. So the widget, its
label, its bounds and its default all come from the server, and a Field(...)
edited in server.py moves the form and the model's tool definition together.
Hand-writing the forms would make the UI a hand-maintained copy of the tool
surface, which is the documentation-instead-of-structure failure the whole
project avoids. test_no_tool_parameter_name_is_written_into_the_client fails
if any parameter name appears in tools.js outside its preset values.
Two behaviours look like omissions and are not:
An empty field is not sent. The server's default applies, and the card's parameter row then shows exactly what was overridden rather than a wall of values nobody chose. Each field prints its default beside it and carries it as the placeholder, so nothing is hidden.
Values are passed verbatim.
min_ratings=0andunit=chaptersreach the server unaltered, andmin/maxare printed on the field rather than set as HTML attributes that would clamp them. The refusal that comes back — with the server's own explanation and the caveats behind the constraint — is the most instructive thing this console can show, and validating in the browser would replace it with silence. The route's only checks are the ones that keep it from being a general-purpose proxy: a known tool name, a flat object, scalar values.
Both paths end in MCPBridge.call() and both build their frame with
frames._result_frame — the same function object, asserted by a test, because
two builders would drift. frames.py exists so the tool path can build a card
without importing agent.py, which constructs an Anthropic client.
config.verify() therefore requires only CHAT_ACCESS_TOKEN. Without a key the
service starts, the chat button is disabled with the reason in its tooltip, and
/api/chat answers 503 naming the mode that does work.
Why a backend-for-frontend, and why an MCP client
A browser cannot call the server directly: MCP is JSON-RPC and the Cloud Run
service is --no-allow-unauthenticated. The console is therefore a BFF holding
two credentials the client never sees — a Google identity token for Cloud Run
and an Anthropic API key.
It connects as an MCP client rather than mirroring the tool definitions.
Tool schemas come from tools/list and the model's steering text from the
server's own instructions, so a docstring edit in server.py reaches the UI
on the next deploy. Mirroring would make the UI a hand-maintained copy of the
tool surface, which is the documentation-instead-of-structure failure the whole
project is built to avoid.
The Anthropic MCP connector (mcp_servers=[{type:"url", ...}]) was rejected
for two independent reasons: it would require either making the MCP service
publicly invokable or handing a Google identity token to a third party, and it
delivers tool results into the model's context rather than to the BFF, which
would make structural rendering of the figures impossible.
Two services, because they need opposite IAM postures
The console must be reachable by browsers, which carry no Google identity; the MCP server must stay private. One Cloud Run service has one IAM policy, so one service cannot be both. Everything else follows from that split:
|
| |
ingress |
|
|
service account |
|
|
BigQuery |
| none |
secrets | none | the access token, and an Anthropic key if chat is wanted |
image | root |
|
deploy-chat.sh adds exactly two IAM bindings: roles/run.invoker on
goodreads-mcp for the console's service account, and
roles/secretmanager.secretAccessor on the two secrets. It grants no
BigQuery role — the console reaches BigQuery only as a consequence of a guarded
tool call running under the MCP service's identity — and no
iam.serviceAccountTokenCreator, because minting an ID token for its own
identity from the metadata server requires no role. That last one is the usual
place this gets over-granted.
Auth flow
Browser → console. No Google identity. A shared secret (
CHAT_ACCESS_TOKEN, from Secret Manager) presented as?k=on first visit, then held in anHttpOnlycookie. The token is required: the service refuses to start without one, with no override flag, because a public endpoint that bills BigQuery on every tool call — and an Anthropic account on every chat turn — is not an acceptable default. It is the only required secret.Console → MCP server. An OIDC identity token minted from the metadata server for
audience = <the MCP service's base URL>, cached until five minutes before it expires, sent asAuthorization: Bearer. Google's edge validates the signature, the audience androles/run.invokerbefore the request reaches the container.Console → Anthropic. Only in chat mode.
ANTHROPIC_API_KEYfrom Secret Manager via--set-secrets, never--set-env-vars, never in the image or the repo. Absent it, this leg does not exist and neither does the mode.
Locally the default is the proxy.sh path: GOODREADS_MCP_URL points at
127.0.0.1:8080 and the console sends no credential of its own, because
gcloud run services proxy injects one. Setting GOODREADS_MCP_TOKEN
(from gcloud auth print-identity-token) is the direct alternative; setting
both it and an audience is a startup error rather than a silent precedence rule.
Neither credential is ever placed in an SSE frame or a log line, and transport
error text is scrubbed of Authorization before it reaches a client.
Spend ceilings
The console is public-by-URL, so the abuse surface is the bill rather than IAM:
a required access token, ten chat turns per IP per five minutes, twenty-five
turns per session, six tool calls per turn, --max-instances 3, and the
server's existing 20 GiB maximum_bytes_billed per query.
Tool mode has its own window — forty calls per IP per five minutes
(CHAT_TOOL_RATE_LIMIT_CALLS). A form submission bills BigQuery bytes but no
Anthropic tokens, and one call per form is a far tighter loop than one call per
sentence, so sharing the chat window would have made the mode unusable long
before it made it expensive.
The rendering contract, and how it is enforced
The model never renders a figure. Every number on screen is drawn by
webchat/static/cards.js from the tool's own envelope, together with its n,
the unit one row counts, the min-ratings threshold and what it excluded, the
caveats, and the query cost. Nothing is computed client-side: if a share is not
in the envelope it is not shown, because a derived figure would be a figure
with no caveats attached to it.
webchat/numcheck.py checks that the model kept to it. Every numeral in the
prose is canonicalised and looked up in the set of numerals the server actually
put in front of the session — tool results including caveat prose, tool
parameters, the server's instructions, and the user's own question. Anything
else is marked in place in the answer. A rounded figure fails by construction:
4.4 does not match 4.42. The check reports rather than blocks; suppressing
the answer would hide the violation instead of showing it.
Caveats attach to fields, not to the card. webchat/attach.py maps each
caveat id to the figure fields it qualifies, so the duplication caveat puts a
marker on n_ratings and pooled_rating specifically, and the caveat text sits
in the same card, always expanded — never a footnote, never behind a disclosure
triangle. test_every_registered_caveat_has_a_field_mapping fails if a caveat
is added to the server without one.
check_column_available: a demonstration probe
QueryGuardError is unreachable from the twelve tools by construction — no tool
interpolates a caller-supplied column into SQL — so a user asking about
publish_day gets nothing from the guard, because there is no tool through
which to ask. That is the design working, not a gap (see CLAUDE.md).
So the console carries one tool of its own, check_column_available, which runs
the server's real bq.guard() against a candidate query it never executes and
reports the verdict, the rule id and the server's own caveat prose for that
column. It is labelled bff / demonstration probe in the UI, distinct from
the mcp badge every real tool carries, and it is deliberately not a thirteenth
MCP tool: adding a tool parameter that reached a banned column would convert a
structural impossibility into a runtime rejection.
Refusals come from two layers, and the console distinguishes them
Live testing turned up something the offline tests cannot see, because they call tool functions directly and so bypass the schema:
layer | fires for | reaches the caller as |
tool schema (FastMCP/pydantic) | anything with a | a validation error, before the tool body runs |
| the unconstrained parameters — | a structured result carrying the server's full explanation |
So min_ratings=0 — the most instructive refusal in the server — never reaches
require_min_ratings() over MCP, and the validation error says only "Input
should be greater than or equal to 1", not why the floor exists. The console
renders that as its own refusal kind (schema) and re-attaches the server's
reasoning from the caveat registry, so the reader still gets the 451,777
unrated books. It re-attaches; it does not write a second explanation.
Both layers are wanted. The body validator is what protects a direct Python caller and what carries the prose, so neither was removed or loosened to make the console simpler.
Design notes
Typefaces. Two, by job. Prose -- paragraphs, headings, labels, buttons --
is set in Noto Sans; data -- numerals, parameters, tool and column names, SQL,
metadata -- is set in JetBrains Mono, with tabular numerals, an unambiguous
0/O, and parameter columns that line up because the characters are the
same width. A reader tells what is being said from what is being measured by
the face alone. Both are subset to Latin plus the punctuation the UI uses and
served from this origin -- about 57 KB for four files, no external request,
and the CSP's font-src 'self' would block a CDN anyway.
JetBrains Mono, © 2020 The JetBrains Mono Project Authors, licensed under the SIL Open Font License 1.1; the licence ships at
webchat/static/fonts/OFL.txt, upstream https://github.com/JetBrains/JetBrainsMono. Noto Sans, © 2012 Google Inc., subset from the system package's Apache-2.0 build; that licence ships atwebchat/static/fonts/NOTO-LICENSE.txt.
Palette. One accent — cyan, #0094a8 light and #00a6bc dark — plus one
reserved status ink for refusals and placeholder-inflated rows. The cyan was
chosen by measurement, not taste: it sits roughly opposite the reserved amber
on the hue wheel, so the two meanings this interface assigns to colour cannot
be confused. Their CVD separation is ΔE 18.9 (deutan) light and 19.0 (protan)
dark, well clear of the ΔE 8 floor. The accent needs three tokens because a
data mark, a text colour and a filled control answer to three different rules:
the mark must sit inside the categorical lightness band at 3:1, the text needs
4.5:1 and therefore cannot sit in that band, and the filled control needs 4.5:1
under white. The dark status amber was re-stepped rather than merely lightened,
because the light one falls outside the dark lightness band and that ink is
drawn as a bar fill on flagged rows.
Motion. One orchestrated moment: at load the shell settles in reading order
— masthead, heading, paragraph, list, composer — with staggered delays and a
backwards fill so nothing flashes before its turn. Deliberately scoped to the
furniture that exists at load; tool cards are never animated, because a card
arrives while its neighbour is being read and a figure that slides in under the
cursor is an interface fighting its reader. prefers-reduced-motion collapses
all of it, and a test asserts the reveal cannot reach a card, a figure or a
table.
Charts are hand-rolled inline SVG: no library, so every bar carries its own
exact value and nothing is read off an axis. Two measures never share an axis — stats_by_year draws volume and rating
as two stacked charts rather than one dual-axis chart. The masthead carries no
dataset figures at all; every number about the data appears inside a tool card,
from that tool's JSON.
Every size in app.css comes from one of two scales — six type steps, an
8-step 4px space scale — and every card section shares one horizontal padding,
so the tool name, the parameters, the figure, the n block and the cost all
start on the same vertical line. Two optical values sit deliberately off the
grid (--pill-pad, --code-pad) because a 10.5px badge looks wrong on layout
spacing; they are named tokens rather than guesses at each use. Colour appears
exactly once in the sheet, in the token blocks. Four tests enforce all of this:
a stray font-size: 14px, a padding: 17px, a hex colour in a rule, a
box-shadow, a removed focus ring, or a token with no dark value each fail the
suite rather than merely looking wrong.
Contrast is measured, not judged. Every ink/surface pair meets 4.5:1 and
every control boundary, data mark and focus ring meets 3:1, in both themes.
That drove three token changes: --ink-3 was darkened (it carries most of the
11–12px metadata and sat at 3.4:1 on --surface-2), filled buttons got
--accent-fill because white on --accent was 4.3:1, and controls got a
dedicated --edge at 3:1 — the hairline --rule tokens stay decorative and
are correctly exempt.
Accessibility. Each chart is role="img" with a label built by
figureDesc() from the envelope: the measure, the mark count, the unit one row
counts, the n, and what the threshold excluded — so a screen-reader user gets
the same grounding the card shows everyone else. Tables carry scope="col" and
a hidden caption; the figure scrolls, so it is focusable; the mode toggle is a
real tablist with roving tabindex and arrow keys; state is never colour alone
(the status dot has text beside it, flagged rows are labelled, unsourced
numerals are underlined); and a polite live region narrates each call.
Not for indexing
The console is private, token-gated, and its URL carries the access key, so
every response sends X-Robots-Tag: noindex, nofollow, noarchive, nosnippet
and /robots.txt returns Disallow: / — the one route deliberately readable
without the key, since a crawler that cannot fetch it never learns to stay
away. The page repeats the directive in a <meta name="robots"> that survives
being saved.
The key gets three separate protections. Referrer-Policy: no-referrer on
every response means it cannot reach a third party's logs; the client deletes
?k= from the address bar with history.replaceState once the cookie is set,
so it leaves the URL bar, the history entry and any screenshot; and no page
ever writes it into an href — the app shell creates no <a> elements at all,
and the locked page's placeholder is the literal string <key>. A test asserts
all three.
A CSP (default-src 'self', no unsafe-inline, img-src 'self' data: for the
inline SVG favicon) makes "zero external requests" enforceable rather than
merely true today: no fonts, no CDN, no analytics, and an added one fails in
the browser console instead of shipping quietly.
A missing key gets a styled 401 that explains what ?k= is and where the key
lives, and an unknown path gets a styled 404 naming what missed — both in the
console's own design, from webchat/pages.py. An unknown /api/ path returns
JSON instead, because that is what a fetch() there can read.
Conversation history is held server-side in memory, keyed by an HttpOnly
cookie, rather than posted back by the client each turn. That is a correctness
choice: a client that supplied the history could forge tool results into the
model's context, and fabricated figures in history is exactly the failure this
project exists to prevent. The cost is that an instance recycle loses the
transcript — the console says so rather than continuing against an empty one.
Tests
.venv/bin/python -m pytest tests/ -q # offline invariant tests, no network
PYTHONPATH=. .venv/bin/python tests/smoke_live.py # 18 live calls + probe, needs ADCtests/test_guards.py covers the dataset's rules; tests/test_webchat.py
covers the console's claims — that every caveat can be attached to the figure it
qualifies, that no numeral in the model's prose escapes the checker, that no
credential can reach a client, and that the no-model mode is the same path and
not a parallel one: the same frame builder, forms with no hard-coded parameter
in them, and values that reach the server exactly as typed.
Licence
MIT — see LICENSE.
Two bundled assets are licensed separately and are not covered by the MIT
grant. webchat/static/fonts/jetbrains-mono-*.woff2 is JetBrains Mono,
© 2020 The JetBrains Mono Project Authors, under the SIL Open Font License 1.1;
its licence travels with it at webchat/static/fonts/OFL.txt. The OFL permits
bundling and redistribution as done here; it does not permit selling the font
files on their own. webchat/static/fonts/noto-sans-*.woff2 is Noto Sans,
© 2012 Google Inc., subset from the Apache-2.0-licensed build shipped in the
system noto-fonts package; that licence travels with it at
webchat/static/fonts/NOTO-LICENSE.txt.
Available Tools
12 toolscompare_user_vs_book_ratingsCompare User Vs Book RatingsA
Where the 4,154-user panel disagrees with the wider Goodreads rating.
This is the only tool that crosses the two tables, and the join is on normalised title text because user_ratings carries no book ID. It reaches 52,016 of 98,686 rated titles (52.7%) -- roughly half the panel's ratings have no book row to match and are simply absent. Editions of the same title are pooled, so book rating counts are summed across up to 36 rows.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Titles to return, max 100. | |
| order_by | No | 'abs_divergence' for the biggest disagreements either way, 'user_higher' where the panel rates above Goodreads, 'book_higher' for the reverse, 'popularity' for the most-rated titles. | abs_divergence |
| min_book_ratings | No | Minimum Goodreads ratings, summed across editions, for a title to appear. | |
| min_user_ratings | No | Minimum ratings from the 4,154-user panel for a title to appear. |
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 full behavioral disclosure. It reveals the join is on normalized title text, explains coverage with exact numbers, notes that half the panel's ratings are absent, and describes edition pooling with counts summed across up to 36 rows. This is rich, honest context beyond what any schema could convey.
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 front-loaded with the core purpose, then uses three sentences to deliver essential data caveats. Almost every clause adds informational value, and the statistical specificity is justified rather than padding.
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?
Given the output schema exists and every parameter is documented, the only missing context would be data-join behavior, which is thoroughly explained. The description covers why rows are absent, how editions are pooled, and which table relationships are involved. An agent has enough to call it correctly and 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?
Schema description coverage is 100%, so all four parameters are already fully documented. The description adds little parameter-specific meaning; 'Editions are pooled' aligns with the schema's 'summed across editions' but does not elevate understanding. Baseline 3 is appropriate.
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 compares the 4,154-user panel's ratings against wider Goodreads ratings, with the specific verb 'disagrees'. It also distinguishes itself as 'the only tool that crosses the two tables', separating it from the sibling table-specific 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 phrase 'the only tool that crosses the two tables' tells an agent this is the right choice when a cross-table comparison is needed. It does not explicitly name alternatives or state when not to use it, but the unique positioning makes usage context clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
dataset_overviewDataset OverviewA
Shape, coverage and known defects of the Goodreads dataset.
Call this before answering anything substantive. It reports live row and population counts for every column that has a coverage problem, and returns the full caveat list, including three defects measured from the loaded tables that the project's own DATA_NOTES.md does not mention.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
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 burden, and it handles this well. It discloses that the tool reports live counts, focuses specifically on columns with coverage problems, returns a full caveat list, and includes three defects not mentioned in DATA_NOTES.md. This gives the agent useful expectations about the tool's behavior and output beyond the tool name.
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 three sentences with no wasted words. The core purpose is front-loaded in the first sentence, the usage directive appears second, and the added value of the tool's specific reporting behavior is in the third. Every sentence earns its place.
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 zero-parameter tool with an output schema, the description is complete. It explains why the tool exists, when to call it, and what it returns. The presence of an output schema means the description does not need to enumerate return fields. No critical context appears missing for an agent deciding to invoke it.
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 tool has zero parameters and the schema coverage is 100%, so there is no parameter information for the description to add. The baseline for zero-parameter tools is 4, and the description appropriately focuses on the tool's purpose and output rather than parameter details.
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 clear purpose: report the shape, coverage, and known defects of the Goodreads dataset. It goes beyond a vague overview by specifying that it reports live row/population counts for columns with coverage problems and returns a full caveat list. This distinguishes it from the sibling tools, which are focused on ratings, statistics, and distributions rather than dataset-level quality.
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 an explicit usage directive: "Call this before answering anything substantive." This tells an agent when to invoke the tool. It does not explicitly name alternatives or exclusions, but the instruction to call it first provides clear context for when it should be used.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
page_count_statsPage Count StatsA
Book length against rating: page-count quartiles overall, and rating statistics for each band of book length.
Answers "do longer books rate higher?". Books with a NULL pages_number are excluded and counted separately -- 11,216 implausible values were nulled during cleaning.
| Name | Required | Description | Default |
|---|---|---|---|
| year_to | No | Latest publish_year, inclusive. | |
| language | No | language_normalised ISO code, e.g. 'en'. | |
| year_from | No | Earliest publish_year, inclusive. | |
| min_ratings | No | Minimum ratings per book. Floor 1. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations exist, so the description carries the burden. It discloses important data-handling behavior: NULL pages_number values are excluded and counted separately, with a specific count of implausible values nulled. However, it does not describe other behavioral aspects such as defaults, filtering semantics, or output shape beyond what the schema implies.
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 and front-loaded, leading with the core relationship and then the exact question and key data caveat. Every sentence adds value, and no unnecessary detail is included.
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 tool with an output schema and fully documented optional parameters, the description is largely complete: it explains the analytical focus, the key exclusion behavior, and the data-cleaning context. It lacks explicit guidance on when to prefer a sibling tool, but this is minor given the specificity of the description.
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 100%, so each parameter already has a clear description. The tool description adds no additional parameter-level meaning beyond the schema, keeping this at a baseline 3.
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 identifies the tool's purpose: relating book length (page-count quartiles/bands) to rating statistics, and explicitly poses the question it answers ('do longer books rate higher?'). This differentiates it from siblings like rating_distribution or stats_by_language, which focus on other dimensions.
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 provides a clear context for when to use this tool: when analyzing whether book length correlates with ratings. It does not explicitly name alternatives or state when-not-to-use, but the analytical question is specific enough to guide selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
publish_month_seasonalityPublish Month SeasonalityA
Coarse publishing seasonality by month, plus per-month rating averages.
January is inflated -- it holds 17.72% of rows against a uniform 8.3% because unknown dates were recorded as January 1. The January row is flagged in the output. Prefer stats_by_year for real time-series work.
| Name | Required | Description | Default |
|---|---|---|---|
| year_to | No | Latest publish_year, inclusive. | |
| language | No | language_normalised ISO code, e.g. 'en'. | |
| year_from | No | Earliest publish_year, inclusive. | |
| min_ratings | No | Minimum ratings for a book to contribute to the rating averages. Publication counts are unaffected by this and cover every book. |
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 present, the description carries the disclosure burden and it delivers a key behavioral trait: January numbers are inflated because unknown dates were recorded as January 1, and that row is flagged in the output. This prevents an agent from misreading a systematic artifact as real seasonality. It doesn't exhaustively describe every edge case, but covers the behavior most likely to mislead.
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 four sentences, front-loading the purpose before the caveat and alternative. Every sentence adds necessary information and there is no padding or repetition.
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, together with the output schema and full param coverage, gives an agent everything needed to invoke and interpret the tool. It covers the output's aggregation level, the major data quirk, the flag, and the right sibling for time-series analysis. No critical piece of context is missing.
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?
All four parameters have full descriptions in the input schema, so the description does not need to restate their semantics. The description does not add param-specific details, so the baseline score of 3 applies.
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 the tool's output as coarse month-level publishing seasonality and per-month rating averages. It also distinguishes the tool from stats_by_year by saying the latter is for real time-series work. The resource and scope are clear without ambiguity.
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 'Prefer stats_by_year for real time-series work,' giving a when-not condition and naming the alternative. It also surfaces the January-inflation quirk, so an agent knows to be cautious in month-level interpretation. This is more guidance than most tool descriptions provide.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
rating_distributionRating DistributionA
How book ratings are distributed: a histogram of per-book mean ratings, plus the pooled share of 1-5 star ratings across every rating in scope.
Answers "are ratings clustered high?", "what does a typical rating look like?", "how unusual is a 4.5?".
| Name | Required | Description | Default |
|---|---|---|---|
| year_to | No | Latest publish_year, inclusive. | |
| language | No | Restrict to one language_normalised ISO code, e.g. 'en'. Only 13.6% of books carry a language label at all. | |
| year_from | No | Earliest publish_year, inclusive. | |
| bucket_size | No | Width of each rating bucket, 0.05 to 1.0. | |
| min_ratings | No | Minimum ratings a book must have to be counted. Floor 1. The median book has 5 ratings, so low values fill the distribution with noise. |
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 behavioral burden. It discloses the two main behaviors: producing a histogram and computing a pooled share across all ratings in scope. It also clarifies that the histogram uses per-book mean ratings, which is a useful aggregation detail. It does not mention side effects or permissions, but as a read-only analytics tool this is not a major 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 three concise sentences with no filler. The output definition is front-loaded, followed by illustrative questions. Every sentence earns its place.
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?
Given the full input schema, complete parameter descriptions, and an output schema, the description is largely complete for correct invocation. It explains the core output metrics well enough to interpret results. The only missing piece is explicit guidance about when a sibling tool would be a better choice.
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 100%, so the baseline is 3 even though the description itself does not explain parameters. The description reinforces the meaning of bucket_size by mentioning per-book mean ratings and 'in scope' filters, but it does not add substantial parameter-level detail beyond the schema.
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 what the tool computes: a histogram of per-book mean ratings plus a pooled share of 1-5 star ratings. This is specific to the rating_distribution resource and distinguishes it from sibling tools focused on top-rated books, language stats, or user-vs-book comparisons. The example questions further make the purpose concrete.
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 clear context for when to use the tool by framing the questions it answers, such as 'are ratings clustered high?' and 'what does a typical rating look like?'. It does not explicitly name sibling alternatives or state when not to use it, so it stops short of full exclusion guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
stats_by_authorStats By AuthorA
Rating statistics grouped by author string.
authors is one free-text field per book, not a list, so a co-authored
book forms its own group rather than counting toward each author. There
are 675,289 distinct author strings.
| Name | Required | Description | Default |
|---|---|---|---|
| unit | No | 'works' collapses editions sharing a normalised title to one row -- the right unit for 'most-read author', so a novel with five editions counts once rather than five times. 'editions' counts one row per edition as stored, matching the raw table. | works |
| limit | No | Author strings to return, max 100. | |
| year_to | No | Latest publish_year, inclusive. | |
| language | No | language_normalised ISO code, e.g. 'en'. | |
| order_by | No | n_books, n_ratings, avg_book_rating or pooled_rating. | n_ratings |
| direction | No | 'desc' or 'asc'. | desc |
| min_books | No | Minimum books an author string must have to appear. Raise this to avoid ranking one-book authors against prolific ones. | |
| year_from | No | Earliest publish_year, inclusive. | |
| min_ratings | No | Minimum ratings per book. Floor 1. |
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 behavioral disclosure burden. It reveals a key behavioral trait: because authors is a free-text field, co-authored books form their own group rather than counting toward each author, and it notes 675,289 distinct author strings, indicating large result sets. This is valuable beyond what the schema states, though it doesn't address other behaviors like default sorting or read-only nature.
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 three sentences with no filler: purpose is stated first, followed by the two most important caveats. It is efficient and well-structured, with every sentence earning its place.
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 tool is complex with 9 parameters, but an output schema exists and the schema already documents parameters well. The description supplies the one non-obvious fact — the author field is a single string and co-authored works group separately — plus scale information. It doesn't cover when-to-use vs alternatives or general behavioral traits, but those are partially inferred from the schema and tool name.
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 100%, so baseline is 3. The description doesn't add parameter-specific semantics beyond what the schema provides; the schema already covers each parameter with rich detail, such as unit collapsing editions, order_by options, and min_books behavior. The grouping nuance contextualizes the author key but doesn't explain or augment any particular parameter.
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 that this tool returns rating statistics grouped by author string, clearly identifying the resource and aggregation. It lacks a verb like 'returns' or 'computes', but the intent is unambiguous. It doesn't explicitly distinguish it from siblings, though the grouping unit is unique among the listed 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 provides no guidance on when to choose this tool over siblings such as stats_by_language or stats_by_year. The only contextual note is about the free-text authors field and co-authored grouping, which is interpretive rather than decision-oriented. This leaves the agent to infer usage from the tool name alone.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
stats_by_languageStats By LanguageA
Rating statistics grouped by language.
Grouped on language_normalised, never the raw language column. Read the
coverage caveat before quoting anything from this: only 13.6% of books
carry a language label, and 83% of those are English.
| Name | Required | Description | Default |
|---|---|---|---|
| unit | No | 'editions' counts one row per edition as stored; 'works' collapses editions sharing a normalised title to one row first. Use 'works' to ask how many distinct works a language has, 'editions' to ask how much was published in it. | editions |
| limit | No | Languages to return, max 100. | |
| year_to | No | Latest publish_year, inclusive. | |
| order_by | No | n_books, n_ratings, avg_book_rating or pooled_rating. | n_books |
| direction | No | 'desc' or 'asc'. | desc |
| min_books | No | Minimum books a language must contribute to appear. Small languages are noisy: Italian has 1,156 labelled books, Portuguese 406. | |
| year_from | No | Earliest publish_year, inclusive. | |
| min_ratings | No | Minimum ratings per book. Floor 1. |
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 bears the transparency burden and handles it well by disclosing that grouping uses language_normalised rather than the raw language column. It also highlights the severe data-coverage limitation: only 13.6% of books have a language label and 83% of those are English. These are important behavioral/statistical pitfalls that an agent would not know from the schema alone.
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 short, front-loaded with the core purpose, and every sentence adds value: the grouping rule, the raw-column warning, and the coverage caveat. There is no filler or redundant restating of the tool name.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given a fully documented eight-parameter schema and an output schema, the description is largely complete for safe and correct use. It adds the two most critical caveats, normalization and sparse coverage, that the schema and annotations cannot express. Only explicit sibling routing is missing, but that gap is already captured in usage_guidelines.
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 100% and each parameter already has detailed semantics, so the baseline is 3. The description adds no parameter-specific meaning beyond the schema, but none is needed here.
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 that this tool returns rating statistics grouped by language, which is a clear and specific function. It does not explicitly contrast with sibling stats_by_year, stats_by_publisher, or stats_by_author, but the language focus is 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 implies this is the tool for language-based statistics and includes a strong caveat about data coverage before quoting results. However, it does not explicitly say when to choose this over sibling statistics tools or when an alternative would be more appropriate.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
stats_by_publisherStats By PublisherA
Rating statistics grouped by publisher string.
publisher is unnormalised free text with 79,423 distinct values, so each
row is one spelling rather than one publisher. Penguin alone occupies six
or more separate rows. Treat every figure here as a lower bound on that
imprint's real output.
| Name | Required | Description | Default |
|---|---|---|---|
| unit | No | 'editions' counts one row per edition as stored -- the right unit for 'most prolific publisher', since issuing five editions is five editions of work. 'works' collapses editions sharing a normalised title within each publisher. | editions |
| limit | No | Publisher strings to return, max 100. | |
| year_to | No | Latest publish_year, inclusive. | |
| order_by | No | n_books, n_ratings, avg_book_rating or pooled_rating. | n_books |
| direction | No | 'desc' or 'asc'. | desc |
| min_books | No | Minimum books a publisher string must have to appear. Because the column is unnormalised, a real publisher's output is split across several strings, so this filters spellings, not publishers. | |
| year_from | No | Earliest publish_year, inclusive. | |
| min_ratings | No | Minimum ratings per book. Floor 1. |
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 full weight and it does so well by exposing the key behavioral trap: publisher is free text, each row is one spelling, 79,423 distinct values exist, and Penguin alone spans multiple rows. This prevents agents from misinterpreting aggregate figures as publisher-level totals.
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 purpose is front-loaded in the opening sentence, and the caveat is delivered in a compact, single backticked paragraph with a concrete example. Every sentence adds value and there is no boilerplate or redundancy.
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 8-paramater tool with a fully described schema and an output schema, the description covers the main conceptual hazard and how to read the numbers. It narrowly misses full completeness because it never explicitly routes the agent toward or away from sibling stats tools, but the tool name and purpose phrase make that gap minor.
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 100% and each parameter already has a rich description, including the same unnormalised-publisher warning in min_books. The description adds texture with cardinality counts and the Penguin example but does not introduce new parameter semantics beyond what the schema already provides, so the baseline 3 applies.
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 statement of what the tool returns: rating statistics grouped by publisher string. The phrase 'publisher string' rather than 'publisher' immediately distinguishes this from stats_by_author, stats_by_language, and stats_by_year, and the warning about unnormalised spelling sets precise expectations.
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 implied usage guidance: use this for publisher-level stats, and treat results as per-spelling lower bounds. However, it does not explicitly name alternative tools or state when not to use this tool, leaving the actual selection to inference from the purpose clause.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
stats_by_yearStats By YearA
Rating statistics and publication volume per publication year.
publish_year is the only reliable temporal field in this dataset -- use this rather than publish_month for any real time series. Always ordered chronologically.
| Name | Required | Description | Default |
|---|---|---|---|
| unit | No | 'editions' counts one row per edition as stored, which is what a publication-volume series usually wants. 'works' collapses editions sharing a normalised title, but note a reissue is dated to its own publish_year, so a work can still appear in several years. | editions |
| limit | No | Years to return, max 200. | |
| year_to | No | Latest publish_year, inclusive. | |
| language | No | language_normalised ISO code, e.g. 'en'. | |
| min_books | No | Minimum books a year must contribute to appear. | |
| year_from | No | Earliest publish_year, inclusive. | |
| min_ratings | No | Minimum ratings per book. Floor 1. |
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 behavioral disclosure burden. It usefully discloses that results are always ordered chronologically and that publish_year is the only reliable temporal field. It does not describe truncation behavior with limit or what happens when a year has no data, but the output schema and parameter descriptions cover much of the remaining contract.
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 definition is compact and front-loaded. The main purpose is in the first sentence, and the follow-up sentences add high-value guidance about temporal reliability and ordering without waste.
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 read-only aggregation tool with 100% schema coverage and an output schema, this definition is substantially complete. It provides the central aggregation, the ordering contract, and the key data-quality caveat. Minor gaps like limit truncation direction are not enough to make it incomplete.
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 100%, so the baseline is 3. The description adds a useful domain note about publish_year reliability that is relevant to year_from/year_to, but does not provide per-parameter semantics beyond what the schema already states.
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 phrase clearly identifies the resource—rating statistics and publication volume—and the grouping by publication year. It stops short of a full verb phrase like 'Returns' or 'Computes', but it does convey the tool's core purpose. The publish_year/publish_month caveat also helps distinguish this from the publish_month_seasonality sibling.
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 clear context for when this tool is appropriate: any real time series should use publish_year rather than publish_month. This implicitly warns against the seasonality sibling and sets expectations for time-series use. It does not explicitly name alternative tools or spell out when to prefer stats_by_language, stats_by_publisher, etc.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
top_books_by_ratingTop Books By RatingA
Highest- or lowest-rated books, subject to a minimum-ratings threshold.
The threshold is the whole point: raise it for a result about well-known books, lower it to reach the long tail. Ties break toward the more heavily rated book.
Under the default unit="editions" a work with several editions can take several places in the list -- all with the same rating, since editions of one work largely share a rating pool. Pass unit="works" for a list of distinct works.
| Name | Required | Description | Default |
|---|---|---|---|
| unit | No | 'editions' ranks the table as stored, so several editions of one work can occupy several places in the list. 'works' collapses editions sharing a normalised title and ranks the best-rated edition of each, giving a list of distinct works. | editions |
| limit | No | Books to return, max 100. | |
| year_to | No | Latest publish_year, inclusive. | |
| language | No | language_normalised ISO code, e.g. 'en'. | |
| direction | No | 'desc' for highest rated first, 'asc' for lowest rated first. | desc |
| year_from | No | Earliest publish_year, inclusive. | |
| min_ratings | No | Minimum ratings a book must have to be ranked. Floor 1. At low values the top of the list is obscure books with a handful of 5-star ratings. |
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 disclosure burden. It reveals non-obvious behavior: editions of the same work can occupy multiple positions under the default, ties resolve toward more heavily rated books, and unit='works' collapses editions. This goes beyond what the name or schema alone would imply.
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, front-loaded with the core purpose, and every sentence contributes: core behavior, threshold rationale, tie-breaking, and the editions-vs-works nuance. There is no filler or repetition of the tool name.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
All 7 parameters are fully documented in the schema, the output schema is provided, and the description covers the behavioral nuances an agent cannot infer from schema alone. For a read-style ranking tool with no destructive or auth concerns, the available context is sufficient.
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 100%, so the baseline is 3. The description adds value by explaining the 'whole point' of the minimum-ratings threshold, how ties are broken, and the real-world effect of the unit default. This enriches the schema's parameter descriptions without redundant restatement.
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 exactly what the tool returns: highest- or lowest-rated books, filtered by a minimum-ratings threshold. This distinguishes it clearly from sibling tools like top_titles_by_user_ratings and rating_distribution, which have different units of 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?
The description gives practical guidance: raise the threshold for well-known books, lower it for the long tail, and pass unit='works' for distinct works instead of editions. It does not explicitly name alternatives among the sibling tools, but the usage context is clear enough to select this tool correctly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
top_titles_by_user_ratingsTop Titles By User RatingsA
Best- or worst-liked titles among the 4,154 users in user_ratings.
Stays entirely inside user_ratings -- no join, so no title-matching loss. These are the opinions of a small user panel, not the books table.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Titles to return, max 100. | |
| order_by | No | 'avg_user_rating' to rank by score, 'n_user_ratings' to rank by how many of these users rated it. | avg_user_rating |
| direction | No | 'desc' for best-liked first, 'asc' for worst-liked first. | desc |
| min_ratings | No | Minimum number of user ratings a title must have to be ranked. Floor 1. With only 4,154 users, titles thin out fast -- keep this well above 1 for a meaningful ranking. |
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 a heavier burden. It adds useful behavioral context: no join means no title-matching loss, and the results represent a small user panel rather than the books table. It does not disclose behavior around defaults, ranking construction, or how min_ratings affects the population, though the schema and output schema partially cover usage.
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 short sentences with the purpose front-loaded and no filler. The additional sentences earn their place by clarifying data-source limits and warning that this is a small panel rather than general book ratings.
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, combined with fully documented parameters and an output schema, is sufficient for an agent to select and invoke the tool correctly. A slightly more explicit mention of sibling routing would improve completeness, but the user-panel vs books-table distinction already covers the main confusion risk.
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 input schema documents all four parameters with 100% coverage, including direction values and a caveat for min_ratings. The description adds no parameter-level semantics, but the baseline of 3 applies because the schema already carries the meaning.
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?
First sentence states a specific verb ('Best- or worst-liked titles') and a specific resource ('4,154 users in user_ratings'). It also draws a clear contrast with the books table, which distinguishes it from the sibling tool top_books_by_rating and lets an agent identify a unique purpose.
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 scope and an exclusion: 'Stays entirely inside user_ratings -- no join' and 'not the books table'. This gives clear context for when this tool applies, but it does not explicitly name sibling alternatives or give a direct when-not-to-use rule, so it falls just short of the strongest routing guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
user_ratings_overviewUser Ratings OverviewA
Shape of the user_ratings table: how the 1-5 stars are distributed, and how active the users are.
This describes 4,154 users only. It is a separate dataset from books,
not a sample of it, and does not generalise to Goodreads.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
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 behavioral disclosure burden. It does this well by stating the exact population covered (4,154 users), clarifying that it is a separate dataset rather than a sample, and warning that results do not generalize to Goodreads. This is meaningful non-obvious context, though it does not explicitly describe the return shape or read-only nature.
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 brief and front-loaded. The first sentence states the core purpose, and the second set of sentences provides essential caveats without filler. Every sentence earns its place.
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 zero-parameter tool with an output schema, the description is complete: it explains the dataset scope, the population analyzed, the limitation, and the core content of the overview. No additional information is needed to invoke it correctly.
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 tool has zero parameters and the schema is completely documented (100% coverage). There is nothing for the description to add about parameters, so it reasonably focuses on what the overview conveys.
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 what the tool covers: the shape of the user_ratings table, star distribution, and user activity. It also distinguishes itself from the books dataset by stating it is a separate dataset, though it does not explicitly differentiate itself from similar-looking siblings like rating_distribution.
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 provides no explicit guidance on when to use this tool versus alternatives. It includes an important scope warning ('separate dataset from books'), but does not tell an agent when to choose this tool over sibling tools such as rating_distribution or top_titles_by_user_ratings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
TDQS
Each tool targets a distinct analytical question, and the detailed descriptions make the boundaries fairly clear. However, top_books_by_rating and top_titles_by_user_ratings, plus dataset_overview and user_ratings_overview, are similar enough at name level that an agent could initially pick the wrong one.
All tool names use snake_case and the stats_by_* family is a recognizable pattern for grouped summaries. But descriptive names like dataset_overview, user_ratings_overview, and publish_month_seasonality break the otherwise consistent pattern.
Twelve tools is a well-scoped size for a dataset-analysis server. Each tool covers a meaningful slice of the data, and none feel redundant or purely decorative.
The server covers the dataset's core analytical needs: overview, rating distributions, top lists, grouping by language/year/publisher/author, page-count effects, seasonality, and cross-table comparison. There are no obvious dead ends or missing operations for the declared purpose.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Valid and reliable data engineering and statistical analysis without hallucinations.
Normalized official data with provenance, aggregations, insights, free samples and agent access.
Normalized official data with provenance, aggregations, insights, free samples and agent access.
The statistical analyst in your AI chat — validated, citable, re-runnable analysis of your data.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceProvides a semantic layer for querying and analyzing BigQuery's Austin Bikeshare public dataset with structured dimensions and measures through a simplified interface.2
- AlicenseNot gradedqualityBmaintenanceA read-only BigQuery MCP server with auto-LIMIT injection, dry-run cost guard, and ADC authentication. Allows safe SQL querying of BigQuery by LLMs without risk of data modification or unexpected costs.1MIT
- AlicenseNot gradedqualityCmaintenanceEnables LLMs to explore BigQuery datasets and tables, run safe read-only queries, and optionally perform vector search using BigQuery embeddings.9MIT
- FlicenseNot gradedqualityDmaintenanceEnables read-only interaction with Google BigQuery, including SQL queries, dataset/table listing, schema retrieval, table preview, and metadata access via service account authentication.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/fahadimmad786-stack/goodreads-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server