26 KiB
Technical Research: Mock GDS MCP Server
Branch: 001-mock-gds-server | Date: 2026-04-07
Phase 0: Technology Decisions
Decision 1: MCP SDK and Protocol Implementation
Decision: Use @modelcontextprotocol/sdk official Node.js SDK
Rationale:
- Official SDK ensures MCP protocol compliance (Constitution Principle I)
- Handles JSON-RPC 2.0 message format automatically
- Provides TypeScript types for type safety
- Active maintenance by Anthropic
- Simplified tool registration and schema management
Alternatives Considered:
- Custom MCP implementation: Rejected - high risk of protocol non-compliance, significant development effort
- Python MCP SDK: Rejected - requirement specifies Node.js with minimal dependencies
Implementation Notes:
- SDK provides
Serverclass for initialization - Tool handlers registered via
server.setRequestHandler - Automatic capability negotiation during handshake
- Built-in error handling with standard MCP error codes
Decision 2: Valkey Client Library
Decision: Use ioredis v5.x as Valkey client
Rationale:
- Valkey is Redis protocol-compatible, ioredis is the most mature Node.js Redis client
- Full support for Redis/Valkey commands (GET, SET, HSET, EXPIRE, etc.)
- Connection pooling and automatic reconnection
- Cluster and sentinel support (for future scaling)
- Pipeline and transaction support
- Active maintenance and TypeScript support
Alternatives Considered:
- node-redis: Rejected - ioredis has better TypeScript support and more features
- Custom Valkey protocol: Rejected - unnecessarily complex
- No persistence (memory-only): Rejected - requirement specifies Valkey for persistence
Implementation Notes:
- Use Redis-compatible commands only (avoid Redis-specific extensions)
- Session data stored with TTL (e.g., 1 hour default)
- Key naming:
gds:session:{sessionId}:bookings:{pnr} - Use hash structures for complex objects (bookings, searches)
- Enable Valkey RDB/AOF persistence via docker-compose configuration
Decision 3: Minimal Dependencies Strategy
Decision: Limit external dependencies to essentials only
Core Dependencies (production):
@modelcontextprotocol/sdk- MCP protocol (required)ioredis- Valkey client (required for persistence)pino- Structured logging (minimal, fast, constitution requires observability)
Development Dependencies:
- Node.js native test runner (
node:test) - no jest/mocha overhead c8- code coverage (lightweight)
Rationale:
- Aligns with "minimal libraries" requirement
- Reduces attack surface and dependency maintenance burden
- Faster container builds and smaller images
- Native Node.js features (test runner, assert) are mature and sufficient
Explicitly Avoided:
- ❌ Express/Fastify/Koa - MCP uses stdio/SSE, no HTTP server needed
- ❌ TypeORM/Prisma - direct Valkey commands sufficient for key-value storage
- ❌ Jest/Mocha - native test runner adequate
- ❌ Lodash/Ramda - native JS methods sufficient
- ❌ Moment.js/date-fns - native Date and Temporal API (when available)
Decision 4: Docker Build Strategy
Decision: Use Docker Buildx with docker-bake.hcl for multi-platform builds
Rationale:
docker buildx bakesupports complex build configurations- Multi-platform builds (linux/amd64, linux/arm64) in single command
- Build matrix for multiple tags (latest, version, dev)
- Better caching and parallelization than traditional Dockerfile
- Aligns with requirement specification
Build Configuration:
# docker-bake.hcl
target "default" {
dockerfile = "docker/Dockerfile"
tags = ["gds-mock-mcp:latest"]
platforms = ["linux/amd64", "linux/arm64"]
cache-from = ["type=registry,ref=gds-mock-mcp:buildcache"]
cache-to = ["type=registry,ref=gds-mock-mcp:buildcache,mode=max"]
}
Dockerfile Strategy:
- Multi-stage build: builder → production
- Builder stage: install dependencies, run tests
- Production stage: copy only production deps and source
- Use Node.js 20 Alpine for minimal image size
- Non-root user for security
- Health check via MCP ping or valkey connection test
Alternatives Considered:
- Traditional Dockerfile only: Rejected - buildx bake provides better DX and multi-platform support
- Docker Compose build: Rejected - less flexible than buildx, no multi-platform
- Podman: Rejected - Docker specified in requirements
Decision 5: Mock Data Architecture
Decision: Embed realistic GDS data in JavaScript modules with deterministic generation
Data Structure:
- Airports: ~100 major airports with IATA codes (JFK, LAX, ORD, etc.)
- Airlines: Major carriers with IATA codes (AA, DL, UA, BA, etc.)
- Hotels: 50+ chains/properties across major cities
- Car Rentals: Major companies (Hertz, Avis, Enterprise) with vehicle types
- Flight Routes: Pre-defined routes with realistic times and prices
- Pricing Tiers: Economy ($200-$600 domestic), Business ($800-$2000), First Class ($2500+)
Generation Strategy:
- Deterministic: Same search inputs produce same results (for testing reproducibility)
- Controlled Randomness: Optional seed parameter for demo variety
- Rule-Based Pricing: Distance-based pricing with time-of-day adjustments
- Availability Simulation: Random sold-out scenarios (10% of flights)
Rationale:
- Embedded data = no external dependencies (fast startup)
- Deterministic = reliable integration tests
- Realistic codes = constitution compliance (Principle II)
- Pre-computed routes = sub-2s response times
Alternatives Considered:
- External API (Skyscanner, etc.): Rejected - violates "no external connections" (Constitution Principle III)
- Database seeding: Rejected - overhead, embedded data sufficient for mock scope
- Fully random data: Rejected - testing requires deterministic outputs
Decision 6: Testing Strategy
Decision: Use Node.js native test runner with three-tier test structure
Test Tiers:
-
Unit Tests: Individual tool handlers, data generators, validators
- Fast (<100ms total), isolated, no external dependencies
- Mock Valkey client for session tests
-
Integration Tests: Full MCP workflows with real Valkey (test container)
- End-to-end booking flows (search → book → retrieve → cancel)
- Multi-service workflows (flight + hotel + car)
- Concurrent session isolation tests
- Use docker-compose test profile for Valkey
-
Contract Tests: MCP protocol compliance validation
- Verify JSON-RPC 2.0 format
- Tool schema validation
- Error response structure
Test Execution:
npm test # All tests
npm run test:unit # Fast unit tests only
npm run test:integration # Requires Valkey
npm run test:coverage # Coverage report with c8
Rationale:
- Native test runner = minimal dependencies
- Three tiers = appropriate test coverage
- Docker test containers = realistic integration tests
- Fast unit tests = quick feedback loop
Decision 7: Configuration Management
Decision: Environment variables with secure defaults
Configuration Variables:
# MCP Server
MCP_TRANSPORT=stdio # stdio or sse
MCP_SESSION_TIMEOUT=3600 # 1 hour session TTL
# Valkey
VALKEY_HOST=localhost
VALKEY_PORT=6379
VALKEY_PASSWORD= # Empty for dev, required for prod
VALKEY_DB=0
VALKEY_KEY_PREFIX=gds:
# Logging
LOG_LEVEL=info # silent, error, warn, info, debug, trace
LOG_PRETTY=false # Pretty print for dev
# Mock Data
MOCK_DATA_SEED=fixed # fixed or random
MOCK_RESPONSE_DELAY=0 # Artificial delay (ms) for demo purposes
Security Defaults:
- No production credentials in code or .env.example
- Configuration validation on startup
- Reject production-like patterns (Constitution Principle III)
Rationale:
- Standard practice for containerized apps
- Easy to override in docker-compose
- Secure defaults prevent accidents
Decision 8: PNR Generation Strategy
Decision: Deterministic PNR generation with TEST prefix
Format: TEST-{BASE32} where BASE32 is 6 characters
Example: TEST-A1B2C3
Generation Algorithm:
- Generate session-scoped sequence number
- Combine with booking timestamp
- Hash with SHA-256
- Take first 6 characters of base32 encoding
- Prefix with "TEST-"
Rationale:
- "TEST-" prefix = clear mock indicator (Constitution Principle III)
- Base32 = human-readable, unambiguous (no 0/O, 1/I confusion)
- 6 characters = 1 billion unique combinations (sufficient for testing)
- Deterministic = reproducible test scenarios
- Session-scoped = prevents conflicts
Alternatives Considered:
- Random UUID: Rejected - too long, not human-friendly
- Sequential numbers: Rejected - predictable, not realistic
- No prefix: Rejected - violates safety requirement
Technology Stack Summary
| Component | Technology | Version | Rationale |
|---|---|---|---|
| Runtime | Node.js | 20 LTS | Current stable, long-term support |
| MCP SDK | @modelcontextprotocol/sdk | Latest | Official SDK, protocol compliance |
| Persistence | Valkey | 8.0+ | Redis-compatible, requirement specified |
| Valkey Client | ioredis | 5.x | Mature, feature-rich, TypeScript support |
| Logging | Pino | Latest | Fast, structured, minimal overhead |
| Testing | node:test | Built-in | Native, zero dependencies |
| Coverage | c8 | Latest | V8 coverage, lightweight |
| Container | Docker | 24+ | Buildx support, multi-platform |
| Orchestration | docker-compose | 2.x | Development environment |
Performance Considerations
Expected Performance Profile
- Search Operations: <500ms (data generation + Valkey lookup)
- Booking Operations: <200ms (validation + Valkey write)
- Retrieval Operations: <100ms (Valkey read)
- Concurrent Sessions: 50+ (limited by Valkey and Node.js event loop)
- Memory Footprint: <100MB per server instance
- Container Image Size: <50MB (Alpine-based)
Optimization Strategies
- Caching: Pre-compute common search results in Valkey
- Connection Pooling: ioredis maintains persistent Valkey connections
- Lazy Loading: Load mock data modules on-demand
- Batch Operations: Use Valkey pipelines for multi-key operations
Security Considerations
Mock Data Safety
- ✅ No real API keys or credentials stored
- ✅ Configuration validation rejects production patterns
- ✅ All PNRs prefixed with "TEST-"
- ✅ No external network calls (except to local Valkey)
- ✅ Non-root container user
Docker Security
- Use official Node.js Alpine base images
- Run as non-root user (node:node)
- Minimal attack surface (no shell, no dev tools in prod image)
- Regular security updates via base image updates
Open Questions (Resolved)
All technical unknowns from initial planning have been resolved through research above. No blocking issues identified.
Next Steps
Proceed to Phase 1: Design & Contracts
- Create data-model.md (data structures)
- Define MCP tool contracts in contracts/
- Generate quickstart.md with usage examples
- Update agent context with technology decisions
Remote Access Research (Added 2026-04-07)
This section documents research findings for remote MCP access requirements based on clarifications received after initial planning.
Decision 8: Streamable HTTP Transport Implementation (MCP Specification Compliant)
Decision: Use MCP SDK's StreamableHTTPServerTransport over HTTP/1.1 with Server-Sent Events (SSE)
Question: How to implement remote transport for MCP SDK per official specification?
Investigation Findings:
The MCP specification (2025-11-25) defines Streamable HTTP as the standard remote transport. The MCP SDK (@modelcontextprotocol/sdk v1.0.4) provides official transport implementations:
- StdioServerTransport - stdio for local process communication
- StreamableHTTPServerTransport - Remote HTTP/1.1 using SSE (spec-compliant)
- WebStandardStreamableHTTPServerTransport - Platform-agnostic HTTP
- SSEServerTransport - Deprecated legacy transport
Key Finding: MCP's Streamable HTTP transport uses HTTP/1.1 with Server-Sent Events (SSE), NOT HTTP/2. The specification requires:
- Single endpoint supporting POST, GET, and DELETE methods
- POST for client→server messages (returns SSE stream or 202)
- GET for server→client message stream (optional)
- DELETE for explicit session termination
- SSE for server→client streaming
- Session management via
Mcp-Session-Idheader - Protocol version via
MCP-Protocol-Versionheader (REQUIRED per clarification 2026-04-08) - Origin header validation for security
- SSE polling pattern: Server sends initial event with ID and empty data, MAY close connection after response, clients reconnect using Last-Event-ID, server sends
retryfield before closing
Integration Approaches Evaluated:
Option A: Native HTTP/1.1 + SSE (MCP Spec Compliant) ⭐ SELECTED
Client → Node.js MCP Server (HTTP/1.1 + SSE via StreamableHTTPServerTransport)
- ✅ Direct implementation per MCP specification
- ✅ Zero code complexity - use SDK's
StreamableHTTPServerTransportdirectly - ✅ Single process deployment (no reverse proxy required for spec compliance)
- ✅ Simplified debugging and local development
- ✅ Meets all MCP security requirements (Origin validation, localhost binding)
- ⚠️ Optional: Can add Nginx/Caddy for TLS termination and HTTP/2 upgrade (production enhancement)
Option B: Reverse Proxy with HTTP/2 Upgrade
Client (HTTP/2) → Nginx/Caddy (HTTP/2 → HTTP/1.1) → Node.js MCP Server (HTTP/1.1+SSE)
- ✅ Adds HTTP/2 multiplexing for client connections
- ✅ TLS termination in reverse proxy
- ⚠️ Adds deployment complexity
- ⚠️ Not required for MCP specification compliance
Rationale for Selection:
- MCP specification explicitly defines Streamable HTTP as HTTP/1.1 + SSE
- HTTP/2 is an optional enhancement, not a requirement
- Simpler deployment path (single Node.js process)
Implementation Strategy:
// src/transports/streamable-http.js
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import http from 'node:http';
import { randomUUID } from 'node:crypto';
export class HTTPTransport {
constructor(options = {}) {
this.port = options.port || 3000;
this.host = options.host || '127.0.0.1'; // Localhost for proxy
this.mcpTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: false // Use SSE streaming
});
this.server = http.createServer(async (req, res) => {
// CORS, rate limiting, health check middleware
// Then delegate to mcpTransport.handleRequest()
});
}
}
Docker Configuration:
# docker-compose.yaml
services:
mcp-server:
environment:
TRANSPORT_MODE: http
HTTP_PORT: 3000
HTTP_HOST: 127.0.0.1 # Only accessible via nginx
nginx:
image: nginx:alpine
ports:
- "8080:8080" # External HTTP/2 port
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- mcp-server
Nginx Configuration (nginx/nginx.conf):
server {
listen 8080 ssl http2;
server_name localhost;
ssl_certificate /etc/nginx/certs/cert.pem;
ssl_certificate_key /etc/nginx/certs/key.pem;
location /mcp {
proxy_pass http://mcp-server:3000;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
# SSE support
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
}
location /health {
proxy_pass http://mcp-server:3000/health;
}
}
Alternatives: Future enhancement can add native HTTP/2 option for single-binary deployment.
Decision 9: MCP Session Management for Remote Access
Decision: Stateful sessions with Valkey backing, MCP Session ID mapped to Valkey session
Question: How should connection lifecycle and session management work for remote MCP?
Key Distinction: HTTP/2 Stream ≠ MCP Session
- HTTP/2 Stream: Single request/response cycle, multiplexed over one TCP connection
- MCP Session: Persistent identifier (
sessionId) spanning multiple HTTP requests - Valkey Session: Business logic session tracking PNRs, searches, user context
Session Lifecycle:
- Initialize: Client connects → Transport generates sessionId (UUID) → Create Valkey session
- Active: Client makes requests with
MCP-Session-IDheader → Refresh session TTL - Idle: No requests for N minutes → Session remains in Valkey (TTL not expired)
- Expired: TTL reaches zero → Valkey auto-deletes session data
- Reconnect: Client can resume with same
MCP-Session-IDheader
Storage Pattern:
session:{sessionId}:metadata → { createdAt, lastActivityAt, transportType, remoteIP }
session:{sessionId}:searches → Recent search results (optional caching)
session:{sessionId}:pnrs → Set of PNR codes created in this session
pnr:{pnr} → Global PNR storage (not session-namespaced)
Implementation:
// src/session/manager.js
async function handleToolCall(request, sessionId) {
if (!sessionId) throw new Error('Session ID required');
let valkeySession = await sessionManager.getSession(sessionId);
if (!valkeySession) {
valkeySession = await sessionManager.createSession(sessionId);
}
await sessionManager.updateActivity(sessionId);
return await toolHandler(request.params, sessionId);
}
Session Isolation: Each session has isolated Valkey namespace. PNRs stored globally (separate from sessions).
Decision 10: IP-Based Rate Limiting Algorithm
Decision: Sliding Window Counter (Hybrid Approach)
Question: What rate limiting algorithm works for IP-based tracking without authentication?
Algorithms Evaluated:
- Fixed Window: Simple but has burst problem (200 req in 1 second across window boundary)
- Sliding Window Log: Accurate but high memory (stores timestamp per request)
- Sliding Window Counter: Approximation balancing accuracy and performance ⭐ SELECTED
- Token Bucket: Good for burst allowance but complex state management
Selected Algorithm: Sliding Window Counter
- ✅ Prevents large bursts (unlike fixed window)
- ✅ Low memory (2 counters per IP)
- ✅ Simple implementation (no Lua scripts required)
- ✅ Accuracy within 1-2% of perfect sliding window
Implementation:
// src/remote/ratelimit.js
async function checkRateLimit(clientIP, limit = 100, windowSeconds = 60) {
const now = Math.floor(Date.now() / 1000);
const currentWindow = Math.floor(now / windowSeconds);
const previousWindow = currentWindow - 1;
const currentKey = `ratelimit:${clientIP}:${currentWindow}`;
const previousKey = `ratelimit:${clientIP}:${previousWindow}`;
const [currentCount, previousCount] = await Promise.all([
storage.incr(currentKey),
storage.get(previousKey) || 0
]);
if (currentCount === 1) {
await storage.expire(currentKey, windowSeconds * 2);
}
const elapsedInWindow = now % windowSeconds;
const previousWeight = 1 - (elapsedInWindow / windowSeconds);
const estimatedCount = (previousCount * previousWeight) + currentCount;
if (estimatedCount > limit) {
throw new RateLimitError({ limit, current: Math.floor(estimatedCount) });
}
return { allowed: true, remaining: limit - Math.floor(estimatedCount) };
}
Performance: ~3 Valkey ops per request, ~100 bytes per IP, within 1-2% of perfect sliding window.
HTTP Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1712486460
Retry-After: 15
Client IP Extraction:
function getClientIP(request) {
const forwarded = request.headers['x-forwarded-for'];
if (forwarded) return forwarded.split(',')[0].trim();
const realIP = request.headers['x-real-ip'];
if (realIP) return realIP;
return request.socket.remoteAddress;
}
Configuration:
RATE_LIMIT_ENABLED=true
RATE_LIMIT_PER_MINUTE=100
RATE_LIMIT_WINDOW_SECONDS=60
Decision 11: Global PNR Storage with TTL
Decision: Global namespace with SETEX, independent PNR lifecycle from sessions
Question: How to implement global PNR retrieval across sessions with configurable expiration?
Requirements:
- PNRs globally retrievable (any session can retrieve any PNR)
- PNRs expire after TTL (default 1 hour)
- PNR creation session logged but doesn't restrict retrieval
- Session expiration doesn't delete PNRs
Storage Pattern:
Global PNR (not session-scoped):
pnr:TEST-ABC123 → { pnr, status, segments, passengers, createdAt, expiresAt, creatingSessionId }
Session Reference (for listBookings tool):
session:{sessionId}:pnrs → Set<pnr> // PNRs created in this session
Implementation:
// Create PNR
async function createPNR(pnrData, ttlHours = 1) {
const pnr = generatePNR(); // TEST-XXXXXX
const ttlSeconds = ttlHours * 3600;
const pnrRecord = {
pnr,
status: 'confirmed',
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
creatingSessionId: pnrData.sessionId, // For logging only
...pnrData
};
// Store globally with TTL
await storage.setex(`pnr:${pnr}`, ttlSeconds, JSON.stringify(pnrRecord));
// Add to session's created PNRs list
await storage.sadd(`session:${pnrData.sessionId}:pnrs`, pnr);
return pnrRecord;
}
// Retrieve PNR (global, any session)
async function retrieveBooking({ pnr }, sessionId) {
const pnrData = await storage.get(`pnr:${pnr}`);
if (!pnrData) {
throw new NotFoundError(`PNR ${pnr} not found or expired`);
}
return JSON.parse(pnrData);
}
// List PNRs created in session
async function listBookings({ limit = 10 }, sessionId) {
const pnrCodes = await storage.smembers(`session:${sessionId}:pnrs`);
const pnrs = await Promise.all(
pnrCodes.map(async (code) => {
const data = await storage.get(`pnr:${code}`);
return data ? JSON.parse(data) : null;
})
);
return pnrs.filter(Boolean).slice(0, limit);
}
Edge Cases:
- PNR Expired:
retrieveBookingreturns "PNR not found or expired" - Session Expires Before PNR: PNR remains globally retrievable
- List After Session Expiry: Returns empty (session reference deleted)
Configuration:
PNR_TTL_HOURS=1
SESSION_TTL_HOURS=24
Storage Efficiency: ~2KB per PNR, 1000 PNRs = 2MB, auto-cleanup via Valkey TTL.
Decision 12: CORS Configuration
Decision: Permissive Wildcard CORS with Network-Level Access Control
Question: How to configure CORS for web-based MCP clients?
CORS Policy: Access-Control-Allow-Origin: * (wildcard)
Rationale:
- ✅ Maximum compatibility - any web client can connect from any domain
- ✅ Simplifies development - no origin whitelist configuration
- ✅ Enables browser tools, Chrome extensions, web IDEs
- ⚠️ Requires network-level security (firewall, VPN, private network)
- ⚠️ Only acceptable for trusted development/testing environments
Security Implications:
- No Credentials: Wildcard incompatible with
Access-Control-Allow-Credentials: true(acceptable - we have no auth) - Public Data: Any website can make requests (acceptable - test data only)
- CSRF Potential: Limited risk (no authentication, state changes require valid PNR)
- Network Security: Deploy within private networks, use firewall rules
Implementation:
// src/remote/cors.js
export function applyCORS(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', 'false');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, MCP-Session-ID');
res.setHeader('Access-Control-Expose-Headers', 'X-RateLimit-Limit, X-RateLimit-Remaining');
res.setHeader('Access-Control-Max-Age', '86400');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return true; // Handled
}
return false; // Continue
}
Nginx Alternative:
location /mcp {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
return 204;
}
add_header Access-Control-Allow-Origin * always;
proxy_pass http://mcp-server:3000;
}
Security Posture: Wildcard CORS acceptable for mock server because:
- Contains only test data (no sensitive information)
- No authentication (no credentials to steal)
- Network-level access controls provide security boundary
- Maximizes developer flexibility for ad-hoc tooling
Configuration:
CORS_ENABLED=true
CORS_ORIGINS=*
CORS_MAX_AGE=86400
Remote Access Technology Summary
| Component | Technology | Decision |
|---|---|---|
| HTTP/2 Server | Nginx reverse proxy | Terminate HTTP/2, proxy to HTTP/1.1 |
| MCP Transport | StreamableHTTPServerTransport | Over HTTP/1.1 (proxied) |
| Rate Limiting | Sliding window counter | Valkey-backed, IP-based |
| PNR Storage | Global with TTL | Valkey SETEX, independent lifecycle |
| CORS | Wildcard policy | Access-Control-Allow-Origin: * |
| Health Check | Unauthenticated endpoint | /health returning JSON status |
Environment Variables (Remote Mode)
# Transport
TRANSPORT_MODE=stdio|http|both
HTTP_PORT=3000
HTTP_HOST=127.0.0.1
# Rate Limiting
RATE_LIMIT_ENABLED=true
RATE_LIMIT_PER_MINUTE=100
RATE_LIMIT_WINDOW_SECONDS=60
# PNR/Session
PNR_TTL_HOURS=1
SESSION_TTL_HOURS=24
# CORS
CORS_ENABLED=true
CORS_ORIGINS=*
CORS_MAX_AGE=86400
Next Steps
All remote access research complete. Proceed to update:
- ✅ data-model.md - Add RemoteConnection, RateLimitRecord, HealthStatus entities
- ✅ contracts/ - Add health endpoint contract
- ✅ quickstart.md - Add remote access setup instructions