We Taught TrackLab to Speak MCP, and OAuth Immediately Tried to Fight Us

There is a very specific kind of optimism that appears right before you wire up OAuth 2.1 for a new MCP server.
You add the package. You register the routes. You point your client at /mcp. You whisper, “surely standards-based auth will be straightforward now.”
And then OAuth politely throws a chair through the window.
That was roughly our experience adding a production-facing MCP server to TrackLab. Two phases later, it exposes 13 read-only tools across navigation, data, and weather — built on Laravel MCP and Passport, with OAuth 2.1 + PKCE, prompts, resources, and 68 tests around the sharp edges.
What we actually shipped
The implementation is intentionally small and boring — in the best way:
- Laravel MCP provides the server surface and OAuth discovery routes.
- Laravel Passport provides the OAuth 2.1 flow and
mcp:usescope. - A dedicated root-level route file keeps
/.well-known/*,/oauth/*, and/mcpout of the app’s existingapi/v1prefixing. - Thin MCP tools wrap existing TrackLab services — including solar tracking and monitoring — instead of recreating business logic.
- Prompts and resources help the model discover the shape of the API without guessing.
That explicit surface matters. For a first version, “a small catalog of obvious tools” is better than pretending every domain needs infinite agent flexibility on day one.
Phase 2: Entity navigation
Phase 1 gave AI clients data tools — measurements, parameters, weather. But using them required knowing UUIDs upfront. Phase 2 adds six navigation tools that let AI clients drill down naturally, the same way a human browses the web UI:
- User Context — “Who am I and what can I see?” Returns accessible companies, default company, no parameters needed.
- Farms List — Browse all farms with collector and section counts. Automatically excludes placeholder farms.
- Farm Sections — Drill into sections within a farm, with tracker counts and display order.
- Collectors List — Find collectors with flexible filtering by farm, section, or type. Capped at 200 results per request.
- Collector Detail — Everything about a specific device: GPS location, hardware version, lifecycle dates, tracker grid position.
- Collector Alerts — Fleet health monitoring. Returns collectors not in healthy states, grouped by status type.
The key innovation: AI clients no longer need UUIDs upfront. A conversation flows naturally from “show me my farms” to “what’s the temperature on NCU-007 over the last week?”
The part where OAuth became a dungeon boss
The implementation landed in one commit, then immediately needed a string of fixes.
Problem 1: Root-level routes vs. prefixed app
MCP routes don’t fit neatly into a prefixed Laravel app. TrackLab prefixes both API and web routes under api/v1, which is fine until a spec expects /.well-known/* and /mcp at canonical root paths.
Fix: Give AI routes their own route file outside the normal prefix.
Problem 2: CSRF vs. JSON-RPC
The MCP endpoint uses JSON-RPC over POST with bearer auth — not a browser form. Without explicit exceptions, POST requests to /mcp produced 419 Page Expired.
Fix: Added MCP and OAuth endpoints to the CSRF exception list. This is one of those bugs that feels insulting because the endpoint is “correct,” but the middleware stack still thinks it is protecting a browser form.
Problem 3: Wrong login route
This was the nastiest one. Passport’s authorization flow throws an AuthenticationException when the user isn’t logged in. TrackLab’s default login route belongs to Fortify and is POST-only. So the browser got redirected into the wrong place and hit a method mismatch instead of a login screen.
Fix: Custom exception handling to redirect OAuth requests to a dedicated MCP login form. The fix was not in MCP at all — it was in exception handling.
Problem 4: Cookie config chaos
After fixing routes and redirects, the OAuth login form still hit 419 Page Expired in one environment. The cause: SESSION_DOMAIN contained a full URL, but cookies need a bare hostname.
Fix: Normalize the domain by parsing the host. A classic “OAuth is working, except for all the browser state underneath OAuth.”
That is the pattern we want to remember: when you embed MCP into an existing product, the hard part is rarely “can the package do OAuth?” The hard part is “what does OAuth do when it collides with the product’s existing auth assumptions?”
The measurements bug that Phase 2 caught
Phase 2 also fixed a subtle bug in tracklab_measurements_query. The tool was returning “Required fields: types and from” even when the AI client sent both parameters correctly. The root cause: Laravel MCP v0.5.9 uses the DI container to resolve handle() method arguments, which meant array parameters (types[]) from the JSON-RPC request were silently dropped. Fixed by switching to the Request object pattern for parameter extraction.
Patterns that made this work
1. Thin wrappers over existing domain services
MCP tools are adapters, not rewrites. They wrap existing services and focus on input shape, output shape, permissions, and guidance text. That keeps MCP-specific code focused and domain logic where it belongs.
2. Company context is resolved once, on purpose
MCP tools are only useful if they don’t accidentally guess the wrong tenant. The resolution follows a clear order: explicit parameter, user default, user’s only company, or fail. This is exactly the kind of logic you want centralized instead of copy-pasted across tools.
3. Prompts and resources do real work
The tools aren’t carrying the whole UX burden. The server exposes prompts and reference resources so the model can discover valid measurement types, parameter shapes, weather concepts, and permission behavior before it starts improvising. A useful middle ground between “one magical giant tool” and “hundreds of undocumented tools.”
4. Test the protocol edges, not just the happy path
The most valuable tests check:
- Discovery endpoints exist
- Unauthenticated calls return 401
- Server registration matches the declared surface
- Company resolution behaves under ambiguity
- Browser login flow renders and redirects correctly
68 tests, 206 assertions across both phases.
Tooling we used
A lot of this was less about inventing new infrastructure and more about choosing the right tools, then respecting their constraints:
- Laravel MCP for server definitions, prompts, resources, transports, and inspector support.
- Laravel Passport for OAuth 2.1 and the browser-based authorization code flow.
- Laravel’s route and middleware stack for keeping MCP at root-level paths while leaving the rest of the API alone.
- Spatie activity log for OAuth event auditing.
One honest caveat
There is still one slightly awkward implementation detail in the auth path: the test suite documents that Laravel MCP’s AddWwwAuthenticateHeader middleware is registered, but the actual AuthenticationException path is handled in TrackLab’s exception handler because the middleware post-processing is bypassed in that flow. Not a blocker, but a good reminder that framework middleware order and exception handling are part of your protocol implementation whether you planned for that or not.
The short version
- Keep MCP routes root-level
- Use Passport, not a custom token story
- Wrap existing services instead of rebuilding domain logic
- Make tenant resolution explicit
- Give the model prompts and resources so it can discover before it guesses
- Test the protocol edges because OAuth will absolutely find the one assumption you forgot
MCP was the easy part. OAuth was the goblin under the bridge. The only winning move was to make every assumption painfully explicit until the goblin got bored.
Want to connect?
Check out our full MCP server documentation for setup instructions across all major AI clients, or explore TrackLab’s features to see what the MCP server exposes.