Skip to content

High-Level Design

Source of truth: plan v1.0. This document explains the shape; the plan records the decisions.

1. Purpose and scope

LedgerLens MCP lets AI agents work with Kosha Financial's operational data and internal knowledge through the Model Context Protocol:

Capability Tools
Customer & KYC lookup search_customers, get_customer_profile, get_kyc_status, list_kyc_documents
Accounts & payments get_account_summary, search_transactions, get_transaction, get_spending_insights
Risk & compliance list_aml_alerts, get_aml_alert, screen_entity
Audited writes raise_dispute, flag_transaction_for_review
Knowledge search_knowledge_base, get_document
Operations get_service_health

Out of scope: moving real money, real customer data, multi-Region operation (not available to the project).

2. System context (C4 level 1)

flowchart LR
    analyst["Ops / compliance staff<br/>(human)"]
    agent["AI agent<br/>LangGraph copilot, Google ADK,<br/>Claude Desktop"]
    lm["LedgerLens MCP<br/>(this system)"]
    cognito["Amazon Cognito<br/>(authorization server)"]
    openai["OpenAI API<br/>(embeddings)"]
    vendor["Sanctions / PEP vendor<br/>(simulated: screening-sim)"]

    analyst -->|"asks questions"| agent
    agent -->|"MCP over Streamable HTTP + OAuth 2.1 bearer"| lm
    agent -->|"OAuth 2.1 + PKCE / client credentials"| cognito
    lm -->|"validates JWT via JWKS"| cognito
    lm -->|"embed queries and chunks"| openai
    lm -->|"screen names"| vendor

Notice: the agent never talks to data stores directly; every capability is mediated by one OAuth-protected MCP endpoint.

3. Containers (C4 level 2)

flowchart TB
    subgraph clients["Clients"]
        copilot["LangGraph copilot + Streamlit console"]
        adk["Google ADK agent"]
        desktop["Claude Desktop"]
    end

    openai["OpenAI API"]

    subgraph ledgerlens["LedgerLens MCP"]
        gw["mcp-gateway<br/>FastMCP, Streamable HTTP<br/>authn, scopes, masking, audit, rate limit"]
        api["core-api<br/>FastAPI<br/>domain logic, persistence, retrieval"]
        sim["screening-sim<br/>FastAPI vendor simulator"]
        ingest["ingest-worker<br/>parse, chunk, embed"]
        relay["outbox-relay<br/>publishes ops.outbox"]
        events["events-worker<br/>dispute SLA, review fan-out"]
        pg[("PostgreSQL 16 + pgvector<br/>core, ledger, kyc, risk, ops, kb")]
        ddb[("DynamoDB<br/>rate-limit buckets, breaker overrides")]
        q1[["SQS ingest-queue + DLQ"]]
        q2[["SQS domain-events + DLQ"]]
        s3[("S3<br/>corpus source documents")]
    end

    copilot & adk & desktop -->|"MCP"| gw
    gw -->|"HTTP, deadline header"| api
    gw -->|"HTTP via breaker"| sim
    gw -->|"token bucket, overrides"| ddb
    api --> pg
    api -->|"embeddings"| openai
    s3 -->|"object created"| q1 --> ingest --> pg
    ingest --> openai
    relay -->|"poll unpublished"| pg
    relay --> q2 --> events --> pg

Notice: the gateway owns cross-cutting concerns (auth, masking, audit, rate limits); core-api owns all domain state; asynchronous work never blocks a tool call.

Container Runtime Owns
mcp-gateway FastMCP on Lambda (Web Adapter) Protocol, auth, authorization, masking, audit, rate limiting, resilience toward downstreams
core-api FastAPI on Lambda (Web Adapter) Domain rules, Postgres schemas, ledger, outbox, idempotency, hybrid retrieval
screening-sim FastAPI on Lambda Simulated external dependency with tunable latency and failure
ingest-worker Lambda, SQS trigger Document parsing, chunking, embedding, upsert
outbox-relay Lambda, scheduled Exactly-once-effect publication of domain events
events-worker Lambda, SQS trigger Dispute TAT timers, review queue fan-out

