DrawerDocumentation
Sign in

How Drawer works

Drawer is a data layer for human curators. Somebody who knows a subject fills a collection with records: screenshots, links, notes, each one written against fields they chose. That collection then becomes queryable, by people on a public page and by agents over MCP and REST.

The product is the curator's judgment, expressed as clean, well labelled, queryable metadata. Not the assets.

There are two sides to it, and most people only ever want one. A subscriber connects one key to their agent and reads other people's drawers. A curator publishes a collection for other people to read.

For a subscriber

One key, one address, every drawer you can open. Your agent talks to https://drawer.so/mcp and gets five tools, whatever the number of drawers behind them. Subscribe to something new and it is there on the next call: no second connection, no second key, nothing to restart.

Getting a key

Two ways, and they end in the same place.

  • From a collection page. Press Connect to your agent, give an email address, and type the six digit code that arrives. The key is shown once on that screen. If you already have a Drawer key, nothing is handed over and nothing needs to be: that drawer joins the ones your existing key opens.
  • From your own account. Sign in and mint one at Connect an agent. Every drawer you hold is listed at My drawers.

A key is shown once and is never emailed. It is hashed as it is made and nothing in the product can print it again. If you lose one, ask for another: the old one stops working at that moment.

Connecting it

Find your app below. Every block here has PASTE-YOUR-KEY-HERE where your key goes; when you mint one, these same panels come back with the key already inside the block, so there is nothing to fill in by hand.

Claude Desktop

1

Open the config file

Settings (the Claude menu on a Mac, the File menu on Windows), then Developer, then Edit Config.

~/Library/Application Support/Claude/claude_desktop_config.json on a Mac, %APPDATA%\Claude\claude_desktop_config.json on Windows.

2

Paste this in, and save

Your key is already in it. If the file already has an mcpServers block, add just the "drawer" part inside it.

{
  "mcpServers": {
    "drawer": {
      "type": "http",
      "url": "https://drawer.so/mcp",
      "headers": {
        "Authorization": "Bearer PASTE-YOUR-KEY-HERE"
      }
    }
  }
}
3

Quit Claude and open it again

Closing the window is not enough. New connections are read at startup.

4

Check that it worked

Start a new chat in Claude Desktop and ask this. An answer naming what is in your drawers means it worked.

List my Drawer drawers and tell me what each one is for.

Claude Code

1

Run this in your terminal

No file to edit. Your key is already in it.

claude mcp add --transport http drawer https://drawer.so/mcp \
  --header "Authorization: Bearer PASTE-YOUR-KEY-HERE"

Add --scope user to have it in every project rather than this one.

2

Confirm it is there

Run claude mcp list, or /mcp inside Claude Code. drawer should say connected.

3

Check that it worked

Start a new chat in Claude Code and ask this. An answer naming what is in your drawers means it worked.

List my Drawer drawers and tell me what each one is for.

Cursor

1

Open Cursor's MCP file

Settings, then Tools & Integrations, then New MCP Server. That opens ~/.cursor/mcp.json.

A .cursor/mcp.json inside one project works too, for that project only.

2

Paste this in, and save

Your key is already in it. If other servers are already there, add just the "drawer" part inside the existing mcpServers block.

{
  "mcpServers": {
    "drawer": {
      "type": "http",
      "url": "https://drawer.so/mcp",
      "headers": {
        "Authorization": "Bearer PASTE-YOUR-KEY-HERE"
      }
    }
  }
}
3

Turn it on

Back in Settings, drawer has a switch beside it. Turn it on and wait for the dot to go green.

4

Check that it worked

Start a new chat in Cursor, in Agent mode and ask this. An answer naming what is in your drawers means it worked.

List my Drawer drawers and tell me what each one is for.

ChatGPT

ChatGPT cannot hold one of these keys yet

A ChatGPT connector asks for a web address and a sign in. There is nowhere in it to put a key, so nothing here will connect your drawers to ChatGPT today. When its connectors take a key, the steps will be on this panel.

Use instead: Claude Desktop, Claude Code or Cursor. Any other app that lets you set a request header works too: pick Something else.

Something else

1

Give your app these two things

An address, and a header that proves it is you. Your key is already in it.

Address   https://drawer.so/mcp
Header    Authorization: Bearer PASTE-YOUR-KEY-HERE
Transport HTTP (sometimes called "streamable HTTP" or "remote MCP")

