Barrio
by paloma222
README.md
# Barrio
**The booking add-on for the businesses on your street.**
Barrio is one self-hosted, multi-business **MCP server** that gives small local businesses (hair salons, physios, podiatrists) a voice booking add-on for Alexa+. Big brands can bring their own MCP server to Alexa+. The salon on the corner never will. Barrio is the shared server that covers that long tail: one deployment, many businesses, strict per-business isolation.
Built for the *Build, Ship, Shape* Amazon Developer Hackathon (Alexa+ track). Alexa+ itself is only available to partners, so this repo also contains a **simulated Alexa+**: an assistant that uses Amazon Bedrock as its brain and talks to Barrio as a **real MCP client** over Streamable HTTP, plus a web simulator with voice, an Echo Show-style card and a live MCP debug panel.
> The three businesses and five people in the seed data (Lucía's Hair Salon, Retiro Physio, Malasaña Podiatry; Carmen, Elena, Javier, Lucía, Pablo) are **demo data**, not real customers.
## What it does
- **Voice booking with confirmation**: find a business, search slots, book, move or cancel. Every write is two steps (`prepare_*` → `confirm`) and the assistant reads the summary back before acting.
- **Memory across sessions**: "Book my hair with Lucía, like last time" reuses the saved service, staff, weekday and time. The assistant usually asks one question (which of two times) and then confirms.
- **Booking for someone you care for**: Elena can book or move appointments for her mother Carmen (`caresFor` permissions). Both get notified.
- **Agentic waitlist**: when a slot is freed, Barrio offers it to the first matching person on the waitlist and holds it for them (1 minute in the demo). If they decline or the offer expires, it moves on to the next person, with no human in the loop. Businesses see a **recovered slots** counter.
- **Simulated deposit** (for example €5 to a saved card). It is refunded when a booking is cancelled more than 24 h ahead, or when the waitlist fills the cancelled slot. No real payments.
- **Business view**: `get_business_dashboard` shows the owner today's bookings, cancellations and recovered slots.
- **MCP Apps card**: a weekly slots card (`ui://barrio/slots-card`) that highlights the two suggested times and turns into a receipt after booking.
## Quick start (local)
Requirements: Node.js 22+, npm 10+. The assistant needs AWS credentials (default AWS SDK chain, for example `aws configure`) with access to a Bedrock model that supports tool use. The server, the tests and the web build do **not** need AWS.
```bash
npm install
cp .env.example .env
# edit .env: set BEDROCK_MODEL_ID (a Bedrock model or inference profile ID your account can use in AWS_REGION)
```
`.env` is read by the assistant only. The MCP server takes its settings (`CLOCK`, `OFFER_TTL_MS`) as shell variables, as in the commands below.
**Bedrock costs money.** Every assistant turn makes a few Bedrock calls, and they count against your AWS bill. For rehearsals pick a **Haiku-class model** (small, cheap, supports tool use) and keep real-Bedrock rehearsals few; the fake-LLM tests in `npm test` cover the flows for free. The assistant keeps each call small: `search_slots` reaches the model as the 2 suggested slots plus a count (the card gets every slot), and only the last 6 user turns are sent. See [`docs/deploy.md`](docs/deploy.md) for the budget.
Then start the three processes in three terminals, from the repo root:
```bash
# 1. MCP server on http://localhost:3001/mcp (in-memory store, seeded with demo data)
# CLOCK=fake enables the "+1 min" demo button (it moves the server clock to expire waitlist offers)
CLOCK=fake npm run dev -w server
# 2. Assistant API on http://localhost:3002 (Bedrock agent loop + real MCP client); reads ../.env
npm run dev -w assistant
# 3. Web simulator on http://localhost:5173 (Vite; proxies /api to the assistant)
npm run dev -w web
```
Open http://localhost:5173 in Chrome (the Web Speech API is needed for the microphone; the text box works everywhere). Pick a demo user and try:
- as **Carmen**: "Book my hair with Lucía, like last time."
- as **Elena**: "Move my mom's physio to Thursday."
- as **Javier**: "Yes, I'll take it." after the waitlist offer appears.
- as **Pablo**: the dashboard shows the recovered slot.
The server restarts with fresh seed data every time it starts (it is in memory). The server also works on its own with any MCP client: `POST http://localhost:3001/mcp` with `Authorization: Bearer demo-carmen` (tokens: `demo-carmen`, `demo-elena`, `demo-javier`, `demo-lucia`, `demo-pablo`).
## Architecture
```mermaid
flowchart TB
subgraph Browser["web/ — simulator (Vite)"]
V["Voice in/out<br/>Web Speech API + text box"]
C["MCP Apps host<br/>sandboxed iframe + postMessage"]
D["Live MCP debug panel"]
end
subgraph Assistant["assistant/ — simulated Alexa+ (Node)"]
L["Agent loop<br/>max 6 tool steps per turn"]
MC["Real MCP client<br/>@modelcontextprotocol/sdk Client"]
end
B[("Amazon Bedrock<br/>Converse API + tool use")]
subgraph Server["server/ — Barrio MCP server"]
T["12 tools · resources · ui://barrio/slots-card"]
DOM["Domain: slots, two-step booking,<br/>memory, permissions, waitlist"]
ST{{"Store"}}
SC{{"Scheduler"}}
end
Browser -- "HTTP + SSE (debug events)" --> Assistant
L <--> B
L --> MC
MC -- "MCP 2025-11-25 · Streamable HTTP · Bearer token" --> T
T --> DOM
DOM --> ST
DOM --> SC
ST -- local / tests --> MEM["InMemoryStore"]
ST -- AWS --> DDB[("DynamoDB<br/>single table")]
SC -- local --> LS["LocalScheduler"]
SC -- AWS --> EBS["EventBridge Scheduler<br/>one-time at() per offer"]
EBS --> ADV["Lambda AdvanceFn<br/>offer → next person"]
ADV --> DDB
```
On AWS, the Barrio server runs as a Lambda function behind a Function URL (`server/src/lambda.ts`). Locally it is the same Express app (`server/src/local.ts`).
| Package | What it is |
|---|---|
| `shared/` | zod schemas for every tool input and output, error codes, demo users |
| `server/` | MCP server + booking domain. Knows nothing about voice or Bedrock. `Store`, `Scheduler` and `Clock` are interfaces with local and AWS implementations |
| `assistant/` | Simulated Alexa+: Bedrock Converse agent loop, real MCP client, HTTP + SSE API for the web |
| `web/` | Browser simulator: user switcher, voice, MCP Apps card host, notifications, business dashboard, debug panel |
| `infra/` | AWS CDK stack: DynamoDB table, two Lambda functions, Function URL, EventBridge Scheduler role |
## MCP tools
All tools have an `inputSchema` and an `outputSchema` generated from zod (structured output). Errors come back as `isError: true` with `{ code, message, alternatives }` (codes: `SLOT_TAKEN`, `CLOSED`, `TOKEN_EXPIRED`, `NOT_ALLOWED`, `NOT_FOUND`), so the assistant can offer an alternative instead of a dead end.
| Tool | What it does | readOnlyHint | destructiveHint | idempotentHint |
|---|---|---|---|---|
| `get_my_context` | Who the user is, who they can book for, saved preferences, recent bookings (memory), open waitlist offers (`pendingOffers`) | true | — | — |
| `find_businesses` | Find a business by name or category | true | — | — |
| `search_slots` | Open slots; `suggested` (top 2 by preference) + `all` for the card. MCP App: `ui://barrio/slots-card` | true | — | — |
| `prepare_booking` | Holds the slot for 5 minutes; returns a summary, the deposit and a `confirmationToken`. Repeating it re-takes your own hold with a new token (the old one stops working), so a retry never blocks you | false | false | true |
| `prepare_reschedule` | Same, to move an existing booking | false | false | true |
| `prepare_cancellation` | Summary and whether the deposit is refunded; returns a `confirmationToken` | true | — | — |
| `confirm` | Runs the prepared action (book, move, cancel); charges or refunds; frees slots to the waitlist. MCP App receipt | false | true | true (per token) |
| `list_bookings` | My bookings and those of the people I care for; each has `past: true/false` (past ones can't be changed) | true | — | — |
| `join_waitlist` | Join the waitlist for a service, day and time window | false | false | true |
| `respond_to_offer` | Accept (books directly) or decline (passes to the next person) a waitlist offer | false | false | false (a retry finds the offer gone) |
| `get_notifications` | Simulated notification inbox | true | — | — |
| `get_business_dashboard` | Business owners only: today's bookings, cancellations, recovered slots | true | — | — |
"—" means the annotation is not set.
**Resources**
- `barrio://businesses/{id}/catalog`: services (duration, price, deposit), staff and opening hours of one business (resource template with `list`).
- `ui://barrio/slots-card`: the MCP App card, served as `text/html;profile=mcp-app`, self-contained (no external scripts).
## How MCP is used in code
Barrio calls MCP in code on both sides; nothing is mocked at the protocol level in the demo.
- **Server** (`server/src/http/app.ts`, `server/src/mcp/`): `McpServer` from `@modelcontextprotocol/sdk` 1.32 with `StreamableHTTPServerTransport` in stateless mode (one server + transport per POST, JSON responses), so the same code runs on Lambda. Protocol 2025-11-25 is negotiated in `initialize` (checked in `server/test/integration/mcp.test.ts`). Tools are registered with zod input/output schemas, `annotations` (`readOnlyHint`, `destructiveHint`, `idempotentHint`) and `structuredContent`. `search_slots` and `confirm` are MCP Apps tools registered with `registerAppTool` from `@modelcontextprotocol/ext-apps` and point to `ui://barrio/slots-card` via `_meta.ui.resourceUri`.
- **Identity**: `Authorization: Bearer <token>` is resolved to a user on every request (`server/src/mcp/auth.ts`). This simulates account linking; real OAuth is on the roadmap and would not change the tools.
- **Client** (`assistant/src/mcpSession.ts`): the SDK `Client` with `StreamableHTTPClientTransport` runs `initialize`, `tools/list`, `tools/call` and `resources/read`. Every request and response is streamed to the web debug panel over SSE.
- **Bedrock bridge** (`assistant/src/toolMapping.ts`, `assistant/src/agentLoop.ts`): MCP tool definitions become Bedrock Converse `toolConfig`; tool results (structured content or structured errors) go back to the model as `toolResult` blocks (for `search_slots`, only the 2 suggestions plus a count, to save tokens). The history sent per call is bounded to the last 6 user turns, cut only at turn starts so no `toolUse` loses its `toolResult` (`assistant/src/history.ts`). When a tool declares a `ui://` resource, the assistant reads it and the web renders the full result.
- **MCP Apps host** (`web/src/cardHost.ts`): renders the card in an iframe with `sandbox="allow-scripts"` (no same-origin) and speaks the MCP Apps postMessage protocol (`ui/initialize`, `ui/notifications/initialized`, `ui/notifications/tool-input`, `ui/notifications/tool-result`, `ui/notifications/size-changed`). We wrote a minimal host instead of pulling the SDK into the browser; see `docs/friction-log.md`.
- **Correctness**: no double bookings (a booking writes one lock per 30 minutes in a single conditional transaction; DynamoDB `TransactWriteItems` on AWS, the same conditions in `InMemoryStore`); `confirm` is idempotent per token; every business query is keyed by an explicit business id; waitlist offer transitions are conditional on a version number.
## Design: Alexa+ add-on principles
| Add-on design principle | How Barrio does it |
|---|---|
| Do at least one thing better because it lives in Alexa+ | Booking by voice while your hands are busy, for someone who can't use an app. The waitlist works in the background: Alexa tells the next person a slot opened, and they say "Yes, I'll take it." |
| Resolve the intent inside the conversation | Search, hold, confirm and receipt all happen in one conversation; the waitlist offer is answered in the same place. No links, no apps. |
| Ask only for what is essential | `get_my_context` first; "like last time" fills service, staff and usual time from memory, so usually one question (which of two times) plus the confirmation. |
| Confirm the outcome clearly | Two-step `prepare_*` → `confirm`; the assistant reads the summary (service, person, time, deposit, card) and waits for a clear yes. After acting it gives a one-sentence summary and the card shows a receipt. |
| Be consistent with Alexa's voice | Short sentences, at most two options out loud, never a list read aloud (the card shows the full week), times said the way people speak, patient tone for older users. |
| Kind errors | Structured errors carry alternatives: a taken slot or a closed time comes back with the next two open times; if nothing suits, the assistant offers the waitlist. |
The voice rules live in `assistant/src/systemPrompt.ts` and in the tool descriptions (for example `search_slots` tells the model to say only the two suggested times). Both are covered by tests.
## AWS services
| Service | How Barrio uses it | Code |
|---|---|---|
| **Amazon Bedrock** | Converse API with tool use: the "brain" of the simulated Alexa+. Model chosen with `BEDROCK_MODEL_ID` | `assistant/src/bedrockLlm.ts` |
| **AWS Lambda** | `McpFn` runs the MCP server (Express via `serverless-http`) behind a Function URL; `AdvanceFn` moves an expired waitlist offer to the next person | `server/src/lambda.ts`, `server/src/advanceLambda.ts` |
| **Amazon DynamoDB** | Single table (`PK`/`SK`, `GSI1` for bookings by customer, TTL on `expiresAt`, on-demand). Conditional transactions prevent double bookings | `server/src/store/dynamo.ts` |
| **Amazon EventBridge Scheduler** | One one-time `at()` schedule per waitlist offer that deletes itself after it runs | `server/src/scheduler/eventbridge.ts` |
| **AWS CDK** (TypeScript) | The whole stack, unit-tested with `aws-cdk-lib/assertions`; Node.js 22 runtime, log groups with 1-week retention | `infra/lib/barrio-stack.ts` |
Deployment steps, budget guards and teardown: [`docs/deploy.md`](docs/deploy.md).
> Status: the stack synthesizes and is unit-tested. TODO(user): update this line after the first real deploy (date, region, what was verified).
## Bonus: Barrio inside Claude
> TODO(user): placeholder until recorded. Fill in after trying it.
The same server works as a remote MCP connector in Claude, because it is a standard Streamable HTTP MCP server:
```bash
claude mcp add --transport http barrio <URL> --header "Authorization: Bearer demo-carmen"
```
`<URL>` is the deployed `McpUrl` (see `docs/deploy.md`) or a tunnel to `http://localhost:3001/mcp`. Do not publish the URL: it is public and the demo tokens are in this repo.
TODO(user): describe what was tried (Claude Desktop or Claude Code), which scene, and link the clip.
## Testing
```bash
npm test # all workspaces (vitest)
```
- **Domain** (in-memory store + fake clock): slot computation, opening hours, preference ranking, deposits and refunds, caregiver permissions, memory updates, waitlist matching, offer expiry and advance.
- **Concurrency and idempotency**: two simultaneous holds on one slot, exactly one wins; repeating `confirm` with one token (even concurrently) gives one booking and the same receipt.
- **MCP integration**: a real SDK client against the HTTP server: `initialize`, `tools/list` with annotations and output schemas, resources, and demo scenes 1 and 2 end to end (`server/test/integration/`).
- **Assistant**: scripted turns with a fake LLM check tool-call order, the 6-step limit, error results, card events, the Bedrock mapping and the voice rules in the system prompt.
- **Web**: MCP Apps host and card handshake (jsdom), voice, session switching.
- **AWS**: DynamoDB store unit tests (mocked SDK), Lambda adapter test with a Function URL event, CDK template assertions. A shared store contract runs against DynamoDB Local when `DYNAMO_ENDPOINT` is set; it is skipped by default because it needs Docker or Java (not yet run, see the friction log).
## Roadmap
- **Onboarding from a photo**: photograph the price list or the Google Maps listing; Bedrock extracts services, durations and prices and proposes opening hours.
- **Google Calendar adapter**, so a business keeps its existing calendar.
- **Real OAuth account linking** (MCP authorization) in place of the demo bearer tokens.
- **Business voice**: "Close my diary on Friday afternoon", plus a fuller business panel.
- Later: real payments, multiple languages.
## Docs
- [`docs/deploy.md`](docs/deploy.md): deploy to AWS with a budget cap
- [`docs/friction-log.md`](docs/friction-log.md): friction log
- [`docs/product-feedback.md`](docs/product-feedback.md): feedback on every tool used, plus feature requests
- [`docs/validation.md`](docs/validation.md): conversations with local businesses
- [`docs/video-script.md`](docs/video-script.md), [`docs/devpost-description.md`](docs/devpost-description.md): submission materials
## License
[MIT](LICENSE). Business associations and city councils can deploy and adapt Barrio for their own neighbourhoods.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues