Agentic Travel Recommendations Service
MCP server for agentic travel recommendations, exposing tools to retrieve member profiles and personalized travel recommendations with partner-specific rules and policies.
README
Agentic Travel Recommendations Service
This project is a TypeScript and Node.js proof-of-concept for a multi-tenant travel recommendation service. It exposes shared recommendation capabilities through a REST API, Streamable HTTP MCP endpoint, and command-line interface.
Key Features
- REST API for health, member profiles, and recommendations
- Streamable HTTP MCP endpoint
- MCP tool:
get_member_profile - MCP tool:
get_recommendations - Authoritative, member-derived tenant resolution
- Partner-specific recommendation caps
- Partner-specific category exclusions
- Deterministic recommendation generation
- Fail-closed partner configuration behavior
- Request IDs and structured JSON logs
- Minimal CLI demonstration
- Multi-stage Docker build
- Automated tests
Architecture at a Glance
REST / MCP / CLI
|
v
RecommendationService
|
v
MemberDataService
|
| member.partnerId
v
PartnerConfigurationService
|
v
CandidateGenerator
|
v
RecommendationPolicy
|
| exclusions then cap
v
Final Recommendations
Callers provide only memberId; they do not select the authoritative partnerId. The member profile determines the partner configuration, and REST, MCP, and CLI all reuse the same business layer.
Quick Start
npm ci
npm run dev
The service is available by default at http://localhost:3000.
Type Checking
npm run typecheck
npm run typecheck:test
npm run typecheck:all
Tests
npm test
The current verified baseline is 46 passing tests across 6 files.
Production Build
npm run build
npm start
REST API
GET /health
GET /api/members/:memberId
GET /api/recommendations/:memberId
Example requests:
curl http://localhost:3000/api/members/MEMBER-001
curl http://localhost:3000/api/recommendations/MEMBER-001
MCP
The MCP server is exposed through:
POST /mcp
It provides these tools:
get_member_profileget_recommendations
Both tools accept only the member identifier:
{
"memberId": "MEMBER-001"
}
The implementation uses the official @modelcontextprotocol/sdk Streamable HTTP transport. The caller does not supply partnerId; it is resolved from the authoritative member profile.
CLI
npm run cli -- MEMBER-001
Demo members:
MEMBER-001→BANK_AMEMBER-002→BANK_BMEMBER-003→CREDIT_UNION_C
Docker
docker build -t agentic-travel-recommendations .
docker run --rm -p 3000:3000 agentic-travel-recommendations
The image uses a multi-stage build, a Node 24 runtime, and a non-root runtime user. The HTTP server handles graceful shutdown signals.
Section A — Architecture & Trade-offs
Architecture Overview
The service is a stateless TypeScript and Node.js application that exposes the same recommendation workflow through REST, Streamable HTTP MCP, and a CLI. Each transport validates its input and delegates to the shared RecommendationService; transport handlers do not implement partner policy themselves.
The authoritative tenant flow is:
memberId
→ MemberDataService
→ MemberProfile.partnerId
→ PartnerConfigurationService
→ CandidateGenerator
→ RecommendationPolicy
→ final recommendations
Callers supply memberId and never select the authoritative partnerId. The member profile returned by MemberDataService determines which partner configuration is retrieved. Both upstream services also correlate the identity embedded in a response with the identity requested, and RecommendationService performs an additional partner-identity check before generation.
Candidate generation is intentionally independent of partner policy. The deterministic generator first produces raw candidates from the member profile; the generic policy layer then removes candidates in excludedCategories and applies recommendationCap, in that order. Only the resulting recommendations are returned. The Member Data Service and Partner Configuration Service are mocked for this proof-of-concept, and partner configuration access is read-only.
Design Trade-offs
Correctness over availability. If authoritative partner configuration is missing, unavailable, schema-invalid, or identity-mismatched, the request fails closed. The service does not substitute permissive defaults or return unrestricted recommendations. This may reduce availability during an upstream failure, but prevents recommendations from escaping the correct tenant policy.
Fresh configuration over caching. The first release retrieves partner configuration for every recommendation request rather than adding cache infrastructure. This keeps behavior simple and ensures each successful request uses current policy. It accepts additional upstream latency and load; a short-lived cache is appropriate later only if measured performance justifies the consistency trade-off.
Deterministic generation over an external LLM. Candidate generation is reproducible, testable, cost-free, and operationally predictable. This limits the sophistication of personalization, but makes policy behavior and assessment results easy to verify. A future LLM or ranking component could replace candidate generation without changing deterministic policy enforcement.
Handling Partner Configuration Changes
The Partner Configuration Service is a read-only dependency. If a partner changes its recommendation cap from unlimited to 3, or adds cruise to excludedCategories, the recommendation service needs no code change and no tenant-specific branch. The next successful request reads the current configuration, and the generic policy logic applies the new exclusion and cap values.
As long as the configuration remains compatible with the existing schema, the application does not require redeployment. If configuration caching is introduced later, it must have an intentionally short TTL or reliable invalidation strategy because stale configuration could temporarily violate the partner’s current policy.
Section B — Production Readiness & Incident Response
Incident Runbook Entry
Scenario: A member reports that the AI Concierge displayed a cruise recommendation even though their partner excludes cruises.
-
Identify and correlate. Obtain the request or correlation ID from the report when available and locate the corresponding structured logs. Record the
operation,memberId, authoritativepartnerIdonce resolved,resultCode, and HTTP status where applicable. Travel history and recommendation payloads are intentionally not logged, so use identifiers and outcome metadata for correlation. -
Verify the authoritative tenant. Retrieve the affected member through
MemberDataServiceand confirm that the requestedmemberIdequals the returnedmember.memberId. Derive the tenant only frommember.partnerId. Do not trust a partner ID supplied by a frontend, MCP caller, query parameter, or support report. -
Verify partner configuration. Retrieve configuration using
member.partnerId, then confirmconfiguration.partnerId === member.partnerId. InspectexcludedCategoriesandrecommendationCap, and determine whethercruiseis excluded in the current authoritative configuration. Missing, unavailable, malformed, or identity-mismatched configuration must cause the service to fail closed rather than use permissive defaults. -
Reproduce the pipeline. Run the member through the same recommendation workflow. A cruise in raw
CandidateGeneratoroutput is not itself a defect because generation deliberately ignores partner policy. Verify thatRecommendationPolicyprocessesraw candidates → remove excluded categories → apply recommendation cap → final recommendations, and confirm that cruises are absent from the final result. -
Isolate the failure location. If cruises appear in raw candidates but not final recommendations, policy is operating correctly. Investigate a stale client response, a response associated with the wrong member, another consumer or endpoint bypassing the expected workflow, or a difference between the reported time and current configuration. If a cruise survives
RecommendationPolicy, inspect category comparison or normalization, authoritative configuration contents and identity, and recent policy changes or regressions. -
Contain. If the correct partner policy cannot be established or safely reproduced, fail closed instead of returning potentially non-compliant recommendations. Do not attempt to modify the read-only Partner Configuration Service from this application.
-
Fix and verify. Correct the defect in the responsible layer and add a regression test reproducing the exact failure. Run:
npm run typecheck:all npm test npm run buildVerify the affected partner, at least one unaffected tenant, REST behavior, and MCP behavior when relevant.
-
Follow up. Record the root cause, affected partner and member scope, impact window, remediation, regression coverage, and preventive action.
Part B2 — Required Reasoning Question
An AI coding assistant could plausibly produce an implementation that Zod-validates upstream member and partner records but never correlates returned identities with requested identities. The code would be type-safe, schema validation and happy-path tests would pass, and a superficial review would see reasonable defensive validation. The missing cross-tenant invariant would still create a serious policy risk.
For example, MEMBER-001 belongs to BANK_A. RecommendationService requests BANK_A’s configuration, but a buggy or misrouted upstream service returns a completely schema-valid BANK_B configuration with an unlimited cap and no category exclusions. Zod correctly accepts its shape, but applying that policy to MEMBER-001 could bypass BANK_A’s restrictions.
I would catch this with an adversarial regression test: request BANK_A while a deliberately faulty configuration-service double returns the valid BANK_B configuration. I would expect InvalidUpstreamDataError, assert that no recommendation result is produced, and explicitly verify that CandidateGenerator.generate was not called. I would add the corresponding member-data test proving that the requested memberId must equal the returned member.memberId.
Before acting on AI-generated code, I would trace authority and execution order rather than rely on types alone. I would verify that member.partnerId, never caller input, selects the tenant; both returned identities match their authoritative requests; and missing, unavailable, or mismatched configuration fails closed without a permissive fallback. I would also confirm that generation cannot begin before policy identity is safely established and that negative, adversarial tests cover these cases alongside normal happy paths.
Section C — AI Usage Log
Interaction 1 — Architecture Review
What I asked
I asked the AI coding assistant to review the challenge and help design a minimal architecture that one engineer could realistically implement. The requested scope included domain models, mocked upstream services, recommendation logic, REST, MCP, CLI, automated testing, and containerization, while avoiding infrastructure that the proof-of-concept did not require.
What the AI provided
It proposed separating domain models, service contracts, mocked upstream services, candidate generation, partner policy, orchestration, and transport adapters. It initially suggested stdio as the simplest MCP transport.
What I kept, changed, or rejected
I kept the layered separation because it lets REST, MCP, and CLI call one business layer instead of implementing rules independently. I rejected stdio as the primary MCP transport and redirected the design to the official MCP SDK’s Streamable HTTP transport at POST /mcp. The assignment describes an internal API that agents should discover and invoke, and HTTP fits the containerized service architecture. I made that choice after comparing the proposal with the assignment’s integration requirements rather than accepting the simplest option automatically.
Interaction 2 — Incremental Implementation
What I asked
I did not ask for the entire application in one prompt. I divided implementation into bounded steps: domain models, schemas and errors, upstream contracts and mocks, candidate generation, recommendation policy, orchestration, REST, MCP, CLI, observability, and Docker. After each step, I reviewed the reported behavior and required typechecking and tests before proceeding.
What the AI provided
The assistant implemented each bounded component with focused tests and reported the files changed and verification results. This made individual design choices visible and reviewable instead of hiding them inside a large generated patch.
What I kept, changed, or rejected
I kept the shared RecommendationService, deterministic CandidateGenerator, separate RecommendationPolicy, member-derived tenant resolution, read-only configuration contract, and shared REST/MCP/CLI business logic. This structure makes tenant policy independently testable and prevents transport-specific rule implementations. I also intentionally retained deterministic generation instead of adding an external LLM dependency. The assessment focuses on service design and policy enforcement, and reproducible output is easier to test, debug, and demonstrate. Each increment was accepted only after its behavior matched the architectural invariants and the checks passed.
Interaction 3 — Production and Security Audit
What I asked
Once the application worked, I asked the AI to stop adding features and audit the repository from the perspectives of a senior engineer, multi-tenant security reviewer, on-call production owner, and REST/MCP API reviewer.
What the AI provided
The audit found that schema-valid upstream responses were not originally correlated with the member or partner identity requested. It also found that malformed JSON could fail before request-ID middleware established request context. Additional lower-priority improvements were suggested.
What I kept, changed, or rejected
I accepted both high-value findings because they affected tenant correctness and safe operations. For identity correlation, the implementation now verifies that a returned memberId matches the requested member, that configuration.partnerId matches the authoritative member.partnerId, and that RecommendationService repeats the configuration identity check defensively. Mismatches fail closed, and adversarial regression tests verify that candidate generation never starts when authoritative configuration cannot be established.
For malformed JSON, request context and the request ID are now established before parsing. Invalid bodies receive a safe structured 400 response without parser details, stack traces, filesystem paths, or raw request content.
I deferred lower-priority ideas such as persistent MCP sessions, additional distributed infrastructure, and more advanced observability because they were unnecessary for the four-week proof-of-concept and would increase operational scope. I evaluated each recommendation against the assignment requirements, tenant correctness, testability, operational risk, and delivery scope. The assistant supplied options and implementation help, but I reviewed the reasoning, selected the changes, and verified them through focused tests and end-to-end checks.
Four-Week First Step
What Ships First
The four-week target is a first shippable internal proof-of-concept. It demonstrates the required workflow with safe tenant enforcement and operational basics; it is not a claim that every capability needed for a broad production rollout is complete.
Week 1 — Service Foundation
- Establish the TypeScript and Node.js service foundation.
- Define domain models, strict Zod boundary validation, and typed errors.
- Add the
MemberDataServicecontract and mock implementation. - Add the read-only
PartnerConfigurationServicecontract and mock implementation. - Establish authoritative, member-derived tenant resolution and the initial unit-test foundation.
Goal: Establish safe service boundaries and tenant authority before implementing recommendation logic.
Week 2 — Recommendation Workflow
- Implement the deterministic
CandidateGeneratorindependently of partner rules. - Implement
RecommendationPolicy, including category exclusions and recommendation caps. - Enforce the required order: exclusions first, then the cap.
- Add
RecommendationServiceorchestration and fail-closed configuration behavior. - Cover policy and orchestration with focused unit tests.
Goal: Prove that contractual partner rules are deterministic and independent of candidate generation.
Week 3 — Interfaces and End-to-End Flow
- Expose the REST endpoints and Streamable HTTP MCP endpoint.
- Provide the MCP tools
get_member_profileandget_recommendations. - Add the CLI demonstration.
- Route REST, MCP, and CLI through the shared business layer.
- Add REST/MCP integration tests and tenant-override tests.
Goal: Demonstrate the complete recommendation workflow through the interfaces required by the assignment.
Week 4 — Production Readiness and Delivery
- Add request IDs, correlation fields, structured JSON logging, and safe error handling.
- Handle malformed JSON safely and implement graceful shutdown.
- Add the Docker multi-stage build and non-root runtime.
- Typecheck production source and tests separately.
- Perform the production/security audit and add adversarial identity-correlation tests.
- Complete final end-to-end and container verification.
- Prepare the README, incident runbook, and demonstration video.
Goal: Make the proof-of-concept supportable by the team that owns it on call.
What Comes Later
The following work is deliberately deferred until after the first four-week release:
-
Real upstream integrations. Replace the mocked
MemberDataServiceandPartnerConfigurationServiceimplementations with real arrivia REST clients while preserving the existing service contracts and identity-correlation invariants. -
Existing authentication and authorization integration. Integrate with arrivia’s existing identity and gateway mechanisms rather than introducing a new identity platform. Authorization must preserve member-derived tenant authority.
-
Network resilience. For real upstream HTTP dependencies, validate and configure request and connection timeouts, bounded retries where operations are safe to retry, and explicit failure behavior. Configuration uncertainty must continue to fail closed.
-
Performance validation. Run realistic load and performance tests before optimizing. Consider short-lived partner-configuration caching only if measurements justify it. Stale policy is a correctness risk, so any cache requires a clear freshness and invalidation strategy.
-
Recommendation intelligence. Potentially replace or augment the deterministic
CandidateGeneratorwith an LLM, ranking model, or richer personalization.RecommendationPolicymust remain deterministic and outside the model so generated output cannot override partner rules. -
Production observability. Connect the existing structured events and correlation IDs to arrivia’s approved metrics, tracing, alerting, and operational tooling.
-
MCP evolution. Consider stateful or resumable MCP behavior only when a concrete product requirement needs cross-request state. The current stateless Streamable HTTP implementation is intentional for this service.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.