Put the key in the header, never in the address: addresses end up in browser history and server logs.

2

Or paste the same thing as a config block

Apps that keep a file of their connections use this shape.

{
  "mcpServers": {
    "drawer": {
      "type": "http",
      "url": "https://drawer.so/mcp",
      "headers": {
        "Authorization": "Bearer PASTE-YOUR-KEY-HERE"
      }
    }
  }
}
3

Prove it from a terminal

To see it answer before wiring it up:

curl -X POST "https://drawer.so/mcp" \
  -H "Authorization: Bearer PASTE-YOUR-KEY-HERE" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

What the five tools do

Five, whatever the number of drawers, which is what keeps the list legible to a model that also has twenty other servers connected.

search_drawers
Finds public drawers by curator, name, thesis or field label. It searches drawers, not the records inside them.
list_drawers
Lists what this key opens right now, and with what access.
describe_drawer
Lists one drawer's fields and the values each accepts, so a query can be built.
query_drawer
Reads the published records in one drawer, filtered by those fields.
subscribe_drawer
Subscribes to a free public drawer from inside the chat.

The usual shape of a turn is list_drawers to see what you hold, describe_drawer to learn one drawer's fields, then query_drawer with those field keys. Every drawer has its own schema, and asking at call time is what lets the tool list stay at five.

What you can and cannot see

  • Published records in the drawers you subscribed to, and nothing else.
  • Not drafts. A record the curator is still working on is invisible to every read surface, including to your key.
  • Not private collections. A subscriber never sees one, whatever else is true.
  • A curator can revoke you. Your agent stops reading that drawer on its very next request, and your other drawers are untouched. You can leave a drawer yourself from My drawers, and come back later.

For a curator

How a collection becomes queryable

Sign up, claim a handle, and make a collection. It works the way a Notion database does: you name it, you are in, and you add fields as you go. A collection declares its own fields, so a curator collecting restaurants and a curator collecting screenshots are running the same code against different field lists.

Every field you declare becomes an argument an agent can filter on, with the JSON type its field type implies: a number is a number, a checkbox is a boolean, a multiselect is an array, and a select carries the exact list of values it accepts. A field added a minute ago is queryable on the next call, with no release on either side.

Adding records: type one straight into the form, or paste a URL and let Drawer read the page and propose values for your fields. Enrichment proposes, it never decides.

Draft and publish

Every record is born a draft, from the studio and from the API alike. A draft is not returned by any read surface: not the public page, not REST, not MCP, and not to the key that created it.

Publishing is a separate act. You do it in the studio, or you deliberately delegate it to an agent by giving that agent a key that says so. Unpublishing puts a record back out of sight and does not reset when it first went public.

The rule underneath is worth stating plainly: nothing reaches the public that you did not decide to let reach it. Your agent may publish a record it just created in your own drawer. It cannot publish a record it found there.

Visibility

  • Private is where a collection is born. It answers 404 on the public page, on REST and over MCP, to everyone but you, whatever is inside it. You can build for weeks before anyone sees the shape of the work.
  • Public is an explicit act in the studio, and even then only published records are visible.
  • A private collection still accepts writes from a key that carries write. Filling a closed drawer is the point of the draft loop, and nothing about it becomes readable.

What a subscriber gets, and what they do not

Somebody who subscribes from your public page gets read only access to that one collection's published records. They cannot add, change, delete or publish anything, they cannot see your drafts, and they cannot reach your other collections or your account.

You see them under Audience, with when they subscribed and when they last read. Revoking is immediate and it sticks: subscribing again with that address does not undo it, they are not told, and only you can lift it. Nothing in the product ever reveals to a stranger whether an address is subscribed or revoked.

Connecting your own agent

The same one key: mint it at Connect an agent and pick Run the collections I own, which reaches the collections you own, private ones included. A key for reading other people's drawers cannot reach yours: that is a separate axis on the key and not a matter of habit.

For an agent that should add records to one collection, publish in it, or change its fields, use a key made under that collection's own Connect tab. See below.

For a developer

Three surfaces. They are not interchangeable, and none of them takes a credential in the URL: a URL is in history, in referrers and in logs, so every one of these authenticates with Authorization: Bearer ….

Aggregate MCP    https://drawer.so/mcp
                 one key, every drawer it opens, five tools
