← back

turning yorck cinema into a public mcp server

one of my favorite tiny agent patterns is taking something i already do manually and turning it into a tool that an agent can call.

this week that was yorck, the berlin cinema chain. i wanted to text my whatsapp assistant something like:

what is playing tomorrow after 6pm, original language if possible, and show me the seats

and then, if i confirm:

book that one, choose a good seat

that second sentence is the scary part. it should be private, authenticated, and impossible for random people on the internet to call. but the first part is useful for anyone, so i split the server into two surfaces:

  • public read-only mcp: showtimes, cinemas, coming soon, seat maps, calendar files
  • private authenticated mcp: everything above, plus yorck unlimited validation and actual booking

public endpoint:

https://yorck-mcp.isiklimahir.workers.dev/public/mcp

private endpoint, only for my agent:

https://yorck-mcp.isiklimahir.workers.dev/mcp

what the public mcp can do

The public server exposes only read-only tools:

  • whats_on, search showtimes by date, time, cinema, format, genre, and title
  • find_film, search film titles and get slugs
  • showtimes, list sessions for a known film
  • coming_soon, list upcoming films
  • cinemas, list yorck cinema slugs and addresses
  • seat_map, render a session's seat plan as SVG and return available row and seat IDs
  • add_to_calendar, generate an .ics file and a normal downloadable calendar URL for a session

No account login. No membership card. No booking.

A model can call whats_on, pick a session, call seat_map, and then show a visual seat map. If someone wants booking, they can wire their own private booking tool and credentials.

how the worker is structured

The worker runs on Cloudflare Workers and uses the Cloudflare Agents MCP helper.

The important trick is that there are two durable object backed MCP agents:

export class PublicYorckMcp extends McpAgent<Env> {
  server = new McpServer({ name: "yorck-public", version: "0.1.0" });

  async init() {
    registerPublicTools(this.server, this.env);
  }
}

export class YorckMcp extends McpAgent<Env> {
  server = new McpServer({ name: "yorck-private", version: "0.1.0" });

  async init() {
    registerPublicTools(this.server, this.env);
    registerPrivateBookingTools(this.server, this.env);
  }
}

Then the request router makes /public/mcp open and keeps /mcp behind a bearer token:

if (url.pathname === "/public/mcp") {
  return publicMcpStreamable.fetch(req, env, ctx);
}

if (url.pathname === "/mcp") {
  if (!isAuthorized(req, env)) return unauthorizedResponse();
  return mcpStreamable.fetch(req, env, ctx);
}

This means the public server literally does not have a book_session tool in its tool list. It is not just hidden in the prompt.

showtimes

Yorck's website has Next.js data files with film and session metadata. The worker caches those in KV for a few minutes and projects them into a cleaner shape:

{
  "film": "The Devil Wears Prada 2",
  "cinema": "Passage",
  "format": "OmU",
  "start": "2026-05-10T19:50:00+02:00",
  "sessionId": "1007-30456",
  "cinemaSlug": "passage"
}

That sessionId is the bridge into seat maps.

seat maps as svg

The worker fetches the Vista seat plan for a session and renders it into SVG.

Available seats are green. Sold seats are dark. Sofa pairs get their own color. The SVG also embeds data-seat-id and data-row attributes so an agent can reason about the seat IDs instead of only looking at pixels.

The public seat_map response returns both:

  1. a structured row list with available IDs
  2. an inline SVG image

That makes it useful for both humans and agents.

private booking

My private agent has a separate tool called book_session.

The safe default is dryRun: true. In dry-run mode it:

  1. creates a temporary order
  2. selects the chosen seat
  3. validates my Yorck Unlimited membership
  4. verifies the total is 0
  5. cancels the hold immediately

Only if I explicitly confirm does it call the same tool with dryRun: false, which commits the booking.

The auth split matters. The private worker endpoint requires:

Authorization: Bearer <YORCK_MCP_AUTH_TOKEN>

and the secrets live outside git:

YORCK_EMAIL
YORCK_PASSWORD
YORCK_UNLIMITED_CARD
YORCK_MCP_AUTH_TOKEN

using the public mcp

If your client supports remote MCP servers, point it at:

https://yorck-mcp.isiklimahir.workers.dev/public/mcp

The calendar tool returns both raw ICS text and a URL like this:

https://yorck-mcp.isiklimahir.workers.dev/v1/calendar/1001-84834.ics?slug=the-devil-wears-prada-2

That URL downloads a normal .ics file, so even if the MCP client cannot write files itself, it can hand the user a link that opens in Apple Calendar, Google Calendar, Outlook, or whatever calendar app they use.

For a local client that uses mcp-remote, the config looks like this:

{
  "mcpServers": {
    "yorck": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://yorck-mcp.isiklimahir.workers.dev/public/mcp"
      ]
    }
  }
}

Example prompt:

Find original-language Yorck showtimes tomorrow after 18:00, pick three good options, and show me the seat map for the best one.

why i like this pattern

The nice thing is not really the cinema booking. It is the permission boundary.

A lot of personal automations have two layers:

  • public knowledge and navigation
  • private actions that spend money, reveal identity, or mutate state

MCP makes that split feel natural. The public server can be linked in a blog post and used by other people. The private server can sit behind my WhatsApp assistant and do the personal part only when I explicitly ask.

That is probably the shape I want for more of my tools: public read-only by default, private action tools only behind auth and confirmation.