Agents & MCP
Storywrangler is built to be driven by LLM agents, not just humans. Everything on this site is also available as plain text and as live tools, so an agent — Claude Code, or any Model Context Protocol client — can read the docs, discover what is registered, and validate a submission without scraping HTML or guessing.
There are three ways in: the llms.txt exports (the docs as plain text), the MCP
server (live tools over the registry and docs), and the Claude skills (durable
workflow craft).
Machine-readable docs
Every page here has a plain-text twin, generated from the same source:
/llms.txt— the entire documentation as one markdown file./sections.json— every section with its discovery keywords./{slug}/llms.txt— a single guide (e.g./querying/llms.txt)./api-reference/{tag}/llms.txt— the endpoint reference for one tag, rendered live from the API's OpenAPI spec.
Point an agent at /llms.txt and it has the whole platform in context.
MCP server
storywrangler-mcp exposes the registry and the docs as MCP tools — the same
tools over two transports.
Local (stdio) — recommended for Claude Code. Drop this into your .mcp.json:
{
"mcpServers": {
"storywrangler": {
"command": "uvx",
"args": ["storywrangler-mcp"]
}
}
} uvx fetches and runs it — no install step. It points at the public deployment by
default; override with STORYWRANGLER_URL and STORYWRANGLER_DOCS_URL, and set
STORYWRANGLER_INSECURE=1 while the uvm.edu TLS certificate mismatch persists.
Remote (streamable HTTP) — the same server is mounted on the API at /mcp
(stateless), for hosted agents that connect over HTTP instead of spawning a
process.
Tools
| Tool | What it does |
|---|---|
list-sections |
List the documentation sections (title + discovery keywords). |
get-documentation |
Fetch one section as markdown. |
list-datasets |
List registered datasets from the live registry. |
get-dataset |
One dataset's level_order, filter_values, and availability — the ground truth for building a valid query. |
validate-submission |
Dry-run a DatasetCreate locally against the real schema and the registration guards, before you POST. |
Because the registry tools return introspected metadata, an agent builds queries from what actually exists — no guessed entity IDs, granularities, or date ranges.
Skills
Two Claude skills carry the when and why — the discovery-first analyst craft
(querying) and the submission craft (registering) — while
the exact field and endpoint reference stays in the docs and MCP tools, so nothing
drifts. Here they are in full; copy either into your agent, or let
storywrangler new scaffold both:
View skill content
---
name: storywrangler-submitter
description: Interactive guide for getting data onto the Storywrangler platform — writing or improving a DatasetCreate payload, deciding between serving through an existing endpoint type or disseminating behind a bespoke endpoint, validating before registration, and verifying what the server derived. Use whenever a user wants to submit, register, share, or publish a dataset on Storywrangler, needs help writing the registration payload, asks whether their data is submittable, or wants to fix or optimize an existing submission — even if they never say the word "submission". Not for building the extract/transform pipeline that produces the data.
---
# Submitting datasets to Storywrangler
The payload is built in conversation with its author: propose, confirm, then
build. Start from the smallest valid payload and add only what earns its place.
## The shared core (every submission)
Whatever else happens, every payload needs:
- **identity**: `catalog`, `domain`, `dataset_id` — check the registry first
(`GET /registry/domains`, `list-datasets`): an existing entry means update,
not create.
- **location**: `data_location` + `data_format` (`parquet` unless the data is
a hive-partitioned tree).
- **governance**: `description`, `ownership.owner_group`, `ownership.contact`,
`lineage.repo`.
An author with a single `data.parquet` should get from "here's my file" to a
valid payload in two or three questions. Governance is the only part they must
be asked; identity can usually be proposed from context.
## The fork: existing endpoint type, or dissemination?
One question then decides how much more of the contract matters:
**Existing endpoint type** — the data's shape matches a recurring endpoint
type (`types-counts`: rank distributions; `time-series`: tabular measures),
so generic endpoints can serve it directly. Declare `endpoint_schema` plus
what the type requires:
- `types-counts` — columns default to `types`/`counts`; declare
`type_column`/`count_column` only when the data's names differ (declare
around the data, never rename it). Requires one comparison axis:
`entity_mapping` or `transform.filter_dimensions`.
- `time-series` — requires `transform.time_dimension` and at least one
`filter_dimension`.
**Dissemination** — the platform hosts the data behind an endpoint written
*for* it (e.g. `/wikimedia/semantic-timeseries`, essentially
`SELECT * WHERE country = ?`). The endpoint hardcodes the data's shape, so no
`endpoint_schema` is needed and the author doesn't have to learn those fields
— the core payload is enough to register. Serving needs the bespoke endpoint
to exist: a PR to the Storywrangler repo, which can come after registration.
When the data plainly reduces to (type, count) rows or GROUP-BY-able
measures, recommend the endpoint-type path; wide bespoke shapes (score
columns, JSON maps) point to dissemination. Recommend, don't decide.
## Performance declarations (both paths, all opt-in)
Everything beyond the core exists for one reason: Storywrangler's query layer
is a thin DuckDB layer over parquet, and each declaration lets it read less.
Offer each as one question with its payoff attached:
- `transform.time_dimension` → time-range pruning, `?dates=` queries, and
coverage auto-derived at registration.
- `transform.filter_dimensions` → categorical slicing on in-file columns
(omitting the parameter = aggregate over all values).
- `entity_mapping` (+ `entities` rows when IDs are opaque) → `?entity=`
resolution and cross-dataset joins — bespoke endpoints use it too.
- `data_format="parquet_hive"` → partition pruning at scale. Only now does
layout matter: every level `col=val/`, `data_location` = the tree root. If
the layout doesn't conform, propose a fix (e.g. a rename script) and wait —
never restructure the author's data unasked.
- `transform.hash_bucket` → term-first lookup routing. Just the column name;
the pipeline must assign rows with `storywrangler.hashing.assign_bucket()`.
Declare the minimum — the server derives the rest at registration
(`level_order`, `filter_values`, availability, bucket counts). Never
enumerate hive levels, never compute availability client-side. And guard the
boundary: `endpoint_schema` = what comes back; `transform` = which rows are
read.
## Running the conversation
- **Look at only what the current decision needs.** A single file →
`DESCRIBE` it and move on. A directory tree's structure matters only once
hive partitioning is on the table. Don't survey a deep tree to answer a
question nobody asked.
- **One decision per question**, with a recommendation and its consequence.
Use AskUserQuestion when available.
- **Stop when it's enough** — an optional field the author doesn't need is
noise, not thoroughness.
- Field-level reference lives in the docs/MCP (`get-documentation`) — fetch
it rather than guessing.
## Close the loop
- `validate-submission` (MCP) on every draft: real schema contract, mirrored
server guards, on-disk layout checks. Show payload and result together;
errors become the next question, not silent fixes.
- Register only on the author's explicit go. `version="latest"` is the
mutable slot — routine re-runs are not version bumps; semver strings are
immutable snapshots (409 on re-register), bumped only on interface changes.
Never set `schema_version`.
- Verify: `GET /registry/{domain}/{dataset_id}` and check the derived fields.
Empty `level_order`/`availability` means introspection failed (unreachable
path, naming violation) — not success, even on a 200.
## Scope
- **No data yet** → settle the fork and (if applicable) the endpoint type
before the pipeline is built — the shape is cheap to design in and
expensive to retrofit. Pipeline craft itself is the `pipelines` guide
(`get-documentation pipelines`), not this skill.
- **Existing submission to review or fix** → start at validate; walk back
only where the errors point.
- **Generic ETL with no submission in it** → out of scope.View skill content
---
name: storywrangler-sveltekit
description: Use the Storywrangler registry to access the Vermont Complex Systems datasets and instruments from a SvelteKit frontend using remote functions. Discover what data is available, its shape, and how to use it out of the box, directly or through text-based instruments for studying collective attention such as the allotaxonometer and wordshift. Load this when building a SvelteKit app against Storywrangler to enforce type-safe queries, handle instrument errors, and keep the user in the loop on which dataset and whether to use an instrument. For deep query craft load storywrangler-analyst; for submitting data load storywrangler-submitter.
---
# Using Storywrangler from a SvelteKit app
Storywrangler is a data platform with two layers: a **registry of datasets**
(corpora like wikimedia n-grams, reddit, babynames) and **instruments** —
computational analyses layered on those datasets, like the allotaxonometer and
wordshift. Your app picks a dataset and either reads its data directly or runs
an instrument over it, then draws the result.
You never carry the specifics in your head — they live on the **MCP**, which has
two discovery surfaces:
- **Docs** (`list-sections`, then `get-documentation`) — the platform guides and
the instrument / endpoint reference: what an instrument does, its parameters,
response shape, examples, and use cases. The endpoint reference lives under
`api-reference/…` (e.g. `api-reference/storywrangler`). Start here to
understand *what exists and how it works*.
- **Registry** (`list-datasets`, then `get-dataset`) — the live catalog: which
datasets exist, and for one dataset its queryable dimensions, valid filter
values, and date availability. This is *what data exists and which values are
valid*.
Two boundaries keep this skill from drifting:
- **SvelteKit mechanics** (remote functions, query vs prerender, validation,
rendering) belong to the Svelte skills and MCP. That API is still evolving —
get the current code from them, don't reproduce it here.
- **Instrument and dataset specifics** belong to the MCP surfaces above. Don't
recite a fixed list of instruments or guess params — look them up.
What this skill owns is the Storywrangler practical layer, and keeping the
**user in the loop** on the decisions that are theirs: which dataset, and
whether to use an instrument at all (and which).
## Start from the registry — don't assume a dataset
When a request is vague about *which* data ("show storywrangler data", "add a
wordshift"), don't silently default to a corpus. List what exists with
`list-datasets`, surface the options, and let the user choose. Then `get-dataset`
gives that dataset's queryable dimensions, valid filter values, entity mapping,
date availability, and `endpoint_schema` (its output shape). Confirm the dataset
and the slice before wiring anything.
## Data directly, or an instrument?
Not every app needs an instrument — often the user just wants to read and show
the dataset's own data (counts, ranks, time series). An **instrument** is a
computational analysis layered on top (the allotaxonometer, wordshift, …).
Don't assume one, and don't recite a fixed menu — the available instruments and
their use cases live in the MCP. If the user isn't sure what to build, look them
up (`get-documentation`, `api-reference/…`) and **propose** the ones whose
documented use case fits their question; let them choose.
Once the target is settled, the lookup splits across the two MCP surfaces: the
endpoint's **parameter contract** (params, response shape, examples) comes from
the API reference, and the dataset's **valid values** (which entities exist,
which dates are available, which filter dims) come from `get-dataset`. Read
both; don't guess.
## Wiring the call — the Storywrangler-specific parts
Deliver the call as a SvelteKit remote function, but the *how* (the remote-
function API, argument validation, rendering) is the Svelte skills' job — and
that API is still moving, so take the current shape from them rather than from
memory. What stays true on the Storywrangler side:
- **Public read, no auth.** The data and instrument endpoints need no key to
consume.
- **Map the error surface to UI.** Endpoints answer **404** (no data for that
entity/date), **400** (bad filter value), **503** (instrument library down).
Turn each into a real state, not a forever-spinner.
- **Discovery-first before hardcoding.** Resolve entities and check date
availability (via the MCP, or `storywrangler-analyst`) before baking an
`entity` or `date` in — otherwise the page 404s for no reason.
- **Large, sanitized payloads.** Responses can be big; bound them with the
endpoint's limit params (documented in the API reference). The server rewrites
non-finite numbers — `NaN` → `null`, `±Infinity` → the strings `"Infinity"` /
`"-Infinity"` — so treat divergence / measure fields as possibly-string.Wire it into your project
storywrangler new scaffolds the agent setup into a fresh dataset project — it
writes .mcp.json and .claude/skills/ automatically, so a Claude Code session
in that project has the MCP tools and skills out of the box.
Prefer a plugin? Install from the in-repo marketplace:
/plugin marketplace add Vermont-Complex-Systems/storywrangler