Lots of CSVs
Lots of CSVs (lotsofcsvs) is a small data warehouse built with CSV and HTTP as its interface. It is intentionally as barebones as possible. It aims to be a simple API for recording and fetching data that works with everything.
The original motivation for it was to create a data API that you could use from iOS Shortcuts. Here is a template shortcut that you can modify.
# Lots of CSVS > Lots of CSVs is a minimalist data warehouse for storing and retrieving CSV files via HTTP. This content is specifically designed for LLMs and not intended for human reading. These instructions provide programmatic access protocols for autonomous data interaction with the Lots of CSVs service. ### Basic principles 1. This is a REST API for retrieving and storing CSV data. 2. Every request must be authenticated using a token passed in an HTTP header: `authorization: Bearer <token>` 3. You will need an authentication token from the user. If you don't have one ask them for it. 4. Every CSV dataset has a unique URL. 5. You will need the user's registered username. If you don't have one ask them for it. 6. URLs are specific to the user and follow a common structure. `https://www.lotsofcsvs.com/api/u/<username>/[path/to/dataset].csv` - `<username>` is your registered username - `[path/to/dataset](.csv)`: the name of the dataset. Dataset paths in Lots of CSVs are treated as object keys, not traditional file system paths. This means paths are flat, hierarchical identifiers, not actual directory structures. The entire string is treated as a single, unique identifier Slashes (/) are part of the identifier, not separators creating directories. Examples: `projects/sales/2023.csv` `projects/sales/2024.csv` - URL Example: `https://www.lotsofcsvs.com/api/u/johndoe/projects/sales/quarterly.csv` 7. Always follow RFC 4180 CSV formatting guidelines when sending CSV data ### API actions #### Fetch a dataset Fetch a dataset by sending an HTTP GET to the URL of the dataset. The data will be returned as an RFC 4180 formatted CSV - Endpoint: `https://www.lotsofcsvs.com/api/u/<username>/[path/to/dataset].csv` - Method: GET - Row ids: every row has an integer id that increases as rows are appended. - JSON reads (`.json`, `?json=true` or `Accept: application/json`) always include it as a `row_id` key, right after `timestamp`. - CSV reads include it as a `row_id` column, second after `timestamp`, only with `?include=row_id`. Without it the CSV is unchanged. - `?after_row_id=N` returns only rows whose id is greater than `N`, ordered by id, in any format. Pass the last `row_id` you have seen to fetch only newer rows. `N` must be an integer; anything else is a `400`. - Example: `https://www.lotsofcsvs.com/api/u/johndoe/projects/sales/quarterly.csv?include=row_id&after_row_id=1042` - Attachment cells come back as stored: space-separated `attachment:///` URIs (see Attachments). - Examples: - ``` curl -H "authorization: Bearer <token>" \ https://www.lotsofcsvs.com/api/u/johndoe/projects/sales/quarterly.csv``` - ``` # JavaScript (fetch) fetch('https://www.lotsofcsvs.com/api/u/johndoe/projects/sales/quarterly.csv', { headers: { 'authorization': 'Bearer <token>' } })``` #### List datasets #### Create or append to a dataset Add to a dataset by sending an HTTP POST to the URL of the dataset. If the dataset does not exist it will be created. - Endpoint: `https://www.lotsofcsvs.com/api/u/<username>/[path/to/dataset].csv` - Method: POST - Rules: - All submissions should include an appropriate Content-Type header `Content-Type: text/csv; header=present` - All submissions must include CSV headers as the first row - Follow RFC 4180 CSV formatting guidelines - Response: `{"rows_inserted": 2, "row_ids": [1041, 1042]}`. `row_ids` are the new rows' ids, in the order they were sent. - Examples: - ``` # Curl example (adding more rows to an existing dataset) echo "Bob,35,Chicago Charlie,40,Boston" | curl -X POST \ -H "Content-Type: text/csv; header=present" \ -H "authorization: Bearer <token>" \ --data-binary @- \ https://www.lotsofcsvs.com/api/u/johndoe/users/demographics.csv``` - ``` # Python (requests library) import requests headers = { 'Content-Type': 'text/csv; header=present', 'authorization': 'Bearer <token>' } additional_data = """name,age,city Bob,35,Chicago Charlie,40,Boston""" response = requests.post( 'https://www.lotsofcsvs.com/api/u/johndoe/users/demographics.csv', headers=headers, data=additional_data )``` #### Update or delete one row Every row has an integer `row_id` (see "Fetch a dataset"). These routes address the dataset by its numeric id, which is returned as `dataset_id` by `/api/uploads/text` and by an attachment's `?meta=1` (see Attachments). - Replace a row: `PUT https://www.lotsofcsvs.com/api/datasets/<datasetId>/rows/<rowId>` with one row as a JSON object (`Content-Type: application/json`), one CSV record in column order (`Content-Type: text/csv`), or `multipart/form-data` with a `row` part plus file parts (see Attachments). The row keeps its id and `timestamp`. Response: `{"row_id": 1042, "timestamp": ..., "updated_at": ..., "removed_attachments": [...]}`. - Delete a row: `DELETE https://www.lotsofcsvs.com/api/datasets/<datasetId>/rows/<rowId>`. Response: `{"row_id": 1042, "rows_deleted": 1, "removed_attachments": [...]}`. - A dataset or row that does not exist, or is not yours, is `404`. Files a row no longer references are deleted. #### Delete a dataset `DELETE https://www.lotsofcsvs.com/api/u/<username>/[path/to/dataset]` deletes the dataset, its rows and its attachments. The path is taken literally: no `.csv` suffix is stripped. ### Attachments (files and images in rows) A row can carry files. A column is declared an `attachment` column, and each of its cells holds zero or more attachment URIs separated by a single space: - URI format: `attachment:///<username>/<attachmentId>`, for example `attachment:///johndoe/att_01JC8X3V5ZQ8M2T9K4R7B6N1PD`. The id is `att_` plus a 26-character ULID. - A URI is a stable reference, not a link. It never expires and grants nothing by itself. Reads (CSV, JSON, SQLite) return the URIs exactly as stored. - Columns that are not `attachment` columns are never checked: a URI there is plain text. - Each file belongs to at most one row of the dataset it was uploaded to. Updating a row so it no longer references a file, deleting the row, or deleting the dataset deletes the file. An upload no row references is deleted after 24 hours. - Limits: 25 MB per file, 100 MB per request, 10 files per row (`413` beyond them). - Allowed types, detected from the file's bytes (`415` otherwise, or when the declared type disagrees): PNG, JPEG, GIF, WebP, AVIF, HEIC, PDF, plain text, CSV and JSON. SVG and HTML are always rejected. #### Declare an attachment column - New dataset with files: a multipart append (below) creates the dataset, and every column holding an `upload:<part>` placeholder becomes an attachment column. - Over MCP: `create_dataset` with `column_types: {"receipt": "attachment"}`. - Existing column: `PATCH https://www.lotsofcsvs.com/api/datasets/<datasetId>/columns/<columnName>` with `{"type": "attachment"}` (or `{"type": null}` to clear it). Every existing cell must already be empty or hold valid URIs of this dataset, otherwise `409` lists the bad cells. #### Append rows and files in one request POST to the dataset URL with `Content-Type: multipart/form-data`: a `rows` part (CSV or a JSON array; give it `Content-Type: text/csv; header=present` to name the columns) plus one part per file. A cell says `upload:<part name>` to reference a file of the same request; it is stored as the file's new URI. ``` curl -X POST -H "authorization: Bearer <token>" \ -F 'rows=[{"date":"2026-11-03","amount":"12.50","receipt":"upload:r1 upload:r2"}]' \ -F 'r1=@receipt.png;type=image/png' \ -F 'r2=@invoice.pdf;type=application/pdf' \ https://www.lotsofcsvs.com/api/u/johndoe/expenses ``` A plain `rows` field is read as JSON when it starts with `[` and as headerless CSV otherwise; to send CSV with a header line, give the part `Content-Type: text/csv; header=present`. Response: `{"rows_inserted": 1, "row_ids": [1043], "attachments": [{"part": "r1", "uri": "attachment:///johndoe/att_...", "size": 48213, "content_type": "image/png"}, {"part": "r2", ...}]}`. #### Upload first, reference later `POST https://www.lotsofcsvs.com/api/datasets/<datasetId>/attachments` with `multipart/form-data` file parts answers `201 {"attachments": [{"uri": "attachment:///johndoe/att_...", "file_name": "receipt.jpg", "content_type": "image/jpeg", "size": 48213, "status": "pending"}]}`. Put the URI in an attachment cell of an ordinary CSV or JSON append, or of a row update, to attach it to that row. #### Read a file - `GET https://www.lotsofcsvs.com/api/attachments/<attachmentId>` streams the file to its owner (API token or session). Anyone else, an unknown id, a file not yet used by a row and a deleted file all get `404`. - `?meta=1` returns `{"uri", "file_name", "content_type", "size", "sha256", "dataset_id", "row_id", "created_at"}`. - `Range` and `If-None-Match` are supported. - `GET https://www.lotsofcsvs.com/api/attachments/usage` returns `{"count": 12, "bytes": 5242880}` for your account. #### Errors A rejected write stores nothing. The JSON body lists every problem in `attachments` (`{index, column, value, reason, status}`): `400` for a value that is not an attachment URI or a known `upload:` placeholder; `409` for a file that is unknown, another user's, another dataset's, already used by a row, or referenced twice; `413` for too many files in a row. #### Exports - `.sqlite` (per dataset and whole account): attachment columns are `TEXT` holding the URIs, plus an `_attachments` table (`uri, file_name, content_type, size, sha256, row_timestamp`; the whole-account file adds `dataset`). File bytes are not embedded. - Whole-account `.zip` and `.tar.gz` (`https://www.lotsofcsvs.com/api/u/<username>.zip`) include every attached file at `attachments/<dataset>/<attachmentId>-<fileName>` and an `attachments.csv` manifest (`dataset,uri,path,file_name,content_type,size,sha256`) mapping each URI to its file. ### MCP server There are two ways to use Lots of CSVs with the Model Context Protocol: #### Option 1: Built-in Remote MCP Server (Recommended) Lots of CSVs has a built-in MCP server at `https://www.lotsofcsvs.com/api/mcp` that uses OAuth 2.1 for authentication. **Setup Steps:** 1. Create an OAuth client (choose one method): **Option A: Via web UI** - Go to `https://www.lotsofcsvs.com/u/<username>/profile` - Click "Create OAuth Client" - Enter a name (e.g., "Claude Desktop") - Select scopes: `mcp.read` and `mcp.ingest` - Save your `client_id` and `client_secret` (shown only once!) **Option B: Dynamic client registration (RFC 7591)** - Use the `/api/oauth/register` endpoint to programmatically register a client - Requires authentication (session token or API token) - Example: ```bash curl -X POST https://www.lotsofcsvs.com/api/oauth/register \ -H "Authorization: Bearer <your_session_token>" \ -H "Content-Type: application/json" \ -d '{ "client_name": "My MCP Client", "grant_types": ["client_credentials"], "scope": "mcp.read mcp.ingest" }' ``` - Response includes `client_id` and `client_secret` **Available Tools:** - `list_datasets`: List all your datasets - `get_dataset_info`: A dataset's columns; attachment columns have `type: "attachment"` - `read_dataset`: Read data from a specific dataset. `include_row_ids: true` adds a `row_id` column after `timestamp`; `after_row_id: N` returns only rows whose id is greater than `N`; `attachments: "metadata"` also lists each URI's file name, type and size (never the bytes) - `append_rows`: Add new rows to a dataset. `attachments: [{name, file_name, content_type, data_base64}]` (at most 5 MB per call) adds files that rows reference as `upload:<name>`. The result lists the new rows' ids and URIs - `create_dataset`: Create a new dataset. `column_types: {"receipt": "attachment"}` declares attachment columns - `upload_attachment`: Upload one base64 file (at most 5 MB) and get a pending `attachment:///` URI to use in a later append or update - `update_row`: Replace one row by `row_id` (same `attachments` argument as `append_rows`); files it no longer references are deleted (`mcp.write`) - `delete_row`: Delete one row by `row_id` and its files (`mcp.write`) - `delete_dataset`: Delete a dataset, its rows and its files (`mcp.admin`) - Resources: `resources/read` of `dataset:///<username>/<dataset>` returns the CSV; of `attachment:///<username>/<attachmentId>` returns the file as a base64 blob (up to 5 MB). `resources/templates/list` lists the `attachment:///` template. #### Option 2: Python MCP Server Download `https://www.lotsofcsvs.com/csv_mcp_server.py` and run it with Python to expose these HTTP endpoints over the Model Context Protocol. Use `--url` or the `LOTSOFCSVS_URL` environment variable to target another installation.
# Documentation: The API## Get a dataset as CSV# dataset/names/can/be/paths/like/this.csv so you can use them for organizationcurl -H "authorization: Bearer <token>" /api/u/user_name/path/to/cool_data.csv# Get a dataset as a SQLite database# The table is named after the dataset: SELECT * FROM "path/to/cool_data"curl -H "authorization: Bearer <token>" -o cool_data.sqlite \ /api/u/user_name/path/to/cool_data.sqlite# Append to the dataset# The .csv suffix is optional and you can use paths to name your datasets.cat example.csv | curl -X POST -H "Content-Type: text/csv; header=absent" -H "authorization: Bearer <token>" \ https://www.lotsofcsvs.com/api/u/<your_username>/totally/going/to/delete/example --data-binary @-# You can post as many rows as you want.# Use the header=present (or header=absent) variable in the mimetype.# When in doubt follow https://www.rfc-editor.org/rfc/rfc4180# If you are appending to a dataset that doesn't exist it will be created.# You must send the CSV headers the first time you post to it.# Otherwise lotsofcsvs has to guess at the names and we're not doing that.## If you need a CSV to test with, we have one.curl -o example.csv https://www.lotsofcsvs.com/example.csvcat example.csv | curl -X POST -H "Content-Type: text/csv; header=present" \ -H "authorization: Bearer <token>" \ https://www.lotsofcsvs.com/api/u/<your_username>/example.csv --data-binary @-# Bulk import a CSV file# For large CSV files, use the bulk import API which processes files asynchronouslycurl -X POST -H "authorization: Bearer <token>" \ -F "file=@/path/to/your/file.csv" \ -F "datasetName=cool_data" \ -F "isNewDataset=true" \ https://www.lotsofcsvs.com/api/import/upload# This returns a jobId that you can use to check the import status:curl -H "authorization: Bearer <token>" \ https://www.lotsofcsvs.com/api/import/status?jobId=<jobId># Exporting your whole account# It is built in the background, so the first call - and the first call after# anything changed - answers 202 with Location and Retry-After: 10.# Poll the same URL until it answers 200; polling while a job is running# does not start a second one.curl -i -H "authorization: Bearer <token>" https://www.lotsofcsvs.com/api/u/user_name.zip{"status":"pending","format":"zip","location":"/api/u/user_name.zip","retry_after":10,"error":null}# Poll until it is ready, then keep the file. Swap .zip for .tar.gz or .sqlite# for the other two. Only 202 means "still building", so stop on anything else.until code=$(curl -s -o user_name.zip -w '%{http_code}' \ -H "authorization: Bearer <token>" \ https://www.lotsofcsvs.com/api/u/user_name.zip); [ "$code" != 202 ]; do sleep 10; done[ "$code" = 200 ] || { echo "not downloaded: HTTP $code"; cat user_name.zip; }# The archives hold one CSV per dataset at csvs/user_name/<dataset>.csv;# the database holds one table per dataset, named exactly after it.# If the database cannot be built for your account, you still get both archives# and .sqlite answers 404 with the reason.# using DuckDBduckdbCREATE SECRET http (
TYPE HTTP,
EXTRA_HTTP_HEADERS MAP {
'authorization': 'Bearer <token>'
}
);CREATE TABLE example AS SELECT * FROM read_csv('https://www.lotsofcsvs.com/example.csv');CREATE TABLE your_table_name AS SELECT * FROM read_csv('https://www.lotsofcsvs.com/api/u/<username>/your.csv');# It is also LLM friendly. There is a prompt telling LLMs how to use this site# hidden in this page. Also, if you programmatically fetch this page# it will return the prompt, similar to https://docs.jina.ai/.curl -L https://www.lotsofcsvs.com/.well-known/llms.txt# Or run a Model Context Protocol servercurl -O https://www.lotsofcsvs.com/requirements.txtcurl -O https://www.lotsofcsvs.com/csv_mcp_server.pypip install -r ./requirements.txtpython csv_mcp_server.py --url https://www.lotsofcsvs.com# Or use the Remote MCP Server (built-in)# lotsofcsvs.com has a built-in MCP server that uses OAuth 2.1 for authentication# Step 1: Create an OAuth client# Option A: Via web UI1. Go to https://www.lotsofcsvs.com/u/<your_username>/profile2. In the OAuth Clients section, click "Create OAuth Client"3. Enter a name (e.g., "Claude Desktop")4. Select scopes: mcp.read and mcp.ingest5. Save your client_id and client_secret (shown only once!)# Option B: Dynamic registration (RFC 7591)curl -X POST https://www.lotsofcsvs.com/api/oauth/register \ -H "Authorization: Bearer <your_session_token>" \ -H "Content-Type: application/json" \ -d '{"client_name":"My MCP Client","grant_types":["client_credentials"],"scope":"mcp.read mcp.ingest"}'