Agentic Travel Recommendations Service
by Varma904
README.md
# 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
```text
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
```bash
npm ci
npm run dev
```
The service is available by default at <http://localhost:3000>.
## Type Checking
```bash
npm run typecheck
npm run typecheck:test
npm run typecheck:all
```
## Tests
```bash
npm test
```
The current verified baseline is 46 passing tests across 6 files.
## Production Build
```bash
npm run build
npm start
```
## REST API
```text
GET /health
GET /api/members/:memberId
GET /api/recommendations/:memberId
```
Example requests:
```bash
curl http://localhost:3000/api/members/MEMBER-001
curl http://localhost:3000/api/recommendations/MEMBER-001
```
## MCP
The MCP server is exposed through:
```text
POST /mcp
```
It provides these tools:
- `get_member_profile`
- `get_recommendations`
Both tools accept only the member identifier:
```json
{
"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
```bash
npm run cli -- MEMBER-001
```
Demo members:
- `MEMBER-001` → `BANK_A`
- `MEMBER-002` → `BANK_B`
- `MEMBER-003` → `CREDIT_UNION_C`
## Docker
```bash
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:
```text
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.
1. **Identify and correlate.** Obtain the request or correlation ID from the report when available and locate the corresponding structured logs. Record the `operation`, `memberId`, authoritative `partnerId` once resolved, `resultCode`, and HTTP status where applicable. Travel history and recommendation payloads are intentionally not logged, so use identifiers and outcome metadata for correlation.
2. **Verify the authoritative tenant.** Retrieve the affected member through `MemberDataService` and confirm that the requested `memberId` equals the returned `member.memberId`. Derive the tenant only from `member.partnerId`. Do not trust a partner ID supplied by a frontend, MCP caller, query parameter, or support report.
3. **Verify partner configuration.** Retrieve configuration using `member.partnerId`, then confirm `configuration.partnerId === member.partnerId`. Inspect `excludedCategories` and `recommendationCap`, and determine whether `cruise` is 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.
4. **Reproduce the pipeline.** Run the member through the same recommendation workflow. A cruise in raw `CandidateGenerator` output is not itself a defect because generation deliberately ignores partner policy. Verify that `RecommendationPolicy` processes `raw candidates → remove excluded categories → apply recommendation cap → final recommendations`, and confirm that cruises are absent from the final result.
5. **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.
6. **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.
7. **Fix and verify.** Correct the defect in the responsible layer and add a regression test reproducing the exact failure. Run:
```bash
npm run typecheck:all
npm test
npm run build
```
Verify the affected partner, at least one unaffected tenant, REST behavior, and MCP behavior when relevant.
8. **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 `MemberDataService` contract and mock implementation.
- Add the read-only `PartnerConfigurationService` contract 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 `CandidateGenerator` independently of partner rules.
- Implement `RecommendationPolicy`, including category exclusions and recommendation caps.
- Enforce the required order: exclusions first, then the cap.
- Add `RecommendationService` orchestration 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_profile` and `get_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:
1. **Real upstream integrations.** Replace the mocked `MemberDataService` and `PartnerConfigurationService` implementations with real arrivia REST clients while preserving the existing service contracts and identity-correlation invariants.
2. **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.
3. **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.
4. **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.
5. **Recommendation intelligence.** Potentially replace or augment the deterministic `CandidateGenerator` with an LLM, ranking model, or richer personalization. `RecommendationPolicy` must remain deterministic and outside the model so generated output cannot override partner rules.
6. **Production observability.** Connect the existing structured events and correlation IDs to arrivia’s approved metrics, tracing, alerting, and operational tooling.
7. **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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues