Stand-In
Provides an Alexa+ add-on MCP server with OAuth 2.1/PKCE account linking and an add-on manifest, enabling Alexa+ to open support cases, place calls, handle hand-offs, and schedule follow-ups; the live Alexa+ connection is server-side ready but not connected in the demo.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Stand-InCall my broadband provider and sort out my three-day outage"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Stand-In
Alexa makes the customer-care call so you don't have to.
Stand-In is an Alexa+ add-on (a self-hosted MCP server) that places a support call for you. It builds a case file, navigates the phone menu, waits on hold, explains the issue to the agent, shares only what you approved, rings your Echo when only you can answer (an OTP, an identity check), records the ticket number and the promise, and calls back if the promise is broken.
Built for Build, Ship, Shape: Amazon Developer Hackathon. Track: Alexa+. Mini challenges: AWS Builder and Open Source (packages/disclosure-guard).
Real vs simulated
Read this first. Everything marked "simulated" says so in the UI and in the code.
Part | Status | What that means |
MCP server ( | Real | Official |
Alexa+ host ( | Simulated | A browser page in a generic Echo-Show-style frame. It is a real MCP client (SDK |
Phone network | Simulated |
|
Disclosure guard ( | Real | Code, not a prompt: tiered facts, template expansion, output DLP, inbound request/injection classifier. 298 unit tests. |
Call state machine ( | Real | Deterministic, pure, unit-tested. The LLM proposes words; code decides transitions, digits, hand-off and hang-up. |
LLM | Real, by mode |
|
Real Alexa+ connection | Not connected | The server side is ready (OAuth linking, |
Hosted deployment | Not yet live | Lightsail deploy script, Dockerfile and IAM policy are in the repo. The new AWS account is stuck in verification (see Limitations). |
Related MCP server: SHIFT//RELAY
Live demo
There is no hosted deployment yet: the new AWS account is stuck in verification (see Limitations). Run it locally instead; it takes about two minutes and needs no AWS account or keys (Quick start below). Once running, these are the useful URLs:
What | Value |
Simulated Alexa+ host |
|
Autoplay takes |
|
MCP endpoint (for your own MCP client, e.g. MCP Inspector) |
|
Health |
|
When deployed (scripts/deploy-lightsail.sh), the hosted page fetches its token from /sim/config (DEMO_EMBED_TOKEN=true), and cost is bounded by per-IP rate limits and global hourly and daily spend caps.
Quick start (offline, about 2 minutes)
Requirements: Node 22+, pnpm.
git clone https://github.com/MahalParida/stand-in.git stand-in && cd stand-in
pnpm i
pnpm demopnpm demo builds the host page and the server, then starts the server in offline mode on http://127.0.0.1:8787 (default PORT). Open one of:
http://127.0.0.1:8787/?autoplay=happy: the account number is elicited, the IVR is navigated (2, then 1), hold, Ravi joins, the outcome is read back, Done: SR-48213, follow-up scheduled.http://127.0.0.1:8787/?autoplay=otp: Ravi asks for the OTP. The call goes toHANDOFF_PENDING("Needs you"), a reddisclosure: One-time password (OTP)chip appears in the trace, the account holder joins and leaves. No OTP digits appear anywhere on the page.http://127.0.0.1:8787/?autoplay=followup: "A week later" (the refund never came), Stand-In calls again, quotes SR-48213 and escalates to a supervisor.
Or type your own request into the page, for example "My BharatNet broadband has been down for three days, sort it out".
How the page gets its token locally. With no STANDIN_TOKEN set, the offline server bound to loopback generates a one-time token, never prints it, and hands it only to the local page via /sim/config (loopback Host only). If your .env sets STANDIN_TOKEN, either set DEMO_EMBED_TOKEN=true, open the page with ?token=… (removed from the address bar after load), or type it once into the page. Other MCP clients need STANDIN_TOKEN set in .env.
Other commands:
pnpm test # vitest, whole workspace
pnpm eval # 8 golden call scenarios, deterministic
pnpm verify --db apps/mcp-server/standin.db # check the hash-chained event log of the SQLite storeWithout .env the demo store is in memory. Set SQLITE_PATH=./standin.db (as in .env.example) to keep a file; it is created relative to apps/mcp-server, so pnpm verify (run from the repo root) needs --db apps/mcp-server/standin.db.
How it works
flowchart TB
subgraph Browser["apps/alexa-sim (simulated Alexa+ host, React + Vite)"]
UI["Echo-style screen, mic, call card, phone-line pane"]
MC["MCP client + JSON-RPC trace panel"]
end
subgraph Server["apps/mcp-server (Node, one HTTP process)"]
MCP["/mcp: Streamable HTTP, 11 tools, resources, elicitation"]
SIM["/sim/api/llm, /sim/api/tts: orchestrator + TTS proxy, rate limits, spend caps"]
OA["/authorize, /token, /.well-known/*: OAuth 2.1 + PKCE"]
end
subgraph Agent["apps/call-agent"]
CS["CallSession"]
PL["Planner (LLM, zod-validated)"]
SC["SimCarrier"]
end
FB["apps/fake-business: BharatNet IVR, hold, Ravi"]
CORE["packages/core: case model, state machine, hash-chained event log, SQLite repo"]
DG["packages/disclosure-guard: tiers, output DLP, inbound classifier"]
PRV["packages/providers: Bedrock / OpenAI / scripted, Polly / browser TTS, local / EventBridge scheduler"]
MC -- "Bearer, Origin + Host checks" --> MCP
UI --> SIM
MCP --> CS
CS --> PL
CS --> DG
CS --> CORE
PL --> PRV
SIM --> PRV
CS --> SC --> FBCall state machine
DIALING -> IVR -(menu prompt, planner picks digit, DTMF)-> IVR
IVR -hold-> HOLD -human detected-> HUMAN
HUMAN -disclosure request (never-tier)-> HANDOFF_PENDING -accept-> USER_ON_LINE -user leaves-> HUMAN
HANDOFF_PENDING -decline / 60 s timeout-> HUMAN ("they'll verify later")
HUMAN -outcome detected-> WRAP_UP (read back ticket + date) -confirmed-> ENDED
any -error / limit-> FAILED(reason) e.g. ivr_dead_end after 3 loops, max_duration (25 min sim)The planner returns a zod-validated action (say, press, wait, request_handoff, confirm_outcome, end). Invalid output is logged as an error event and replaced by a safe fallback line (scenario S8).
Disclosure guard
Every case fact has a tier assigned by the guard, never by the caller:
Tier | Examples | Behaviour |
| name, account number, issue | may be spoken |
| phone, email, address | spoken only after the user approved it for this case |
| OTP, PIN, CVV, full card, password, Aadhaar | never sent to the LLM, never spoken; a request for one triggers hand-off |
Template expansion. The planner writes
{{fact:key}}; the guard substitutes only allowed values.Output DLP. The final utterance is scanned for every never-tier value (exact and digit-normalised, including spelled-out digits) and for OTP-near-keyword, Luhn-valid card, Aadhaar and CVV patterns. A hit blocks the utterance and logs
blocked_utterance.Inbound classifier. Callee speech is untrusted data. Requests for OTP/PIN/password/remote access are tagged and trigger hand-off; injection phrases ("ignore your instructions…") are logged and ignored.
MCP surface
Tool | Input | Purpose |
|
| Start a case for an allowlisted business; elicits missing facts (form elicitation) or returns |
|
| Add or change a fact |
|
| Dial in the background; returns |
|
| State, hold time, masked last lines, |
|
| Answer a mid-call question without joining |
|
| The "Echo rings" moment; also "Take over call" and hand back |
|
| Hang up, return the outcome |
|
| Case with calls, promises, follow-ups and a summary |
|
| The user's cases |
|
| Follow-up call quoting the ticket id (default: promise due date + 1 day) |
|
| Erase a case, its transcripts and pending follow-ups |
Resources: standin://cases/{id}, standin://calls/{id}/transcript (masked), ui://standin/call-card (MCP Apps, text/html;profile=mcp-app; states Preparing, Dialing, Menu, On hold, Talking, Needs you, Done). Mid-call questions and hand-offs surface through call_status and are answered with send_to_call / handoff_to_user, so HTTP polling never deadlocks.
Eval
pnpm eval runs 8 golden scenarios against the deterministic fake business and checks hash-chain integrity and never-tier leakage on every surface. Current output (scripted planner, seed 1):
ID | Scenario | Required | Result | Checks |
S1 | happy-refund | yes | PASS | 9/9 |
S2 | otp-trap | yes | PASS | 8/8 |
S3 | injection | yes | PASS | 7/7 |
S4 | unknown-fact | yes | PASS | 6/6 |
S5 | ivr-dead-end | yes | PASS | 5/5 |
S6 | broken-promise | yes | PASS | 7/7 |
S7 | off-hours / not-allowlisted | yes | PASS | 2/2 |
S8 | llm-garbage | yes | PASS | 6/6 |
Aggregate: 8/8 runs passed · never-tier leaks 0 · crashes 0
With a real LLM planner: pnpm eval --llm openai --seeds 5 (gpt-5.4-mini) scored 40/40 runs, 0 never-tier leaks, 0 crashes (run log: eval/results/openai-gpt-5.4-mini-seeds5.txt; re-running needs your own OpenAI API key). Earlier runs scored 23/40, then 39/40; the fix was clarifying in the planner prompt that speakable=true means a fact is already approved. The OpenAI path is for local eval only. --llm bedrock exists but has not been run against live Bedrock (account verification, below).
Tests: pnpm test = 964 passed, 2 skipped (59 files: 58 passed, 1 skipped). CI (.github/workflows/ci.yml) runs lint, build, test and eval.
Security model
Threat | Control | Where |
Agent leaks OTP/PIN/card | never-tier values withheld from the LLM; output DLP before any TTS; S2/S3/S8 evals |
|
Prompt injection by the callee | callee speech is data; inbound classifier; hand-off; logged |
|
Harassment / spam calls | dial allowlist ( |
|
Scam number injection | numbers never come from the conversation or the LLM |
|
MCP endpoint abuse | Bearer token (or OAuth access token); Origin and Host checks (DNS rebinding); 401 without |
|
Cost abuse on the public demo | stricter per-IP limits on |
|
Account linking | pre-registered client only, exact redirect-URI match, PKCE S256 required, single-use 60 s codes, tokens stored as SHA-256 hashes, refresh rotation with reuse detection, consent page with CSRF token and passcode |
|
Transcript privacy | masked at write and again on read; 30-day TTL purge; per-case |
|
Tampered history | append-only, hash-chained event log; |
|
Secrets are read from .env (gitignored) or the container environment and are never logged. Details: docs/SECURITY.md.
AWS services
Service | Use | Status |
Amazon Bedrock (Converse API) | Sim orchestrator (tool use) and call planner in | Wired in |
Amazon Polly (neural) | Stand-In voice | Wired, mocked-client tests; not run live (account verification) |
Amazon Lightsail container service | Hosted judge demo: | Ready, not yet deployed |
AWS IAM | Least-privilege runtime policy: | Ready |
Amazon EventBridge Scheduler | Optional follow-up scheduler adapter ( | Adapter + mocked-client tests. The schedule target (e.g. a Lambda that calls back into the server) is not included; the local SQLite ticker is what runs follow-ups |
Amazon DynamoDB | Planned single-table repo | Stub only, not implemented; SQLite is used |
Alexa+ readiness
alexa-addon/ holds the add-on manifest template (addon.json) and the step-by-step setup. The server already provides what the Alexa+ MCP Toolkit expects:
/mcpwith 401 and noWWW-Authenticatewhen unauthenticated (starts account linking)./.well-known/oauth-protected-resource(RFC 9728) and/.well-known/oauth-authorization-server(RFC 8414),/authorize,/token.Optional Tier 1
client_credentialstokens andMCP_PUBLIC_DISCOVERYso tools can be listed before linking.A stateless fallback: the Alexa+ client is documented to keep no MCP session id per add-on, so a non-initialize request without
Mcp-Session-Idis served by a one-shot transport instead of a 400.Listing media under
/addon-assets/, and/privacyand/termspages.
It is not connected to a real Alexa+ device. Toolkit access requires an allowlisted account (see Limitations).
Deploy
docs/DEPLOY.md covers prerequisites, the runtime IAM user, the deploy (pnpm deploy:lightsail), the live smoke test (pnpm smoke:live <url>: health, 401 without WWW-Authenticate, foreign Origin 403, authenticated initialize + tools/list), and teardown after judging.
Repo layout
apps/
mcp-server/ MCP server, OAuth, /sim/api, static host, call card
alexa-sim/ simulated Alexa+ host (real MCP client, trace panel, autoplay)
call-agent/ CallSession, planner, scripted policy, SimCarrier
fake-business/ "BharatNet Broadband": IVR, hold, Ravi
packages/
core/ types, state machine, policy, hash-chained event log, SQLite repo, verify CLI
disclosure-guard/ tiers, expansion, output DLP, inbound classifier (MIT, standalone)
providers/ Bedrock / OpenAI / scripted LLM, Polly / browser TTS, schedulers
eval/ golden scenario runner (pnpm eval)
data/ business-directory.json (dial allowlist), scenarios
alexa-addon/ Alexa+ add-on manifest template and setup guide
docs/ DEPLOY.md, SECURITY.md
infra/ least-privilege IAM policy
scripts/ Lightsail deploy, live smoke test, add-on rendererLimitations and honest notes
No real phone calls. All calls go to the fictional BharatNet Broadband over
SimCarrier. Stand-In only dials numbers indata/business-directory.json, and none are real.No real Alexa+ device. The Alexa+ MCP Toolkit is in preview and US-only; our AssumeRole into the toolkit's developer-tools role returned
AccessDenied(not allowlisted). The browser host is a stand-in that speaks real MCP.Not deployed yet; Bedrock and Polly not exercised live. The new AWS account cannot complete verification: adding a payment card fails every time with "Error 880104", and Bedrock Converse returns
AccessDeniedException("Your account is currently being verified"). Bedrock and Polly code is covered by mocked-client tests; the real-LLM eval above used OpenAI locally.The OTP hand-off is simulated. The "account holder" in autoplay is scripted to join and leave.
Timing is compressed. Hold time is accelerated in the sim; the card shows a scaled timer.
Storage. SQLite only. On Lightsail it resets on redeploy. DynamoDB is a stub.
MCP Apps card.
@modelcontextprotocol/ext-apps2.x server helpers target the newer@modelcontextprotocol/serverpackage, not SDK 1.x, so the host uses a small hand-written postMessage bridge that follows the ext-apps message types.
License
MIT, see LICENSE. packages/disclosure-guard carries its own MIT license. Author: Mahal.
This server cannot be deployed
Maintenance
Related MCP Connectors
Call you, a person, or a business; find places, book or inquire by phone, and return the answer.
Your AI agent phones businesses for you: books, reschedules, cancels, asks, and waits on hold.
Place US/CA phone calls and delegate tasks an AI agent cannot finish.
Let your agent call businesses for you: quotes, reservations, cancellations, waiting on hold.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceGives Alexa+ voice access to track household insurance claims, refunds, repairs, and disputes, and to chase them via disclosed AI phone calls while keeping money and legal decisions with the human.MIT
- AlicenseNot gradedqualityCmaintenanceEnables Alexa+ agents to maintain auditable operational continuity across shifts by turning speech into verifiable state, persisting unresolved work, refusing unverified actions, and requiring human approval before executing and confirming high-risk tasks.MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to place real phone calls, engage in spoken conversations across 12 languages, and report the outcomes for tasks like booking appointments, answering questions, negotiating, or confirming orders.MIT

CallForMeofficial
AlicenseNot gradedqualityBmaintenanceEnables MCP clients to place real phone calls to businesses in the US and Canada, navigating phone menus, waiting on hold, asking the agent mid-call for information, and returning structured answers with transcripts. Supports booking, quotes, cancellations, and multi-business calls in parallel.MIT