Arkana MCP Server
The Arkana MCP server makes Arkana (the Volāre Laravel/Filament back office) agent-ready through a single, generic MCP endpoint that grows domain by domain. People connect from Claude Desktop, claude.ai, Claude Code or Codex by signing in with their Arkana account; machine agents such as the Hermes Slack bot use a token. It starts with bookings.
Why it exists
Section titled “Why it exists”Ops asked Hermes for one CSV per confirmed booking with the client name and the “How the price was built (at purchase)” table. That table is computed when the Cost Breakdown page is viewed; it is not stored. The Analytics MCP gateway only has raw SQL views, so recomputing it there would give wrong numbers.
Arkana is the source of truth. The MCP server is only a connector: each tool calls the same domain class the admin screen uses and never re-implements its logic, so agents and operators can never see different figures.
How a call flows
Section titled “How a call flows”-
The client sends a Streamable HTTP request to one of two endpoints of the same server (
routes/ai.php):- People:
/mcp/arkana, public, with an OAuth access token (auth:api= Laravel Passport, scopemcp:use). - Agents:
/mcp/arkana-agent, internal only, with a Sanctum token (auth:sanctum).
Both add
throttle:120,1andLogMcpToolCall. - People:
-
ArkanaServerlists its tools; each tool’sArkanaTool::shouldRegister()decides whether this caller may see and call it. -
The tool validates its input and calls a domain class under
app/Services/. -
That domain class is the one the Arkana screen consumes, so the tool returns the screen’s figures, only rounded to 2 decimals as the screen displays them (
ArkanaTool::asDisplayed()).
Definitions shared by Filament and the tools:
| Definition | Used by the screen in | Used by the tools for |
|---|---|---|
BookingStatusGroup enum |
Arkana › Bookings tabs, Booking::open() / inStatusGroup() scopes |
status_group filter |
Booking::scopeVisibleTo() |
BookingResource (supplier-manager scoping) |
Every booking lookup |
User::canViewInternalPricing() |
GatesSupplierManagerPrices trait |
Hiding pricing from the cost breakdown tool |
BookingPricingData (and its RELATIONS) |
Booking › Cost Breakdown (ViewBooking) |
bookings_get_cost_breakdown |
Access
Section titled “Access”Access is per tool, decided once in ArkanaTool::shouldRegister(). A tool is
listed and callable only when the user holds the Spatie permissions mapped to
the tool’s ability, and the request carries one of:
- a person’s OAuth token (Passport) with the
mcp:usescope. The person acts with their own Arkana account: their roles, supplier scoping and pricing visibility apply exactly as on screen; - an agent’s Sanctum token (
PersonalAccessToken) granted the tool’s ability.
Session-authenticated requests (Sanctum TransientToken) never qualify. An
OAuth token is refused on the agent endpoint and a Sanctum token on the people
endpoint.
People: OAuth sign-in
Section titled “People: OAuth sign-in”Mcp::oauthRoutes() publishes the OAuth discovery metadata and dynamic client
registration (/oauth/register, rate limited). Clients may only register
callbacks on https://claude.ai, https://claude.com or http://localhost
(config/mcp.php). Signing in goes through the normal Arkana login (password or
Google) and a Filament consent screen (resources/views/mcp/authorize.blade.php).
OAuth sessions do not expire on their own; access ends when the person loses the
Arkana permissions, or when the token is revoked. Passport signs tokens with
PASSPORT_PRIVATE_KEY / PASSPORT_PUBLIC_KEY from the backend environment, the
same on every instance.
Agents: tokens
Section titled “Agents: tokens”Each tool belongs to an ability, named {domain}:read / {domain}:write, in
App\Enums\McpAbility. The ability’s permissions decide whether a person sees
the tool:
| Ability | Required permissions | Tools |
|---|---|---|
bookings:read |
view_booking |
bookings_list, bookings_get_cost_breakdown |
products:read |
view_market_product, view_offer |
products_list, products_get, products_list_offers, products_diagnose_offers |
tours:read |
view_supplier_tour, view_market_product |
tours_list, tours_get |
People get the same tools through their roles: admins all of them; sales agents the bookings tools. Finance and supplier managers hold none of these permissions today.
Agents get a role-less service user created by mcp:agent-token (see
Connecting). With no role it cannot sign in to the
admin panel. Its token carries the mcp:* ability
(McpAbility::ALL, accepted only by the MCP tools, not by the REST API) and
the user the * permission, so an agent reaches every tool, including tools
added later. The abilities above decide which tools a person sees.
Audit log
Section titled “Audit log”LogMcpToolCall records every tools/call in the activity log under the mcp
log name, with the caller (the person or the agent’s service user), the
agent’s token name (e.g. mcp-hermes) or the person’s OAuth client (e.g.
oauth:Claude), the tool name, the
arguments, the outcome (status: ok or error, where a validation error or
an unknown tool counts as error; null for streamed SSE responses, whose
outcome is not visible there) and duration_ms. Nightwatch and Sentry see
every call as the same POST to the MCP endpoint, so this log is the per-tool
view.
Tool catalogue
Section titled “Tool catalogue”Tools are named {domain}_{verb}[_{object}].
bookings_list
Section titled “bookings_list”Lists bookings as Arkana › Bookings does, newest booking date first. Use it to find references and ids.
-
Arguments:
status_group: an Arkana › Bookings tab (open,action_required,active,checkout,quotations,completed,closed), orallfor every status. Defaults toopen, the admin default.status: booking statuses, within the group.attention: the booking page’s Attention badges (flight_booking_failed,balance_overdue,payment_pending,needs_admin_action,flight_repriced,flight_fare_lost,ticket_not_issued), fromApp\Enums\BookingAttention. Fare lost and ticket not issued reuseFlightBooking::hasLostItsFare()/canIssueTicket()and their query scopes (withLostFare,issuable); the Flight Bookings “Fare expired” filter uses the same scope.product_id, orproduct: title text that also matches quote copies (“Beduino - Aida Fernández”).country: ISO-2 code of a country the trip visits; see Destination countries.location: a place the itinerary stops at, by name: a POI name or translation, or a legacy stop name (ProductTemplate::scopeVisitingLocation; arrival and departure endpoints are not stops).market: code, e.g.ES.departure_from/departure_toandbooked_from/booked_to:YYYY-MM-DD, inclusive. Booked dates are calendar days in the viewer’s display timezone (Europe/Madridby default), like the admin.sort:booked(default) ordeparture(soonest first).per_page(default 25, max 100) andcursor.
Arkana › Bookings has the same Attention, Destination, Location, Product and Departure filters, built on the same
Bookingscopes. -
Output:
{data, next_cursor}. Each row hasid,reference,status,client_name,product(id,title,sku),countries,locations(itinerary stops),market,departure_date,currency,total_amount,number_of_travelers,booked_at. Passnext_cursorback ascursor;nullmeans no more pages. Any othercursorvalue is rejected with a validation error, rather than silently restarting from the first page. -
Visibility: the same
Booking::visibleTo()scope as the admin list: supplier managers see only their supplier’s bookings, and none while no supplier is assigned to them. -
Source:
backend/app/Mcp/Tools/Bookings/ListBookingsTool.php→backend/app/Services/Booking/ListBookingsQuery.php
bookings_get_cost_breakdown
Section titled “bookings_get_cost_breakdown”Returns one booking’s Arkana › Booking › Cost Breakdown tables.
-
Arguments:
reference(e.g.BK-28776C91) orid. -
Output:
id,reference,status,client_name,currency,pax_count,total_amount,gross_profit,gross_profit_percent,reconciles(cost lines add up to the total),has_snapshot, and the tables:Field Screen table pricing_viewHow the price was built (at purchase) pnl_summaryP&L summary payment_scheduleCustomer payments cost_linesDirect supplier costs land_componentsLand cost composition land_fxLand FX position ( exposure+detail;nullfor EUR suppliers)land_componentsare priced from current supplier rates (computed byApp\Services\Offers\LandServicesBreakdown, the same service the offer preview uses) and are informative.land_components_totalis that current-rates total. The authoritative land cost is thecost_lines“Land cost” row, which is checkout-time, andland_components_gap_to_checkoutis the current-rates total minus that row:0means rates have not moved, andnullmeans the two cannot be compared. -
Gotchas:
has_snapshot = false(manual or pre-snapshot bookings) meanspricing_viewandpnl_summaryare empty andcost_linesholds a single package line. Users who cannot view internal pricing (supplier managers, always) get an error instead of figures. Bookings outsidevisibleTo()return “not found”. -
Source:
backend/app/Mcp/Tools/Bookings/GetBookingCostBreakdownTool.php→backend/app/Services/Booking/BookingPricingData.php(looked up viaFindBookingQuery)
products_list
Section titled “products_list”Lists products by market as Arkana › Products By Market does, newest first.
- Arguments:
market,status(draft,active,inactive),country(ISO-2),location,title(text, quote copies included),tour_id,show_itinerary_module(true: only products whose customers see the new day-by-day itinerary;false: only those still on the current one),per_page(max 50),cursor. - Output:
id,title,sku,market,locale,status,show_itinerary_module,countries(code,name),locations,tour_id,published_offers_count(Active offers),created_at. - Source:
ListProductsTool→App\Services\Products\ListProductsByMarketQuery(ProductByMarket::scopeForListing,scopeVisitingCountry,scopeTitled,scopeUsingTour, shared with the list’s filters).
products_get
Section titled “products_get”One product’s page: flights (trip duration, route, search window, excluded
dates, each departure airport’s legs), land (the tour’s itinerary nights and
the supplier services that price the land part, with rate periods, blackouts
and net prices per room type, from SupplierTour::landPricingServices(), the
same services the offer generator and LandServicesBreakdown price),
content (translated texts, itinerary, inclusions, SEO), status,
show_itinerary_module (the
Show the new day-by-day itinerary
switch), countries, locations, tour and public_url (active products only).
products_list_offers
Section titled “products_list_offers”A product’s offers as its Offers tab shows them, soonest departure first:
display_status (with the “Stop sale” and release-window overlays),
departure_date, arrival_date, departure_airport, room and CUG type,
final_price, price_per_pax, the internal breakdown (land_price,
flight_price, base_price, margin), the bound international flight
(route, stops, airlines, stopovers, elapsed minutes, from
App\Services\Offers\OfferFlightSummary) and flight_price_searched_at.
Filters: status, bookable (true bookable now, false expired for sale),
airports, departure dates, min_price / max_price, best_price_only.
Source: ListOffersQuery (Offer::scopeForListing and the filter scopes, shared
with the Offers table).
products_diagnose_offers
Section titled “products_diagnose_offers”The Diagnose missing offers action: for each (airport, date) slot, the
verdict and every gate (pass, fail, skip, with its reason) the offer
generator applies. Arguments: product_id, airports (IATA codes of the
product’s active flight configurations), dates; at most
OfferGenerationDiagnosticsService::MAX_SLOTS_PER_RUN (100) slots per call.
tours_list
Section titled “tours_list”Lists supplier tours as Arkana › Suppliers › Tours does, newest first.
Filters: supplier_id, status (draft, complete), has_guide, search
(title or supplier name). Each row: title, suppliers, status,
duration_days, guide and languages, rate_periods_count,
products_by_market_count / active_products_by_market_count and locked.
Visibility follows SupplierTour::scopeVisibleTo(), the same rule as bookings.
tours_get
Section titled “tours_get”One tour’s page: details, completion, route and itinerary,
travel_windows (with blackouts), hotels, activities and transfers with
their tiers and itinerary days (App\Services\SupplierTourAssignments, also
behind the page’s Hotels, Activities and Transfers tables), package_services
(with alternative_group), guide and inclusions, products_by_market and
active_stop_sales.
Destination countries
Section titled “Destination countries”A product visits the countries its CMS country pages link plus the
countries its flight-leg airports reach, minus its market’s own countries
(ProductByMarket::getDestinationCountries(); scopeVisitingCountry() is the
query form). Both sources are needed: quote copies have no CMS link, and some
countries are reached by land (Maya’s Honduras). The Products By Market
Destination column and filter, the Bookings Destination filter and the
country arguments all use it.
Arkana MCP vs Analytics MCP
Section titled “Arkana MCP vs Analytics MCP”The two servers are independent; there is no proxy between them. The server
instructions in ArkanaServer tell agents how to route:
| Question | Server |
|---|---|
| Anything Arkana computes (price breakdown, P&L, margins, payment plan) or a record “as Arkana shows it” | Arkana |
| Counts, trends, groupings, ad-hoc slices across many records | Analytics (db) |
| Website traffic and behaviour | Analytics (GA, Clarity) |
| Mixed (e.g. “margin of every booking from Spain last month”) | Analytics for the ids, then Arkana for the figures |
Operations
Section titled “Operations”-
Who can connect. Only Arkana staff (a user with an Arkana role) can authorize a client; anyone else gets a 403 on the consent screen. The consent screen shows where the client sends the person back (e.g.
claude.ai), since the client name is chosen by whoever registered it. -
Session lifetime. OAuth access and refresh tokens last 100 years, so in practice they never expire (product decision). Access ends when the person loses the Arkana permissions the tools check, or when their sessions are revoked:
Terminal window php artisan mcp:revoke-oauth person@byvolare.comThis revokes every access and refresh token of that person; their MCP clients must sign in again. Agent tokens are revoked with
mcp:agent-token <agent> --revoke. -
Signing keys. Passport signs and verifies tokens with
PASSPORT_PRIVATE_KEY/PASSPORT_PUBLIC_KEYfrom the backend environment (/volare/<env>/backend/env). Every API instance must have the same pair: if they are missing,/oauth/tokenfails; if they differ between instances, tokens fail intermittently with 401. -
Load balancers.
/mcp/arkana,/oauth/*and/.well-known/oauth-*must be reachable from the internet on the Arkana host (Claude connectors connect from Anthropic’s cloud)./mcp/arkana-agentmust be denied on the public load balancer and reachable only through the internal one.
Related
Section titled “Related”- Use it from Claude Desktop (people)
- Connecting an agent (Hermes, scripts)
- Adding a tool
- Source:
backend/app/Mcp/,backend/routes/ai.php