← All notes

An API key setting that doesn't exist. An MCP server for Zakupomat in one evening

· Not by Prompt Alone · episode 4 · Czytaj po polsku

You tell Claude: "take this cake recipe and add everything except the eggs to my list, and if something's already there, bump up the quantity". A moment later Marta sees flour, sugar and butter in Zakupomat on her phone. No retyping, no copy-paste.

That's how it works today. But the road to get there was more interesting than the result. This episode is longer and more technical than the previous ones - this is where all the meat is ;-)

Why MCP, and not AI inside Zakupomat

Before the technology - the most important decision in this story wasn't a technical one.

Zakupomat is meant to stay free forever. No ads, no selling data, no subscription. And AI features are great, but they cost money - usually per use. Every "make me a list from this recipe" is a model call someone pays for. The more clever features, the higher the bill for running an app that has no way to cover it. Sooner or later that ends with ads, a subscription, or a "free" version that can't do anything. No, thank you!

And yet most of us already have our own AI assistant - Claude, ChatGPT, Gemini. Often even the free version is enough to do what we need. And here's the second, more important thing: that assistant knows much more about us than Zakupomat ever will. It has access to our e-mail and calendar. It knows we're throwing a party for 10 people on Saturday. And that after our recent holiday in Mexico there'll be Mexican treats ;-) AI built into a shopping list app will never see any of that.

So instead of putting AI into Zakupomat, I'm letting Zakupomat into the AI you already use. The model "thinks" on your account and on your (often zero) budget, and Zakupomat does what it's good at - it keeps the family's shared list and syncs it live across all phones.

The cost on my side? A few extra requests to a server that's running anyway. Zero spent on tokens.

The idea: "I put the key in the settings and it just works"

On the evening of September 27th, right after version 1.4.0 went live in both stores, I wrote to Claude:

"For now I'd like an MCP server that can be added to Claude and ChatGPT, where I put the key in the settings and I can work with the list right away. Eventually I'd like the plugins to be right there in the add-on lists in the "stores" of both these agents."

For the record: MCP (Model Context Protocol) is an open protocol (created by Anthropic) through which an AI assistant can use the tools of an external service. An MCP server exposes its menu - a list of tools ("show the list", "add a product", "check off an item"), and the assistant decides on its own when to call which.

Zakupomat has no accounts. A family has one random access key and that's it. So "I put the key in the settings" sounded natural.

Except that setting doesn't exist. The agent first read the current Anthropic and OpenAI docs, and it turned out that a remote MCP server in claude.ai and in ChatGPT works either with no authentication at all, or via OAuth. And both assistants' directories require OAuth. There's simply no "paste your API key" field.

OAuth without accounts

The solution kept the intent: the user still just pastes the family key. Only not in the assistant's settings, but on a Zakupomat page the assistant redirects them to as part of the standard OAuth flow. The family key is the "login", and the assistant gets a token that points to that one family.

Rejected options:

  • The key in the server URL (/mcp?key=…). It ends up in the server logs, and the MCP spec explicitly forbids passing tokens in the query string.
  • A server with no authentication. No comment needed ;-)

What's inside:

  • an OAuth 2.1 authorization server with PKCE (S256) and refresh token rotation,
  • client registration two ways: DCR (Dynamic Client Registration - the client registers itself) and CIMD (Client ID Metadata Document - the client gives the address of a document describing it, and the server fetches that document),
  • a "Connect to Zakupomat" page in three languages,
  • an /mcp endpoint and tools for the shopping list.

And two safeguards that are easy to miss:

  1. Redirect URIs only for Claude, ChatGPT and loopback. Without that, any website could register its own "client" and harvest family keys through the real Zakupomat page. Phishing with an official certificate.
  2. CIMD documents fetched only from Anthropic and OpenAI hosts. Otherwise someone could make my server query arbitrary addresses (classic SSRF).

On top of that, guessing the key on the connect page counts towards the same limit as logging in to the app - 10 failures per 15 minutes from one IP.

Python 3.11 was there. Just not mine

The official MCP SDK for Python requires version 3.10 or newer. The agent checked the server over SSH: the system Python is 3.9.2. I replied that I'd built something recently and there's 3.11 on the server. And I was right! Except that 3.11 had been installed by uv in the directory of a completely different project ;-)

The options were:

  • a separate service on Python 3.11 with the official SDK - it would have to store family keys or get a second way into the database,
  • borrow Python from someone else's project - no, thank you,
  • move the whole backend to 3.11 - dependency conflicts and production risk for a single feature,
  • build MCP into the existing backend and write the protocol by hand.

The last option won, and looking back it was the best decision of the evening. Why:

  • the token points to a household_id, so the family key doesn't need to be stored anywhere,
  • the MCP tools call the same functions as the apps, so a change made by the assistant goes straight to open phones via SSE (the live connection that lives in the memory of the same process),
  • shopping finished by the assistant lands in the same shopping history,
  • deleting a family also deletes its connections to assistants.

"By hand" sounds scary, but only a small subset of the protocol was needed: initialize, ping, tools/list and tools/call. Stateless, plain JSON responses, no sessions. Compatibility checked with the official client from MCP SDK 2.2.0 - locally and in production.

All of this lives in the project memory as decision D-026, together with the rejected alternatives.

Tools the assistant understands

