HoldDesk
Provides a voice front-desk MCP surface for Amazon Alexa+ add-ons over Streamable HTTP, letting the assistant check appointment availability, place holds for consultations/policy reviews/quote walkthroughs, look up or request cancellation of its own caller's appointments, answer office FAQs from a bundled knowledge file, give non-binding home-insurance quote ranges, and escalate to a human approval queue — while approvals, rejections and audit trails remain exclusively on the staff-signed-in /desk surface.
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., "@HoldDeskhold a consultation for Maria Lopez on Sept 18 at 10am"
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.
HoldDesk
The assistant holds, a human confirms.
A voice front desk for a small professional-services office where the assistant can ask but only a human can approve. Self-hosted Model Context Protocol server over Streamable HTTP (MCP spec 2025-11-25), built for the Alexa+ track of the Amazon Build, Ship, Shape Developer Hackathon. Everything in this repository is new, synthetic and self-contained (MIT). (Prototyped under the working name AgentDesk Concierge.)
Caller --voice--> Alexa+ / any MCP client --Streamable HTTP--> HoldDesk --approval queue--> Human on /deskProduct principle
Voice assistants are great at taking a request and terrible at being accountable for it. HoldDesk splits the two:
The assistant can (MCP tools) | Only a signed-in person can ( |
check openings, hold a slot, check or cancel its own caller's appointment, answer office FAQs from a bundled knowledge file, give a clearly non-binding home-insurance range, escalate anything | see the approval queue, approve or reject each held slot / cancellation / escalation, with their name and staff account on the audit trail |
There is deliberately no MCP tool that resolves an approval, or even lists the queue. A hold is never a booking until a human says so, and the assistant is told exactly that in its tool descriptions and server instructions.
Related MCP server: Partner Booking MCP Server
Tools
The MCP surface contains exactly seven tools:
Tool | Kind | Scope | What it does |
| read-only | public | Open start times for |
| proposes | user | Places a hold ( |
| read-only | user | Status lookup; only the linked account that booked it, and the booking contact (or last-4 of the phone) must match. Unknown id, another account's booking and wrong contact all return the same |
| proposes | user | Same ownership check, with full booking contact required (last-four is lookup-only); marks |
| read-only | public | Deterministic keyword retrieval over |
| read-only | public | Illustrative annual/monthly range from a synthetic rate table. Always |
| proposes | user | Generic escalation into the human queue (callback, binding quote, speak to advisor...). SSN/card numbers anywhere in |
The approval queue is staff-only: it is shown on the desk (and its JSON API) after staff sign-in, never over MCP.
Also exposed: resource office://info (JSON office profile) and prompt front_desk_greeting.
Every result carries a spoken_summary string written for voice, plus structured fields for screens (MCP structuredContent).
Domain refusals come back as {"ok": false, "error": {"code": ..., "message": ...}, "spoken_summary": ...} so the assistant can recover conversationally; schema violations surface as normal MCP tool errors.
Quick start
git clone https://github.com/alanwen168/holddesk.git
cd holddesk
python3 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
python -m pytest # every tool incl. refusal/validation paths, live HTTP, desk login/CSRF, OAuth AS + scopes, Alexa+ simulator
python -m holddesk # MCP: http://127.0.0.1:8641/mcp Human desk: http://127.0.0.1:8641/desk (staff sign-in)Options: --auth static|oauth (see Authentication for Alexa+), --port, --host, --stateful (issue MCP-Session-Id instead of stateless mode), --json-response (plain JSON instead of SSE), --allowed-host/--allowed-origin (needed behind a tunnel, see below).
Environment: HOLDDESK_PORT, HOLDDESK_HOST, HOLDDESK_BEARER_TOKEN, HOLDDESK_STATE_FILE (persist the in-memory desk to JSON), HOLDDESK_FIXED_NOW (freeze the clock, e.g. 2026-09-14T10:00:00, for reproducible demos), HOLDDESK_ALLOWED_HOSTS, HOLDDESK_ALLOWED_ORIGINS (comma-separated), HOLDDESK_AUTH (static/oauth), plus the HOLDDESK_OAUTH_* / HOLDDESK_DEMO_* / HOLDDESK_DESK_* variables below.
Try it with the MCP Inspector
npx @modelcontextprotocol/inspector # web UI: choose "Streamable HTTP", URL http://127.0.0.1:8641/mcp
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8641/mcp --transport http --method tools/list
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8641/mcp --transport http --method tools/call \
--tool-name check_availability --tool-arg date=2026-09-16 --tool-arg service=consultationTry the whole journey with the official Python client
HOLDDESK_DESK_PASSWORD=<desk password> python scripts/smoke_client.py http://127.0.0.1:8641/mcp --date 2026-09-16It calls every tool, signs in to the desk and approves the held slot through the desk JSON API, and prints the transcript. Open http://127.0.0.1:8641/desk in a browser and sign in to approve/reject by hand instead.
The human desk (staff sign-in)
/desk is the most consequential surface, so it has its own staff login, separate from the callers' OAuth consent login:
HOLDDESK_DESK_USERNAME(defaultstaff) /HOLDDESK_DESK_PASSWORD. If the password is unset a random one is generated for the process and printed once to stderr (development only).Session cookie
holddesk_desk: opaque random id (only its SHA-256 is kept, in memory),HttpOnly,SameSite=Strict,Path=/desk, 8 h, andSecurewhen the public URL / OAuth issuer ishttps://.CSRF: every state-changing request needs the session's CSRF token (hidden
csrf_tokenfield in the desk forms,X-CSRF-Tokenheader onPOST /desk/api/approvals/{id}/decision); the login form carries a double-submit token.Failed logins are throttled per client IP + username (5 failures -> 30 s lockout, doubling up to 15 min) and per IP across usernames (20); during a lockout the password is not even checked. The OAuth consent login uses the same throttle.
Framing:
HOLDDESK_DESK_FRAME_ANCESTORS(space/comma-separated origins) becomesContent-Security-Policy: frame-ancestors <origins>; unset meansframe-ancestors 'none'+X-Frame-Options: DENY. The simulator launcher sets it to the simulator origin only.JSON API for scripts:
GET /desk/api/session(returns the CSRF token),GET /desk/api/approvals,POST /desk/api/approvals/{id}/decision; all401without a session.
The throttle is in-memory and keyed on the client IP as seen by the server: behind a reverse proxy all clients share the proxy's IP, so a burst of failures can lock the (single) demo account out for everyone for a while. Put SSO or an allow-list in front of the desk for anything beyond a demo.
Connect from any MCP client
examples/mcp_client_config.example.json shows the generic remote-server config (type: http, url: .../mcp). Claude Code: claude mcp add --transport http holddesk http://127.0.0.1:8641/mcp.
Connect to Alexa+ (MCP Toolkit)
Alexa+ add-ons talk to a remote MCP server over Streamable HTTP, spec 2025-11-25 (overview, quickstart).
Expose the server with a tunnel during development:
python -m holddesk --allowed-host mcp.example.com --allowed-origin https://mcp.example.com cloudflared tunnel --url http://127.0.0.1:8641 # or ngrok; use the https URL it prints(
--allowed-hostis required: the server enforces the spec's DNS-rebinding protection and rejects unknownHost/Originvalues with 403/421.)Install the Alexa AI CLI and log in:
alexa-ai configure.Either let the add-on Agent Skill scaffold it ("I have an MCP server at https://... for a small office front desk. Help me create an Alexa+ add-on.") or do it manually:
alexa-ai new mcp --name "HoldDesk" --locale en-US --mcp-server-url "https://<your-host>/mcp" # edit addon-package/addon.json (see examples/addon.json.example), then alexa-ai deploy && alexa-ai submit # deploy = test in the web simulator / dev-stage deviceRedeploy after changing tools: Alexa+ refreshes tool metadata only on
alexa-ai deploy.
The Alexa+ Local Inspector (mcp-toolkit-local-inspector) can produce a certification readiness report against this server before you deploy.
Auth: see the next section. For Alexa+ run with --auth oauth and register the client id/secret, the authorization URL (/oauth/authorize) and the token URL (/oauth/token) with the add-on.
Authentication for Alexa+
The Alexa+ MCP Toolkit authentication guide asks for a two-tier OAuth 2.0 setup. HoldDesk ships a small built-in authorization server (holddesk/oauth.py) that follows it:
Alexa+ requirement | HoldDesk |
Metadata document |
|
Tier 1, service level |
|
Tier 2, user level |
|
Token hygiene | Opaque random tokens ( |
Client authentication | HTTP Basic ( |
Not supported, on purpose | Dynamic Client Registration, OpenID Connect, step-up auth, |
Scope policy (PUBLIC_TOOLS / USER_TOOLS in holddesk/server.py):
Public (any valid token, including Tier 1
mcp:service):check_availability,answer_office_faq,estimate_home_insurance_quote_range. They read office-wide, non-personal data only. Theoffice://inforesource and greeting prompt are available to any authenticated MCP caller;mcp:resourcesis advertised for client compatibility but is not a separate resource-level authorization gate in this demo.User level (need
mcp:tools, i.e. a Tier 2 token):book_appointment,get_appointment_status,cancel_appointment,request_human_approval. They create, read or change something tied to a caller, and are bound to the token subject: what a token creates is owned by its subject (user:<login>), and status/cancel only find appointments owned by the same subject.The approval queue (other callers' requests) is not an MCP tool at all; only signed-in staff see it on the desk.
A user-level tool called with a Tier 1 token does nothing and returns a normal tool result:
{"ok": false,
"error": {"code": "insufficient_scope", "required_scope": "mcp:tools", "granted_scopes": ["mcp:service"],
"grant_type_hint": "authorization_code", "message": "This tool acts for a specific caller and needs a user-level token."},
"spoken_summary": "I need you to link your account before I can do that for you."}The check fails closed: if no principal reached the tool, it is refused.
Run in OAuth mode
export HOLDDESK_OAUTH_ISSUER=https://mcp.example.com # public base URL (default http://127.0.0.1:<port>)
export HOLDDESK_OAUTH_CLIENT_ID=holddesk-alexa # default holddesk-dev-client
export HOLDDESK_OAUTH_CLIENT_SECRET="$(python -c 'import secrets;print(secrets.token_urlsafe(32))')"
export HOLDDESK_OAUTH_REDIRECT_URIS=https://<redirect-uri-given-by-alexa> # comma-separated, exact match
export HOLDDESK_DEMO_USERNAME=demo # the one demo account for the consent page
export HOLDDESK_DEMO_PASSWORD="$(python -c 'import secrets;print(secrets.token_urlsafe(12))')"
# optional: HOLDDESK_OAUTH_TOKEN_TTL=3600 (capped at 3600), HOLDDESK_OAUTH_RESOURCE (default <issuer>/mcp)
python -m holddesk --auth oauth --allowed-host mcp.example.com --allowed-origin https://mcp.example.com--auth defaults to oauth when HOLDDESK_OAUTH_CLIENT_SECRET is set, otherwise static. If you force --auth oauth without a secret or demo password, random values are generated for that process and printed once to stderr (development only). The default redirect URI is <issuer>/oauth/debug-callback, a local page that just shows the code, so the flow can be tried by hand:
B=http://127.0.0.1:8641
curl -s $B/.well-known/oauth-authorization-server
# Tier 1
curl -s -u "$HOLDDESK_OAUTH_CLIENT_ID:$HOLDDESK_OAUTH_CLIENT_SECRET" -d grant_type=client_credentials -d scope=mcp:service $B/oauth/token
# Tier 2: open $B/oauth/authorize?response_type=code&client_id=...&redirect_uri=$B/oauth/debug-callback&code_challenge=<S256>&code_challenge_method=S256&state=x
# log in, then exchange: -d grant_type=authorization_code -d code=... -d redirect_uri=... -d code_verifier=...Tokens live in memory, so restarting the server logs every client out; that is intended for a single-instance demo.
Static mode (local and tunnelled testing)
--auth static keeps the original behaviour: set HOLDDESK_BEARER_TOKEN=<random> and /mcp answers 401 (JSON, no WWW-Authenticate) unless Authorization: Bearer <random> is present; the holder of that token gets every scope. With no token set the endpoint is open (local development only); in open and static mode every caller shares one subject. In every mode /healthz and the OAuth endpoints are public, and /desk is behind its own staff sign-in (not the MCP gate).
Alexa+ simulator (voice assistant simulator, Alexa+ style)
The hackathon rules allow "a simulated Alexa+ experience ... via a web app". holddesk_sim/ is that web app: an Echo Show 15-style screen (1280x800) with a voice ring, driven by a small orchestrator that talks to the real HoldDesk server over MCP exactly the way the Alexa+ MCP Toolkit describes. It is a separate package in this repo; the MCP server does not depend on it.

pip install -e ".[sim]"
python -m holddesk_sim.demo # or scripts/run_demo.sh
# Simulator UI : http://127.0.0.1:8650/
# Human desk : http://127.0.0.1:8641/desk
# Caller login : demo / <random, printed once> (account-linking consent page)
# Desk login : staff / <random, printed once> (staff sign-in on the desk)The launcher starts HoldDesk in --auth oauth mode on :8641 with generated dev secrets, the simulator's /link/callback registered as the only redirect URI and the simulator origin as the only allowed desk frame ancestor, then the simulator on :8650. Both logins are printed in the terminal only; the simulator API does not hand them out. Sign in to the desk inside its iframe once. Open the UI in Chrome/Edge, tap the ring (Web Speech API; the text box is the fallback) and speak.
What happens on each utterance (holddesk_sim/orchestrator.py)
At session start the orchestrator gets a Tier 1 token (
client_credentials, scopemcp:service), connects with the officialmcpPython client over Streamable HTTP and callstools/list; the MCP tool schemas become LLM function-calling schemas.The utterance + tool schemas go to the LLM; every tool call it makes is executed on HoldDesk via MCP
tools/call, results go back to the model, until it produces a short spoken reply (read aloud withspeechSynthesis, en-US).When a tool answers
error.code == "insufficient_scope"(e.g.book_appointmenton a Tier 1 token) the turn pauses and the UI asks the user to link their account. That opens HoldDesk's own consent page in a popup: a realauthorization_code+ PKCE S256 flow (/oauth/authorize->/link/callback-> code exchange at/oauth/token). The paused tool call is then retried with the Tier 2 token (mcp:tools mcp:resources).Structured results render as cards: availability slots, the booking hold with a "Pending staff approval" badge, FAQ answer with its source id, the insurance range with its non-binding disclaimer. A collapsible developer trace shows every MCP call (tool, arguments, latency, token tier), every LLM step and every OAuth event. The human desk (
/desk) sits beside it in an iframe: approve the hold there and the next "is it confirmed?" comes back Confirmed by staff.
LLM providers (holddesk_sim/llm.py)
Provider | Use |
| Local |
| Deterministic keyword/regex intent router emitting the same tool calls. Used by the tests and as the automatic fallback when Ollama is down ( |
| Stub only (shape of an Amazon Bedrock Converse mapping); never called. |
Guards for a small model (all visible in the trace as guard:* and in the reply metadata):
No invented caller details:
client_name/contactmust have been said by the caller; otherwise the call is rejected before it reaches HoldDesk and the assistant asks for them (qwen 7b tried to book as "User, 123-456-7890" in testing).No unbacked claims: a reply like "I've booked it" with no tool call gets one nudge, then is replaced.
Grounding: a question answered from the model's own memory (e.g. a stale status, or "capital of France") is re-routed through the deterministic intent router to the right tool.
No false confirmations: "confirmed" is only allowed if
get_appointment_statusreturnedconfirmed; booking replies keep the pending notice, reference id and disclaimers.Arguments are coerced to the tool schema (unknown keys dropped, "$500,000" -> 500000).
Environment for the launcher: HOLDDESK_PUBLIC_URL / SIM_PUBLIC_URL (public HTTPS hostnames behind a tunnel or proxy; they become the OAuth issuer, redirect URI and --allowed-host/--allowed-origin), HOLDDESK_PORT / SIM_PORT, SIM_BIND, HOLDDESK_OAUTH_CLIENT_SECRET / HOLDDESK_DEMO_PASSWORD / HOLDDESK_DESK_USERNAME / HOLDDESK_DESK_PASSWORD (random if unset), HOLDDESK_FIXED_NOW, SIM_LLM=ollama|scripted, SIM_OLLAMA_MODEL, OLLAMA_URL, SIM_SHOW_DEMO_CREDS=1 (local recording only: /api/config then returns the caller demo login and the linking dialog shows it; off by default).
scripts/sim_e2e.py drives the whole journey through the simulator's HTTP API (it plays the caller in the consent popup and the staff member signing in to the desk) and scores tool selection; it uses the httpx dependency from the sim extra. scripts/sim_screenshot.py does the same through the real UI in headless Chromium. These repository scripts run from a source checkout; for the screenshot script, install its optional dependency and browser once:
pip install -e ".[sim,screenshot]"
python -m playwright install chromium
python scripts/sim_screenshot.py docs/sim_screenshot.pngRelease verification (2026-10-16): 108 automated tests passed, including live HTTP negotiation of 2025-11-25, two-tier OAuth, scope and subject checks, staff authentication, concurrent holds, and cancellation transitions. The recorded demo uses SIM_LLM=scripted, a deterministic intent router, with real MCP transport, OAuth consent and staff decisions. It demonstrates the server workflow rather than measuring LLM accuracy. The optional Ollama integration remains available; historical local-model measurements are not release guarantees.
Simulator limits: sessions and tokens live in memory; the simulator holds the Tier 2 token server-side for its browser session, so it is a single-user demo surface, not a multi-tenant app. It is not an Amazon product and uses no Amazon marks.
Design notes for voice
Tool descriptions state the approval rule so the orchestrator never promises a confirmed booking.
spoken_summaryis short, states the reference id, and never enumerates more than three slots.Sensitive numbers are refused at the boundary (
sensitive_data_refused) rather than logged: in names, notes, cancel reasons, escalation summaries and every key/value of escalationdetails.Responses are deterministic for the same inputs and clock, which keeps demos reproducible.
Repository layout
holddesk/
domain.py pure logic: scheduling, approvals, FAQ retrieval, synthetic rating
server.py FastMCP tools/resource/prompt, scope policy, /desk human UI + JSON API, auth gate, CLI
oauth.py minimal OAuth 2.0 AS: metadata, client_credentials, auth code + PKCE S256, refresh (family revocation)
desk_auth.py staff sign-in for /desk: sessions, cookie flags, CSRF, frame-ancestors
security.py in-memory login throttle (consent + desk)
data/ office.json, faq.json, rate_table.json (all synthetic)
holddesk_sim/ voice assistant simulator (Alexa+ style)
orchestrator.py utterance -> LLM -> MCP tool calls loop, two-tier token handling, account linking, guards, cards
mcp_link.py official mcp client over Streamable HTTP + OAuth token requests (both tiers, PKCE)
llm.py Ollama / scripted / fallback providers, Bedrock stub
app.py Starlette API + /link/callback; demo.py one-command launcher; static/ UI (no build step)
tests/ pytest: domain unit tests, live Streamable HTTP end-to-end tests, desk auth/CSRF/XSS, OAuth AS + scope/subject tests, simulator
scripts/smoke_client.py exercises every tool over HTTP
scripts/run_demo.sh, sim_e2e.py, sim_screenshot.py launch / score / screenshot the simulator
docs/ simulator screenshots and release verification notes
examples/ addon.json.example (Alexa+ manifest template with placeholders), mcp_client_config.example.json
DEMO_SCRIPT.md <3-minute video plan friction_log.md product feedback for the judgesStatus and honesty
Synthetic office, staff, FAQ and rate table. No real customer, insurer or rate data anywhere.
The quote tool is an illustration of how a voice assistant should hand off to a licensed human, not a rating engine.
Not yet tested on a physical Alexa+ device (developer-preview access pending); tested with the MCP Inspector, the official Python client over Streamable HTTP, and the bundled Alexa+-style simulator (deterministic routing for the release recording; optional local model integration).
The OAuth server is verified by its own tests and by hand with curl and the Python client; it has not yet been exercised by Alexa's real account-linking flow. It is a single-instance, in-memory reference (one client, one demo user, one staff account), not a production identity provider.
examples/addon.json.exampleis a template: privacy/terms URLs, media and the MCP URL are placeholders to fill in before any deploy.
License: MIT.
This server cannot be deployed
Maintenance
Related MCP Connectors
An AI concierge that turns static forms into adaptive AI conversations. From any MCP client.
- AstrocalOAuthdev.astrocal
AI-native scheduling: check availability, book meetings, cancel and reschedule via MCP
JooVoice lets your AI assistant make real phone calls for you through MCP. Ask it to book a table or appointment, change a reservation, cancel a subscription, request a refund, or check stock and opening hours. Get the outcome back in your chat, with the call recording and transcript. You choose when calls need your approval. Personal calls, such as birthday wishes or family check-ins, use a recipient consent link. Learn more at https://joovoice.com.
AI agent platform: manage leads, conversations, bots, calendar and CRM via MCP.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables interaction with IntelliMatch recruiting and iSuite insurance systems through spec-driven MCP tools, allowing natural language queries for candidate search, policy management, claims, and workflows.28-
- FlicenseNot gradedqualityDmaintenanceSimulates a third-party appointment booking agent, enabling your AI platform to check availability and book appointments via MCP interoperability.-
- FlicenseNot gradedqualityBmaintenanceEnables AI agents to search for available desk space, submit room reservations, and let admins approve or reject booking requests through a scoped MCP API.-
- AlicenseNot gradedqualityBmaintenanceEnables voice-driven appointment booking for small businesses, letting customers check availability, book, reschedule, and cancel appointments through any MCP client.1 npmMIT