TravelLog's official MCP server lets an AI assistant the user connects read their trips, notes, expenses, travel history and saved places and, if the user allows it, create trips, file suggestions, add the user's own bookings and want-to-go places, and edit or remove itinerary entries. Setup for end users is on Connect TravelLog to your AI assistant. This page is the technical reference for each tool.
https://travellogapp.web.app/mcp
Streamable HTTP (single JSON responses, stateless) · MCP 2026-07-28, 2025-11-25 and
2025-06-18 · OAuth 2.1 with PKCE (S256) · protected-resource metadata at
/.well-known/oauth-protected-resource/mcp · dynamic client registration, client ID metadata
documents and pre-registered clients · or a personal access key (tlpat_…) for clients that only take a
static bearer token
Tools: list_trips get_itinerary list_suggestions create_trip add_suggestions add_booking update_entry remove_entry get_wishlist add_to_wishlist get_travel_history get_trip_notes get_trip_expenses add_entry add_expense add_day_note add_been_place import_trips accept_suggestion dismiss_suggestion · request_account_deletion · Without the app · Let an assistant set the user up · Access keys (Grok, custom bots)
https://travellogapp.web.app/mcp?signup=1
instead (below)./mcp without a token returns 401 with a WWW-Authenticate challenge naming
the resource metadata. The client then runs OAuth; the user signs in at
travellogapp.web.app/connect, sees the assistant's name and return site, and approves.
They can pick Read only.travellogapp.web.app/account.| Scope | Consent screen wording | Tools |
|---|---|---|
trips:read | See your trips, itineraries, notes, expenses, travel history and saved places | list_trips, get_itinerary, list_suggestions, get_wishlist, get_travel_history, get_trip_notes, get_trip_expenses, request_account_deletion |
trips:write | Create and import trips, add the entries, expenses, notes and places you ask for, and edit or remove entries in trips you can edit | create_trip, update_entry, remove_entry, add_to_wishlist, add_entry, add_expense, add_day_note, add_been_place, import_trips, accept_suggestion |
suggestions:write | Add suggestions to your trips, and dismiss them | add_suggestions, dismiss_suggestion |
bookings:write | Add your own bookings to your trips | add_booking |
tools/list shows only the tools the token's scopes allow. Calling a tool outside them returns HTTP
403 with error="insufficient_scope" and the scope needed. A Read only link (or a read-only
access key) sees the seven read tools plus request_account_deletion (which only emails the user a link).
destructiveHint: true). The tool descriptions tell the assistant to state the change and get the
user's explicit OK first. We ask hosts that support it to confirm every use of remove_entry.Every tool sets all four MCP annotations explicitly: readOnlyHint,
destructiveHint, idempotentHint: true (backed by clientRequestId) and
openWorldHint: false (TravelLog's own data only). None involves payments or sending.
v: 1. Response shapes only gain fields.YYYY-MM-DD, times HH:MM (24-hour) read in an IANA
time zone (tz, e.g. Asia/Tokyo). Reads also give startUtc / endUtc.clientRequestId (8–64 letters, digits, _ or
-). Reuse the same value when retrying: a replay returns status: "duplicate" and writes
nothing. Use a new value for each new change.status: "applied" is returned only after the change is committed.get_trip_expenses, only
on a trip the user owns or can edit.trips:readcursor (optional): nextCursor from the previous pagelimit (optional): 1–50, default 20trips[]: tripId, name, destinations[], startDate, endDate, days, role
(owner, editor or viewer), entryCount; total; nextCursor when there is more.invalid (a cursor this server didn't issue), plus the common errors.trips:readtripId (required)fromDay, toDay (optional, 1-based)trip; days[] of {day, date, entries[], notes[]}; unscheduled[]
(full range only); suggestionBatches[] (batchId, label, from, count). Each entry:
id, title, category, date, tz, start, end, endDate, endTz, startUtc, endUtc, place {name, address, lat, lng},
from/to {name, code} (transit), flightNumber, notes, group, status (when not PLANNED).
Up to 400 entries; truncated: true beyond that (narrow the day range).not_found (no such trip, or not shared with this user), invalid
(fromDay after toDay).trips:readtripId (required)batches[]: batchId, label, from, count, createdAt, items[]
(id, title, category, day, start, end, tz, place, why, groupId, groupName).not_found.trips:writename (1–120 chars), destinations[] (1–10), startDate,
endDate (at most a year apart), clientRequestId: requirednotes (optional, ≤ 2000)status, tripId, trip.applied · duplicate (replay: the same trip, nothing new).invalid (bad dates, end before start, too long).suggestions:writetripId, clientRequestId, items[] (1–40): requiredtitle and day (required); category, start, end, tz (tz required with a
time), place {name, address, lat, lng, types[]}, why (≤ 200), groupId,
groupNamestatus, batchId, label, count, perDay[] {day, count}, skipped[] {index, title, reason}.skipped with reason health_place.applied · duplicate · skipped (every item was filtered; nothing written).forbidden (viewer), not_found, invalid (day outside the trip, a time
without tz, more than 40 items).bookings:writetripId, category, title, startDate, tz, clientRequestId: requiredstartTime, endDate, endTime, endTz (arrival zone), confirmationCode, place, from/to {name, code},
flightNumber, notes: optionalstatus, entryId, day, entry (as in get_itinerary).applied · duplicate (replay, or the same confirmation code and start time
is already on the trip: returns the existing entry and adds nothing).forbidden (viewer), not_found, invalid (bad zone or date, ends
before it starts).trips:writeget_itinerary returns) on a trip the user
owns or can edit. Only the fields passed change. It overwrites the user's data, so the assistant must say
what will change and get the user's OK first.tripId, entryId, clientRequestId: requiredtitle, category, notes ("" clears), place (replaces the whole
place), startDate, startTime, endDate, endTime, tz, endTz, status (PLANNED, VISITED, COMPLETED,
SKIPPED), confirmationCode, flightNumber (flights), from/to (transit)endDate, is read as the next day.tz keeps the wall-clock time and re-reads it in that zone.tz is required when the entry has no zone yet. A startDate schedules an unscheduled idea.status, entryId, changed[] (stored field names), entry (after the change);
on conflict also notApplied[] and message.applied · duplicate (replay) · conflict (someone changed the same
field at the same moment and their change was kept; read the entry again and ask the user).forbidden (viewer), not_found (no such entry, a private entry, or already
removed; an edit never brings back a removed entry), not_editable (times recorded by location
tracking, a cruise's times, or a stop's status on a shared trip, which each person marks for themselves in
the app), invalid (nothing to change, from/to on a non-transit entry, ends before it starts).trips:writetripId, entryId, clientRequestId: all required.status, entryId, removed (the entry as it was, so the assistant can tell the user what
went); on conflict, entry and message.applied · duplicate (replay, or the entry was already removed) ·
conflict (it was edited at the same moment and kept).forbidden (viewer), not_found (no such entry, or a private entry).trips:readlist (optional): all (default), been or wantsynced (false until the user's lists have synced to their account), been[],
wantToGo[]: each name, country, lat, lng, note, savedAt (YYYY-MM-DD); counts {been, wantToGo}.invalid (unknown list).trips:writename (1–200), lat, lng, clientRequestId: requiredcountry (≤ 120), note (≤ 2000): optionalstatus, place {name, country, lat, lng, note, savedAt}, list (want, or
been when the place was already on the been list).applied · duplicate (replay, or the same place, by name within about 10 km,
is already on either list: nothing changes).not_editable (the user's lists haven't synced to TravelLog yet: they open Profile → Places
in the app once), invalid (bad coordinates, a health-care place, or 500 places already saved).trips:readlimit (optional): past trips to list, 1–100, default 50asOf, tripCount (trips that have started), daysAway (days on
those trips up to today, overlaps counted once), countries[] and cities[]
({name, trips}, most trips first, from the addresses of the places on each trip),
trips[] (as list_trips, plus cities[], countries[], newest first), upcoming[],
upcomingCount, partial when places were read for only the newest 60 trips.trips:readtripId (required)trip, tripNotes, days[] of {day, date, notes[]}.not_found.trips:readtripId (required)trip; expenses[]: id, title, amountCents (minor units),
currency, category, date, day, paidBy (the trip roster name; empty = not set), split {mode, everyone,
people[]} with mode EQUAL (people[].name when not everyone), PERCENT
(percent), EXACT (amountCents) or ITEMIZED (personalCents;
the rest is split evenly); totals[] of {currency, totalCents, count}.forbidden (a viewer of a shared trip), not_found.These write directly into the user's TravelLog, so call them only when the user explicitly asked you to add that thing (for example "log my trip to Rome", "add the €32 tickets", "import my travel spreadsheet"). Your own plans and ideas always go in with add_suggestions. The descriptions and the server instructions say the same.
trips:writetripId, title (1–120), tz (IANA zone of the place), clientRequestId: requireddate (YYYY-MM-DD) or day (1-based): required, inside the tripcategory (default OTHER), startTime / endTime (HH:MM; an end before the start is the
next day), endDate (a stay's check-out day, or with endTime), endTz, place {name, address, lat, lng},
notes (≤ 2000), status, transit from / to / flightNumber, confirmationCode: optionalvisited, planned or skipped. Default: visited on a trip that has ended,
otherwise planned; never visited for a day that hasn't happened. Transit is stored as completed.
On a trip shared with others, a visited or skipped stop is recorded as this user's own visit
(yourVisit in the result) and the stop stays the group's plan, exactly as when one person marks a shared stop
in the app. Another member's visit is never written.status, entryId, day, entry (as get_itinerary shows it), yourVisit (shared trips).applied · duplicate (replay, or the same title at the same time is already on the trip).forbidden (viewer), not_found, invalid (date outside the trip, bad time or zone, …).trips:writetripId, title, amount (normal units, e.g. 12.50), currency (3 letters), clientRequestId: requiredcategory (FOOD, LODGING, TRANSPORT, ACTIVITIES, SHOPPING, OTHER), date (inside the trip; default its first day),
paidBy (default "me"), split {mode: equal|exact, people: [{name, amount?}]}: optionalpaidBy and the split may name only people already on the trip (the names
get_trip_expenses shows) or "me" for the user. Equal splits between everyone unless people are listed; an exact
split must add up to the amount.status, expenseId, expense (as get_trip_expenses shows it).applied · duplicate (replay, or the same title, amount, currency and date).forbidden, not_found, invalid (someone not on the trip, a split that doesn't add up, …).trips:writetripId, text (1–2000), date or day, clientRequestId.status, day, date, text (the day's whole note).applied (new note) · appended · duplicate (replay, or the text is already there) ·
conflict (someone edited the note at the same moment; theirs was kept).trips:writenot_editable (an account made in the app whose lists haven't synced yet; an account made on the website
has no such wait), invalid.trips:writeclientRequestId and trips[] (1–20), each: name, destinations[], startDate,
endDate (required); key (your stable id for the trip; default its position), notes,
entries[] (≤ 80, the add_entry fields without tripId), dayNotes[] {date | day, text} (≤ 15; two for one day are joined).status (applied, duplicate, partial or failed), results[] one per trip
{index, key, status, tripId?, entries?, dayNotes?, reason?, message?, fields?}, counts.applied · duplicate (replay with the same clientRequestId + key) · exists (the user
already has a trip with this name and dates: named in tripId, nothing added to it) · invalid (with the field) ·
skipped (daily_limit: import it tomorrow with the same clientRequestId and key) · failed.
Retry only the trips that didn't go in.trips:writetripId, suggestionIds[] (1–40, ids from list_suggestions), clientRequestId.status, results[] {suggestionId, status, entryId?, entry?}.applied · duplicate (already added: the same entry) · not_found (dismissed, or not this user's) · partial.suggestions:writeduplicate = already gone.trips:read (any connected assistant or key may start it; only the user can finish it)/account?action=delete, which signs the user in, lists exactly what will be deleted (profile, trips they own —
for everyone on them — trips they leave, places, connected assistants and keys, the sign-in) and needs one tap on
Delete my account. That runs the same deletion as the apps. Afterwards every key, token and session of the account
stops working, including the one that asked. Tell the user it's permanent and get their OK first.{status: "sent", deleted: false, sentTo: "a•••@example.com"}. One link a minute, 3 a day
(rate); no_email if the account has no email address.A person doesn't need the TravelLog app to use this connector.
/connect): email and password, or Google. They accept the Terms and Privacy Policy, confirm they
are at least 18, and pick a name and @handle — the same account the app makes.email_unverified.import_trips, add_entry, add_expense,
add_day_note, add_been_place), plan new ones (create_trip, add_suggestions) and
fold ideas in (accept_suggestion, dismiss_suggestion).An assistant can create the user's TravelLog account for them. The user's only step is tapping the link in one email.
Use the sign-up URL https://travellogapp.web.app/mcp?signup=1 with no token.
It serves a signed-out MCP session (the server returns an Mcp-Session-Id on initialize) that lists
only three tools. The plain /mcp URL is unchanged: without a token it answers 401, so clients that sign
in with OAuth keep doing that (and can create an account on the sign-in page).
| Client | What it sees |
|---|---|
Claude, ChatGPT, grok.com connector on /mcp | Unchanged: 401 → OAuth → TravelLog's sign-in page,
where the user signs in or creates an account. The sign-up tools never appear. |
The same clients on /mcp?signup=1 | Connect without signing in and see get_started,
create_account, check_account_status. When the account is active, the same connection is signed in:
the server sends notifications/tools/list_changed in that response, and the next tools/list returns
the full set. A later, new connection on this URL starts signed out again, so for everyday use switch the connector to
/mcp (OAuth) or use the access key. |
| xAI (Grok) API remote MCP tool, custom bots | server_url = the sign-up URL, no authorization.
Within one response, if the client resends the Mcp-Session-Id (MCP clients must), the bot gets the full tools after the claim. Save the accessKey
that check_account_status returns and put it in authorization (with /mcp) from the next request on. |
| Clients that send no session id (stateless, or the 2026-07-28 revision) | The same three tools; the claim returns the key
with connectionUpgraded: false. Reconnect with the key as the bearer. |
email, displayName (≤ 50), handle (required: 3–20 letters,
numbers or _, the user's choice), acceptedTerms: true, ageConfirmed: true. Ask the user for each first;
pass the two trues only if the user explicitly said yes. Never fill them in yourself.{status: "pending", signupId, claimSecret, handle, expiresAt}. The claimSecret is shown once,
valid 24 hours; keep it out of the chat.invalid (fields[]: email, displayName, handle, acceptedTerms, ageConfirmed),
handle_taken (with 2–3 free suggestions: let the user choose), account_exists ("an account
already exists; sign in instead" — it never says how), rate, unavailable.signupId, claimSecretpending until the user taps the email link; then active with
accessKey (a Read & write key named "<assistant> (created at sign-up)", 90 days) exactly once
and connectionUpgraded; after that claimed; after 24 hours expired. Five wrong secrets lock the
claim. Poll at most every 30 seconds.Signing in later. The account has no password. On the web: /account → Email me a sign-in link, or Forgot password? to set one. In the app: the same email. An account whose email nobody verifies within 72 hours is deleted.
Limits (signed out). create_account: 3 a minute and 10 a day per network, 3 a day per email address (Gmail dots and +tags count as one), and a daily cap for all of TravelLog. check_account_status and get_started: 30 a minute per network. New sessions: 20 a minute per network. Networks and email addresses are counted by salted one-way hashes kept 2 days.
xAI (Grok) API: let the bot sign the user up.
{
"model": "grok-4.7",
"input": [{"role": "user", "content": "Set me up with TravelLog. My email is jane@example.com, name Jane, handle jane_travels. I accept the Terms and Privacy Policy and I'm over 18."}],
"tools": [{
"type": "mcp",
"server_url": "https://travellogapp.web.app/mcp?signup=1",
"server_label": "travellog"
}]
}Some MCP clients can't run OAuth and only take a fixed bearer token, for example the xAI (Grok) API's remote MCP tool
or a bot you write yourself. For those, the user creates a personal access key in the TravelLog app:
Settings → Connected apps → Create access key. They name it, choose Read only
(trips:read) or Read & write (every scope above), and an expiry (30 days, 90 days,
1 year or never; 90 days by default).
tlpat_ followed by 43 characters (256 random bits). It is shown once. TravelLog stores
only a SHA-256 hash of it.Authorization: Bearer tlpat_…. The server also accepts the bare key in the
Authorization header.401. Deleting the account revokes every key.xAI (Grok) API, remote MCP tool. In the Responses API (POST https://api.x.ai/v1/responses),
add an mcp tool. xAI documents server_url and server_label (required) and
authorization ("a token that will be set in the Authorization header"), plus optional
server_description, allowed_tools and headers; xAI's own SDK names the last two
allowed_tool_names and extra_headers. Put the key itself in authorization:
{
"model": "grok-4.7",
"input": [{"role": "user", "content": "Look at where I've been and what's on my want-to-go list, and suggest my next trip."}],
"tools": [{
"type": "mcp",
"server_url": "https://travellogapp.web.app/mcp",
"server_label": "travellog",
"server_description": "My TravelLog trips, places lists and travel history",
"authorization": "tlpat_…"
}]
}xAI's docs don't say whether authorization should include the word "Bearer". TravelLog accepts the
key with or without it, so "tlpat_…" and "Bearer tlpat_…" both work. Use the model name xAI
currently lists. To keep a bot read-only, use a Read only key (or allowed_tools).
Other clients that take a server URL plus a header: URL https://travellogapp.web.app/mcp,
header Authorization: Bearer tlpat_….
A tool error is a normal result with isError: true and structuredContent
{v, reason, message, …}. The message is a plain sentence the assistant can pass on.
| reason | Meaning | What to do |
|---|---|---|
rate | Over a limit below. Includes resetsAt (ISO 8601 UTC) for when the window resets. | Tell the user when to try again; don't retry before resetsAt. |
not_found | No such trip or entry, or not visible to this user. | Re-read with list_trips / get_itinerary. |
forbidden | The user is a viewer on this shared trip (writes, and get_trip_expenses). | Only read it. |
not_editable | This field of this entry is managed in the app (tracking times, cruises, a shared stop's status), or the user's places list hasn't synced yet. | Tell the user to change it in TravelLog (or open Places once). |
invalid | Bad input. Includes fields[]. | Fix the named field. |
email_unverified | The account was created on the website and its email address isn't verified yet (writes only; reads work). | Ask the user to open the verification email, or resend it at /account. |
account_exists | create_account: that email already has a TravelLog account. | Help the user sign in (OAuth on /mcp, or /account). |
handle_taken | create_account: the @handle is taken. Includes suggestions[]. | Let the user pick one, call again. |
sign_up_first | A signed-out session called a tool that needs an account. | Call get_started. |
signed_in | A signed-in session called a sign-up tool. | Use the full tools. |
no_email | request_account_deletion: the account has no email address. | Delete in the app or at /account. |
unavailable | TravelLog paused the connector, this tool, this assistant or access keys, or the account is gone. | Try later; don't loop. |
HTTP-level: 401 (no, expired, revoked or wrong-audience token: re-run OAuth; or an unknown, revoked or
expired access key: make a new one in the app), 403
(insufficient_scope, or an Origin that isn't allowed), 400 (-32020 header mismatch,
-32022 unsupported protocol version), 405 (GET or DELETE on /mcp), 404 (an unknown or expired Mcp-Session-Id on the
sign-up URL, or one whose account was deleted: start a new session), 429 (too many new sessions from one network).
What a connected assistant can see and do is set out in section 3a of our Privacy Policy; the connector terms are in section 7a of our Terms. We keep a 90-day record of each request (which assistant, which tool, whether it worked, when) without any trip content.
Report a vulnerability or a suspected incident to armgardtj@outlook.com (subject "Security"). We can switch off the connector, a single tool or a single assistant at once without users having to reconnect.