4. Deployment view (AWS, eu-north-1)

flowchart TB
    user["MCP client"] --> cf["CloudFront<br/>(global)"]

    subgraph region["eu-north-1"]
        apigw["API Gateway HTTP API"]
        simfn["Lambda: screening-sim"]

        subgraph vpc["VPC"]
            subgraph priv["Private subnets"]
                gwfn["Lambda: mcp-gateway<br/>alias live"]
                apifn["Lambda: core-api<br/>alias live"]
                ingestfn["Lambda: ingest-worker"]
                relayfn["Lambda: outbox-relay"]
                eventsfn["Lambda: events-worker"]
                rds[("RDS PostgreSQL 16<br/>db.t4g.micro, single-AZ")]
            end
            subgraph pub["Public subnet"]
                nat["NAT instance<br/>t4g.nano"]
            end
            gwep["Gateway endpoints<br/>S3, DynamoDB"]
        end

        apigw --> gwfn
        apigw --> simfn
        gwfn --> apifn
        apifn --> rds
        ingestfn --> rds
        relayfn --> rds
        eventsfn --> rds
        gwfn --> gwep
        ingestfn --> gwep
        priv -->|"OpenAI, Cognito JWKS,<br/>Secrets Manager"| nat
        nat --> igw["Internet gateway"]

        ddb[("DynamoDB")]
        sqs[["SQS + DLQs"]]
        s3[("S3 corpus bucket")]
        sm["Secrets Manager<br/>OpenAI key, DB credentials,<br/>chaos HMAC key"]
        cognito["Cognito user pool"]
        cw["CloudWatch + X-Ray"]
        cd["CodeDeploy<br/>canary 10% / 5 min"]
        ecr["ECR (arm64 images)"]
    end

    cf --> apigw
    gwep --> ddb
    gwep --> s3
    s3 --> sqs
    cd -.->|"shifts alias traffic"| gwfn
    cd -.-> apifn
    cw -.->|"alarm triggers rollback"| cd

Notice: everything Regional is in eu-north-1; only CloudFront is global. S3 and DynamoDB traffic stays on free gateway endpoints; only internet-bound calls pay for the NAT instance.

Network rules:

From To Path
Lambdas in VPC RDS Security group to security group, port 5432
Lambdas in VPC S3, DynamoDB Gateway VPC endpoints (no NAT cost)
Lambdas in VPC OpenAI, Cognito JWKS, Secrets Manager, SQS NAT instance
screening-sim none Runs outside the VPC; it has no private dependencies

5. Request lifecycle

sequenceDiagram
    autonumber
    participant A as Agent (MCP client)
    participant G as mcp-gateway
    participant D as DynamoDB
    participant C as core-api
    participant P as PostgreSQL
    participant O as OpenAI

    A->>G: tools/call search_knowledge_base (Bearer JWT)
    G->>D: Token bucket: conditional decrement for client_id
    D-->>G: Allowed (tokens remaining)
    G->>G: Validate JWT (cached JWKS), check scope kb:read
    G->>G: Start deadline budget (e.g. 8 s), open trace span
    G->>C: POST /v1/kb/search (X-Request-Deadline, traceparent)
    C->>O: Embed query (timeout = min(remaining - margin, 2 s))
    O-->>C: 1024-dim vector
    C->>P: set kb_ceiling = confidential, then hybrid RRF query
    P-->>C: Top-k chunks (RLS-filtered)
    C-->>G: Chunks with citations, degraded=false
    G->>G: Mask fields per role, cap payload size
    G->>C: POST /v1/audit (actor, tool, args_hash, outcome, latency)
    C->>P: INSERT ops.audit_log
    G-->>A: Tool result (structured content + text)

Notice: every hop receives the remaining deadline, so a slow embedding call cannot push the overall request past its budget.

