How I Built Traxelio MCP: OpenAPI to MCP PROXY, OAuth DCR, and Cursor Connect
Draft Disclaimer: Please note that this article is currently in draft form and may undergo revisions before final publication. The content, including information, opinions, and recommendations, is subject to change and may not represent the final version. We appreciate your understanding and patience as we work to refine and improve the quality of this article. Your feedback is valuable in shaping the final release.
Language Mismatch Disclaimer: Please be aware that the language of this article may not match the language settings of your browser or device.
Do you want to read articles in English instead ?
I did not want a second API for AI assistants.
Traxelio already publishes a public OpenAPI document. REST clients hit the same Actions, FormRequests, policies, and OAuth scopes I spent years hardening. When Cursor and other MCP hosts asked for tools, the wrong move was obvious: hand-write a parallel tool catalogue that would drift from OpenAPI the week after launch.
So I built a PROXY. One MCP server at https://traxelio.com/mcp. One tool per public OpenAPI operationId (minus a small Auth login/token-mint denylist). Every call is an in-process HTTP hop through the documented named route, with the caller's own bearer token. OpenAPI stays the single capability map.
This is the builder cut. The Traxelio blog commercial cut (fleet in the IDE, start-now Discover) will live at /post/fleet-tools-in-cursor-via-traxelio-mcp-{id} once seeded. Same product, different lens.
The constraint that locked the design
Fleet tracking is high-consequence software. Immobilize, destroy, checkout, and account mutations already go through scoped routes. An MCP surface that reimplemented those rules in tool handlers would be a second security model with half the test coverage.
I looked at three shapes:
- Direct Action DI per tool. Inject the Action into each tool. Fast for a handful of ops. Painful at catalogue scale, and you still need a second place to declare scopes and validation.
- Curated hand tools. Ship a pretty subset. Guarantees drift: OpenAPI says one thing, MCP says another.
- PROXY. Tool = OpenAPI
operationId→ nested request to the documented route with the user bearer.
I locked PROXY after a spike proved nested Auth/scope rebind, single outer metering, and a production operationId→route resolver. The industry pattern of OpenAPI→MCP generators pointed the same way. Traxelio just had to do it inside a real Laravel app with Passport scopes, not as a standalone stub server.
What PROXY means in this codebase
OpenApiMcpToolFactory reads the OpenAPI document and mints a ProxiedOpenApiTool per admitted operationId. The tool holds no business logic. handle() maps tool arguments to path params + body, then ProxyOpenApiOperationAction does the real work:
- Resolve the operation to method + templated URI via the production documented-route resolver.
- Substitute path parameters.
- Build an internal
RequestwithAuthorization: Bearer …andmcp.proxiedset so outer/mcpthrottle/usage/CRM meters once (nested hops do not double-charge). - Rebind the Auth user and the Passport
AccessTokenon that nested request soEnsureTokenScopestill runs. app()->handle($request)through the named route + full middleware stack: FormRequest, policies, scopes, controllers, Actions.
Same code path a REST caller hits. MCP is a transport adapter, not a second product surface.
Tool names are a bijection of OpenAPI ids: dots become underscores (operate.device.show → operate_device_show). OpenAPI keeps the dotted ids. The map fails loudly if an operationId ever contains an underscore, so the round trip stays reversible.
The only denylist
Login and first-party token-mint operations never become tools. A delegated OAuth token exists so an assistant never handles the user's password or session mint path. Exposing auth.login.* through MCP would hand that credential path straight back. Everything else that is documented and enabled follows the PROXY rule.
I do not claim this is the "full private API." MCP claims the documented public OpenAPI contract minus that Auth denylist, not every unscoped internal route in the monorepo.
Why /mcp is never anonymous
Public catalogue reads (device categories, countries, insurance coverage, and similar Discover data) are available over plain HTTP OpenAPI without logging in.
Through /mcp, they are not. The transport always requires an authenticated OAuth caller. Same middleware stack as every other tool. There is no lighter anonymous MCP path.
That was a deliberate product call. An assistant session is a credentialed actor. Mixing anonymous browse with authenticated writes on one transport makes consent and revocation harder to reason about. If you only want to browse the public document, hit https://traxelio.com/openapi.json. If you want tools in Cursor, you log in.
OAuth: DCR first, curated consent, immediate revoke
Dynamic client registration
The normal connect path is RFC 7591 DCR. POST https://traxelio.com/oauth/register mints an OAuth client on the fly. A DCR-capable host (Cursor included) never needs a Traxelio-issued client ID pasted into settings.
Redirect URIs accepted at register time: https://…, loopback http:// (localhost / 127.0.0.1 / ::1) per RFC 8252, or a private-use scheme for a native client (for example Cursor's cursor://… callback). No per-client URI allowlist. Non-loopback http://, fragments, and unsafe schemes are rejected.
If a host cannot self-register, the fallback is a manually pre-registered OAuth client. Same authorize flow, same scopes. No second auth system.
Protected-resource metadata and first consent
RFC 9728 path-insertion metadata for the MCP resource lives at:
https://traxelio.com/.well-known/oauth-protected-resource/mcp
Authorization-server metadata:
https://traxelio.com/.well-known/oauth-authorization-server
scopes_supported on the protected-resource document is a curated, read-oriented starting grant, not the full ~45-scope catalogue and not an empty admit:
tracking.readuser.viewcart.update
Important split: /mcp itself is admit-only (route_scopes.mcp is empty). The transport does not gate on one mega-scope. Each PROXY tool re-enforces its own OpenAPI operation's scope on the nested hop. Reaching a write or high-consequence tool outside the starting grant surfaces an explicit insufficient-scope refusal (HTTP 403 + WWW-Authenticate: insufficient_scope on /mcp) so the client can re-consent instead of failing silently or widening silently.
Revocation
Disconnect from Connected Apps on https://traxelio.com/user/profile. The next /mcp call with that token fails authentication. Reconnect means the OAuth flow again.
Connecting Cursor (what I actually documented)
- Cursor → Settings → MCP → add a server pointing at
https://traxelio.com/mcp(POST only; do not treat it as a browser page). - Cursor discovers the protected-resource metadata, runs OAuth, and shows the requested scopes on the consent screen before you approve.
- After connect,
tools/listpages vianextCursor. Drain until the cursor is absent. A client that stops on page one silently misses the tail of the catalogue. - Each listed tool maps 1:1 to a documented OpenAPI operation (minus the Auth denylist).
Automated proof in the repo: CursorConnectSmokeTest runs the handshake + cursor-draining tools/list against the app and asserts bijection with OpenAPI parity tests. A short manual production walk (paste the URL, consent, list tools, call one read, revoke) remains a human checklist. I am not claiming every MCP host is verified here. This post covers Cursor connect as documented and smoke-tested.
What I would not do again the other way
- A second capability map. OpenAPI as the only declaration force-multiplies every Action you already ship. Hand tools were a temporary onboarding shim; they got folded back into OpenAPI and generated.
- Service-token merge on nested hops. Forward only the authenticating user's bearer. Elevating to a service identity inside PROXY would break the trust model the consent screen describes.
- All-scopes-at-first-consent. The curated PRM list is a starting hint. High-consequence tools should cost an explicit re-consent.
- Anonymous MCP "just for reads." Public reads stay on HTTP OpenAPI.
/mcpstays authenticated.
Practical takeaways if you are wiring OpenAPI→MCP in a real app
- Generate tools from
operationId, then dispatch through the real router. Do not reimplement controllers in tool classes. - Rebind Auth and the token object the scope middleware inspects. User-only rebind is how nested scope checks silently no-op.
- Meter the outer MCP transport once. Mark nested proxied requests so throttle/usage/CRM do not double-count.
- Keep a tiny denylist for credential-mint paths. Everything else should follow the document.
- Paginate
tools/listand teach your gates to drain cursors. Catalogue size will exceed a single page. - Publish OAuth protected-resource + authorization-server metadata, and prefer DCR so hosts do not need a pre-shared client ID.
Soft close
If you operate a fleet on Traxelio and already live in Cursor, paste https://traxelio.com/mcp, complete consent, and ask about devices, trips, or cart state under the scopes you granted.
If you are building a similar PROXY for your own OpenAPI surface, the boring answer is the right one: one document, one transport, real middleware, user bearer only.
For the customer-facing story (fleet tools in the IDE and Discover for prospects), see the Traxelio blog commercial cut once it is seeded at /post/fleet-tools-in-cursor-via-traxelio-mcp-{id}.