The first version had 7 shopping list tools. I connected Claude and right away wanted to check two things that had been bugging me for ages: would it fix typos in product names, and would it put new products in the right place on the list, the way we walk through the store. And... nope! It couldn't. The agent built a great shopping list, but forgot about the product catalog.

"Why can't Claude edit the product list - adding, renaming, deleting, reordering should all be options available in MCP..." - I wrote. A quarter of an hour later 4 more product catalog tools arrived. 11 in total: 2 read-only, 9 that write. And there wasn't even a need to reconnect - a new conversation was enough.

Two design decisions that make a difference:

  • Products are referred to by name, not by ID (case-insensitive). The assistant says "milk", not id: 4711.
  • Reordering is relative: "after product X", "before Y", "to the top", "to the bottom". The assistant doesn't have to send the whole list of IDs in the new order.

When you design an API for a developer - you give them IDs. When you design for an agent - you give it the language it thinks in.

One evening, five PRs

TimePRWhatChanges
23:27#110MCP server with family key login+1863 / -25
23:37#111CIMD with its own User-Agent+29 / -2
23:48#112Editing the product catalog via MCP+309 / -16
00:09#113Help page + MCP in line with the notes decision+803 / -22
00:24#114Up-to-date steps for ChatGPT+12 / -12

Around 1460 lines of server code (OAuth, protocol, connect page), backend tests up from 71 to 111 after the first PR. From the first merge to the last - under an hour, crossing midnight ;-)

And the best part: I did the whole thing on my phone, through a /remote-control session in Claude Code. Sitting in the bathtub! The agent worked on my computer, and I read, decided and typed "deploy" between top-ups of hot water ;-)

More interesting than the table is that three of those five PRs were fixes after a collision with reality.

The test passed. Claude didn't

The agent tested the whole flow with the official SDK client. Green. Deployed.

I add the connector in Claude on my phone and... "Can't connect: this access request isn't valid or comes from an unknown app". I sent Claude a screenshot with one question: "What's going on?" (except I used different words :P).

The answer was in the Apache logs. The official SDK client registers via DCR. The real Claude connects via CIMD - it gives the address of its document on claude.ai, and my server fetches that document. And here's the surprise: claude.ai returns 403 to Python's default User-Agent (Python-urllib). With a custom User-Agent - 200.

The fix is one line (PR #111, +29/-2 including the test and logging). But finding it without logs would have been impossible, because the failed document fetch was silently swallowed. Now it goes to the log.

A lesson for anyone testing integrations: the test went down a different path than the one with the bug. The official test client is not the same as the real client in production. The agent has its tests. The human has a real phone.

The safety brake

During the first deployment the agent ran a database backup and then... couldn't go any further. The automatic permission classifier in Claude Code stopped the production deployment until I typed "deploy".

At first it annoyed me a bit. Today I think that's exactly how it should be - the agent can prepare everything, but the final decision to go to production should belong to the human, who first checks whatever they see fit.

The agent caught itself breaking a project decision

While writing the help page, the agent compared it with llms.txt (the description of Zakupomat for AI models) and noticed that llms.txt says "No notes on items", while its own MCP tools let you add notes to items. A contradiction with decision D-017 from July, when I deliberately removed notes from the app.

The agent flagged it with the decision number and fixed MCP, instead of quietly working around it. The assistant now puts details in the quantity field, and it sees old notes and doesn't delete them.

This is exactly why the project brain was created. Without a written-down decision the agent would have had nothing to compare against.

ChatGPT: the ground shifting underfoot

In Claude, after the fix, the connector was added right away. In ChatGPT - not so simple.

The docs checked at the start of the evening talked about "Apps". An hour later ChatGPT already had "Plugins", and the developer mode toggle had moved somewhere else. The instructions on the page had to be fixed the same evening (PR #114).

And then another surprise. In the ChatGPT mobile app and in the browser on the phone, the option to add your own server simply wasn't there. From the phone it looked as if ChatGPT wouldn't let us in. Only in the desktop app did it work - though, if I remember correctly, just entering the MCP server address wasn't enough and a whole plugin was needed. That's actually how I use Zakupomat in ChatGPT to this day.

But a regular user won't be installing plugins from a package on their computer. For Zakupomat to be in ChatGPT for everyone, it has to get into the directory. So the next day I registered ANSLAN as a developer with OpenAI.

What happened next with the directories - how Codex prepared the package, Claude repeated its mistake, and the model tried to pay for the shopping with my card - I'll tell you in the next episode ;-)

What this means for your team

AI assistants are a new way into your product - alongside the app, the website and the API. And this channel has its own traps:

  • Test with real clients, not just the official SDK. The test went down a different path than the one with the bug. And have logs that don't swallow errors.
  • OAuth security isn't just PKCE. An allowlist of redirect URIs and fetching metadata only from trusted hosts is the difference between an integration and a phishing tool.
  • Tools for agents speak the agents' language: names instead of IDs, relative operations instead of full state.
  • The simplest architecture is often the best. MCP built into the existing backend gave us family isolation, live changes and shopping history for free.

And above all - the agent will build this in one evening, but only if someone knows what to look for and has written-down decisions the agent can check itself against.

At ANSLAN we help prepare existing systems for working with AI agents - including on the side where AI agents are the users of your product.

And Zakupomat itself? You can just grab it and use it: zakupomat.app. Free forever, no ads, no selling data, no subscription. Android, iPhone, the browser - and Claude. How to connect your assistant: zakupomat.app/en/ai.html.

See you next week :-)