Skip to main content
Glama
fahadimmad786-stack

goodreads-mcp

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 up

Register with Claude Code:

claude mcp add goodreads -- /home/safilo/projects/goodreads-mcp/.venv/bin/python \
    -m goodreads_mcp

Environment 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

dataset_overview

shape, live column coverage, every known defect

rating_distribution

histogram of book ratings + pooled star share

top_books_by_rating

best/worst books above a ratings threshold — unit

stats_by_language

ratings grouped by language_normalisedunit

stats_by_year

ratings and volume per publication year — unit

stats_by_publisher

ratings grouped by publisher string — unit

stats_by_author

ratings grouped by author string — unit

page_count_stats

book length against rating

publish_month_seasonality

coarse monthly seasonality

user_ratings_overview

shape of the 4,154-user panel

top_titles_by_user_ratings

best/worst liked titles in the panel

compare_user_vs_book_ratings

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:

  1. n_ratings is 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.

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

  3. Works are identified by title text alone, so the collapse still errs in both directions — see Two title keys and Why authors is 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 World vs Calvin & 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

title_norm()

the user_ratings join only

stripped entirely

work_key()

all work-level dedup and n_distinct_titles

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

Twilight — Meyer, Erin Hunter, Christie Golden, Elie Wiesel

correct

one work, adapter credited

Pride and Prejudice — Jane Austen + graded-reader adapters Clare West, Diana Stewart, Evelyn Attwood

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_author is structurally immune — it groups by author before collapsing, so a collision can never cross authors there. This is the tool the works unit 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_rating is the only tool partitioning on work_key alone, and exposure falls with the threshold — 11.2% of keys at min_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_day is 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 bare language column; the regex leaves language_normalised alone because \b does not match between language and _normalised.

  • SELECT * is rejected on a 1.85M-row table.

  • Every ranking takes min_ratings, floored at 1, not 0 — see below.

  • order_by and direction are 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

  1. 451,777 books (24.4%) have no ratings and are stored as rating = 0.0. Not books rated zero. A naive AVG(rating) is dragged toward zero by a quarter of the table. This is why min_ratings is floored at 1 rather than 0 — the floor excludes them structurally, and require_min_ratings explains why when it rejects 0.

  2. language_normalised is 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.

  3. 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 — and pooled_rating over-weights works with many editions. Every grouped result reports n_distinct_titles and editions_per_title so 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.)

  4. publish_day = 1 for 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 --json

Deploying 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.comread-only, no key files. Credentials come from the Cloud Run metadata server.

role

scope

why

roles/bigquery.jobUser

project

bigquery.jobs.create. Every query is a job. Must be project-scoped — job creation cannot be granted on a dataset.

roles/bigquery.dataViewer

dataset goodreads only

bigquery.tables.getData / tables.get / datasets.get. Scoped to one dataset so the SA cannot read anything else in the project.

roles/logging.logWriter

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/mcp

No header, no token, nothing in ~/.claude.jsonproxy.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

MIN_INSTANCES=1

no cold start

one always-on instance, billed at the idle CPU rate

MIN_INSTANCES=0

+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:

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

  2. Real traffic varies parameters. Cache keys include parameter values. Model-driven calls varying min_ratings, limit and unit will 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 ANTHROPIC_API_KEY

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= URL

run-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=0 and unit=chapters reach the server unaltered, and min/max are 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:

goodreads-mcp

goodreads-chat

ingress

--no-allow-unauthenticated

--allow-unauthenticated + shared token

service account

goodreads-mcp-run

goodreads-chat-run

BigQuery

roles/bigquery.jobUser

none

secrets

none

the access token, and an Anthropic key if chat is wanted

image

root Dockerfile

webchat/Dockerfile

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

  1. Browser → console. No Google identity. A shared secret (CHAT_ACCESS_TOKEN, from Secret Manager) presented as ?k= on first visit, then held in an HttpOnly cookie. 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.

  2. 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 as Authorization: Bearer. Google's edge validates the signature, the audience and roles/run.invoker before the request reaches the container.

  3. Console → Anthropic. Only in chat mode. ANTHROPIC_API_KEY from 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 Field(ge=…) bound — min_ratings, min_books, limit

a validation error, before the tool body runs

ParamError_fail()

the unconstrained parameters — unit, order_by, direction, empty language, year_from > year_to

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 at webchat/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 ADC

