Inspiration

A hair colourist mixes a client's colour by weight: 40 grams of 5N, 10 grams of 6.3, developer at one to one, 35 minutes. Next visit, that formula is in a notebook or a phone across the room, and the colourist has gloves covered in colour. Salons already pay for scales and apps to save formulas (SalonScale says over 8,000 salons use its software; Vish and Daviana charge monthly for per-client formula history), but every one of them is typed or tapped. Nobody captures the formula by voice, at the bowl, and scales it to today's bowl by voice. Alexa+ add-ons are MCP servers, so the tool a colourist needs is one MCP server away.

What it does

Formula Book is a self-hosted MCP server (Streamable HTTP, spec 2025-11-25) with three tools and an MCP Apps card. With gloves on, a colourist says "Lena's last formula" and hears every gram, the developer, the ratio and the time. "Make it one and a half times" scales it for today's bowl; the server does the maths. "Colour Marta: 30 grams 6N and 20 grams 7.1, 20 volume, one to one and a half" saves a new formula in one sentence. Two clients with the same name get a question through MCP elicitation, not a guess. An unknown client gets a refusal and the next step, never an invented formula. On screen devices an MCP Apps card shows the formula; on a phone the same card reflows with no new request; on a speaker one sentence carries every number.

With the Claude chooser, it works across services. "Who's booked in today? Get their bowls ready" makes Claude read the salon's booking diary (a second MCP server), get each booked client's formula from Formula Book, see that Rohan has none on file, and close with one sentence. Each round's results go back to Claude before it picks the next calls, up to three rounds, and every call shows in the traffic panel. "Lena and Priya Shah are both in, get both bowls ready at one and a half times" becomes two scale calls from one sentence.

A free plan of 50 formulas is enforced by the server. The price is a hypothesis and payments are not live; the waitlist is a local demo store.

Who it is for. The first user is the independent colourist: the US Bureau of Labor Statistics counts about 575,200 hairdressers, hairstylists and cosmetologists (2024), 48% self-employed. Vendors sell against waste too: SalonScale says salons waste 20 to 40% of colour per bowl from overmixing (a vendor claim with no published method), which is what scaling the last formula to today's bowl targets. These figures show the job is paid for; they do not prove demand for voice, and we have not talked to colourists yet. Sources and limits are in docs/market-evidence.md.

How we built it

TypeScript. The MCP server uses the MCP TypeScript SDK 1.32.1: three tools with zod schemas, structuredContent on every result, Streamable HTTP sessions, and elicitation/create sent on the same POST stream when the client can elicit (a needs_choice result otherwise, so a 2025-03-26 client still works). A pure maths package handles scaling, totals, client-name matching and speech normalising ("a hundred and fifty" to 150, "one to one and a half" to a ratio). The card is one self-contained HTML file served as a ui:// resource (text/html;profile=mcp-app, content-hashed URI, no network), rendered by our host through the ext-apps AppBridge with hostContext per device.

The Alexa+ client is simulated in this repo and labelled on every screen: a Next.js page that is a real MCP client. It lists the server's tools, picks one with a rule parser or with Claude Haiku 5.5 reading the server's own tools/list, calls it over MCP, renders the card and shows every JSON-RPC message. A second, stateless MCP server (apps/diary-server) serves the salon diary; the host connects to both and routes each call to the server that listed the tool. The Claude path is an agent loop of up to three rounds: Claude plans up to three calls per round, all or nothing, and sees each result as a tool_result before the next round. Voice is Chrome's Web Speech API in and speechSynthesis out.

The hosted demo runs the same server code inside the browser tab over in-memory MCP transports, because a serverless host cannot keep an MCP session (and its elicitation round trip) on one process. Streamable HTTP runs locally with npm run dev:all.

Tests: 172 unit and server tests (Vitest), and Playwright in Chrome: a ten-step golden path run ten times in a row, with every card assertion made inside the sandboxed iframe, plus specs for replay mode, in-tab mode and real Claude calls across both servers.

Challenges we ran into

  • Norton rewrote local HTTP responses that carried a Content-Length, which broke tools/list and resources/read in every client, Chrome included. It looked like a routing bug for hours. The server now streams every reply (friction log entry 4).
  • @modelcontextprotocol/ext-apps 2.x needs the SDK v2 packages, which npm does not make obvious; we pinned 1.7.5 with SDK 1.32.1 and sent the README fix upstream (PR 824).
  • Clicks right after a scroll landed inside the out-of-process card iframe in Playwright; waiting two animation frames fixed it.
  • Hosting: MCP sessions need a sticky process, so the demo runs the server code in the tab instead of on a serverless function.
  • No public way to test on real Alexa+; the add-on tools and the device-class list are partner-only, so the deviceClass values here are our guess.

Accomplishments that we're proud of

  • A judge can run the real server code and real Claude in a browser with nothing to install.
  • Elicitation done properly: the server asks which Priya, and falls back to a needs_choice result for clients that cannot elicit.
  • Claude working across two MCP servers from one sentence, with every call visible.
  • The honest labelling: what is real, what is simulated, what is a guess, on every screen and in the README.
  • Seven friction-log entries written when the problems were hit, and one fix sent upstream.

What we learned

  • An MCP server with structuredContent and a card is a small surface for a host's model to drive; the hard parts are refusals, same-name clients and keeping state on the server rather than in the chat.
  • Feeding results back as tool_result blocks was enough for Haiku to plan across two servers with no extra prompting.
  • Local security software can corrupt loopback HTTP in ways that look like protocol bugs.
  • Stateful Streamable HTTP and serverless hosting do not mix; the deployment story needs deciding on day one.

What's next for Formula Book

Ask three colourists the questions in docs/validation.md; test on real Alexa+ when access opens; swap the demo diary for a real booking system's API; connectors that write formulas into salon software.

Honesty notes

Not tested on real Alexa+: add-on tools are partner-only. The deviceClass values are our guess. The default tool chooser is a rule parser; Claude is an optional second chooser, not Alexa's model. The salon diary is a real MCP server with demo appointments, not a connection to real booking software. The price is a hypothesis and payments are not live; the waitlist is a local demo store. The hosted demo runs the server code in the browser tab rather than over Streamable HTTP, and says so on screen. The hosted Claude chooser is capped at 400 calls a day. No colourist conversations have happened yet. Prior art: Kenra Color skill (2019) answers brand questions; Daviana ColorLab and salon software already keep per-client formulas. Our difference is voice capture and spoken scaling at the bowl on any scale. The demo video's narration is text-to-speech; the footage is the product running in Chrome.

Open source

The repo is MIT: https://github.com/let-the-dreamers-rise/formula-book. Upstream contribution during the hackathon: https://github.com/modelcontextprotocol/ext-apps/pull/824, a README note on which ext-apps line pairs with the v1 MCP SDK.

Product feedback

MCP TypeScript SDK 1.32.1. Used for the server: three tools with zod schemas, Streamable HTTP sessions, structuredContent, and elicitation/create on the same POST stream. Worked well: registerTool and the session handling took little code; docs and types were enough to go from zero to a working tools/call in under an hour. Needs work: the negotiated protocol version is not exposed on the server session, so we read it from the initialize body to decide whether a session can elicit; short replies are buffered with a Content-Length, which local security software corrupted (friction log 4); no guidance on which features need a sticky process when hosting serverless (friction log 5). Would build with it again: yes.

MCP Apps, @modelcontextprotocol/ext-apps 1.7.5. Used for the formula card and the host side (AppBridge, PostMessageTransport, setHostContext per device). Worked well: the host side took a few dozen lines; setHostContext re-adapts the card on a device switch without a new tool call. Needs work: ext-apps 2.x needs the SDK v2 packages, which npm does not make obvious (friction log 3, our PR 824); the view bundle is 337 KB, too big to inline, so the card speaks the protocol by hand. Onboarding: a day, mostly on the version pairing. Again: yes.

Alexa+ developer documentation. Used for the card layout (deviceClass, maxWidth) and the client lifecycle (result shape with _meta.ui). Worked well: the payload shape was clear enough to copy. Needs work: the device-class list and the add-on tools are partner-only, so our values are a guess; the lifecycle sample sends protocol 2025-03-26 while the hackathon asks for 2025-11-25; there is no public simulator (friction log 1 and 2). Onboarding: could not reach hello world on a real device. Again: yes, once a public test path exists.

