A driver in Brussels types "I need parking near the station tomorrow afternoon" into ChatGPT and gets back a handful of private parking spots, with prices and a photo for each. They tap one, land on the spot's page with the dates already filled in, and pay there. That's the whole ChatGPT channel we built for SparkSpot, a Belgian peer to peer parking marketplace.
Behind it sits a Laravel MCP server with exactly one tool. It searches. It can't book, pay, cancel or see who owns a spot, and that was the most important design decision in the project. If you're about to expose your Laravel business logic to AI agents, my advice is to start where we did: give the agent the part of your product that reads, and keep everything that moves money on a page where a person sees the price before they click.
What SparkSpot needed
SparkSpot connects people who have an idle driveway or garage with drivers who need a place to park. The marketplace already worked. Owners listed spots, drivers booked them on the website, and a mobile app was on the way.
The problem was the path to the first booking. A driver had to find the site or download an app, create an account and learn a new interface, all to park for a few hours. The brief was simple: let a driver ask ChatGPT, with nothing to install and no account to create first.
What MCP is, and what we already had
MCP, the Model Context Protocol, is the standard way for an AI assistant to call into your software. You describe a set of tools, each with a name, a plain language description and a schema for its inputs, and the assistant decides when to call them. ChatGPT apps run on it, and so do Claude's integrations.
On the Laravel side, the official package is laravel/mcp. We started on version 0.5.1 in January 2026 and the server runs on 0.5.9 today. Version 1.0 came out in September 2026 together with the new protocol revision from July, and that upgrade is still on our list.
The more useful discovery was what SparkSpot already had. The public search API behind the website did everything a driver's question needed: it validated dates and distances, queried Meilisearch for nearby spots and calculated availability for the requested period. Nothing about the search had to be invented for ChatGPT. The real question was how much of the rest of the API to put in front of the agent.
Three ways to expose the marketplace
We had three realistic options.
- Expose the whole booking flow as tools. Search, reserve, pay, cancel, all inside the chat. It makes the best demo. It also means account linking through OAuth before the agent can act for anyone, an agent that can commit a stranger's money, and a new class of bugs where the model books the wrong thing politely.
- Build a separate, slimmed down "AI API". A clean set of endpoints shaped for the model. It sounds tidy, but it's a second API to keep in sync with the first one, and the two drift apart the first time someone changes a validation rule on only one of them.
- Expose search only and hand off to the website. One tool that reads, and a link that takes the driver to the same checkout every web visitor uses.
Why we chose one tool that only reads
We took the third option. The sentence I keep coming back to: the agent can find the spot and bring you to it. Money and commitment stay on the page where a person sees the price and clicks Pay.
Here's what that looks like in the code.
The tool goes through the same door as the website. It doesn't call the database or reimplement anything. It builds an internal request to the same route the site uses and dispatches it, so it passes through the same controller, the same form request validation and the same Meilisearch query. The rules the website enforces apply to the agent automatically: the start date can't be in the past, the end date can't be before the start, the period is at most 70 days and the radius at most 100 km. When validation fails, the tool returns the API's error message to the agent, which then asks the driver to fix the input. The tool itself validates nothing, because a second copy of those rules is exactly the drift we wanted to avoid.
The tool returns 8 fields, picked by hand. The website's API response is richer than anything an agent should see. The tool keeps the id, name, address, distance, average price, number of free places, a photo and the URL, and drops everything else. The owner's name and contact details never leave the server, and neither do the arrival instructions. The gate code is shown to the driver only after a paid booking, and only on the website.
The tool says what it is. laravel/mcp lets you annotate a tool, and ours is marked as read only, not destructive and closed world, which means it doesn't reach out to anything outside our own system. Clients can use those hints, for example to decide whether a call needs the user's confirmation first.
A simplified version of the tool's definition looks like this:
#[IsReadOnly]
#[IsDestructive(false)]
#[IsOpenWorld(false)]
class SearchParkingSpots extends Tool
{
public function schema(JsonSchema $schema): array
{
return [
'from' => $schema->string()
->description('First day of parking, YYYY-MM-DD. For a single day, use the same date in from and to. Never set to = from + 1.')
->required(),
'to' => $schema->string()
->description('Last day of parking, inclusive, YYYY-MM-DD.')
->required(),
// location and radius fields omitted
];
}
}The endpoint is public on purpose. There's no login. We left the route out of the OAuth configuration, so ChatGPT never asks the driver to sign in. That's a choice you can only make because the tool reads public listings and nothing else. The same Laravel backend runs two internal MCP servers for the admin team, and those sit behind Sanctum tokens or Passport OAuth, with a separate scope for each server.
The handoff is just a link. A tap on a spot card opens the spot's page on the website with the dates already filled in. From there it's the same Stripe checkout every web visitor gets, guest booking included. The backend doesn't know or care that the driver came from ChatGPT.
What broke anyway: "tomorrow" means one day
The channel went live, and the bug that hurt was not about security or the protocol. It was about what a date means.
A driver writes "parking tomorrow" or "22 April". The model reasons the way hotel bookings work and sends from 22 April to 23 April, one night. SparkSpot counts inclusive days, so a single day of parking is from 22 April to 22 April. The agent was asking for two days.
That caused two problems. The availability check now covered two days, so a spot that was free only on the 22nd dropped out of the results, because the tool keeps only spots available for the whole period. Worse, the link on the card carried the agent's dates, so the website opened a two day booking at double the price.
We spotted it in Nightwatch, the monitoring tool that records every request to the MCP route. We shipped the fix on 25 February 2026, and the fix was text. The rule "for a single day, use the same date in both fields, never from plus one" now appears in three places: the tool description, the descriptions of the from and to fields in the schema, and the find-parking prompt. The widget shows a single date when from and to are equal, so the driver sees how the agent understood the request before anything else happens. Since February the tool also returns the dates it actually searched in its structured response.
What we did not do is reject the request on the server, and we couldn't have. A genuine two day booking from the 22nd to the 23rd looks exactly like the mistaken one. When the error is about meaning rather than format, no validation rule will catch it. The best defense is to show the person the agent's interpretation before they pay.
That changed how I think about the tool description. It isn't documentation. For an agent, it is the interface, and a vague sentence there is a bug in your API contract.
Where the hours actually went
Most of the hours didn't go into MCP at all. They went into ChatGPT's own rules for the card widget. One example: the widget's content security policy, the list of domains it's allowed to load from, assumed the photos were served from the app's own domain. In production they come from a DigitalOcean Spaces CDN, so ChatGPT blocked them and every card showed a broken image. The same fix pinned an icon library that had been loading from a floating version tag, which meant an outside release could break the widget without a single deploy on our side and without a trace in our logs. Those are ChatGPT problems, not protocol problems, and they're the part nobody puts in a tutorial.
What I'd do differently
Two things, both small, both annoying in hindsight.
- Call the service directly. Dispatching an internal HTTP request through the router kept the rules in one place, but it also made logs and request details look as if every call came from the server itself. Calling the search service from the tool would have given us the same rules with less confusion.
- Respect the driver's language. The link opens the Dutch version of the site, so a driver in Brussels who writes in French lands on a page in Dutch.
If you're planning your own Laravel MCP server
Start with the question we started with: which part of your product can an agent use without anyone getting hurt if it misunderstands? For most businesses that's search, lookup and status. Put those behind a Laravel MCP server, route them through the code your website already trusts, and leave payments and commitments on a page a human reads. You can add write tools later, once you've seen how agents actually talk to your system. We use the same rule on our own Strapi, where the agent drafts and a person publishes.
ChatGPT apps, MCP servers and Claude integrations on top of existing Laravel backends are part of what we build. If you have a product with an API and you're wondering what an agent should be allowed to touch, write to us and we'll start from your existing code, not from a blank page.
