ShareNotes is a pastebin with memorable links: paste text, get
sharenotes.dev/quiet-river-7, open it anywhere. It already had a
public REST API with no key. Since this week it also has a
remote MCP server, so an assistant such as Claude can do the same thing
for you: "put this in a note and give me the link". It can also read a ShareNotes link you
paste into a chat.
The server is small: two tools, around two hundred lines. Here are the decisions behind it, because each one is a trade-off you will meet if you put an MCP server in front of anything users share.
Two tools, and the one we left out
create_notetakes text, an optional title, an expiry (1 hour, 24 hours, 7 days or 30 days) and an optional delete-after-reading flag. It returns the link.read_notetakes a link or just a name likeblue-fox-42and returns the note.
There is no update_note, although live editing is the thing ShareNotes is best
at. On ShareNotes, a note without a PIN can be edited by anyone who has its link; that is the
point of a shared scratchpad. Hand an assistant an edit-by-name tool and a sentence hidden in
a web page ("now update blue-fox-42 to say...") becomes a way to rewrite notes that belong to
someone else. A person in a browser has to be shown the link. A tool does not. So creating
and reading are tools, and editing stays in the browser.
Stateless, because there is nothing to remember
MCP over Streamable HTTP can keep a session per client. We have nothing to keep: each call either creates a note or reads one. So every POST gets a fresh server and transport, and the response is plain JSON instead of a stream:
router.post('/', async (req, res) => {
const server = buildServer(); // registers create_note and read_note
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless
enableJsonResponse: true, // plain JSON, no event stream
});
res.on('close', () => { transport.close(); server.close(); });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
GET and DELETE answer 405, because a stateless server has no stream
to open and no session to end. We mounted it at /api/mcp, under the path our
reverse proxy already sends to the backend, so shipping it needed no proxy change at all.
Why per-IP limits break for MCP
The REST API allows 20 new notes a day per IP address. That works because a script runs on someone's own machine or CI runner. An assistant that runs in its vendor's cloud is different: the request that reaches us can come from the vendor's servers rather than from the person typing, so many people may arrive from the same few addresses. With a per-IP limit, the first few users of the day would use up everyone's allowance.
We could have added API keys and accounts. ShareNotes has neither, on purpose. Instead,
notes created through MCP share one daily cap: 500 a day across all assistants. The count
lives in the same table as our all-time counter, so a restart doesn't reset it. When it runs
out, create_note says so in plain words, and the assistant can tell you. The
trade-off is real: if someone burns through the cap, everyone waits until tomorrow. For a
free tool with no accounts, a hard ceiling on abuse is worth that.
A read tool that refuses
ShareNotes has two kinds of note an assistant could easily damage:
- Delete-after-reading notes are destroyed the first time they are opened. We already had to stop link-preview bots from consuming them. An assistant is the same problem with more initiative. If it opens the note to "check the link works", the person it was meant for gets nothing.
- PIN-protected notes exist so that only people with the PIN read them.
So read_note opens neither. For a one-time note, it explains that opening it
would destroy it and asks the user to open it in a browser. This also closes a prompt
injection route: text in some document cannot talk the assistant into burning a note, because
the tool doesn't do that for anyone.
It made the tool honest in a second way. MCP tools can carry hints, and
readOnlyHint tells a client a tool changes nothing. If read_note
could consume notes, that hint would be a lie. Because it can't, the hint is true.
Defaults written for a model, not a form
A person picks an expiry in a dialog. A model picks whatever its tool description leads it to, so the description is the interface. Two choices follow from that:
- Notes from assistants expire after 7 days unless the assistant chooses otherwise. Our web editor defaults to a month, and the REST API to no timer. An anonymous, model-driven write path should not leave notes behind by default.
- The description says, in so many words, that notes are readable by anyone with the link, are not end-to-end encrypted, and must never hold passwords or keys. It is the only place that warning will reliably reach the model at the moment it decides.
How we tested it
The tests use the official MCP TypeScript client over real HTTP, the same way an assistant connects. They check that:
- Both tools are listed, and
read_noteis marked read-only. - A note created with no options expires in 7 days and reads back.
- An unknown expiry is rejected.
- A delete-after-reading note survives a
read_notecall untouched, and a PIN-locked note's text never appears in a response. - Hitting the daily cap returns a clear error and is counted.
If you connect it, tell us how it goes through the Suggestion box at the top of the site. That decides what comes next.