Architecture
Overview
Section titled “Overview”The NEMT MCP Service follows Clean/Hexagonal architecture, mirroring nemt-facility-backend conventions. The service is organized into four layers and now spans eight application modules. 31 tools total after the v2.0 audit remediation (removed get_draft_status, get_cancellation_reasons, create_standing_order; added get_member_phone, get_member_addresses).
(Claude, GPT, AI Agents)"] -->|"HTTP POST /mcp"| B["MCP Transport Layer
internal/mcp/"] B --> B1["RequestID → Logging → BearerAuth → AuditLog"] B1 --> B2["StreamableHTTPHandler
(Stateless mode)"] B2 -->|"tool/call"| C1["🔐 verify
3 tools"] B2 -->|"tool/call"| CM["👤 member
4 tools"] B2 -->|"tool/call"| C2["📋 booking
5 tools"] B2 -->|"tool/call"| C3["📍 address
3 tools"] B2 -->|"tool/call"| C4["🚗 trip
11 tools"] B2 -->|"tool/call"| CE["🪪 enrollment
2 tools"] B2 -->|"tool/call"| CP["🗺️ planning
1 tool"] B2 -->|"tool/call"| CN["✉️ notification
1 tool"] C1 --> D["Shared Infrastructure
internal/pkg/ — Pkg facade"] CM --> D C2 --> D C3 --> D C4 --> D CE --> D CP --> D CN --> D D --> E1[("☁️ Spanner
Member, Trip, Facility data")] D --> E2[("⚡ Valkey
Sessions, Drafts, Cache")] D --> E3["🗺️ Google Maps / Directions / Places"] D --> E4["🌤️ Open-Meteo (weather)"] D --> E5["✉️ SendGrid (email)"] D --> E6["📦 nemt-objects
nemt-trip-service
nemt-standing-order"] style A fill:#4a9eff,color:#fff style B fill:#2d3748,color:#fff style B1 fill:#2d3748,color:#a0aec0 style B2 fill:#2d3748,color:#fff style D fill:#1a202c,color:#fff
Modules
Section titled “Modules”| Module | Tools | Purpose |
|---|---|---|
verify |
3 | Member identity verification + session creation |
member (v2.0) |
6 | PII contact CRUD — email/phone/address (read + INTENT-gated write) |
booking |
4 | Incremental draft assembly + commit (no-op set_booking_details returns status) |
address |
3 | Address resolution, facility search (with Places Details enrichment), reverse geocoding |
trip |
10 | Trip read/edit, cancellation (single-tool two-step), will-call, MR driver assignment, TP assignment, standing orders |
enrollment (v2.0) |
2 | Mileage-reimbursement driver onboarding + PCI banking setup |
planning (v2.0) |
1 | Pre-book estimation (route + weather + suggested pickup) |
notification (v2.0) |
1 | Email confirmations (HTML + standing-order PDFs) |
Layer Details
Section titled “Layer Details”1. MCP Transport Layer (internal/mcp/)
Section titled “1. MCP Transport Layer (internal/mcp/)”Handles HTTP routing, authentication, and middleware. Analogous to internal/grpc/ in facility-backend.
server.go– HTTP handler setup, route wiring (/mcp, /health)auth/– BearerAuth middleware, CachedTokenValidatorinterceptors/– RequestID, StructuredLogging, HIPAA audit loggingports/– TokenCache, TokenDB, HealthChecker interfacesadapters/– APIKeyTokenDB, health probe implementations
API key mapping: MCP_API_KEY_MAPPINGS maps token:org_id:client_name. No lob_id in the token – it is resolved from member data during verify_member.
2. Application Modules (internal/app/)
Section titled “2. Application Modules (internal/app/)”Each module is a bounded context following the hexagonal pattern:
internal/app/{module}/ {module}.go # Module factory: New(Params) -> Module register.go # Tool registration domain/ # Pure domain logic (no I/O) ports/ # Interfaces (contracts) adapters/ repo/ # Data access (wraps nemt-objects) cache/ # Valkey-backed caches (per-module as needed) crypto/ # (enrollment only) AES-GCM encryptor validation/ # (enrollment only) ABA routing validator geocoding/ directions/ weather/ # (address / planning) external clients mcp/handlers/ # MCP tool handlers (thin adapters) usecases/ # Business logic interactorsKey principle: Handlers are thin. They map MCP args to use case input, call the interactor, map output to MCP response. All business logic lives in usecases.
3. Shared Infrastructure (internal/pkg/)
Section titled “3. Shared Infrastructure (internal/pkg/)”Central dependency injection via Pkg facade. New collaborators were added in v2.0 to back the new modules.
internal/pkg/ pkg.go # New(ctx) initializes all deps in order types.go # Pkg struct definition infra/ config/ # tools/config.Load() with secrets db/ # Spanner client valkey/ # Valkey client log/ # Logger integrations/ directions/ # Google Directions client (planning) email/ # SendGrid v3 client (notification) geo/ # Google Geocoding / Places pdf/ # go-pdf/fpdf generator (notification) vendors/ # Nemt vendor clientsPkg fields (v2.0 additions)
EmailClient— SendGrid v3 wrapper used bysend_confirmation. Disabled whenSENDGRID_API_KEYis empty.PDFGenerator— go-pdf/fpdf wrapper used for standing-order trip-schedule PDFs.
Initialization order: env -> config -> logger -> Spanner -> Cache -> nemt-objects -> integrations (directions, email, pdf, geo) -> TripService -> StandingOrder
Close order (reverse): Cache -> Spanner
4. Shared Domain (internal/domain/)
Section titled “4. Shared Domain (internal/domain/)”Types shared across all modules:
session.go– Session struct (Valkey, 1h TTL, with Version for optimistic locking). v2.0 addedLastConfirmedDraft(by-value snapshot of the draft at confirm_trip success),LastTripIDs,LastFriendlyIDs,IsStandingOrder,StandingOrderID, andStandingOrderMeta— populated byconfirm_tripand consumed bysend_confirmationto authorize the follow-up email and prevent cross-session PHI leakage.draft.go– BookingDraft struct (Valkey, 24h TTL, typed Address fields)address.go– Typed Address struct (Street, City, State, ZipCode, Lat, Lng)response.go– ToolResponse, Progress, Guidance, MarshalToolResult, MarshalResult[T]errors.go– ErrorCode taxonomy + the v2.0 INTENT primitive:IntentRequiredErrorcarriesAction,CurrentValues,NewValue, andHint;ErrIntentRequiredSentinelplusUnwrap()lets handlers detect it througherrors.Ischains;BuildIntentRequiredToolError(err)upgrades it into a typedINTENT_REQUIREDToolError.ErrSessionMissingsentinel is here too.session_loader.go– LoadSession, LoadSessionAndDraft, ValidateDraftOwnershiphelpers.go– DerefStr, DerefInt (nil-safe pointer dereference)phone.go– NormalizePhone, ValidatePhoneaudit.go– AuditEntry, AuditWriter interfacecontext.go– Context key accessors (RequestID, SessionID, OrgID)annotations.go– Tool annotation helpers (BoolPtr)
5. Shared Adapters (internal/adapters/)
Section titled “5. Shared Adapters (internal/adapters/)”audit/slog_writer.go– Single HIPAA audit logging implementation (shared across all modules)
Server Instructions (v2.0)
Section titled “Server Instructions (v2.0)”internal/mcp/instructions.go exposes a ~1100-character ServerInstructions constant wired into the MCP SDK via mcpsdk.ServerOptions{Instructions: ServerInstructions}. The SDK ships these instructions in the initialize result, so every connecting AI agent receives them ONCE per session — they are prepended to the agent’s system prompt and never re-sent on subsequent calls.
This replaces the old pattern of duplicating workflow rules across every tool description. The instructions cover:
- SESSIONS — 1-hour TTL,
session_idis required by all stateful tools. - BOOKING FLOW —
verify_member→resolve_address(pickup + dropoff) →set_booking_details→review_trip→confirm_trip. Highlights the no-opset_booking_detailsstatus-snapshot pattern. - INTENT — when
update_member_emailorupdate_member_phonereturnsINTENT_REQUIRED, STOP and ASK the member; never silently overwrite. - CANCEL (single-tool two-step) — call
cancel_trip/cancel_standing_orderWITHOUTreasonto receive the list of valid reasons; call again WITH the chosen reason. - CODES — use the EXACT
treatment_type/service_typecodes returned byverify_member. - HIPAA — tool responses contain PHI; never echo raw PHI to logs or untrusted clients.
Total token cost is paid once per session, not per call. This is the largest payload reduction in the v2.0 audit (~10-25 KB saved per booking session compared with the prior pattern of repeating workflow guidance in every tool description).
Sequence Middleware (v2.0)
Section titled “Sequence Middleware (v2.0)”internal/mcp/interceptors/sequence.go implements SequenceMiddleware, a stateful middleware that:
- Tracks per-session monotonic call sequences. Counters live in a
sync.Mapkeyed bysession_id, each entry carries anatomic.Uint64. Safe under concurrent fan-in. - Has a 90-minute TTL with a 5-minute janitor sweep. A background goroutine (lifetime tied to the app context, CC-2) prunes idle entries. The TTL exceeds the 1-hour session TTL so the counter outlives any live conversation by a buffer.
- Emits
{tool, sequence, session_id, request_id}on everytools/call. Gives multi-turn AI conversations a unique increasing number for cross-call correlation when debugging. - Injects
session_idinto the request context viadomain.WithSessionIDBEFORE the next middleware runs.
The middleware sits in the chain as: BearerAuth → SequenceMiddleware → AuditLogging. The bonus payoff of injecting session_id into context is that AuditLogging now reliably picks up session_id from context rather than re-parsing the JSON-RPC body — fixes the silent gap where audit rows used to lack session_id for tools that loaded the session inside the use case rather than the handler.
INTENT Primitive (v2.0)
Section titled “INTENT Primitive (v2.0)”The domain.IntentRequiredError + ErrIntentRequiredSentinel pair (in internal/domain/errors.go) gives every member-mutation use case a single way to surface the “existing primary would be silently overwritten” condition. The error carries the current values, the new value, and a hint; ClassifyAndBuildError recognizes it through wrap chains via errors.As and emits a typed INTENT_REQUIRED ToolError with a stock recovery suggestion (“Ask the member: replace, add_secondary, or add_as_primary…”). Used by update_member_email and update_member_phone. (update_member_address does not use INTENT — Place rows lack a “primary” concept; dedup + home-exclusivity cover the same ground.)
Module Dependencies
Section titled “Module Dependencies”No module imports another module directly. Shared state (sessions, drafts) is accessed through port interfaces backed by the same Valkey cache.
v2.0 Integrations
Section titled “v2.0 Integrations”| Integration | Location | Used By |
|---|---|---|
| Google Directions | internal/pkg/integrations/directions/ |
planning (estimate_trip) |
| Google Places Text Search | internal/pkg/integrations/geo/ |
address (find_place) |
| Google Reverse Geocoding | internal/pkg/integrations/geo/ |
address (get_location_info, update_member_address) |
| Open-Meteo | internal/app/planning/adapters/weather/ |
planning (estimate_trip) |
| SendGrid v3 | internal/pkg/integrations/email/ |
notification (send_confirmation) |
| go-pdf/fpdf | internal/pkg/integrations/pdf/ |
notification (standing-order attachments) |
Configuration Secrets
Section titled “Configuration Secrets”v2.0 introduced the following secrets (pulled via tools/config/secret, validated at startup):
PAYEE_ENCRYPTION_KEY– Base64-encoded 32-byte AES-256 key used byinternal/app/enrollment/adapters/crypto/to encrypt account numbers and tax IDs. Required forsetup_payee.SENDGRID_API_KEY– SendGrid v3 API key. When empty,send_confirmationreturns a configuration error (fail-safe).EMAIL_FROM_ADDRESS– Sender address for confirmation emails.EMAIL_FROM_NAME– Sender display name.
Extending with New Tools
Section titled “Extending with New Tools”- Create new module directory:
internal/app/{name}/ - Define domain types in
domain/ - Define port interfaces in
ports/ - Implement adapters in
adapters/ - Write use case interactors in
usecases/ - Create MCP handler in
adapters/mcp/handlers/(ormcp_handlers.gofor small modules) - Wire module in
{name}.go/module.go - Register tools in
register.go - Add module to
app.goandtools.RegisterAll()