tests/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 tools
compare_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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoTitles to return, max 100.
order_byNo'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_ratingsNoMinimum Goodreads ratings, summed across editions, for a title to appear.
min_user_ratingsNoMinimum ratings from the 4,154-user panel for a title to appear.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.5/5.0
Behavior5/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.7/5.0
Behavior5/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
year_toNoLatest publish_year, inclusive.
languageNolanguage_normalised ISO code, e.g. 'en'.
year_fromNoEarliest publish_year, inclusive.
min_ratingsNoMinimum ratings per book. Floor 1.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4/5.0
Behavior3/5

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

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
year_toNoLatest publish_year, inclusive.
languageNolanguage_normalised ISO code, e.g. 'en'.
year_fromNoEarliest publish_year, inclusive.
min_ratingsNoMinimum ratings for a book to contribute to the rating averages. Publication counts are unaffected by this and cover every book.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.5/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines5/5

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?".

ParametersJSON Schema
NameRequiredDescriptionDefault
year_toNoLatest publish_year, inclusive.
languageNoRestrict to one language_normalised ISO code, e.g. 'en'. Only 13.6% of books carry a language label at all.
year_fromNoEarliest publish_year, inclusive.
bucket_sizeNoWidth of each rating bucket, 0.05 to 1.0.
min_ratingsNoMinimum 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

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.2/5.0
Behavior4/5

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

With no annotations provided, the description carries the 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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
unitNo'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
limitNoAuthor strings to return, max 100.
year_toNoLatest publish_year, inclusive.
languageNolanguage_normalised ISO code, e.g. 'en'.
order_byNon_books, n_ratings, avg_book_rating or pooled_rating.n_ratings
directionNo'desc' or 'asc'.desc
min_booksNoMinimum books an author string must have to appear. Raise this to avoid ranking one-book authors against prolific ones.
year_fromNoEarliest publish_year, inclusive.
min_ratingsNoMinimum ratings per book. Floor 1.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.6/5.0
Behavior4/5

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

With no annotations, the description carries the full 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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines2/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
unitNo'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
limitNoLanguages to return, max 100.
year_toNoLatest publish_year, inclusive.
order_byNon_books, n_ratings, avg_book_rating or pooled_rating.n_books
directionNo'desc' or 'asc'.desc
min_booksNoMinimum books a language must contribute to appear. Small languages are noisy: Italian has 1,156 labelled books, Portuguese 406.
year_fromNoEarliest publish_year, inclusive.
min_ratingsNoMinimum ratings per book. Floor 1.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.8/5.0
Behavior4/5

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

With no annotations, the description 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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
unitNo'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
limitNoPublisher strings to return, max 100.
year_toNoLatest publish_year, inclusive.
order_byNon_books, n_ratings, avg_book_rating or pooled_rating.n_books
directionNo'desc' or 'asc'.desc
min_booksNoMinimum 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_fromNoEarliest publish_year, inclusive.
min_ratingsNoMinimum ratings per book. Floor 1.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4/5.0
Behavior4/5

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

With no annotations, the description carries 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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines3/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
unitNo'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
limitNoYears to return, max 200.
year_toNoLatest publish_year, inclusive.
languageNolanguage_normalised ISO code, e.g. 'en'.
min_booksNoMinimum books a year must contribute to appear.
year_fromNoEarliest publish_year, inclusive.
min_ratingsNoMinimum ratings per book. Floor 1.

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4/5.0
Behavior4/5

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

With no annotations, the description carries the 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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
unitNo'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
limitNoBooks to return, max 100.
year_toNoLatest publish_year, inclusive.
languageNolanguage_normalised ISO code, e.g. 'en'.
directionNo'desc' for highest rated first, 'asc' for lowest rated first.desc
year_fromNoEarliest publish_year, inclusive.
min_ratingsNoMinimum 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

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4.5/5.0
Behavior4/5

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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoTitles to return, max 100.
order_byNo'avg_user_rating' to rank by score, 'n_user_ratings' to rank by how many of these users rated it.avg_user_rating
directionNo'desc' for best-liked first, 'asc' for worst-liked first.desc
min_ratingsNoMinimum 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

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A4/5.0
Behavior3/5

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

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.

Conciseness5/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose5/5

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.

Usage Guidelines4/5

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.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription

No output parameters

TDQS

A3.8/5.0
Behavior4/5

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

With no annotations, the description carries the 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.

Conciseness5/5

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.

Completeness5/5

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.

Parameters4/5

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.

Purpose4/5

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.

Usage Guidelines2/5

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

A4.1/5.0
Disambiguation4/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness5/5

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

ActivityMaintained
ResponsivenessNo issues

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

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides a semantic layer for querying and analyzing BigQuery's Austin Bikeshare public dataset with structured dimensions and measures through a simplified interface.
    2
  • A
    license
    Not graded
    quality
    B
    maintenance
    A 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.
    1
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables 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

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