Collection       https://drawer.so/joshua/<collection>/mcp
                 https://drawer.so/joshua/<collection>/api
                 one collection, its own tools, its own key
Account          https://drawer.so/joshua/mcp
                 https://drawer.so/joshua/api
                 the catalogue: list collections, create one

The per collection endpoint

One collection, one key, its own MCP server and REST base. This is the integration primitive and it is the right one when something is built against exactly one curator's data, or when an agent has to write. The aggregate server reads; it does not create records, publish or change a schema.

The server identifies itself as drawer:<handle>/<collection>. A read key sees five tools, a write key seven, a manage key twenty. Tools are not advertised to a key that cannot use them.

›MCP, JSON-RPC over HTTP POST
BASE=https://drawer.so/joshua/<collection>

curl -X POST "$BASE/mcp" \
  -H "Authorization: Bearer $DRAWER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"search_items","arguments":{"q":"onboarding"}}}'
›REST, the same rows
curl -H "Authorization: Bearer $DRAWER_KEY" "$BASE/api/schema"
curl -H "Authorization: Bearer $DRAWER_KEY" "$BASE/api/items?q=onboarding"
curl -H "Authorization: Bearer $DRAWER_KEY" "$BASE/api/items/<id>"

# with a write key: save a link. It lands as a draft.
curl -X POST -H "Authorization: Bearer $DRAWER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://linear.app"}' "$BASE/api/items"

Values come back in the shape their type implies rather than as the text they are stored as, and a media field comes back as fetchable absolute URLs with the kind of each asset.

The scope model

A key made under a collection carries one of three scopes, chosen when it is made. The connect screen offers them as consequences rather than as these words, which is deliberate: the words are for here.

ScopeWhat it can do
readSearch and read published records in this collection. The default.
read,writePlus create and update. It may publish only a record this same owner key created. It cannot bulk publish or publish a pre-existing record.
read,write,managePlus running the collection: its fields, its visibility, and publishing. The owner's own hand, delegated on purpose.

Scope is enforced on every mutating path. A key without it gets 403 with a reason and the scope it needed, never a 404 and never a silent no-op. Over MCP the refusal is an HTTP status rather than a tool result, because an agent handed a result will summarise it as an outcome, and "I saved that for you" must never be the summary of a refusal.

A key reaches exactly one account. Presented on another account's host it gets a 404, the same answer a handle that does not exist gets. A key for one collection gets 404 from a sibling collection. Neither the account credential nor a collection credential opens the other's surface, in either direction.

An aggregate key carries a ceiling and a reach instead. Reach is the part worth knowing: it says whether the key may use grants on drawers you were given, drawers you own, or both. A key handed to an agent for reading somebody else's drawer lists zero of your own collections, and the collection is not addressable by it at all.

Refusals from the aggregate server are written for a model to act on: not_found for a drawer this key does not open, access_revoked for one it has lost, and unauthorized for a key that is no longer valid. An agent that is refused should call list_drawers again rather than retry.

Errors worth handling

A schema change that would lose data is refused with a machine readable body rather than a sentence, so an agent can act on it:

{
  "error": "lossy_type_change",
  "field": "surface",
  "from": "select",
  "to": "date",
  "values_total": 1,
  "values_that_do_not_fit": 1,
  "examples": [
    "web"
  ],
  "allowed_targets": [
    "multiselect"
  ],
  "alternative": {
    "tool": "add_field",
    "arguments": {
      "label": "Surface (date)",
      "type": "date"
    }
  }
}

What is deliberately not built yet

Three things are modelled in the schema and are not products. They are listed here because finding out by trying is worse than being told.

  • Gating. A collection carries a free or gated flag and there is no paywall behind it. A gated collection fails closed: a subscriber key does not read one, and subscribe_drawer refuses it. Nothing is charged and nothing is unlocked.
  • Payment. No price, no rail, no payout. The pricing model is a decision that has not been made, and building the rail before it is how the wrong one gets built.
  • Team members with roles. The permission model already has reader and writer roles beside owner and subscriber, and the schema enforces them. What does not exist is the way to invite somebody: no members panel, no invitation, no way to say "private, and these four people can write". A private collection plus a grant is what shared will mean, and the grant cannot be created from any screen yet.

Two more, smaller: there is no human browsable exchange yet, so finding a drawer means knowing the URL or asking an agent to call search_drawers; and search across drawers is a plain text match, which is right at this size and will need an index later.