If OpenAI fails or the deadline is too short for embedding, core-api runs the full-text half of the query only and returns degraded: true.

6. Authorization discovery

sequenceDiagram
    autonumber
    participant A as MCP client
    participant G as mcp-gateway (resource server)
    participant X as FastMCP OAuth proxy
    participant K as Cognito

    A->>G: POST /mcp (no token)
    G-->>A: 401 + WWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource"
    A->>G: GET /.well-known/oauth-protected-resource
    G-->>A: RFC 9728 metadata (authorization_servers, scopes_supported)
    A->>X: GET /.well-known/oauth-authorization-server
    X-->>A: Endpoints (authorize, token, register)
    A->>X: POST /register (dynamic client registration)
    X-->>A: client_id (proxied to a pre-registered Cognito app client)
    A->>X: /authorize with PKCE (code_challenge, scopes)
    X->>K: Redirect to Cognito managed login
    K-->>X: Authorization code
    X-->>A: Code (bound to the client's PKCE challenge)
    A->>X: /token (code_verifier)
    X->>K: Exchange code
    K-->>X: Access token (JWT, aud = gateway, scope, cognito:groups)
    X-->>A: Access token
    A->>G: POST /mcp (Bearer JWT)
    G-->>A: 200 tool result

Notice: Cognito does not support dynamic client registration, so FastMCP's OAuth proxy supplies it while Cognito stays the real identity provider. Service-to-service callers skip the browser flow and use client credentials.

7. Quality attributes

Attribute Target Mechanism Verified by
Latency, simple reads p95 < 300 ms warm Indexed queries, connection reuse per instance, payload caps Locust in CI against deployed stack
Latency, KB search p95 < 900 ms HNSW index, single SQL round trip, 1024-dim embeddings Locust, RAG eval suite
Cold start < 2.5 s Slim arm64 images, lazy imports, no work at import time Synthetic probe after deploy
Availability 99.5% Canary + rollback, breakers, degradation, retries with budget CloudWatch SLO alarms
Fairness 60 req/min/client, burst 20 DynamoDB token bucket Integration tests, Locust
Correctness of money Ledger reconciles to the paisa Double-entry postings, BIGINT paise, nightly reconciliation test Property tests, reconciliation job
Confidentiality No PII beyond scope Scope checks, masking, RLS on KB classification Per-tool failure matrix

8. Cost (approximate, monthly, while running)

Item Cost
RDS PostgreSQL db.t4g.micro + 20 GB gp3 ~$15
NAT instance t4g.nano + public IPv4 ~$7
Lambda, API Gateway, CloudFront, SQS, DynamoDB at demo volume ~$1
Secrets Manager (3 secrets) ~$1.20
CloudWatch logs, metrics, X-Ray ~$1
Total ≈ $25

poe pause stops RDS and the NAT instance between demos; poe resume starts them again.

9. Key trade-offs

Decision Alternative rejected Why ADR topic
Lambda container images ECS Fargate ~2.5x cheaper idle; same image runs anywhere Compute platform
Separate gateway and core API Single FastMCP process Real network seam makes resilience patterns meaningful and keeps protocol concerns out of the domain Service split
PostgreSQL as system of record DynamoDB single-table Ledger needs ACID, joins, aggregates; pgvector needs Postgres anyway Datastore
pgvector + FTS hybrid in one query OpenSearch Serverless, S3 Vectors OpenSearch ~$170/month minimum; S3 Vectors unavailable on the plan Vector store
NAT instance NAT Gateway ~$7 vs ~$33/month; single point of failure accepted for a demo Egress
In-memory breakers + DynamoDB override Fully shared breaker state Avoids a network call per request; override covers fleet-wide incidents Breaker state
In-app JWT validation API Gateway JWT authorizer Spec requires WWW-Authenticate with resource metadata on 401 Auth placement
SQS + transactional outbox Kafka / MSK No streaming workload; MSK not on the plan Messaging
CodeDeploy canary All-at-once deploy Alarm-driven automatic rollback Deployment safety