Agentic Travel Recommendations MCP Server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Agentic Travel Recommendations MCP Serverrecommend travel for member M-plat-nocruise"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Agentic Travel Recommendations Service
Internal service that lets an external AI agent (the "AI Concierge") fetch a member's profile and generate partner-rule-compliant travel recommendations on arrivia's multi-tenant, white-label platform. Deterministic backend; the LLM is the MCP client, never the rule enforcer.
📹 Video walkthrough: https://www.loom.com/share/540f530d27d541968ee61f78ada758bf
How it works (at a glance)
Deterministic generator turns member facts into candidate offers.
Deterministic rule engine applies partner category exclusions, then caps.
A tenant guard runs before any member data is returned.
REST, MCP (stdio), and a CLI all call the same service brain.
No LLM call inside the service. No API key required to run, test, or demo.
Related MCP server: autonomad-travel
Setup
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txtRun the REST API
uvicorn app.main:app --reload
curl -s localhost:8000/health
curl -s -X POST localhost:8000/recommendations \
-H "x-partner-id: partner_no_cruise" -H "x-agent-id: agent-demo" \
-H "content-type: application/json" -d '{"member_id":"M-plat-nocruise"}'Run the MCP server (agent-discoverable)
Caller identity is the trusted launch context; one process = one partner session.
PARTNER_ID=partner_no_cruise AGENT_ID=agent-demo python -m app.mcp.serverDiscover + invoke with MCP Inspector:
PARTNER_ID=partner_capped AGENT_ID=agent-demo
npx @modelcontextprotocol/inspector python -m app.mcp.server
Then: list tools -> inspect get_recommendations schema -> invoke with
{"member_id":"M-plat-capped"} (cap of 3) and, with a partner_no_cruise-scoped
server, {"member_id":"M-plat-nocruise"} (cruise removed).
Run the CLI demo
python -m cli.demo --member-id M-plat-nocruise --partner-id partner_no_cruise --agent-id aTests and evals
pytest # full suite
pytest tests/evals # safety evals onlySection A — Architecture & Trade-offs
The service has one shared brain, RecommendationService, and three thin
callers — REST (app/api/recommendations.py), an MCP stdio server
(app/mcp/server.py), and a CLI (cli/demo.py) — that all build an
AgentContext and delegate to it. Nothing about rule enforcement or data
access lives in any of the three callers, so there is no code path where an
agent talks directly to member or partner data. get_recommendations runs a
fixed pipeline: fetch the member from MemberService, authorize the caller
against that member's partner_id via TenantGuard.authorize (fail closed on
mismatch), read that partner's config from the read-only PartnerConfigService,
generate candidate offers from member facts alone
(RecommendationGenerator), and run the ordered RuleEngine
(CategoryExclusionRule then MaxRecommendationsRule) over those candidates.
get_member_profile runs the same fetch-then-authorize guard before returning
anything, so both tools share one authorization gate rather than each
reimplementing it.
Trade-off 1 — deterministic rule engine vs. LLM enforcement. An LLM could
read partner config and "decide" what to show, but that makes rule compliance
probabilistic and unauditable — exactly what a travel partner relationship
cannot tolerate. Keeping exclusion and cap logic in typed, unit-tested Rule
classes means every response is reproducible from the same inputs, and
rule_metadata gives on-call a literal trail (applied_rules,
removed_recommendations) instead of an LLM's self-report. The cost is that
the service can't yet handle a rule too nuanced to express as code — an
acceptable trade for a four-week first step.
Trade-off 2 — env-scoped MCP identity vs. a per-call partner argument. MCP
tools take only member_id; partner_id and agent_id come from the server's
launch environment (PARTNER_ID/AGENT_ID), not from tool arguments. A
per-call partner argument would let a compromised or careless agent claim any
partner scope it wants. Binding identity to the process that launched the
server means one MCP session is hard-scoped to one partner for its lifetime;
production replaces this launch context with signed workload identity, but the
one-session-one-partner shape stays.
Config and rule changes. PartnerConfigService is read at request time, so
if a partner's cap or excluded-category list changes upstream, the very next
get_recommendations call picks it up with zero code changes or redeploys —
the service never caches or writes config. Adding an entirely new kind of
rule (e.g. a minimum-tier gate) means writing one new Rule class and adding
it to the ordered list in RuleEngine.__init__; the pipeline, metadata shape,
and both callers are untouched.
Section B — Production Readiness & Incident Response
Incident Runbook: "AI Concierge shows cruises for a cruise-excluded partner"
Get
request_id/partner_id/member_idfrom the report or logs.Confirm partner config: does
partner_configreturnCruiseinexcluded_categories? If not, this is a config-source issue, not a service bug.Reproduce deterministically: call
get_recommendationsfor that member; inspectrule_metadata.applied_rulesandremoved_recommendations— wascategory_exclusionapplied? Was a cruise inremoved_recommendations?If exclusion did not fire: check the rule order and the
CategoryExclusionRule; check whether the client is calling our tool vs. rendering its own offers.Resolve: fix + regression eval (
test_cruise_exclusion_eval); if config-source, escalate to the partner-config owner since that service is read-only to us.
B2 — Required Reasoning Question (answer WITHOUT AI assistance)
Prompt: Describe a scenario where an AI coding assistant gives a plausible-but-wrong answer for enforcing partner rules, how you'd catch it, and what you'd verify before acting.
A similar problem came up while I was building an AI clinical co-pilot on top of OpenEMR. An AI coding assistant could generate an API implementation that looks correct, passes basic tests, and returns valid data, but fails to enforce business rules at the correct boundary. For example, the assistant might trust a member or partner ID provided by the client instead of validating the member's actual partner association before retrieving data or applying recommendation rules. The API could appear to work correctly while allowing data or configuration to cross tenant boundaries.
I encountered a similar issue when my OpenEMR agent answered questions using information related to other patients. I addressed the problem by creating a tool registry factory that restricted which tools and records were available within the current patient context. I also added verification middleware that ran after the model generated a response but before anything reached the user. Every cited source had to match a record the agent actually retrieved, and specific values were checked against the underlying data.
I would apply the same approach to this travel recommendations API. I would keep partner-rule enforcement deterministic and outside the model, validate the member-to-partner relationship before reading partner configuration or generating recommendations, and enforce rules such as cruise exclusions and recommendation limits server-side.
I would catch an incorrect AI-generated implementation with negative tests that attempt cross-tenant access, modified member or partner IDs, excluded cruise recommendations, and requests that exceed partner-specific limits. Before accepting the implementation, I would trace the full request path through authentication, tenant validation, partner configuration lookup, recommendation generation, and final response filtering.
The main lesson from my OpenEMR project was that plausible code is not the same as correct code. Before acting on an AI-generated solution, I verify the implementation against the actual business requirements, test isolation boundaries and failure paths, and confirm that automated tests would catch future regressions.
Section C — AI Usage Log
This project was built with AI assistance throughout design and implementation,
under a rule that the deterministic backend and its enforcement logic are
human-reviewed line by line, and that Section B2 above is answered by a human
with no AI involvement. Three representative interactions, recorded in
questions/decision-log.md:
Scope-down from a large multi-PR plan to a lean MVP. The starting plan sized this as a large system (agent orchestrator, citations + citation validation, response validation layer, full eval dashboards). Asked to scope it to a realistic four-week, single-engineer first step, the AI proposed cutting to: mock member + partner config services, a deterministic generator, an ordered rule engine (exclusions then caps), a tenant guard, REST + MCP + CLI over one shared service, and a focused test/eval set — with the rest explicitly deferred to a "ships later" list. Kept as proposed: the cut lines up with what the assignment actually rewards (correctness and judgment under scope discipline, not feature count), and it left the repo with one honest story instead of an aspirational one it couldn't finish. See decision-log D1.
Removing LangSmith to honor the "existing infrastructure only" constraint. An early observability pass suggested LangSmith for LLM-specific tracing. Flagged in review as violating the assignment's explicit "no new third-party platforms" rule, the AI was asked to re-propose observability using only infrastructure arrivia already runs. The replacement — structured JSON logs with
request_id/member_id/partner_id/tool_name/rule_decisions/failure_reason, framed as CloudWatch/Application Insights for logs and metrics and X-Ray/ OpenTelemetry for traces — was kept, and any future LLM-observability idea is now phrased generically ("arrivia-approved observability tooling") so it can't reintroduce a named third-party tool by accident. See decision-log D2.get_member_profiletenant-guard gap caught in spec review. During human review of the design spec, the drafted MCPget_member_profiletool calledMemberServicedirectly rather than going throughRecommendationService, which meant it would return a member's profile without ever checking that the caller'spartner_idmatched the member'spartner_id— the exact cross-tenant leak this whole design exists to prevent. Rejected the direct-call version; the AI's fix, accepted, added a guardedRecommendationService.get_member_profile(ctx, member_id)that runs fetch →TenantGuard.authorize→ return, mirroringget_recommendations, with a cross-partner-access test added to prove the 403/safe-MCP-error path. See decision-log D16.
What ships first vs. later
Ships now (four-week first step) | Deferred (later) |
Deterministic generator + rule engine | LLM phrasing/ranking/orchestration |
REST + MCP (2 tools) + CLI | Citations + citation validation |
Tenant guard + multi-tenant evals | Real auth (signed identity / JWT) |
Structured JSON logs (PII-safe) | CloudWatch/App Insights/X-Ray wiring |
Mock member + partner config | Real DB persistence, partner admin UI |
Four-week plan
This is the four-week first step; the table above bounds what ships now. Indicative week-by-week:
Week | Focus |
1 | Domain models + fixtures (Silver/Gold/Platinum, last-5 bookings), mock member and read-only partner-config services, deterministic candidate generator. |
2 | Rule engine with fixed order (category exclusions → caps), tenant guard, shared recommendation service brain; unit tests for cap enforcement, cruise exclusion, and isolation. |
3 | REST endpoint, MCP stdio server (two tools), CLI demo — all sharing the one service layer; PII-safe structured JSON logging with correlation IDs. |
4 | Deterministic safety evals, Docker + compose, MCP Inspector walkthrough, README + incident runbook; buffer for hardening (non-root image, healthcheck) noted as later work. |
Notes
Logs never include full profiles, travel history, or recommendation reasons; production would apply arrivia-approved redaction/tokenization.
MCP caller identity is a trusted launch context in the mock; production replaces it with signed workload identity / JWT claims / gateway-injected tenant.
Partner IDs are human-readable (
partner_capped,partner_no_cruise,partner_unlimited) as a deliberate demo-readability choice — they make the Inspector/CLI walkthrough self-explanatory about which rule is under test. The tenant guard treatspartner_idas an opaque string (plain equality match), so the format carries no correctness or security meaning; production would use opaque, tenant-scoped identifiers (e.g. UUIDs) supplied by the verified identity layer, never a client-supplied header. Note that UUID-vs-string is cosmetic — the security property is thatpartner_idcomes from signed identity, not that it looks random.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/andypnguyen11-gif/agentic-travel-recommendations-api'
If you have feedback or need assistance with the MCP directory API, please join our Discord server