Inspiration Voice agents still place the wrong order when they mishear a number or a time. Checkout inside one store can ask “is this right?” Households use more than one brand. We wanted one gate in front of those tools: read it back, wait for yes, then place it. Partner Alexa+ add-on APIs were not available, so we built the same pattern as a self-hosted MCP proxy and a web client.

What it does VoiceGate sits between the speaker and a fake cafe. “Two pizzas” is held, not placed. It asks for a time (8.30 means eight thirty, not eight pizzas). It speaks the item, count, and dollars. “Make it three” replaces the hold. “Yes” is the only call that writes the order. Hours and menu pass through. Tools that look like a question but would charge still have to prepare. Orders over $100 are blocked. Undo works for 60 seconds. Money is fake.

How we built it Two Node MCP servers over Streamable HTTP, spec 2025-11-25. The cafe owns place_order. VoiceGate owns prepare_action, confirm_action, cancel_action, undo_last_action, and get_audit_log. voicegate.tools.json is default-deny; only get_hours and get_menu skip confirm. The page at :3002 is a real client: initialize, tools/list, tools/call. Browser speech is optional. Typed routing still drives the demo. Checks cover time vs quantity, the get_order trap, the cap, and undo.

Challenges we ran into The cafe MCP is not the product; exposing place_order on the client would skip the gate. Time and quantity share the same words, so “seven” and 8.30 were parsed as pizza counts until the clock branch ran first. An old process on port 3002 kept answering “at seven.” Partner tools were preview-only, so the web page had to be the MCP client. Mic and TTS only work in Chrome or Edge after a click.

Accomplishments that we're proud of A working prepare-then-confirm path a judge can run locally. Speakable readback before any write. A household cap and undo that survive the session. A trap tool that cannot bypass the gate. Checks that fail if the page is not a real MCP client.

What we learned Confirm has to live in front of the mutating tool, not in the UI copy. Default deny is simpler than guessing from names. Clock parsing has to beat quantity parsing. Streamable HTTP is enough for a local demo if initialize, list, and call are real.

What's next for VoiceGate A second upstream (delivery or rides) behind the same proxy. Persisted identity so the cap is per household, not per process. A longer audit a parent can read. Typed input stays so the demo never depends on the mic.

Built With

  • css
  • express.js
  • html
  • javascript
  • javascript-(es-modules)
  • json-rpc-initialize-/-tools/list-/-tools/call-frontend-vanilla-html/css/js-(web/public).-manrope-and-jetbrains-mono-(google-fonts).-web-speech-api:-speechrecognition
  • json-runtime-node.js-20+-frameworks-and-libraries-express-5-(http-+-static-page)-@modelcontextprotocol/sdk-(mcpserver
  • node.js
  • redis
  • speechsynthesis-database-none.-no-sql
  • streamable-http)-zod-(tool-input-schemas)-native-fetch-(browser-mcp-client)-protocol-mcp-spec-2025-11-25
  • transport-streamable-http
  • typescript
Share this project:

Updates

Submission history