Connect an AI agent
This guide connects an MCP client (an AI assistant, an agent framework, the MCP Inspector, or any client that supports remote MCP servers with OAuth) to Bike Trip Planner, so that it can read and plan your trips. The tools, parameters and error codes are listed in the MCP server reference.
Before you start
You need a Bike Trip Planner account, a browser signed in to the web app, and an MCP client that supports:
- the Streamable HTTP transport (stdio is not offered);
- the OAuth 2.1 authorization code flow with PKCE
S256, as a public client (no secret); - Client ID Metadata Documents: the client identifies itself with the HTTPS URL of its metadata document. Dynamic client registration is not supported. A client that cannot do this needs a client registered by an operator (see Local development for the command);
- redirect URIs that are
https://...or a literal loopback address (http://127.0.0.1:<port>/...orhttp://[::1]:<port>/...).http://localhostis refused in a metadata document.
1. Add the server to your client
In your client, add a remote MCP server with this URL:
https://<host>/mcp
<host> is the host the deployment is served from, the host of DEFAULT_URI (in production
www.<DOMAIN>, see Deployment). Use exactly that host: requests with another
Host header are refused. In local development the URL is https://localhost/mcp.
No token has to be pasted. On the first request the server answers 401 with a
WWW-Authenticate header pointing at
https://<host>/.well-known/oauth-protected-resource/mcp, from which a compliant client finds
the authorization server and starts the flow below by itself.
2. Authorize the client
- The client opens your browser on
https://<host>/oauth/authorize. - If the browser is not signed in to Bike Trip Planner, you are sent to the login page. The authorization request is not kept: sign in (magic link), then start the connection again from the client.
- The consent page Authorize this application shows:
- the name the application gives itself (text it chose, so check the next line too);
- what it asks to do, one line per scope;
- the host it will be sent back to;
- a warning when the application only returns to your own machine (loopback address): authorize it only if you just started it yourself.
- Click Authorize or Refuse. The browser returns to the client, which finishes the exchange. The consent page is valid for 10 minutes and can be answered once.
The permissions an application can ask for:
| Scope | Shown as | Allows |
|---|---|---|
trips:read |
read your trips and their stages | list_trips, get_trip, get_stage, search_places |
trips:write |
create and edit your trips | create, change, analyze, share, unshare and delete trips |
The consent is all or nothing for what the client asks. Both scopes cover every trip of
your account; there is no per-trip consent. An agent that edits trips needs both scopes,
because edits require the trip version returned by get_trip. A client given only
trips:read does not even see the write tools.
After consent, the client holds an access token valid 15 minutes and a refresh token valid one month, renewed as it is used. You are not asked again while the refresh token lives.
3. Confirm destructive actions
Three tools never act on the first call:
| Tool | What is at stake |
|---|---|
delete_trip |
The trip, its days and its public link, permanently |
unshare_trip |
The public link; anyone holding it loses access |
update_trip_settings |
Usually recuts every day, discarding manual day changes and chosen accommodations |
Called without confirmationToken, such a tool changes nothing and answers with what would be
affected (trip title, number of days, dates, whether a public link is active) and a token. A
well-behaved agent reports that to you and waits. If you agree, it calls the same tool again
with the token and the same arguments. The token works once, for 5 minutes, for those exact
arguments only.
This is not a lock against the agent: the same agent receives and spends the token. It makes
the impact visible in the conversation before anything happens. Other writes are not
confirmed; in particular share_trip publishes a link immediately, and edit_stages changes
days directly.
4. Review or revoke access
Every application you authorized is listed under Authorized applications:
- Web: click your profile circle in the top bar to open the account settings
(
/account/settings), then the Authorized applications section. - Mobile: open the Account tab, then Authorized applications.
Each entry shows the application name, its host (the part of its identity it cannot choose: trust this line more than the name), when it was authorized and when it last called a tool (or "never used"). An entry disappears by itself once the application no longer holds a valid token.
To cut an application off, click Revoke and confirm. Its tokens stop working at once, for your account only. To use it again, authorize it again from the application.
Access is also revoked when you change your email address or delete your account. Signing out of the web or mobile app does not revoke it.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 with "error": "invalid_token" |
No token, token expired or revoked, token not issued for https://<host>/mcp, or an app session token used instead of an OAuth token |
Let the client re-authorize. Check the server URL uses the deployment host |
403 with "error": "insufficient_scope", or a JSON-RPC error insufficient_scope: ... whose data.scope names a scope |
The token lacks the scope the tool needs | Re-authorize, asking for both trips:read and trips:write |
| Write tools missing from the tool list | The token only carries trips:read |
Same as above |
400 invalid_client on /oauth/authorize |
The client's metadata document could not be fetched or was rejected (not HTTPS, redirect, private address, over 64 KiB, client_id not equal to its URL, missing client_name, bad redirect_uris) |
Fix the client's metadata document; the exact reason is only in the server log |
400 invalid_scope |
The authorization request named no scope | Configure the client to request trips:read trips:write |
400 invalid_target |
The client sent a resource other than https://<host>/mcp |
Use the exact server URL |
400 server_error, "This account cannot be granted to an application." |
Your email address is longer than 128 characters | Use an account with a shorter address |
| After signing in you land on the home page, not the consent page | The authorization request was dropped at login | Start the connection again from the client while signed in |
| Consent page says "This request is no longer valid" | Expired (10 minutes), already answered, or started by another account | Start the connection again from the client |
JSON-RPC error rate_limited: ... Retry in N seconds. |
More than 60 tool calls, or 20 write calls, per minute for this user and this client | Wait data.retryAfter seconds |
429 with Retry-After |
More than 300 requests per minute from one IP to /mcp, or too many OAuth requests |
Wait and retry |
Trip <id> has moved on: ... |
The trip changed since the version the agent sent |
Read it again with get_trip and reapply |
This trip is locked: ... |
The trip's start date is today or past; its days can no longer be edited | Nothing to fix; deleting and sharing still work |
This "confirmationToken" is not valid for this call: ... |
Token expired, already used, or arguments changed | Call the tool again without a token |
Local development
The development stack serves the MCP server at https://localhost/mcp behind Caddy's locally
signed certificate, so the client must trust that certificate.
- Start the stack with
make start-dev(see Getting started). On boot, thephpcontainer generates the authorization server's signing keys inapi/config/oauth/if they are missing (unencrypted, git-ignored). They must differ from the session JWT keys inapi/config/jwt/. For the test suite,make keypairs-testregenerates both pairs withscripts/generate-keypairs.sh;make startdoes the same for the iso-prod stack in.docker/oauth-recette/. - Sign in to
https://localhostwith a magic link read in Mailcatcher (http://localhost:1080). -
A client whose metadata document is not publicly reachable cannot use CIMD here (the server refuses private network addresses and its own host). Register a public client instead, with the exact callback URL your client uses, from
make php-shell:bin/console league:oauth2-server:create-client \ --public \ --redirect-uri=http://127.0.0.1:6274/oauth/callback \ --grant-type=authorization_code --grant-type=refresh_token \ --scope=trips:read --scope=trips:write \ "MCP Inspector (dev)" mcp-inspector-devThen configure the client with the client ID
mcp-inspector-dev. The callback URL above is an example; register the one your client actually sends. -
bin/console debug:mcplists the configured server and its tools.