Skip to main content
CakeDC Blog

That "other day" from our MCP article? It came from a bakery

A year ago, my colleague Jorge wrote about building an MCP server in CakePHP and connecting Claude Desktop to it. It was a local setup on purpose: stdio, your own machine, no login. He even listed what was missing for production (authentication, authorization, rate limiting, audit logs) and left it "for another day".

Well, I got that other day. And it came from my wife's bakery.

It's a small artisan bakery. Cakes, cookies, brownies, cheesecakes, and an online shop where people order for pickup or home delivery on a given day. The website is a CakePHP 5 application with an admin panel for everything: products, orders, delivery zones, closed days, the blog. I built it, which also makes me the person who gets asked where something is in that admin.

One question came up more than any other: "¿Qué hay que preparar mañana?" (what do we have to make tomorrow?). The answer is in the database. Getting it meant opening the admin, filtering the orders by date, opening them one by one and adding up cookies on a piece of paper. Nobody at the bakery is going to edit a JSON file or keep an API token somewhere safe, and I was not going to ask them to. They already have a username and a password for the admin. That's what they should use.

Version 2.1 of cakedc/cakephp-mcp is built for that. The plugin was rewritten for 2.0, still on the official PHP SDK but now on a tagged release of it instead of the development snapshot Jorge used. It serves MCP over HTTP from a normal Cake route, and remote clients like claude.ai or Claude Code can sign people in with OAuth 2.1. It also works with the sign-in you may already have, by the way: if your users sign in at Auth0, Keycloak or any other OpenID Connect provider that issues JWT access tokens, the plugin checks those tokens itself. But the bakery doesn't have any of that. It has a users table and a login page, so the sign-in comes from a second plugin, cakedc/cakephp-oauth-server, which turns your own application into the OAuth server.

This article is about that pair, and about what we exposed for the bakery and how people use it. The step-by-step guide, with the real code, comes on December 18 in the CakeDC Advent Calendar.

What the bakery's site exposes today

The bakery's MCP server has 33 tools. That sounds like a lot, so here they are by area:

Area Tools
Orders production_summary, search_orders, get_order, update_order_status
Products search_products, get_product, create_product, update_product
Pages, blog, FAQ, services search_contents, get_content, create_content, update_content, publish_content, unpublish_content, delete_content, list_terms, create_term, list_authors
Images search_media, get_media, upload_image, upload_image_from_url, update_media_alt
The rest of the admin list_resources, describe_resource, search_records, get_record, create_record, update_record, delete_record, run_action, transition_record
The house rules get_site_info

And this is the kind of thing people ask (they ask in Spanish; these are my translations):

  • "What do we have to make tomorrow?"
  • "Move order B-2026-000124 to shipped, tracking code ABC123."
  • "Which catering requests are still new? Move the June wedding one to in progress, assign it to me and note that I called them."
  • "Write a blog post about how to store a cheesecake at home, put it in the right category and leave it as a draft."
  • "Put 'Christmas orders are open' in the announcement bar from December 1 to 15."
  • "Set the shop hours: Monday to Saturday from 10:00 to 19:00."

The first one is the reason all this exists. Claude calls production_summary with the date and gets back something like this (trimmed and translated):

{
  "date": "2026-11-14",
  "statuses": ["paid", "preparing"],
  "orders": [
    {"ref": "B-2026-000123", "status": "preparing", "fulfilment": "pickup", "slot": "10:00-12:00", "pickup_location": "Shop"}
  ],
  "products": [
    {"name": "Chocolate Cookie", "variant": "Box of 6", "quantity": 4, "in_boxes": 0, "personalisations": []},
    {"name": "Carrot Cake", "variant": "Large", "quantity": 2, "in_boxes": 0,
     "personalisations": [{"ref": "B-2026-000123", "quantity": 1, "values": [{"label": "Message", "value": "Happy birthday"}]}]}
  ],
  "boxes": [],
  "addons": [{"name": "Birthday Candle", "quantity": 3}]
}

and turns it into a list the kitchen can read. A couple of hours later: "la B-2026-000123 ya está lista" (B-2026-000123 is done), and Claude moves that order to ready. Nobody opened the admin.

Five decisions that mattered more than the code

The plugin part was the easy part. What took real thought was deciding what the tools should do, and especially what they should not do.

1. Tools that answer the bakery's questions, not tools per table

It's tempting to expose one tool per table and let the model figure out the rest. It works, more or less. It's also slow, it burns tokens, and when Claude adds up thirty orders by itself, sooner or later it adds them up wrong. production_summary does the sum on the server and returns exactly what the kitchen needs: products per variant, the personalizations with the order they belong to, and the gift boxes and add-ons listed separately, all in one call. If a question gets asked every day, it deserves its own tool.

2. Claude doesn't need the customer's contact details

In every order tool the customer is a name and a town. Never the email, the phone or the address. The model doesn't need a phone number to tell you how many cakes to bake, and what it never receives, it can't repeat somewhere it shouldn't. The same goes for the contact details of a catering request. When someone needs to call a customer, every order comes with its link to the admin, and the admin shows the rest to the people allowed to see it.

3. Anything that touches money or reaches a customer needs a person

Canceling and refunding are not tools. create_product never publishes: a new product is a draft until someone says "publish it". And moving an order to ready sends nothing to the customer. The server's instructions say so in plain words, so Claude doesn't tell you "I've let the customer know" when it hasn't.

4. The server explains its own rules

Tool descriptions are good for explaining how a tool works. They are a bad place for rules that apply to all of them. So the server sends short instructions when a client connects, and get_site_info returns who you are, which scopes your connection has, the content types and a writing guide: Spanish from Spain, address the reader as tú, never invent a price or a delivery promise, link to the shop instead of copying prices into a post. And the one I care about most: never say a product is free of an allergen unless a person said so for that product. The instructions tell Claude to call get_site_info before it writes anything.

5. The admin's rules apply to Claude too

Besides orders and products, 18 of the admin's lists are open to Claude: delivery zones, time slots, closed days, coupons, gift boxes, workshops, the announcement bar... Five tools for each would be 90 tools. Instead, nine generic tools go through the admin's own definitions. describe_resource tells Claude the fields, the choices and the actions of a list, and every write runs the same validation, hooks and role rules as the admin forms. An editor can't touch the coupons from Claude because an editor can't touch them in the admin.

What those tools can reach is denied by default: a list is open to MCP only if the application says so, and for each one it can hide fields (the email and phone of a catering request, the custom code of the announcement bar) or keep the whole list read-only. The reviews are read-only, for example: a write could change a customer's words.

Not only bakeries

The bakery is the one with the most tools, but it isn't the only one. Two other sites run the same pair of plugins, and each one made different choices. A third one uses the MCP plugin with an OAuth server it already had.

A content site uses the same building blocks as the bakery minus the shop, so the tools are the content, media and admin ones. Two details stand out there. Every change Claude makes lands in the admin's own change history under the person's name, with "MCP (Claude)" as the origin, so nobody has to guess later who changed a post. And only admins and editors can connect at all: canAuthorize decides who may approve a connection, whatever other users the admin has.

Another site we work on is a different kind of risk. The pages are the business, and a broken page is visible to everyone at once. So Claude never edits a live page. It creates a draft, edits the draft, and gives the person who asked a preview link. Publishing only works after someone has opened the latest preview, every publish keeps a backup that can be brought back, and menu changes, which go live at once, can be undone with their own tool. One scope and admins only: when there's one kind of person and one kind of job, more scopes only make the consent page longer.

The third site is the odd one out. It already had its own OAuth and OpenID Connect server long before anyone talked about MCP, and nobody wanted a second login or a second place to manage access. So there the MCP plugin runs in external mode against that server: a few settings say which issuer and audience to expect and which claim holds the scopes, and a small invokable class, set as Mcp.auth.identity, turns each token into the site's own user, so the permissions it already had keep working. "External" doesn't have to mean another company. It means the MCP plugin doesn't issue the tokens, it only checks them.

And since MCP is a standard, nothing in any of this is specific to Claude: any MCP client can use the same tools and the same sign-in.

How it fits together

If you want to try it before December 18, this is the short version. cakedc/cakephp-mcp is the MCP server. Your tools are plain PHP methods with the SDK's #[McpTool] attribute, and the plugin serves them at /mcp. It doesn't care who issues the tokens, and Mcp.auth.mode says who does:

  • host: tokens your own authentication already understands. Fine for scripts and for the team.
  • external: an identity provider you already have (Auth0, Keycloak, Entra ID, or one of your own, like that third site). The plugin only checks the tokens.
  • oauth: your application becomes the OAuth server, with cakedc/cakephp-oauth-server. Your login page, a consent page, and a page where people see and revoke their connections. That's the bakery.

cakedc/cakephp-mcp 2.1 and cakedc/cakephp-oauth-server 1.0 are out, both on Packagist:

composer require cakedc/cakephp-mcp:^2.1 cakedc/cakephp-oauth-server:^1.0

Then bin/cake mcp check (and, with the OAuth server, bin/cake oauth check) tells you, line by line, what's still missing before a client can connect. The cakephp-mcp and cakephp-oauth-server docs on GitHub cover every option.

That leaves two items from Jorge's list. Audit logs: 2.1 can record every tool call, allowed or refused, with who made it, from which client, the outcome and how long it took. It writes one log line per call, and dispatches an Mcp.audit event you can listen to if you'd rather keep the records in a table. Rate limiting: the OAuth server throttles its own endpoints out of the box, and CakePHP's own RateLimitMiddleware (CakePHP 5.3 and newer), which Jorge also wrote about, can limit the MCP endpoint per access token.

Back to Jorge's article

Jorge's article ended with Claude writing posts into a CMS running on your laptop. This one ends with someone at the bakery asking Claude what to make for Saturday and getting the answer from orders they were already allowed to see, through the login they already had.

That's really the whole idea. Putting an MCP server in front of real people took very little protocol work. Most of the time went into deciding what the tools should do and what they should never show, and into reusing permissions that took years to get right. The plugins take care of the OAuth part, with our own server or with the one you already have. The rest, as usual, is knowing your users. Or, in my case, being married to one.

On December 18, in the CakeDC Advent Calendar, comes the second part: the same bakery, step by step and with the real code. The tools, the scopes, the OAuth server configuration, the tests, the audit log, and the things that bite in a normal CakePHP application (a login page where a 401 should be, a cart created on every call, a setting that is silently ignored).

If you don't want to miss it, sign up on the CakeDC Advent Calendar page and you'll get an email with each day's article, that one included.

Need help with this in your own CakePHP app?

Talk to a CakePHP expert

Get the CakeDC newsletter

One email a month: releases, security advisories and one article worth reading.

Back to all articles
We Bake with CakePHP