Skip to main content
Glama
README.md
# Udhaar

A voice-first credit ledger (udhaar/khata) for small shopkeepers, built as a
self-hosted **Alexa+ MCP server** over Streamable HTTP, backed by **Amazon
Bedrock** for understanding mixed-language speech and drafting collection
messages.

Built for Amazon's Build, Ship, Shape hackathon — Alexa+ track, plus the AWS
Builder and Open Source mini-challenges.

## The problem

Millions of small Indian shops run on a paper notebook (*khata*) of who owes
what. It's error-prone, hard to search, impossible to back up, and useless
hands-free when a shopkeeper is mid-sale with both hands full of goods. A
voice assistant that can record "Ramu took two hundred rupees of rice on
credit" while the shopkeeper keeps working — and later tell them who to
follow up with — replaces that notebook without asking anyone to learn an
app.

## The demo in 30 seconds

Open the web simulator (`GET /`, see [Setup](#setup-and-run)) and speak or
type:

1. *"Ramu took two hundred rupees of rice on credit and Kumar paid five
   hundred"* → both entries recorded in one sentence, parsed by Bedrock
   (or a regex fallback if Bedrock isn't configured).
2. *"How much does Ramu owe?"* → spoken balance.
3. *"Who should I follow up with today?"* → ranked list with a drafted,
   polite reminder message per customer.
4. The ledger panel on the right updates live after every action.

## Why this isn't a thin wrapper

- **Bedrock-backed understanding with a graceful fallback.** Spoken,
  code-mixed sentences (English + transliterated Tamil/Hindi — *udhaar*,
  *baaki*, *kadan*) are parsed by a Bedrock `ConverseCommand` call
  (`src/parser.ts`). If Bedrock is unavailable, misconfigured, or disabled
  (`UDHAAR_DISABLE_BEDROCK=1`), a regex-based rules engine takes over
  automatically — the tool never crashes or goes silent, it just reports
  which engine it used (`structuredContent.engine`).
- **Proactive collection planning, not just a balance lookup.**
  `collection_plan` ranks customers by `balance × recency` and drafts a
  warm, non-threatening reminder per customer — in the shopkeeper's
  requested language — via Bedrock, again falling back to a plain template
  if Bedrock is unavailable.
- **Multilingual by design**, not just English with a translation layer
  bolted on: the Bedrock prompts are written to understand code-mixed
  Tamil/Hindi/English input directly, and `collection_plan` can draft
  output in a requested language.
- **Voice-shaped responses.** Every tool returns one or two short spoken
  sentences (plus `structuredContent` for UIs) — not JSON dumps, not
  markdown tables, because the Alexa+ assistant on the other end has to
  read it aloud.
- **A real, spec-compliant MCP server**, not a demo stub: stateless
  Streamable HTTP per MCP 2025-11-25, validated against the official
  `@modelcontextprotocol/inspector` CLI and the official SDK client
  end-to-end (`src/smoke-test.ts`), with bearer-token auth and a
  Docker/App Runner deployment path.

## Architecture

```mermaid
flowchart LR
    subgraph Client
        Alexa["Alexa+ (voice)<br/>— unverified, see docs/ALEXA_SETUP.md"]
        Sim["Web simulator<br/>(public/, this repo)"]
        Inspector["MCP Inspector / SDK client"]
    end

    subgraph Server["Express server (src/server.ts)"]
        MCP["POST /mcp — Streamable HTTP<br/>(stateless, MCP 2025-11-25)"]
        Ask["POST /api/ask — intent router<br/>(src/intent.ts)"]
        State["GET /api/state"]
        Health["GET /health"]
    end

    subgraph Logic
        Parser["src/parser.ts<br/>parseUtterance()"]
        Planner["src/planner.ts<br/>collectionPlan()"]
        Ledger["src/ledger.ts<br/>JSON-file store"]
    end

    Bedrock["Amazon Bedrock<br/>ConverseCommand"]

    Alexa -->|Bearer auth| MCP
    Inspector -->|Bearer auth| MCP
    Sim --> Ask
    Ask -->|self-call, same auth| MCP
    MCP --> Parser
    MCP --> Planner
    MCP --> Ledger
    Parser -.fallback to regex.-> Parser
    Parser --> Bedrock
    Planner --> Bedrock
    Planner --> Ledger
```

## Tools

| Tool | Use when the shopkeeper says... | Notes |
|---|---|---|
| `record_credit` | "Ramu took 200 rupees of rice on credit" | Direct, structured input |
| `record_payment` | "Kumar paid 500" | Reports "fully settled" at balance ≤ 0 |
| `record_from_speech` | Any free-form sentence, possibly multiple entries, possibly code-mixed | Bedrock or rules fallback |
| `get_balance` | "How much does Ramu owe?" | Includes last 3 movements |
| `list_dues` | "Who owes me money?" | Biggest balance first, optional overdue filter |
| `daily_summary` | "How was today?" | Credit, payments, entry count, total outstanding |
| `payment_reminder` | "Remind Ramu to pay" | Optional UPI deep link (`upi://pay?...`) |
| `collection_plan` | "Who should I follow up with today?" | Ranked by `balance × recency`, Bedrock-drafted or templated messages |

## Setup and run

```bash
npm install
cp .env.example .env    # fill in what you need; see below
npm run dev              # tsx src/server.ts, serves on :3000
```

Open `http://localhost:3000/` for the web simulator, or point an MCP client
at `http://localhost:3000/mcp`.

Everything works with **no AWS credentials at all** if you set
`UDHAAR_DISABLE_BEDROCK=1` — speech parsing and reminder drafting fall back
to deterministic regex/template logic, useful for offline development or a
no-AWS demo.

### Bedrock setup

1. In the [Bedrock console](https://console.aws.amazon.com/bedrock/) for
   your target region, go to **Model access** and request access to an
   Anthropic Claude model (usually instant approval).
2. Find a valid, invokable model ID for your region/credentials:
   ```bash
   aws bedrock list-inference-profiles --region <your-region> \
     --query "inferenceProfileSummaries[].inferenceProfileId"
   ```
   Many current Claude models require an **inference profile ID**
   (`us.anthropic.claude-...`, `eu.anthropic.claude-...`,
   `global.anthropic.claude-...`), not the raw foundation-model ID.
3. Set in `.env`:
   ```
   AWS_REGION=<your-region>
   BEDROCK_MODEL_ID=<the inference profile ID from step 2>
   ```
4. Standard AWS credential chain only (`aws configure`, `AWS_PROFILE`,
   instance/task role) — no credentials are ever hardcoded.

### Environment variables

See `.env.example` for the full, documented list (`PORT`, `AWS_REGION`,
`BEDROCK_MODEL_ID`, `UDHAAR_API_KEY`, `UDHAAR_DISABLE_BEDROCK`,
`UDHAAR_DATA_FILE`).

## Tests

```bash
npx tsc --noEmit    # type check
npm run test:unit   # node:test unit tests — ledger, parser, planner, intent router
npm test             # end-to-end: spins up a real server, drives it with the
                      # official MCP SDK client over Streamable HTTP, asserts
                      # on actual tool results (not just "it responded")
```

## Deploy

Docker + AWS App Runner, with an IAM instance role scoped to exactly
`bedrock:InvokeModel`, a health-check wired to `/health`, and a cost/shutdown
note: see [`deploy/README.md`](deploy/README.md). The Docker image has been
built and smoke-tested locally (non-root user, `/health` and `/mcp` both
verified inside the container); the App Runner deployment steps themselves
require an AWS account and have not been executed by anyone but you.

## Security notes

- No hardcoded credentials anywhere; AWS access only via the standard SDK
  credential chain.
- `/mcp` and `/api/*` require `Authorization: Bearer <UDHAAR_API_KEY>` when
  that variable is set (constant-time comparison); unset means auth is open
  — set it before exposing the server publicly. `/health` always stays open
  for load balancer health checks.
- The bearer-token scheme here is **not** full OAuth 2.1 — Alexa+'s
  documented requirement for real device registration is OAuth 2.1 with
  PKCE, which this repo does not implement (see
  [`docs/ALEXA_SETUP.md`](docs/ALEXA_SETUP.md) for the honest gap analysis).
- The JSON-file ledger store is demo-grade (not concurrent-write-safe,
  not durable across container restarts on App Runner) — see the DynamoDB
  note in `deploy/README.md` for the production path.

## What's verified vs simulated

- **Verified**: Streamable HTTP MCP server behavior (Inspector CLI + the
  official SDK client, both over real HTTP), bearer auth (401/200 paths),
  Bedrock parsing/drafting with regex/template fallback, the Docker image
  running as non-root locally.
- **Simulated**: the "Alexa+" experience is demonstrated via the included
  web simulator (`public/`), which speaks to the same `/mcp` tools over the
  same Streamable HTTP protocol an Alexa+ registration would use — browser
  speech recognition/synthesis stand in for the device. Real Alexa+ device
  registration has not been completed; see `docs/ALEXA_SETUP.md` for exactly
  what that would take and why it's marked unverified.

## Hackathon mapping

- **Alexa+ track (primary)**: self-hosted MCP server, Streamable HTTP, MCP
  spec 2025-11-25, voice-shaped tool responses.
- **AWS Builder mini-challenge**: Amazon Bedrock (`ConverseCommand`) for
  speech parsing and message drafting; AWS App Runner + ECR deployment path.
- **Open Source mini-challenge**: see the project's hackathon submission
  notes for the external contribution made during the hackathon window
  (tracked separately from this repo's own commits).

## License

MIT — see [`LICENSE`](LICENSE).