Skip to main content
Glama

Shared List

Lists you share as a link. Each one is locked with a password, works the same on a phone and a laptop, and can carry its own CSS. An MCP server is built in, so an AI agent can make the list, fill it in and restyle it for you.

Think of a small spreadsheet that is pleasant to look at: typed columns (text, numbers, money, links, pictures, checkboxes, choices, dates), rows you tick off, totals, grouping, search. It began as a furniture list for two roommates and is built for anything you'd otherwise drop in a shared sheet.

Live at https://reallyitsandi.com/shared-list/.

What it does

  • No accounts. Press Create list, give it a title and a password, and send the link. Anyone with both can open it, edit it, change its password or delete it. The lock screen shows nothing about the list, not even its title. Sessions last 30 days per list and per device, and a new password signs everyone else out.

  • Edit together. Tick rows, edit cells in place (click on a computer, tap on a phone), add rows, add and retype columns. Open tabs pick up each other's changes within a few seconds.

  • Typed columns. Money columns total up, along with "how much is left" for rows that aren't ticked yet. Choice columns become colored tags and can group the rows. Links show their site; image links show a thumbnail. Columns can be narrow or wide, hidden from the table, and the list can have a default sort.

  • Paste a link, get the price. A shop link fills in the row's price, photo and name from the page. Paste one straight into Add item… and that's the whole row. See Link lookups.

  • Per-list CSS. Every list has a stylesheet that restyles its page for everyone. The default look is built on --sl-* variables and stable class names, so most themes are a few lines. See Styling.

  • MCP. Eighteen tools over Streamable HTTP. Agents can create, read, edit and restyle lists, lay out their columns, and look links up. Tokens can only be made by the server's operator. See For agents.

  • Small. One Node process and one SQLite file. No build step on the server, two runtime dependencies, about 50 MB of memory.

Related MCP server: vnsh-mcp

For agents

The server speaks MCP at <base>/mcp, statelessly. Each agent connects with its own token, sent as a bearer token.

Tokens

Tokens are made on the server and nowhere else. No page and no API route can mint one, so handing one out takes a shell on the box:

node server/cli.ts token create "Roommate"     # prints the token once
node server/cli.ts token list
node server/cli.ts token revoke Roommate        # by name or id

In Docker, prefix each one with docker exec <container>. Only a hash of each token is stored.

A token sees the lists it created, plus any list it has opened with open_list and that list's password. A token you give someone shows them none of your other lists. Access through open_list ends when the list's password changes, and only the token that created a list can make it view-only.

Connecting

Claude Code

claude mcp add --transport http shared-list https://your.host/mcp \
  --header "Authorization: Bearer <token>"

Any other client that takes a JSON config:

{
  "mcpServers": {
    "shared-list": {
      "type": "http",
      "url": "https://your.host/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}

A client that can only be given a URL can put the token in the path instead: https://your.host/mcp/<token>. That URL is a credential, so keep it private, and remember that it ends up in the proxy's access log.

Tool

What it does

list_lists

The token's lists: id, title, link, item count

open_list

Get access to someone else's list with its password

get_list

Columns, items (with ids) and view settings

create_list

A new list with a password, from columns or a preset, optionally with items and CSS

update_list

Title, description, grouping, default sort, password, edit access

delete_list

Delete a list

add_columns / update_column / remove_column

Change the columns: name, type, options, order, width, hidden, and what a link column fills. Retyping keeps the values that still fit

add_items / update_items / remove_items / move_item

Change the rows

preview_link

Read a shop page's name, photo and price without saving anything

look_up_links

Look the links in some rows up again and fill what is empty

get_style_guide

The class names and variables a list's CSS can use

get_css / set_css

Read or replace a list's CSS

Agents refer to columns by name or id and send values the way people type them: "$1,299" for money, "yes" for a checkbox, ikea.com/p/123 for a link. A new label in a choice column becomes a new option. A batch with one bad value adds nothing, and the error says which column refused what.

Try asking: "Make a shared list for our apartment furniture with item, room, price, link and who's buying it, password couch123. Add a sofa, a bed frame and a dining table. Then make it look warm and cozy."

When a link lands in a link column, the server reads the page in the background. It fills the row's price, photo and name, but only cells that are empty. Nothing anyone typed is overwritten, and a price in a different currency from the column's is kept with the row rather than filled in. The row shows Looking up… while it works, and the item's form says what was found, or why nothing was.

It reads, in order of trust:

  1. schema.org Product data (JSON-LD), which most shop platforms publish,

  2. Open Graph and product:price meta tags,

  3. microdata (itemprop="price"),

and Amazon's own markup, which has none of those for its price. Temu, Amazon and Shopify stores work. Some big retailers (IKEA, Walmart, Home Depot, Etsy, eBay, and some furniture brands) refuse to answer any server at all, and those lookups fail with that reason. The fetcher first identifies itself as SharedListBot, which gets some shops to serve their product data. If that leaves gaps, it asks again as a browser would.

Each link column has its own switch for this, and a choice of which columns it fills (fill in update_column). Lookups are capped per list and per hour.

The fetcher only ever connects to the public internet. The check happens in the connection's own DNS lookup and again after every redirect. A link to localhost, a private network, a cloud metadata address, or a port other than 80 and 443 is refused. Bodies are cut off at 2.5 MB, and a page gets ten seconds.

Styling

A list's CSS loads after the built-in stylesheet and applies only to that list. Edit it under Settings → Style, which previews as you type, or with the set_css tool.

:root {
  --sl-paper: #fff8f0;      /* page */
  --sl-rule: #f0dcc8;       /* lines between rows */
  --sl-margin: #c2410c;     /* the heavy line under the headings */
  --sl-accent: #c2410c;     /* ticks, strike-through, progress */
}

/* A whole row, by the value of one of its columns */
.sl-row[data-col-who="Split"] .sl-cell[data-primary] { font-style: italic; }

The full reference is shared/styleguide.ts. It is the same text agents get from get_style_guide and people see in the Style tab. Fonts can be imported from Google Fonts. The page's Content-Security-Policy blocks other font hosts and every script.

Run your own

Docker

git clone https://github.com/realAndi/shared-list && cd shared-list
docker build -t shared-list .
docker run -d --name shared-list -p 127.0.0.1:8787:8787 -v "$PWD/data:/data" shared-list
docker exec shared-list node server/cli.ts token create "Me"   # only if you want MCP

Open http://localhost:8787/ and press Create list. deploy/compose.example.yml and deploy/nginx.example.conf put it behind a reverse proxy, at a whole domain or under a sub-path.

Without Docker

Node 24 or newer runs the TypeScript directly, so there is nothing to compile on the server.

npm ci && npm run build   # builds the web client into dist/web
npm start                 # http://localhost:8787/

Configuration

All optional.

Variable

Default

LISTEN

8787

A port, or a path to listen on as a Unix socket

BASE_PATH

/

Where the app is mounted, e.g. /shared-list

PUBLIC_URL

from the request

Origin for links the server hands out, e.g. https://example.com

DATA_DIR

./data

The SQLite file, backups, and generated secrets

SESSION_SECRET

generated

Signs session cookies. Written to DATA_DIR/session-secret if unset

TRUST_PROXY

on for a socket

Believe X-Real-IP and X-Forwarded-Proto. Turn on behind a proxy

BACKUP_KEEP

7

Daily copies of the database to keep in DATA_DIR/backups; 0 turns them off

MAX_LISTS

200

MAX_CREATES_PER_HOUR

10

New lists one address can make from the web

MAX_ITEMS

1000

Per list

MAX_COLUMNS

30

Per list

MAX_CELL_CHARS

5000

MAX_CSS_BYTES

65536

Per list

MAX_DB_MB

100

Creating lists and adding rows stop when the database reaches this

Images are links, never uploads, so the database stays small. A list with a thousand rows is a few hundred KB.

How it is put together

server/
  index.ts      start, listen on a port or a socket, shut down cleanly
  app.ts        request context (base path, proxy headers) and dispatch
  api.ts        the JSON API the web client uses
  mcp.ts        the MCP tools
  lookups.ts    looking links up and filling rows from them
  extract.ts    a page's name, photo and price, from its HTML
  fetcher.ts    fetching a page without letting a link reach a private address
  tokens.ts     MCP tokens: made, checked, revoked
  cli.ts        the only place tokens are made
  service.ts    each operation as one transaction
  model.ts      the list document: columns, values, items — pure functions
  store.ts      SQLite via node:sqlite
  auth.ts       scrypt passwords, signed session cookies, rate limits
  static.ts     the built client, served from memory with a strict CSP
  backup.ts     a daily VACUUM INTO
shared/         types, presets and the style guide, used by both sides
web/            the client: plain TypeScript and DOM, built by Vite
test/           node:test, including an MCP client end to end

A list is one row in SQLite holding a JSON document of columns and items. Every change reads it, edits it and writes it back inside a transaction, and bumps a version number. Clients poll with that version and get 204 No Content until something changes.

Security

  • Passwords are hashed with scrypt. The session cookie is HMAC-signed, HttpOnly, SameSite=Lax, and scoped to the app's path. It names one list and the password generation it was issued under.

  • Ten wrong passwords for a list from one address in 15 minutes, or forty across all lists, and that address waits. Wrong MCP tokens, and wrong passwords given to open_list, have limits of their own.

  • Writes accept only application/json and are refused when Origin or Sec-Fetch-Site says they came from another site.

  • Pages are built with DOM calls rather than HTML strings, so list content can't turn into markup. The CSP allows scripts only from the app itself.

  • Custom CSS is served as its own stylesheet, behind the same session as the list.

  • Anyone can create a list, so storage is capped in three places: new lists per address per hour, lists on the server, and the size of the database.

  • There is no admin account, page or master key. MCP tokens are made with the CLI, which needs a shell on the server, and only their SHA-256 is stored.

Development

npm install
npm run dev        # API on :8787 (restarts on change), client on :5173
npm run check      # typecheck, test, build

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to interact with Slack Lists through comprehensive tools for creating, retrieving, filtering, and managing list items. Supports bulk operations, data export, subtask creation, and all Slack List field types with robust error handling and production-ready implementation.
    8
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to create, list, read, update, and restore encrypted shared workspaces so multiple agents and people can collaborate on the same living document through one link. Supports version-checked writes that prevent concurrent agents from silently overwriting each other, plus one-shot share and read tools for quick content exchange.
    14 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to create, read, update, list, restore and renew encrypted shared workspaces and one-shot links, so multiple agents and people can collaborate on the same versioned document through one URL. Content is encrypted client-side before upload, supports view-only sharing, and is deleted 24 hours after the last edit.
    14 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI agents to create, read, update, list, and restore shared encrypted workspaces and one-shot links that both people and other agents can open and edit through a single URL. Content is encrypted client-side, version-checked on write to prevent clobbering, and expires 24 hours after the last edit.
    14 npm
    MIT