v1.0 — Architecture & delivery plan (ACCEPTED)
Supersedes v0.1-discovery . Data model and corpus: v0.2 .
Change history: CHANGELOG . Decisions with lasting consequences: docs/adr/ .
1. What we are building
LedgerLens MCP — a production-grade Model Context Protocol server for Kosha Financial , a fictional Indian neobank. AI agents use it to:
look up customers, accounts, KYC state and transactions (synthetic, regulator-realistic data);
investigate AML alerts and screen entities against sanctions/PEP lists;
raise disputes and flag transactions (idempotent, audited writes);
search ~85 internal SOPs, policies and API docs across 9 departments (hybrid RAG with citations).
It exists to demonstrate production engineering: OAuth 2.1 auth, scope-level authorization, PII masking, resilience patterns, observability, IaC, CI/CD and a complete test pyramid.
2. Decision summary
Area
Decision
Language / tooling
Python 3.13, uv workspace monorepo, poethepoet tasks, ruff, pyright (strict), pytest + hypothesis
MCP server
FastMCP , Streamable HTTP, stateless JSON-response mode
Domain API
FastAPI "Core Banking API" — the MCP gateway calls it over HTTP
Third-party sim
screening-sim — FastAPI sanctions/PEP vendor with tunable latency & failure
Compute
AWS Lambda container images (arm64) + Lambda Web Adapter ; same image runs in docker compose
Edge
CloudFront → API Gateway HTTP API → Lambda. No ALB (API Gateway is the managed front door)
Auth
MCP Authorization spec: server = OAuth 2.1 Resource Server, RFC 9728 metadata; Cognito via FastMCP's Cognito/OAuth-proxy provider; scopes + roles; JWT validated in-app
System of record
RDS PostgreSQL 16 db.t4g.micro single-AZ + pgvector
Hot control state
DynamoDB : per-client token-bucket rate limits, breaker force-open overrides
Embeddings
OpenAI text-embedding-3-large @ 1024 dims , HNSW; key in Secrets Manager
Retrieval
Hybrid pgvector + Postgres FTS fused by RRF in one SQL query; RLS on document classification; degrades to FTS-only
Async
SQS + DLQ : (1) document ingestion, (2) transactional-outbox domain events. No Kafka
Network
VPC, private subnets for Lambda + RDS, NAT instance (t4g.nano), free S3/DynamoDB gateway endpoints
IaC
AWS CDK (Python) + cdk-nag
Deploy
GitHub Actions → OIDC → cdk deploy; CodeDeploy canary (10% for 5 min) on Lambda aliases with alarm-based automatic rollback
Observability (AWS)
Powertools for AWS Lambda (logs, metrics, idempotency) + OpenTelemetry → X-Ray; CloudWatch dashboard & alarms
Observability (local)
OpenTelemetry Collector → Prometheus + Grafana + Tempo , provisioned dashboards (compose profile observability)
Agent observability
Langfuse (LangGraph agent traces)
Clients
LangGraph "Kosha Ops Copilot" (primary), Google ADK example, Streamlit console UI, Claude Desktop config
Load testing
Locust (Python) — replaces k6 to keep one language
Docs
MkDocs Material on GitHub Pages; Mermaid + Excalidraw + animated SVG infographics
Region / account
Single AWS project, Region eu-north-1, FREE plan
Budget
≈ $25/month while running; poe pause / poe resume stop RDS + NAT when not demoing
Tool
Verdict
Reason
ngrok
Dev-only, optional (poe tunnel)
Lets claude.ai reach a local server during development. Never part of production; the deployed CloudFront URL replaces it
Prometheus + Grafana
Yes — locally
Vendor-neutral OTel instrumentation feeds Grafana dashboards in compose (great screenshots). In AWS, Amazon Managed Grafana is not available on the plan and self-hosting costs money, so the same OTel data goes to CloudWatch/X-Ray
Load balancer (ALB)
No
~$18/month for nothing API Gateway + CloudFront don't already provide for Lambda
CodeDeploy
Yes
Canary traffic shifting + automatic rollback on CloudWatch alarms — real production deployment safety at near-zero cost
CodePipeline
No
GitHub Actions is the pipeline; adding a second orchestrator adds nothing
Kafka / MSK
No
Paid-plan only, ~$100s/month, no streaming workload
3. Deployables
Deployable
Kind
Responsibility
mcp-gateway
FastMCP on Lambda
MCP protocol, OAuth resource server, scope checks, masking, audit, rate limiting, resilience toward downstreams
core-api
FastAPI on Lambda
Domain logic & persistence: customers, accounts, ledger, KYC, AML, disputes, KB retrieval, outbox
screening-sim
FastAPI on Lambda
Simulated external sanctions/PEP vendor with chaos knobs
ingest-worker
Lambda (SQS)
Parse → chunk → embed → upsert documents
outbox-relay
Lambda (scheduled)
Publish ops.outbox rows to SQS exactly-once-effect
events-worker
Lambda (SQS)
Dispute SLA timers, review queue fan-out
4. MCP surface
Tools — Customer & KYC: search_customers, get_customer_profile, get_kyc_status, list_kyc_documents · Accounts & payments: get_account_summary, search_transactions, get_transaction, get_spending_insights · Risk: list_aml_alerts, get_aml_alert, screen_entity · Writes: raise_dispute, flag_transaction_for_review · Knowledge: search_knowledge_base, get_document · Ops: get_service_health
Resources — kb://{department}/{doc_code}, customer://{customer_id}
Prompts — investigate_suspicious_activity, kyc_periodic_review, dispute_triage
Scopes and roles
Scope
support_agent
analyst
compliance_officer
demo_readonly
customers:read
✓
✓
✓
✓
transactions:read
✓
✓
✓
✓
kyc:read
✓
✓
✓
✓
kyc:pii (unmasked PAN/contact)
✓
aml:read
✓
✓
screening:run
✓
✓
disputes:write
✓
✓
reviews:write
✓
✓
kb:read (classification ceiling)
internal
confidential
restricted
public
5. Resilience design
Pattern
Where
Notes
Deadline propagation
every hop
Request budget set at gateway; each downstream call gets min(remaining − margin, per-call cap)
Timeouts
httpx, psycopg, OpenAI
Connect / read / pool timeouts set explicitly; never library defaults
Retry
idempotent calls only
Exponential backoff + full jitter, max attempts, retry budget (≤ 10% extra load)
Circuit breaker
per downstream, per instance
closed → open → half-open; failure-rate window; DynamoDB force-open override shared across instances
Bulkhead
per downstream
asyncio.Semaphore caps; Lambda reserved concurrency caps DB connections
Rate limiting
per OAuth client
DynamoDB token bucket (60 req/min, burst 20) → 429 + Retry-After
Idempotency
write tools
Key + request hash stored in the same Postgres transaction as the write
Load shedding
gateway
Reject early when in-flight > limit or remaining budget too small
Graceful degradation
KB search, screening
FTS-only search when OpenAI fails; screen_entity returns status: deferred when vendor breaker open
Fault injection
gateway + sim
Signed chaos header / config: latency, errors, timeouts per dependency
6. SLOs
Indicator
Target
p95 latency, warm, simple read tools
< 300 ms
p95 latency, search_knowledge_base
< 900 ms
Cold start
< 2.5 s
Availability (error budget)
99.5%
Rate limit per client
60 req/min, burst 20
7. Repository layout
ledgerlens-mcp/
├── packages/ # importable libraries (no I/O at import time)
│ ├── kosha-domain/ # money, ids, statuses & state machines, validators, masking
│ ├── kosha-resilience/ # deadline, retry, breaker, bulkhead, rate-limit interfaces
│ ├── kosha-observability/ # logging, tracing, metrics setup shared by services
│ ├── kosha-datagen/ # deterministic synthetic data generator
│ └── kosha-kb/ # parsing, chunking, embedding, hybrid retrieval
├── services/
│ ├── core-api/ # FastAPI + Alembic migrations
│ ├── mcp-gateway/ # FastMCP
│ ├── screening-sim/ # FastAPI vendor simulator
│ └── workers/ # ingest-worker, outbox-relay, events-worker
├── agents/ # LangGraph copilot, ADK example, Streamlit console
├── infra/ # AWS CDK app (Python)
├── corpus/ # SOP / policy / API source documents
├── tests/ # cross-service e2e, contract, load (Locust), chaos
├── docker/ # shared Dockerfile, compose overrides, observability config
└── docs/ # MkDocs site: HLD, LLD, ADRs, plans, diagrams
8. Test strategy
Layer
Scope
Tooling
Unit
domain rules, validators, masking, state machines, resilience primitives
pytest, hypothesis (property-based)
Integration
repositories, migrations, SQL retrieval, outbox, SQS consumers
testcontainers (Postgres+pgvector), LocalStack
Tool
every MCP tool via FastMCP in-memory client with real core-api
pytest-asyncio
Contract
tool/resource/prompt schemas snapshot; OpenAPI snapshot
syrupy
E2E
deployed stack, real Cognito tokens, every tool
pytest + MCP client
Load / chaos
SLO verification; breaker open → half-open → close
Locust, chaos header
RAG eval
recall@k, MRR, faithfulness on golden set
pytest + DeepEval
Per-tool failure matrix (each row tested for each tool where applicable): missing / expired / wrong-audience token · missing scope · masking per role · invalid input · not found · empty result · oversized result truncation · tampered cursor · downstream timeout · downstream 5xx · breaker open · rate limited · idempotent replay · idempotency conflict · concurrent duplicate · OpenAI outage → degraded · DB pool exhausted · prompt-injection content in KB results.
Coverage gate: 90% lines on packages/ and services/.
9. Delivery phases
Phase
Delivers
Exit criteria
P0
Repo, uv workspace, CI/CD, Docker build, HLD/LLD, ADRs, glossary, docs site shell, CLAUDE.md
CI green; images build; docs build
P1
Generator, schema + Alembic migrations, compose stack
poe seed loads full dataset; ledger reconciles
P2
Core API, test-first
All routes + failure tests green
P3
MCP gateway: tools/resources/prompts, auth, masking, audit
Tool matrix green against compose
P4
Resilience package, chaos, screening-sim
Breaker lifecycle demonstrated in tests
P5
Corpus, ingestion pipeline, hybrid retrieval, RAG evals
Eval thresholds met in CI
P6
CDK, OIDC deploy, CodeDeploy canary, e2e, Locust
Deployed; SLOs measured
P7
LangGraph + ADK agents, Streamlit console, demo GIF
Demo script runs end to end
P8
README hero, infographics, animations, docs polish
Recruiter-ready