Pixlib
Pixlib is a remote MCP server that gives Claude a curated image library. Claude searches by tag or purpose, gets a public URL, and uses it in a landing page, a doc, a PDF, a deck.
I built it because I kept watching Claude skip the picture or invent a placeholder. I wanted to ask for a sunflower and get a real file.
The whole thing is one Node file and a handful of scripts. No framework, no build step.
The shape of it
Two read-only tools. search_images filters on purpose and tags, list_images pages through everything. Neither one writes.
The same file boots two ways. With no port set it speaks stdio, which is what Claude Desktop spawns. With a port it comes up as an HTTP server and carries the whole OAuth surface.
Writing happens somewhere else. bulk.js and manage.js hold the service key and run on my laptop. The deployed server has no code path that writes. That split is the decision the rest of the repo hangs off.
What's in it
- index.js the server, both tools, all the OAuth
- bulk.js import a folder, upload, auto-tag
- manage.js add, remove, list, one at a time
- migrate-to-r2.js point every file_path at R2
- migrate-back-to-supabase.js the undo
- setup.sql one table, two indexes
- railway.json, netlify.toml deploy config
One table holds all of it: name, URL, purpose, tags, description. Tags are a Postgres array with a GIN index on it, so a search for dark and minimal is one query that matches both, not a pile of LIKEs.
bulk.js is the one I like. It uploads the file first, then hands Claude Haiku the resulting public URL instead of the bytes. Anything over 5MB used to blow past the vision limit once it was base64. Pointing at a URL sidesteps that, and Haiku writes back the purpose, three to six tags, and a sentence.
Signing in
Claude.ai wants OAuth 2.1. Clerk handles the identity part, but its tokens carry a client_id where the spec wants an aud, and Claude rejected them on that alone.
So the server stopped passing Clerk's token through at all.
That map lives in memory, which has two consequences I have not fixed. A redeploy logs everyone out, and I cannot run a second instance. Moving it into the same Postgres the images already sit in is the next real piece of work.
The IP problem
Anthropic's servers sit in 160.79.104.0/21. Both Supabase Storage and R2 refuse that range.
Images load fine in a browser. Claude cannot fetch them server-side, so a PDF with embedded pictures still comes out broken. Each migration script skips any row already pointing at its destination, which is the only reason running them twice is safe.
The fix is a proxy in front of the bucket, not a fourth storage provider.
What's live
Local stdio works in Claude Desktop. The remote server is up with StreamableHTTP and OAuth, and both tools show up in the connector list. Bulk import auto-tags on the way in.
Everything on the read path works. The write path is mine and stays mine.