← all notes

Cozi MCP logo MCP · open source · github.com/mjucius/cozi_mcp

Cozi MCP

Take a picture. Get it on the family calendar.

The trigger was a paper printout of doctor appointments stuck to the fridge. Adding each one to our shared Cozi calendar meant a stack of taps through a UI my family had quietly given up on, so we had quietly stopped adding things we should have. I asked the obvious question: why can't I photograph this and have an AI do the extraction. Cozi has no public API, so the answer was an MCP server.

Somebody else did the hardest part

I did not reverse engineer the Cozi API. Somebody already had, and the piece that matters is small and completely undiscoverable on your own: the auth endpoint requires an apikey query parameter lifted from the live web bundle. Without it every login from a server fails with a 401 that looks exactly like a wrong password, so you would spend a long time debugging your credentials instead of your request. That discovery came from Wetzel402/py-cozi PR #3, and it is credited in the README because it deserves to be.

The rest is a REST wrapper. Cozi has not changed anything under me since September 2025, though nothing obliges them not to.

Python first, then TypeScript

The first cut was a Python client, py-cozi-client, which still ships on its own. Converting to TypeScript was not a rewrite in any painful sense. The client is a thin wrapper over REST calls, which is exactly the kind of code an AI ports quickly and correctly, and TypeScript is the better fit for MCP: deployment becomes one command instead of asking people to manage a virtualenv. Future maintainability drove it more than anything clever.

I pointed a vulnerability scanner at my own server

This is the part worth writing down. An MCP server for a family calendar holds real credentials and reads private schedules, potentially including children's. I am not a security expert. That combination felt like it carried an obligation, so when I came across a vulnerability scanner I ran it against my own code.

It found eight issues: three High, four Medium, one Low. Every one was reproduced with an executed exploit test rather than asserted, nineteen tests in total, all passing against the real code.

Two path traversals let a crafted list_id or item_id escape into the Cozi API path, which in the second case meant an arbitrary authenticated PUT. The credential cache never expired or revalidated. update_appointment silently destroyed recurrence and end-date fields it was not asked to touch. Login-response tokens leaked into an error message. And the login path was an unmetered credential oracle: something could sit there testing Cozi passwords through my server as fast as it liked.

None of those are exotic. They are the ordinary mistakes you make when you are focused on whether the feature works. That is the actual argument for scanning your own code: not that you wrote something reckless, but that "it works" and "it is safe" are different questions and only one of them is answered by using the app.

All of them are fixed, each with a regression test named after the finding it closes.

The prompt-injection one is different

One finding does not have a tidy fix, because it is structural to what MCP servers do. Everything this server returns is content somebody in the household typed: item text, appointment notes, family member names. All of it flows straight into a model's context, and any of it could say "ignore your previous instructions."

The mitigation is a fence. Every response is wrapped in a <cozi_data> marker whose boundary token is a fresh random UUID per response, and the server instructions tell the model to treat everything up to the matching boundary as data and never as instructions. The randomness is the point. Household content cannot forge a closing marker it cannot predict, so it cannot break out of the fence and start issuing commands.

That reduces the risk rather than eliminating it. A model can still be persuaded by data it correctly identifies as data. But it removes the easy version of the attack, and being explicit about which content is untrusted is better than pretending the question does not arise.

Cozi says 200 when it means no

An undocumented API has undocumented manners. Cozi answers HTTP 200 to some calendar writes that did not actually happen. A client that trusts the status code reports success and moves on, and you find out days later when the appointment is not there. Writes are now verified by reading back rather than believing the response.

Shipping to four places at once

The server goes out as an installable bundle for Claude Desktop, an npm package, a hosted listing on Smithery, and source. That created a problem I did not anticipate: the version number lives in three files that ship independently, and when they drift a running server misreports which build it is. "Did my update take effect" becomes unanswerable from the outside. There is now a test that fails CI if the three disagree.

Publishing to npm moved to Trusted Publishing over OIDC rather than a long-lived token, which is one fewer secret to leak.

The hosted case also needed an honest answer about trust. Over stdio, the local caller is the user and that is fine. On a hosted endpoint the caller is remote, and the credentials you supply are the only thing standing between a session and the account. Rather than invent an access-control layer, the config schema now says exactly that, in the field descriptions, where somebody entering a password will actually read it.

A stranger sent a pull request

I was not paying much attention to this project when a PR appeared from someone I had never met, adding a read-only mode that hides every write tool. I read it closely, had a few agents review it independently, and merged it. It was a good feature, implemented well, and it is a better idea than anything I had planned for the project that month. Three stars and one outside contributor is not a movement, but it is more than I expected from something I built for a fridge printout.

Does anyone use it?

Probably not, other than me, and I am genuinely fine with that. It solved my problem.

What I did not expect is how much better the workflow is than the feature it replaced. Handing a photo, or an email, or a forwarded schedule to Claude and letting it write the entries works considerably better than the smart-add built into the app itself. The interesting part was never the API wrapper. It was that a general model plus a small, boring, well-tested tool beats a purpose-built feature inside the product.

Sourcegithub.com/mjucius/cozi_mcp Installinstall instructions