Interactive flow diagrams showing how the MCP tools chain together for each business process. Click any card to expand the diagram. Tool names shown in teal are the MCP tools used at each step.
Complete reference for all 67 MCP tools. Each tool is callable via JSON-RPC using the tools/call method.
How the TazWorks MCP Server fits within the PCS backend microservices ecosystem.
Comprehensive technical flows with timing, database interactions, and production test coverage. All 202 tests documented with real-world scenarios.
┌─────────────────────────────────────────────────────────────────────────────┐
│ Frontend → API Gateway → Background-Service → Kafka → TazWorks → Database │
│ │
│ PHASE 1: Candidate Creation (< 200ms) │
│ POST /candidates → INSERT candidates (status=NEW) → 201 Created │
│ │
│ PHASE 2: Order Submission (< 500ms, async) │
│ POST /orders → INSERT orders (status=QUEUED) → Publish Kafka event │
│ → 202 Accepted {order_id, status: "queued", progress_pct: 0} │
│ │
│ PHASE 3: TazWorks Submission (2-5s, background consumer) │
│ Consumer polls Kafka → handle_order_requested() │
│ → Check if applicant exists in TazWorks │
│ → [IF NOT] POST /v1/clients/{guid}/applicants │
│ → POST /v1/clients/{guid}/orders {applicantGuid, productCode} │
│ → UPDATE orders (status=SUBMITTED, tazworks_order_guid) │
│ → Publish "pcs.background.order.submitted" │
│ │
│ PHASE 4: TazWorks Processing (10s - 72 hours, external) │
│ TazWorks runs searches: County Criminal, Employment, Education, Driving │
│ Each search transitions: PENDING → IN_PROGRESS → COMPLETE │
│ │
│ PHASE 5: Webhook Notification (< 100ms) │
│ TazWorks → POST /webhooks/tazworks │
│ → Validate HMAC signature │
│ → Publish "pcs.background.webhook.received" │
│ → 200 OK │
│ │
│ PHASE 6: Result Processing (5-30s, background consumer) │
│ Consumer polls webhook topic → handle_webhook_received() │
│ → Find order by tazworks_order_guid │
│ → GET /v1/clients/{guid}/orders/{orderGuid} (order status) │
│ → GET /v1/clients/{guid}/orders/{orderGuid}/searches (all searches) │
│ → FOR EACH COMPLETE SEARCH: │
│ → GET /v1/results/{searchGuid} (raw results) │
│ → ResultParser.parse_XXX_results() (criminal/civil/employment/etc) │
│ → INSERT search_results (raw_data + parsed fields) │
│ → UPDATE searches (status=COMPLETE, result="clear"/"records_found") │
│ → Publish "pcs.background.search.completed" │
│ → Calculate progress_pct = (completed_count / total) * 100 │
│ → UPDATE orders (progress_pct) │
│ → [IF ALL COMPLETE] UPDATE orders (status=COMPLETE, progress_pct=100) │
│ → [IF ALL COMPLETE] Publish "pcs.background.order.completed" │
│ │
│ PHASE 7: Client Retrieves Results (< 200ms cached, < 500ms DB) │
│ GET /orders/{id} → SELECT order + searches → 200 OK │
│ GET /searches/{id}/results → Check Redis → [HIT] Return cached │
│ → [MISS] SELECT search_results → Cache 1h │
│ │
│ Status Transitions: │
│ Order: QUEUED → SUBMITTED → IN_PROGRESS → PARTIAL_RESULTS → COMPLETE │
│ Search: NEW → IN_PROGRESS → COMPLETE │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ FCRA ADVERSE ACTION - Complete Lifecycle with Validation & Time Tracking │
│ │
│ DAY 0: Create & Send Pre-Adverse Notice │
│ POST /adverse-actions │
│ {order_id, reason: "Criminal record...", delivery_method, send_to} │
│ → Validate order has findings (flagged=true) │
│ → INSERT adverse_actions (status=DRAFT, type=PRE_ADVERSE) │
│ → 201 Created {action_id, status: "draft"} │
│ │
│ POST /adverse-actions/{id}/send │
│ → Validate status=DRAFT │
│ → Calculate can_finalize_at = now() + 7 days │
│ → UPDATE adverse_actions (status=SENT, sent_at, can_finalize_at) │
│ → Send email/mail via delivery service │
│ → 200 OK {status: "sent", can_finalize_at} │
│ │
│ DAY 0-7: Waiting Period (FCRA Required) │
│ Candidate receives pre-adverse letter explaining: │
│ - Which records triggered adverse action │
│ - Right to dispute inaccuracies │
│ - 7-day period to respond │
│ - Copy of consumer rights summary │
│ │
│ DAY 3 (Optional): Candidate Disputes │
│ POST /adverse-actions/{id}/dispute │
│ {dispute_notes: "The criminal record is not mine..."} │
│ → Validate status in [SENT, WAITING] │
│ → UPDATE adverse_actions (status=DISPUTED, dispute_received_at, │
│ dispute_notes, disputed=true) │
│ → Trigger reinvestigation workflow │
│ → 200 OK {status: "disputed", dispute_received_at} │
│ │
│ DAY 7+: Finalize Adverse Action │
│ POST /adverse-actions/{id}/finalize │
│ {final_decision: "NOT_HIRED", decision_notes: "..."} │
│ → Validate status in [SENT, WAITING, DISPUTED] │
│ → Validate datetime.now(UTC) >= can_finalize_at │
│ → [FAIL] 400 Bad Request "Cannot finalize before {date}" │
│ → [PASS] UPDATE adverse_actions (status=FINALIZED, finalized_at, │
│ final_decision, dispute_resolved_at) │
│ → Send final adverse action letter │
│ → 200 OK {status: "finalized", finalized_at} │
│ │
│ State Machine: │
│ DRAFT → SENT → [DISPUTED] → FINALIZED │
│ ↓ │
│ WAITING (after 7 days) │
│ │
│ Validation Rules: │
│ ✅ Cannot send unless status=DRAFT │
│ ✅ Cannot dispute unless status in [SENT, WAITING] │
│ ✅ Cannot finalize unless status in [SENT, WAITING, DISPUTED] │
│ ✅ Cannot finalize before 7-day waiting period elapses │
│ ✅ Timezone normalization (SQLite naive → PostgreSQL aware) │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ORDER ATTACHMENTS - FCRA Authorization Forms & Supporting Documents │
│ │
│ Step 1: Get QuickApp Signed Disclosure │
│ GET /v1/clients/{guid}/orders/{orderGuid}/attach/quickapp │
│ ?type=APPLICANT_AUTHORIZATION │
│ → Returns base64-encoded signed disclosure form │
│ → Includes applicant e-signature and timestamp │
│ → Response: {base64_content, created_at, order_guid} │
│ │
│ Step 2: Upload Authorization Form (Multipart) │
│ POST /v1/clients/{guid}/orders/{orderGuid}/attach │
│ { │
│ name: "FCRA Authorization Form", │
│ originalFileName: "authorization.pdf", │
│ encodedContent: "base64...", │
│ isAuthorizationForm: true, │
│ isCraOnly: false // False = visible to client │
│ } │
│ → TazWorks stores attachment │
│ → Returns {attachment_guid, upload_timestamp} │
│ │
│ Step 3: Upload Supporting Documents (Base64) │
│ POST /v1/clients/{guid}/orders/{orderGuid}/attach │
│ { │
│ name: "Driver License Copy", │
│ encodedContent: "data:image/jpeg;base64,...", │
│ type: "SUPPORTING_DOCUMENT" │
│ } │
│ │
│ Step 4: List All Attachments │
│ GET /v1/clients/{guid}/orders/{orderGuid}/attachments │
│ → Returns [{attachment_guid, name, file_size, uploaded_at}, ...] │
│ │
│ Step 5: Delete Attachment (CRA API Only) │
│ DELETE /v1/clients/{guid}/orders/{orderGuid}/attach/{attachmentGuid} │
│ → Requires CRA API permissions │
│ → 403 Forbidden if insufficient permissions │
│ → 204 No Content on success │
│ │
│ Attachment Types: │
│ - APPLICANT_AUTHORIZATION (FCRA disclosure) │
│ - AUTHORIZATION_FORMS (consent forms) │
│ - E_SIGNATURE (digital signatures) │
│ - SUPPORTING_DOCUMENT (ID copies, etc.) │
│ - CUSTOM_DOCUMENT (client-defined) │
│ │
│ MCP Tools: │
│ • get_order_disclosure_forms (client, order, type) │
│ • upload_authorization_form (client, order, filename, content, cra_only)│
│ • upload_base64_document (client, order, name, base64, is_auth) │
│ • list_order_attachments (client, order) │
│ • delete_order_attachment (client, order, attachment) [CRA only] │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ADVANCED ORDER OPERATIONS - Modify Orders Post-Creation │
│ │
│ 1. Add Searches to Existing Order │
│ POST /v1/clients/{guid}/orders/{orderGuid}/searches │
│ [ │
│ { │
│ type: "STATE_CRIMINAL_COURT", │
│ jurisdiction: {state: "CA", county: "Los Angeles"}, │
│ required: true, │
│ priority: 1 │
│ }, │
│ { │
│ type: "PROFESSIONAL_LICENSE", │
│ values: {licenseType: "RN", state: "CA"} │
│ } │
│ ] │
│ → TazWorks adds searches to order │
│ → Triggers additional screening workflows │
│ → Returns {order_guid, searches_added: 2, total_searches: 7} │
│ │
│ 2. Add Notes to Order │
│ PUT /v1/clients/{guid}/orders/{orderGuid}/notes │
│ { │
│ client_notes: "Candidate explained gap in employment", │
│ report_notes: "Internal: check with previous employer" // CRA only │
│ } │
│ → client_notes visible to all parties │
│ → report_notes visible to CRA only (restricted) │
│ → Useful for documenting adjudication decisions │
│ │
│ 3. Get Results PDF Download Link │
│ GET /v1/clients/{guid}/orders/{orderGuid}/resultsAsPdf │
│ → Returns {pdf_url: "https://...", expires_at: "2026-02-08T..."} │
│ → Direct download link (no re-fetch of result data) │
│ → Link expires after 1 hour │
│ │
│ 4. Set Hiring Decision │
│ POST /v1/clients/{guid}/orders/{orderGuid}/decision │
│ {decision_guid: "uuid-of-decision-template"} │
│ → Records adjudication outcome (hired/not_hired/pending) │
│ → Tracks who made decision and when │
│ → Can trigger automated workflows (adverse action, notifications) │
│ │
│ 5. Get Candidate Order History │
│ GET /v1/clients/{guid}/applicants/{applicantGuid}/orders │
│ → Returns all historical orders for candidate │
│ → Useful for annual re-screening │
│ → [{order_guid, status, product, completed_at}, ...] │
│ │
│ MCP Tools: │
│ • add_background_searches (client, order, search_types[]) │
│ • add_report_notes (client, order, client_notes, report_notes) │
│ • get_results_pdf_link (client, order) │
│ • set_hiring_decision (client, order, decision_guid) │
│ • get_candidate_order_history (client, applicant) │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLIENT PREFERENCES - Configuration & Drug Testing Provider Settings │
│ │
│ General Preferences (PII Masking & SLA) │
│ GET /v1/clients/{guid}/preferences/general │
│ → {mask_ssn: true, mask_year_of_birth: true, mask_drivers_license: true,│
│ service_level_agreement_hours: 24, enable_auto_archive: false} │
│ │
│ PUT /v1/clients/{guid}/preferences/general │
│ {mask_ssn: false, service_level_agreement_hours: 48} │
│ → Cache invalidated (preferences:{client}:general) │
│ → 200 OK {updated preferences} │
│ │
│ Order Preferences (Email Notifications) │
│ GET /v1/clients/{guid}/preferences/order │
│ → {send_completed_email_timing: "UPON_COMPLETION_OF_REPORT", │
│ notify_if_flags_are_present: true, │
│ notify_when_flags_are_present_email: "alerts@company.com", │
│ allow_client_to_add_searches: false} │
│ │
│ Drug Testing Provider Settings (I3Screen, Quest, ClearMD, EScreen) │
│ GET /v1/clients/{guid}/drug-providers/i3screen │
│ → {organization_id: "...", location_code: "...", │
│ manual_permitted: true, sso_permitted: true, │
│ applicant_invite_permitted: false, │
│ send_status_to_user_guids: ["user-guid-1"], │
│ copy_status_to_pending_notes: true} │
│ │
│ PUT /v1/clients/{guid}/drug-providers/quest │
│ {preferred_network: true, preferred_fee: 45.00, │
│ include_patient_service_centers: true, │
│ send_copy_of_donor_passport: true, │
│ copy_of_donor_passport_send_email: "hr@company.com"} │
│ → Cached for 600 seconds (preferences rarely change) │
│ → Cache key: preferences:{client}:quest │
│ │
│ Report Decision Tool │
│ GET /v1/clients/{guid}/preferences/report-decision │
│ → {enable_report_decision_tool: true, │
│ report_decision_tool_style: "GRAPHICAL", // or "TEXTUAL" │
│ email_all_decisions_to_requestor: true, │
│ require_decision_before_release: false} │
│ │
│ Caching Strategy: │
│ - TTL: 600 seconds (10 minutes) │
│ - Key pattern: preferences:{client_guid}:{category} │
│ - Invalidation: On PUT operations │
│ - Cache miss: Fetch from TazWorks API │
│ │
│ MCP Tools (7): │
│ • get_client_general_preferences (client) │
│ • update_ssn_masking (client, mask_ssn, mask_yob, mask_dl) │
│ • get_order_notification_settings (client) │
│ • update_order_notifications (client, settings{}) │
│ • get_drug_provider_settings (client, provider) │
│ • configure_drug_testing_provider (client, provider, settings{}) │
│ • get_report_decision_workflow (client) │
└─────────────────────────────────────────────────────────────────────────────┘
Scenario: Order 3 different packages for 10 candidates across 5 states
- Create 10 candidates (varied locations: CA, NY, TX, FL, IL)
- Submit 30 orders (10 candidates × 3 packages)
• Package A: County Criminal + Employment Verification
• Package B: State Criminal + Education Verification
• Package C: Federal Criminal + Professional License
- Simulate concurrent webhook arrivals for all 30 orders
- Track progress for all orders simultaneously
- Verify multi-tenant isolation (3 orgs, same names)
Expected Results:
✅ All 30 orders transition: QUEUED → SUBMITTED → IN_PROGRESS → COMPLETE
✅ No cross-contamination between orgs
✅ Correct search type matching (90 total searches)
✅ Progress calculations accurate (0% → 100%)
✅ All Kafka events published (order.requested, order.submitted, search.completed, order.completed)
File: background-service/tests/e2e/test_bulk_multi_candidate_screening.py
Status: Requires full integration environment (TazWorks API + Kafka)
Scenario: 50 webhooks arriving simultaneously, testing race conditions
- Create 1 order with 5 searches
- Simulate 50 concurrent webhooks using asyncio.gather():
• 10 webhooks for search 1 (COMPLETE)
• 10 webhooks for search 2 (COMPLETE)
• 10 webhooks for search 3 (COMPLETE)
• 10 webhooks for search 4 (IN_PROGRESS)
• 10 webhooks for search 5 (IN_PROGRESS)
- Test database locking and transaction isolation
- Verify no duplicate SearchResult records created
- Test idempotency (same webhook received multiple times)
Expected Results:
✅ Only 1 SearchResult per search (no duplicates)
✅ Final order status correctly reflects all searches
✅ Progress percentage accurate despite race conditions
✅ No database deadlocks or conflicts
✅ Idempotent processing (repeated webhooks don't cause errors)
Race Conditions Tested:
- Same order, multiple searches completing simultaneously
- Out-of-order webhook arrival (COMPLETE before IN_PROGRESS)
- Duplicate webhooks for same search
- Concurrent progress_pct calculations
File: background-service/tests/e2e/test_concurrent_webhook_processing.py
Status: Tests database transaction handling and concurrent processing
Scenario: Complete adverse action cycle with 7-day waiting period
- Create order with findings (flagged=true)
- Create pre-adverse action
- Send pre-adverse letter (starts 7-day clock)
- [Time mocked] Advance 3 days
- Candidate disputes findings
- [Time mocked] Advance 5 more days (total 8 days)
- Finalize with decision (NOT_HIRED)
- Verify all state transitions and timestamps
Expected Results:
✅ Status transitions: DRAFT → SENT → DISPUTED → FINALIZED
✅ can_finalize_at calculated correctly (sent_at + 7 days)
✅ Finalize rejected if called before 7 days (400 Bad Request)
✅ Finalize succeeds after waiting period
✅ All timestamps recorded correctly (sent_at, dispute_received_at, finalized_at)
✅ Timezone handling works (SQLite naive → PostgreSQL aware)
State Machine Tested:
DRAFT ──send()──> SENT ──wait 7d──> WAITING ──dispute()──> DISPUTED ──finalize()──> FINALIZED
✅ ✅ ✅ ✅ ✅
FCRA Compliance:
✅ 7-day waiting period enforced
✅ Dispute mechanism functional
✅ Final decision recorded with notes
File: background-service/tests/e2e/test_fcra_adverse_action_full_workflow.py
Status: Uses freezegun for time mocking, tests FCRA compliance
Scenario: Various webhook payload errors to test graceful degradation
- Missing required fields (order_guid, search_guid, status)
- Invalid UUIDs (malformed, wrong format)
- Wrong enum values (status="INVALID_STATUS")
- Null values where not expected
- Mismatched search types (TazWorks returns type not in our database)
Test Cases:
1. Webhook with missing order_guid → Early return, log warning, no crash
2. Webhook with invalid UUID → Validation error, 422 response
3. Status not in enum → Default to "UNKNOWN", log warning
4. Null search results → Skip result parsing, mark search complete without data
5. Search type mismatch → Log debug message, continue processing other searches
Expected Results:
✅ No service crashes or exceptions propagated to client
✅ All errors logged with context (order_id, search_guid, error details)
✅ Partial processing continues (don't fail entire batch for one error)
✅ Database state remains consistent (no half-updated records)
✅ Kafka events published for successful parts
Error Handling Patterns Tested:
- Try/except with specific error logging
- Graceful degradation (skip invalid, continue processing)
- Validation at API boundary (Pydantic schemas)
- Database rollback on transaction failures
File: background-service/tests/e2e/test_malformed_payload_handling.py
Status: Tests production-grade error resilience
Scenario: Single order with 7 searches, mixed outcomes
- Search 1 (County Criminal) → COMPLETE, clear ✅
- Search 2 (State Criminal) → COMPLETE, clear ✅
- Search 3 (Employment) → COMPLETE, verified ✅
- Search 4 (Federal Criminal) → COMPLETE, records_found, flagged=true 🚩
- Search 5 (Education) → COMPLETE, unable_to_verify ⚠️
- Search 6 (Driving) → FAILED, network error ❌
- Search 7 (License) → TIMEOUT, slow search ⏱️
Expected Results:
✅ Order reaches PARTIAL_RESULTS state (not COMPLETE)
✅ Progress: 71% (5 complete / 7 total)
✅ Flagged searches marked correctly
✅ Failed searches logged, don't block other searches
✅ Timeout handling (mark as PENDING, retry later)
✅ Client receives partial results via API
Progress Calculation Edge Cases:
- 0 searches complete: 0%
- 1 of 7 complete: 14%
- 5 of 7 complete: 71%
- 7 of 7 complete: 100%
- Division by zero guard (no searches)
File: background-service/tests/e2e/test_partial_results_with_failures.py
Status: Tests real-world failure scenarios and partial result handling
Scenario: Comprehensive result parsing for all search types
- Criminal Results (7 types):
• County Criminal, State Criminal, Federal Criminal
• International Criminal, National Criminal Database
• Sex Offender Registry, National Criminal Database Alias
- Civil Results (4 types):
• County Civil, Federal Civil
• Liens & Judgments, Bankruptcy Filings
- Employment Verification:
• Verified with dates, position, reason for leaving
• Unable to verify (company closed, no records)
- Education Verification:
• Degree verified (BS, MS, PhD)
• Attendance verified but no degree
• Unable to verify (school closed)
- Professional License:
• Active license (RN, MD, CPA, etc.)
• Expired license
• License not found
- Driving Records (2 types):
• Instant Driving Record (MVR)
• Commercial Driver's License (CDL)
Parser Logic Tested:
✅ records vs record fallback (API inconsistency)
✅ Nested field extraction (court.name, defendant.address.city)
✅ Date parsing (ISO 8601 format)
✅ Status mapping ("VERIFIED" → verified=true)
✅ Result categorization (clear, records_found, verified, unable_to_verify)
✅ Flagging logic (criminal/civil with records → flagged=true)
File: background-service/tests/e2e/test_result_parser_all_types.py
Status: Tests all 6 result parser functions with realistic payloads
| Service | Test Type | Count | Status |
|---|---|---|---|
| background-service | Unit Tests | 19 | ✅ All Pass |
| background-service | Integration Tests | 36 | ✅ All Pass |
| background-service | E2E Tests | 17 | ⚠️ Need Infrastructure |
| tazworks-mcp | Tool Tests | 86 | ✅ All Pass |
| tazworks-mcp | Client Tests | 9 | ✅ All Pass |
| tazworks-mcp | New Tool Tests | 35 | ✅ All Pass (attachments, preferences, advanced orders) |
| TOTAL | 202 | 185 Pass, 17 Require Infrastructure | |
Complete breakdown of all 175 TazWorks API endpoints and implementation status.
| Category | Documented | Implemented | Coverage | Status |
|---|---|---|---|---|
| Orders (Core) | 13 | 13 | 100% | ✅ Complete |
| Order Attachments | 4 | 4 | 100% | ✅ Complete (P1) |
| Advanced Orders | 5 | 5 | 100% | ✅ Complete |
| Applicants | 16 | 16 | 100% | ✅ Complete |
| Searches | 5 | 5 | 100% | ✅ Complete |
| Webhooks | 5 | 5 | 100% | ✅ Complete |
| Client Preferences | 20 | 13 | 65% | ✅ Core Complete (P2) |
| Disclosure Settings | 6 | 4 | 67% | ⚠️ Missing jurisdiction tags |
| Client Users | 9 | 5 | 56% | ⚠️ Missing delete, link, add permissions |
| Client Products | 14 | 3 | 21% | ❌ Missing CRUD operations |
| Co-Applicants | 2 | 0 | 0% | ❌ Schemas ready, no client methods |
| QuickApp Config | 9 | 0 | 0% | ❌ Schemas ready, no client methods |
| Applicant Portal | 4 | 0 | 0% | ❌ Schemas ready, no client methods |
| Billing & Fees | 11 | 0 | 0% | ❌ Not implemented |
| Client Management | 5 | 2 | 40% | ⚠️ Read-only, no CRUD |
| General/Utility | 3 | 0 | 0% | ❌ ISO countries, usage stats |
| TOTAL | 175 | 119 | 68% | +22% this session |
For complete technical details, see:
docs/api/FLOWS.md - All workflow diagrams with timing, caching, error pathsdocs/api/COVERAGE.md - Complete API endpoint inventory and implementation roadmapCLAUDE.md - Project context, architecture overview, development guidelinesbackground-service/tests/e2e/README.md - E2E test setup and infrastructure requirementstazworks-mcp/app/schemas/SCHEMA_COVERAGE.md - Pydantic model usage and examples