Skip to main content
Glama

Agentic Travel Recommendations Service

A multi-tenant travel recommendations service that exposes both a REST API and an MCP server, with partner-specific rule enforcement (recommendation caps, category exclusions). Built as an exploration of MCP server design and multi-tenant rule enforcement patterns for AI-agent-facing internal services.

Node TypeScript Tests License

What it does

An AI agent (e.g. a travel concierge embedded in a partner-branded portal) needs two things from this service:

  1. A member's profile — loyalty tier, past bookings, partner affiliation.

  2. Personalized travel recommendations that respect the partner's business rules.

Different partners have different rules. Some cap recommendations at 3 per session; others exclude entire categories like cruises. The service enforces those rules in a single code path so they can't drift between the REST and MCP interfaces.

Quick start

npm install
npm test          # 13 tests: rules engine + integration
npm run demo      # spawns the MCP server and walks 4 partner scenarios
npm run dev:api   # REST API on :3000
npm run dev:mcp   # MCP server on stdio

The demo is an actual MCP client that spawns the server as a subprocess and calls its tools the way an AI agent would.

Seeded scenarios

The mocks include four partners exercising every combination of rule shape:

Member

Partner

Loyalty

Partner rules

M-1001

Bank A

Gold

No cap, no exclusions

M-1002

Credit Union B

Platinum

Excludes cruises

M-1003

Membership Org C

Silver

Cap of 3 recommendations

M-1004

Airline D

Gold

Cap of 5, excludes cars and cruises

Project structure

src/
  mcp/server.ts                    # MCP server, stdio transport, two tools
  api/server.ts                    # REST API (Express)
  api/routes/recommendations.ts
  services/
    recommendationsService.ts      # orchestrator — shared by REST & MCP
    memberDataService.ts           # client for the (mocked) member data upstream
    partnerConfigService.ts        # client for the (mocked) partner config upstream, 60s TTL cache
  rules/partnerRulesEngine.ts      # single source of truth for partner-rule enforcement
  mocks/                           # in-memory mocks of the two upstreams
  types/index.ts                   # shared domain types
  utils/logger.ts                  # pino JSON logger
cli/demo.ts                        # MCP client that spawns the server and walks the scenarios
tests/                             # vitest — unit tests for the rules engine + integration
docker/Dockerfile                  # multi-stage image; default CMD runs the REST API

Architecture

Four moving parts:

  1. AI agent speaks MCP.

  2. This service exposes two MCP tools (get_member_profile, get_recommendations) and an equivalent REST API for non-MCP callers and for debugging.

  3. Member data service (mocked) — returns member ID, loyalty tier, partner ID, and last five bookings.

  4. Partner configuration service (mocked, read-only) — returns per-partner rules.

Two design properties matter most:

  • REST and MCP share the same service layer. recommendationsService.ts is called from both api/routes/recommendations.ts and mcp/server.ts. Partner rules run in exactly one code path — it's structurally impossible to ship an MCP path that bypasses them.

  • Rules enforcement is a pure function. enforcePartnerRules(candidates, partnerConfig, requestedMax?) in src/rules/partnerRulesEngine.ts has no I/O and no side effects. Roughly 40 lines with a dedicated unit-test file, so partner-rule regressions can be caught in seconds.

Every response includes an appliedRules block reporting which categories were filtered and how many results were capped — so a downstream agent can explain to a user why cruises aren't showing up, and an on-call engineer can diagnose "empty result" cases without reading logs.

Design trade-offs

stdio MCP transport rather than Streamable HTTP.
Stdio has no public network surface — no auth, no rate limiting, no session management to design. The MCP server co-locates as a sidecar with the AI agent that consumes it. Streamable HTTP would be the upgrade path for remotely-hosted use; the tool contracts wouldn't change.

Rule-based candidate scoring, not ML.
The recommender uses a legible scoring function: baseline + historical-type affinity + tier boost − recent-destination penalty. Swapping in an ML model later would leave the rules engine, MCP tools, and REST contracts untouched. A broken model would silently degrade recommendations; a broken rules engine would violate a partner contract. Ship the trustworthy piece first.

60-second partner config cache.
Short enough that a partner rule change propagates within about a minute, long enough to reduce upstream load by roughly 99% on the hot path. A cache-bust endpoint could be added later for partners that need instant propagation.

Handling partner configuration changes

Partner config is read-only from this service's perspective, so a partner changing their cap or adding an exclusion happens entirely upstream. Within the 60-second TTL, the change is picked up on the next call. The rules engine reads it fresh, and the appliedRules block in the response reflects the new state.

The only case that requires a code change here is if a partner adds a new kind of rule (e.g. a time-of-day restriction). That would need a new field in the PartnerConfig type and a new step in enforcePartnerRules. A new instance of an existing rule shape needs no code change — that's what the four seeded partners demonstrate.

Production considerations

Things designed for the person on-call at 2 a.m.:

  • Every recommendation call logs one INFO line with { memberId, partnerId, returned, filteredCount, cappedCount }. One grep answers "did the rules run?"

  • The appliedRules block on every response means the same question can be answered by a curl, without logs.

  • The rules engine has 7 unit tests including a specific "order matters" test that fails if filter and cap are reversed.

  • Known error types map to stable HTTP status codes and structured MCP isError: true responses.

Things I'd add before running this in production:

  • Timeouts and one retry with backoff on upstream calls, plus a circuit breaker with a documented fallback behavior.

  • Per-partner dashboards for error rate and p99 latency; an alert if filteredCount suddenly spikes for a partner (usually the signal that someone shipped a bad config).

  • Load testing with realistic traffic mix to validate the cache TTL choice.

  • Streamable HTTP transport for MCP if remote hosting becomes a requirement.

About the build process

The initial scaffold — types, mocks, service layer skeleton, and the first pass at the MCP server — was AI-assisted. Everything after that (the Partner D onboarding to prove multi-tenant behavior, the integration test for it, the real readiness probe replacing the stub, and a cross-platform fix for a Windows path bug in the MCP entrypoint) was written by me. The Windows bug is the one I'd point out: the AI's original entrypoint check used import.meta.url === \file://${process.argv[1]}`, which silently fails on Windows because the two sides of the comparison use different path formats. Green unit tests didn't catch it because they never spawn the MCP server as a subprocess. I caught it by running the demo end-to-end, and fixed it by normalizing both sides through pathToFileURL`.

License

MIT — see LICENSE.

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

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/aryanyeole/agentic-travel-recs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server