# StoreYog A platform for small stores with three connected experiences: 1. **Owner workspace** (`server/owner/`, port 3101): sign in, manage inventory, run demo checkout, review restocks, and request inventory reports. 2. **Echo Show simulator** (`simulator/`, port 3100): voice and text conversations, store cards, approvals, and tool traces, using the authenticated MCP service. 3. **MCP server + admin console** (`server/`, port 3101): Streamable HTTP tools, SQLite persistence, isolated store data, scoped access, OAuth account linking, and request/SQL observability. The owner workspace and MCP server share one backend; the simulator is a separate client service. Adapt catalogs and workflows for other small retailers. Seeded demo data and simulated Amazon Business orders are used; no purchases or payments are processed, and there is no third-party POS integration. Live Alexa account linking remains to validate. ## Hackathon video [Watch the 2:59 StoreYog platform demo](docs/pitch-v4/storeyog-platform-3min.mp4) · [Story, architecture sources and rebuild instructions](docs/pitch-v4/README.md). ## Clone and run — judge setup Requires **Node 22+** and npm. No Python, Docker, FFmpeg, or voice/model API key is required to run the scripted MCP demo. The optional MP4 is excluded from Git; copy an existing export to `docs/pitch-v4/storeyog-platform-3min.mp4` or rebuild it using the video guide. ```sh git clone https://github.com/dlyog/store-pulse.git storeyog cd storeyog ./manage.sh setup ./manage.sh start ``` Open **http://localhost:3100/** for the judge landing page. It links the simulator, MCP console, embedded video, source, and technical notes. ```sh ./manage.sh restart # Gracefully restart both services ./manage.sh status # PID, URLs, health, demo access mode ./manage.sh log # Follow logs; Ctrl-C stops following ./manage.sh log --no-follow # Print recent logs once ./manage.sh stop # Stop the managed services ``` `setup` runs `npm ci`, creates `.env` only if missing, and prepares private runtime credentials. It preserves existing `.env`, store data, and keys. `start` runs both services in the background, checks their health, and writes `.runtime/service.json` and `.runtime/storeyog.log`. It refuses to kill an unrelated process or take an occupied port. Repeated starts reuse the running instance. - Judge landing page: **http://localhost:3100/** - Echo Simulator: **http://localhost:3100/simulator** - MCP endpoint: **http://localhost:3101/mcp** - Admin console: **http://localhost:3101/console/** - Non-sensitive health: **http://localhost:3101/health** ### Preconfigured judge admin Managed starts enable **Judge demo administrator — preconfigured** in the console sign-in dropdown. Leave that profile selected and click **Open console**. No key copying is needed. The masked credential field is a demo placeholder; the real admin key stays server-side. This mode deliberately allows visitors to administer the seeded demo instance, including stores and keys. Normal authentication is available through **Custom admin key**. For a server that should require that credential, start with: ```sh MCP_DEMO_LOGIN=false ./manage.sh start # If already running: MCP_DEMO_LOGIN=false ./manage.sh restart ``` The ordinary `npm run dev` / `npm run mcp` commands leave demo access disabled unless `MCP_DEMO_LOGIN=true` is explicitly set. Console APIs still require a signed-in HttpOnly session or admin credential. Store MCP requests always require store-scoped Bearer keys, including in judge mode. To display private bootstrap credentials in your own terminal, run `npm run credentials`. Do not include that output in a recording. Credentials live in ignored `.runtime/credentials.json` (mode 0600). SQLite lives in `.runtime/storeyog.sqlite`, persists across restarts, and stores issued store keys as hashes. Live voice and local model requests use the configured lab services. If they are unreachable on the judge’s machine, the video, text UI, and **Run demo script** remain usable; unavailable voice output is skipped. The scripted flow still executes real authenticated MCP calls and SQLite statements. Optional live Claude requests require an Anthropic key. For foreground development, `npm install && npm run dev` still starts both processes. Stop a managed instance first if it uses the same ports. ## Voice experience Click **Start listening** once and grant microphone permission. The label becomes **Mic is live / Stop listening**. Speak naturally and pause for about one second to submit a turn. You do not need to click between utterances. For an inventory or sales request: 1. The screen shows “Let me check the details…” and Kokoro speaks that acknowledgement. 2. Quiet, locally synthesized hold music plays while the request and answer audio are being prepared. 3. Music stops and the final Kokoro response plays when ready. 4. Speak during playback to interrupt; your microphone stays open. Click the live mic button to stop listening and release the microphone. The Web Audio / MediaRecorder path uses echo cancellation, noise suppression, and automatic gain control. Barge-in also stops the acknowledgement/hold music. If an earlier model request is still working, a new utterance queues until it finishes. The lab speech APIs are utterance-based HTTP, not streaming audio APIs. Browser/hardware acoustic echo behavior still needs a physical mic test; Chrome on localhost/HTTPS is recommended. The sound button mutes spoken output and music. Text input always remains available. The speech requests match the reference app in `trainium-frontier-old/webui/backend/`: Whisper `POST /transcribe`, multipart field `audio`, reads `transcription`; Kokoro `POST /generate-audio-binary/`, `{text,voice,speed:1.0}`, receives WAV. Audio is proxied through the simulator backend and is not persisted. ## Judge landing page The root URL is a marketing page for judges, with product context, cards for the owner workspace, Echo Simulator and authenticated MCP Console, an embedded customer-focused pitch video under three minutes, a download link, the original technical walkthrough, and source/setup/verification resources. The simulator remains a separate view at `/simulator`. `PUBLIC_MCP_URL` and `MCP_CONSOLE_URL` set browser-facing MCP links when you deploy the services behind different hostnames. Video files are served with byte-range support for seeking. ## Demo Click **Run demo script** for a repeatable closing request → draft → yes → simulated confirmation. This path uses a clearly labeled scripted assistant and real authenticated MCP tools, without calling a model. Typed/spoken requests use your local Gemma API, or Claude if an Anthropic key is configured. Common local-model requests have enforced MCP workflows before the model produces its summary. This prevents malformed model JSON from blocking inventory checks. Unrecognized requests can use the JSON action loop. Claude uses Anthropic's native tool loop. Neither model can approve an order: a fresh explicit yes is required for the exact pending basket. The seeded day has 15 grocery essentials, 40 receipts, **$1,034.70** in sales (spoken as $1,035), and a seven-day revenue history. Reconciliation leaves basmati rice and toor dal at **30%**, suggesting 28 rice bags and 42 dal bags for a simulated **$882** restock. Example order: `SP-LITTLE-INDIA-20261021-001`. **Reset demo data**, `npm run reset-demo`, or `npm run seed` resets only the simulator's authenticated store for today. It retains other stores and audit history. Reconciliation is idempotent per store-local day. Identical same-day baskets return the same simulated order, including after a restart. Orders do not increase stock; receiving is not implemented. New demo days are seeded independently, preserving historical rows. ## MCP server and authentication The official v1 `@modelcontextprotocol/sdk` exposes six tools over MCP 2025-11-25 Streamable HTTP. POST `/mcp` requires `Authorization: Bearer ` on every request. Authenticated GET/DELETE return 405 in stateless mode. No legacy SSE transport is exposed. | Tool | Required scope | Behavior | | --- | --- | --- | | `get_inventory` | `inventory:read` | Current stock, units, thresholds, full-stock percentages; optional item filter | | `get_todays_transactions` | `inventory:read` | Receipts, sales total, count, previous seven days | | `reconcile_inventory` | `inventory:write` | Atomic stock deduction, low-stock report, suggested restock | | `place_reorder` | `orders:write` | Draft or confirmed simulated order; never a real purchase | | `update_inventory` | `inventory:write` | Explicit physical stock adjustment with an expected current count and reason | | `send_inventory_report` | `reports:send` | Email latest stock to the store’s configured recipient | The server derives the store from the verified key. Tool arguments cannot change store identity. SQL statements bind all data and store IDs as parameters. Domain operations run in SQLite transactions. Inventory/order tools make no model, speech or supplier network calls. The report tool additionally calls the configured SMTP service. Results contain structured JSON and a spoken `summary`. MCP result `_meta` contains the request ID, store ID, and **the actual executed SQL**, bound parameters, row counts, and statement timings. The simulator displays these in its expandable tool trace. The simulator independently enforces explicit order confirmation using a server-side session cookie. External MCP hosts such as Alexa must obtain their own trusted owner confirmation, as the tool description requires. An API key authenticates a client; it does not prove that someone spoke. ## Admin console The console belongs to the MCP server, not the simulator. It shows: - Persisted tool-call totals, tool errors, P95 handling latency, and SQL statement count. - Recent request volume, per-tool usage and latency, search, error filtering, and store filtering. - Expandable requests with arguments, summaries, error details, exact SQL, parameters, row counts, and timings. - Store creation with isolated seeded data, scoped key issuance/revocation, and store enable/disable. - Authentication failure counts. These are server-wide because an invalid credential cannot be assigned to a verified store. The activity view retains the latest 100 matching calls; aggregate metrics cover all persisted calls. Metrics survive restarts. Live refresh runs every three seconds and can be paused. Auth values and key hashes are excluded from API responses and audit traces. The console does not provide an arbitrary SQL execution endpoint. To connect **another store**, sign into the console, select **Add store**, save its one-time key, and configure any MCP client with the same `/mcp` URL and that store's Bearer key. A second simulator can run independently: ```sh PORT=3200 MCP_SERVER_URL=http://localhost:3101/mcp \ MCP_API_KEY='' npm run simulator ``` This simulator needs no database volume or admin key. Its reset button requires the internal reset credential as well as the store key; ordinary MCP operations do not. ## Deploy the components separately For non-local deployments, supply explicit secrets through environment variables. Start the MCP server on one host/container and point the simulator at its URL: ```sh # Server environment: MCP_API_KEY, MCP_ADMIN_KEY, INTERNAL_TOKEN npm run mcp # Separate client environment: MCP_API_KEY, MCP_SERVER_URL, speech/model URLs npm run simulator ``` `npm run dev` is only a convenience launcher for local work. It supplies the generated primary store key to the simulator automatically; standalone `npm run simulator` expects `MCP_API_KEY` explicitly. The Dockerfile has `mcp`, `simulator`, and combined `demo` targets. For two containers, set explicit `MCP_API_KEY`, `MCP_ADMIN_KEY`, and `INTERNAL_TOKEN` in `.env`, then: ```sh docker compose up --build ``` The `mcp-data` volume holds only the MCP service's SQLite/credential files. The simulator has no database volume. Lab names must resolve inside containers; use routable LAN addresses if needed. Docker engine was stopped on the development machine, so container builds have not been verified. **Alexa+ is a separate integration with the MCP server**, independent of this simulator. Static Bearer authentication works with this simulator and MCP clients configured with keys. The public ingress implements OAuth authorization code with PKCE S256, discovery, consent, canonical resource validation, scoped/expiring tokens, refresh rotation and revocation. Public HTTPS, Alexa client callback registration and an eligible developer account are still needed for actual mobile/device linking. See [docs/ALEXA_DEPLOY.md](docs/ALEXA_DEPLOY.md) for CLI, tunnel, metadata, and phone/device readiness steps. ## Environment Existing `.env` aliases `STP_API`, `TTS_API`, `LLM_API` work unchanged. See [.env.example](.env.example) for all settings. | Variable | Purpose / default | | --- | --- | | `MCP_API_KEY` | Simulator's store key; primary store bootstrap key when set on MCP server | | `MCP_ADMIN_KEY` | MCP admin credential; never needed by simulator | | `MCP_DEMO_LOGIN` | Preconfigured judge console login; enabled by manage.sh, off by default for ordinary starts | | `INTERNAL_TOKEN` | Extra credential for demo-reset control | | `MCP_SERVER_URL` | Simulator MCP URL; default `http://127.0.0.1:3101/mcp` | | `MCP_CONSOLE_URL` | Optional browser-facing console URL, useful for containers | | `MCP_STORE_ID`, `MCP_STORE_NAME` | Primary store bootstrap identity; default `little-india`, `Little India Grocery` | | `RUNTIME_DIR`, `DATABASE_PATH` | MCP private state directory / SQLite path | | `STP_API`, `TTS_API` | Whisper `http://dlyog05:5002`, Kokoro `http://dlyog05:5151` | | `TRAINIUM_STT_URL`, `local_stt` | STT overrides, in precedence order | | `TRAINIUM_TTS_URL`, `local_tts` | TTS overrides, in precedence order | | `TRAINIUM_TTS_VOICE` | `af_heart` | | `LLM_API`, `TRAINIUM_LLM_BASE_URL` | Local OpenAI-compatible model API / override | | `TRAINIUM_LLM_MODEL` | `gemma-4-E2B` | | `ANTHROPIC_API_KEY`, `CLAUDE_MODEL` | Optional Claude SDK path; model default `claude-sonnet-5-5` per brief | | `LAB_HTTP_TRANSPORT` | `auto` on macOS, `fetch` elsewhere; `curl` forces curl. Auto works around macOS Node LAN permissions. | | `PORT`, `MCP_PORT` | `3100`, `3101` | | `STORE_TIMEZONE` | `America/Los_Angeles` | | `COOKIE_SECURE`, `TRUST_PROXY` | Secure cookies / trusted proxy range when using HTTPS reverse proxies | | `MCP_ALLOWED_ORIGINS` | Optional comma-separated browser origins; non-browser clients send no Origin | ## Verification With the stack running, run scenarios sequentially because demo scripts share/reset the primary store: ```sh npm test # SQLite persistence, isolation, logic, confirmation npm run typecheck npm run smoke # authenticated tools, SQL, <500ms HTTP round trips npm run demo # scripted assistant → authenticated MCP → SQLite npm run demo -- --live # actual configured local/Claude model npm run test:security # auth, scopes, isolation, revocation, errors audited node --import tsx scripts/safety-check.ts node --import tsx scripts/browser-check.ts node --import tsx scripts/console-check.ts node --import tsx scripts/feedback-check.ts node --import tsx scripts/duplex-check.ts ``` Browser scripts use installed Google Chrome. Generated screenshots are ignored in `artifacts/`. Voice behavior tests use synthetic microphone input/mocked agent replies; separate integration checks exercise real Whisper/Kokoro. See [verification notes](docs/VERIFICATION.md) for the tested behavior and remaining limits. ### Voice controls Starting the microphone speaks: “Welcome to StoreYog, your store assistant. How can I help you today?” The microphone stays open across turns, and speaking interrupts playback. Say **“stop”**, **“stop listening”**, or **“microphone off”** to stop playback and release the microphone. Explicit stop commands are handled before the inventory agent; phrases such as “stop ordering rice” are not treated as microphone commands. The new [pitch video](docs/pitch-v4/README.md) includes the real greeting, inventory request, spoken approval and spoken stop. It identifies the experience as an Alexa+ web simulation, uses prerecorded voice inputs through real services, and preserves the original technical video. [Submission preparation notes](docs/DEVPOST_SUBMISSION.md) include the remaining public-video upload step. ### Deploy to Rocky Linux Run `./deploy_server.sh` to sync to `dlyog@dlyog02:/home/dlyog/dlyog_apps/git/alexa`, install with the app-specific Node 22, and use `manage.sh` to set up/restart. Server data and credentials are preserved. [Deployment, private/public routing and OAuth linking](docs/SERVER_DEPLOYMENT.md). One MCP service exposes a private API-key route and a loopback OAuth-only ingress for your public HTTPS proxy; the simulator and console remain private. `npm run test:oauth` verifies linking, PKCE, token rotation/revocation and route isolation. ### Store owner web app and inventory reports Open **http://dlyog02:3101/store/** (local: `http://localhost:3101/store/`). Sign in with the private judge-owner profile or the owner email/password. Inventory edits, reviewed restocks and orders share the same database as the voice experience. Say “Set basmati rice stock to 15” or “Email me my latest inventory report” to use the new `update_inventory` and `send_inventory_report` MCP tools. SMTP sends from `noreply@dlyog.com` to the primary store’s configured `INVENTORY_REPORT_TO=admin@dlyog.com`, reusing the InvestYog Python transport. [Owner setup and email details](docs/OWNER_APP.md). ### StoreYog platform demo — 2:59 [Watch the submission video](docs/pitch-v4/storeyog-platform-3min.mp4). Raj checks inventory while cleaning, revises the basket to rice only, approves the new draft, and stops listening. The final section explains MCP, the Alexa+ connection path and reuse for other small stores, narrated with `am_michael`. [Script, sources and production evidence](docs/pitch-v4/README.md). The demo uses the working web simulation and simulated purchases. MCP/OAuth are implemented; public HTTPS and live Alexa account linking remain to validate. Earlier exports are kept in a private local archive and are no longer linked or served by the judge site. ## Local media and archives Audio/video exports and the entire `archive/` directory are excluded from Git. Earlier video versions, raw recordings, intermediate renders and verification screenshots are preserved locally in `archive/`. The final local video stays at `docs/pitch-v4/storeyog-platform-3min.mp4`; generate it with the documented capture/render scripts or copy an existing export there. Source checkouts do not include the MP4.