Anthropic API, Claude Haiku 5.5 as the tool chooser. Used to pick tools from the server's own tools/list, as a host's model would, and to run a loop across two MCP servers. Worked well: MCP tool schemas went straight into the tools field; feeding results back as tool_result blocks let Haiku read the diary and then call Formula Book per client with no extra prompting. Needs work: the inputSchema and input_schema TypeScript types differ slightly, so a cast was needed. Onboarding: twenty minutes. Again: yes.

Web Speech API and speechSynthesis (Chrome). Used for voice in and out. Worked well: one-shot recognition with no key. Needs work: no accuracy data on shade codes like "6N"; recognition may use a network service; no phrase hints. Again: for a demo, yes; for a product, on-device recognition.

Next.js 16, Playwright 1.64, Vercel. Next: fast reloads and transpilePackages worked; it writes an AGENTS.md unless agentRules is false, and build type-checking pulled in test files. Playwright: frameLocator inside the sandboxed card worked; clicks right after a scroll landed in the out-of-process iframe until we waited two frames; recordVideo wants its own ffmpeg (friction log 6). Vercel: deploying from a workspace needed three fixes (friction log 7), and MCP sessions cannot live on serverless (friction log 5). Again: yes to all three, with those notes.

Claude Code. Wrote most of the code under my direction, tests first for each milestone. Needs work: long sessions needed context summaries. Again: yes.

Friction log

  1. Testing against real Alexa+. Steps: read the add-ons and MCP Toolkit pages. Expected: a public way to connect a self-hosted MCP server. Actual: the toolkit and Local Inspector are partner-only. Severity: high. Workaround: a labelled simulated client that is a real MCP client. Suggestion: a public sandbox that accepts a self-hosted MCP URL.
  2. Protocol version in the lifecycle sample. Steps: compared the initialize sample with the MCP changelog. Expected: the sample's version supports every field it uses. Actual: it sends 2025-03-26 but uses structuredContent (2025-06-18). Severity: low. Workaround: accept both versions; elicit only when the client can. Suggestion: state the minimum version per feature and whether Alexa+ declares elicitation.
  3. ext-apps and SDK majors. Steps: npm view peerDependencies. Expected: latest ext-apps works with SDK v1. Actual: 2.x needs SDK v2. Severity: medium. Workaround: pin 1.7.5 with SDK 1.32.1. Suggestion: note the pairing in the README (our PR 824).
  4. Security software rewrites loopback MCP traffic. Steps: initialize, tools/list, tools/call with curl, Node and Chrome. Expected: responses as written. Actual: with Norton running, every reply sent with a Content-Length arrived marked chunked with an unchunked body; only buffered short replies failed. Severity: high on that machine. Workaround: the server strips Content-Length so Node sends every reply chunked. Suggestion: an SDK option to stream every response.
  5. MCP sessions on a serverless host. Steps: planned the Vercel deployment. Expected: run the server as a function. Actual: Streamable HTTP sessions and the elicitation round trip need one sticky process. Severity: medium. Workaround: the hosted demo runs the server code in the browser tab over in-memory transports. Suggestion: SDK docs on stateless versus stateful deployments.
  6. Recording the video with Playwright on Windows. Steps: recordVideo. Expected: a file, ffmpeg was installed. Actual: Playwright wants its own ffmpeg download. Severity: low. Workaround: CDP screencast frames plus the installed ffmpeg. Suggestion: accept a path to an existing ffmpeg.
  7. Vercel build of an npm workspace from Windows. Steps: vercel build locally, then on Vercel. Expected: an output folder. Actual: the local build fails on symlinks without Developer Mode; the remote install skipped root dev dependencies; next build type-checked test files. Severity: medium, forty minutes lost. Workaround: build on Vercel, declare @types/node in the app, a tsconfig.build.json for the build. Suggestion: document symlink rights; install root dev dependencies for workspaces.

Feature requests

  • Public list of Alexa+ deviceClass values and hostContext fields. Why: we had to guess for the card layout. Priority: Critical.
  • A public Alexa+ add-on sandbox or simulator for self-hosted MCP servers. Why: without partner access there is no way to test on real Alexa+. Priority: Critical.
  • Docs on how Alexa+ chooses between tools from several add-ons in one request. Why: our two-server flow depends on that routing. Priority: Important.
  • An ext-apps host test helper for out-of-process card iframes. Why: clicks right after a scroll landed inside the card until we waited two frames. Priority: Nice-to-have.

Built With

Share this project:

Updates

Submission history