merch-store-mcp
# merch-store-mcp
A local MCP server that lets any agent shop the **Twilio Merch Store** demo — an
ecommerce store that publishes its capabilities to agents over
[WebMCP](https://developer.chrome.com/docs/ai/webmcp), gated by an agent
credential the account holder grants and revokes in Authy.
```
your MCP client ──stdio──▶ merch-store-mcp ──CDP──▶ Chrome ──▶ the store's own tools
```
## Why this exists
The store's tools are registered **by the page**, in a browser tab. That is the
whole point of WebMCP — capabilities travel with the site, not with a separate
server — and it is also why you cannot just point an MCP client at a URL. There
is no server to point at, and as of August 2026 no mainstream agent speaks
WebMCP anyway.
So this attaches to Chrome over the DevTools Protocol, reads the tools the page
registered, and republishes them over stdio MCP under their real names.
It **never reimplements a tool**. Every call is dispatched to the page's own
implementation, so an agent and a person clicking the UI take the same code path
and cannot drift apart. You can watch it happen: the store logs every call in
its own console panel while your agent works.
## Setup
Needs Node 20+ and Google Chrome. Nothing else — no clone, and you do not need
the store's source.
### Claude Code
```bash
claude mcp add merch-store -- npx -y github:kmorope/merch-store-mcp
```
### Claude Desktop, Cursor, and anything else reading a JSON config
```json
{
"mcpServers": {
"merch-store": {
"command": "npx",
"args": ["-y", "github:kmorope/merch-store-mcp"]
}
}
}
```
Then ask your agent for `store_status`. It reports the connected page, the tool
count, and which WebMCP path is in use.
Chrome is **started for you** on first use, in a profile of its own at
`~/.merch-store-chrome`, with the store already open. If a Chrome is already
listening on the DevTools port, the bridge attaches to that instead.
### From a clone, if you want to change it
```bash
git clone https://github.com/kmorope/merch-store-mcp
cd merch-store-mcp && npm install
npm run smoke # end-to-end check over real stdio MCP, ~15s
```
Point your client at `node /absolute/path/to/merch-store-mcp/server.mjs`. Use an
**absolute path** — these clients do not run servers from your project
directory, so a relative one resolves against something you did not choose.
## Serving it over HTTP, for a tunnel
There is also an HTTP mode, for clients that cannot spawn a process — which in
practice means putting this behind a tunnel so nobody installs anything at all.
```bash
node server.mjs --http --port 7331 # prints a bearer token
ngrok http 7331 # in another terminal
```
```bash
claude mcp add --transport http merch-store https://<your-host>/mcp \
--header "Authorization: Bearer <token>"
```
`npm run smoke:http` covers this path: token refused when absent or wrong, the
full tool surface over Streamable HTTP, a real call reaching the page, and two
clients getting two sessions.
**Read this before you expose it.** The bridge drives a browser, so one instance
behind a tunnel means everyone drives **the same Chrome on the host machine**:
one shared cart, one shared page, interleaved tool calls — one client's
`set_filters` changes what another's `get_page_state` returns. There is a test
asserting exactly that, because it is behaviour to know about, not a bug to fix.
It is right for a demo one person drives while others watch, and wrong for
people exploring in parallel. For that, each person runs their own copy over
stdio — the `npx` line above.
A bearer token is generated when you do not pass one, because anyone who can
reach the endpoint can drive a real browser on your machine. `--no-auth` exists
for localhost and should never meet a tunnel. Sessions are capped at 32 and
reaped after 30 minutes idle, since a client that vanishes without saying
goodbye would otherwise leave its session behind forever.
## What your agent gets
Every tool the page registers — currently 19, including `get_page_state`,
`search_products`, `add_to_cart`, `checkout`, `present_credential`,
`get_purchase_attempts` and `join_drop` — plus three of the bridge's own:
| Tool | Purpose |
| --- | --- |
| `store_status` | Connected page, tool count, and whether native WebMCP is live |
| `store_open` | Navigate the connected page to a store path |
| `store_read_log` | Read the store's live call log, to confirm what really happened |
The authoritative list, with schemas and the error contract, is published by the
store itself at
[`/.well-known/mcp.json`](https://twilio-mcp-store.ngrok.dev/.well-known/mcp.json),
and the store's own [`/docs`](https://twilio-mcp-store.ngrok.dev/docs) page
documents the flows.
### How the store expects an agent to behave
1. Call `get_page_state` before deciding anything.
2. Treat structured errors as instructions, not failures — each one names the
next move. `402 identity_required` means call `present_credential`.
3. **Stop** on `approval_required`, `credential_revoked`, `rate_limited`,
`ai_disabled` and `backend_unreachable`. Retrying is wrong in all five.
4. Never report a purchase as complete while it is waiting on a human. Read
`get_purchase_attempts` and say what is pending.
There is deliberately no tool to approve a purchase. That decision belongs to
the account holder's phone, and an agent that could answer its own request would
make the demo meaningless.
## Options
| Flag | Environment | Default |
| --- | --- | --- |
| `--url <url>` | `MERCH_STORE_URL` | `https://twilio-mcp-store.ngrok.dev` |
| `--cdp <url>` | `MERCH_STORE_CDP_URL` | `http://127.0.0.1:9222` |
| `--no-launch` | `MERCH_STORE_LAUNCH=0` | launching enabled |
Point it at a store running on your own machine with
`--url http://localhost:5173`.
## Real WebMCP, optionally
The bridge works whether or not the browser API exists, because it calls the
page's tool functions either way. To exercise the genuine standard, open
`chrome://flags/#enable-webmcp-testing` in the `~/.merch-store-chrome` profile,
set it to **Enabled**, and restart Chrome. `store_status` then reports
`nativeWebMcp: true`.
## Two things worth knowing
**The debug profile is separate on purpose.** `--remote-debugging-port` lets any
local process read and drive every tab in that browser. Pointing it at your
everyday Chrome would hand your logged-in sessions to anything running on your
machine. This profile only ever has the demo in it — keep it that way.
**Writes need a moment to land.** A tool call schedules a UI update that has not
committed the instant the call resolves. The page waits two animation frames
before returning, so back-to-back calls through this bridge are safe; if you
drive the page by other means, wait yourself.
## Troubleshooting
**"never published a tool registry"** — the store is not up at that URL, or you
are pointed at a different site on that host. Open the URL in a browser first. A
production build of the store only exposes its registry with `?agent=1`, which
the bridge adds for you.
**"Chrome was started but never answered"** — an already-running Chrome from your
normal profile can swallow the launch. Quit it, or start the debug profile by
hand and use `--no-launch`.
**The backend is unreachable** — you will see a red banner in the store itself.
Spending limits, purchase attempts and approvals live on the Authy backend and
nothing is cached, so the store would rather say nothing than show a stale
answer. That is the demo's backend, not this bridge; ask whoever is running it.
## License
MIT
TDQS
Scored across 3 tools
Each tool targets a distinct concern: store_status checks connection/bridge health, store_open handles navigation, and store_read_log inspects the execution log. There is no overlap in purpose or ambiguous boundaries between them.
All tools share the store_ prefix and use snake_case, creating a predictable pattern. Minor variation exists between noun-style (store_status) and verb-style (store_open, store_read_log), but the convention is uniform enough for an agent to infer behavior.
Three tools is on the lower end but well-scoped for a lightweight bridge/debugging server. Each tool serves a clear and necessary role, and the count aligns with the server's narrow purpose.
The server covers the core diagnostic workflow: check connection, navigate if needed, and verify via logs. It lacks direct control or configuration tools, but those appear intentionally delegated to the page's own tools, so no significant gaps for its intended use.