A tourist opens Claude Desktop and asks, "What should I do in Maui this weekend?"
Six months ago, Claude would hallucinate a link, quote an outdated price, or suggest a tour that closed during the pandemic. The answer would be a paragraph of confident-sounding fiction. There was no mechanism for Claude to actually know anything about Hawaii tourism in real time.
This weekend I fixed that for one island chain. I built a dedicated Hawaii tourism MCP server, published it to the official Model Context Protocol registry, and now every AI assistant that speaks MCP can call real tools against 2,464 bookable tours, 495 events, 541 restaurants, and live weather data for all four Hawaiian islands.
Here's how it works, why it matters, and the architecture choices I'd make again.
What is an MCP server, in one paragraph
The Model Context Protocol is Anthropic's open standard for giving AI assistants tool-use capability over a simple JSON-RPC transport. A server advertises a list of tools (functions with typed inputs), the client (Claude Desktop, Cursor, Cline, etc.) calls them when the user's prompt matches the tool's description, and the server returns structured results that the model can reason about. It's REST endpoints for the AI era — except the AI decides when to call them, not a developer wiring up a frontend.
For a vertical business like aloha.fyi — Hawaii's AI tourism concierge — this is a distribution inflection point. Instead of buying Google ads to compete with every other "Hawaii tours" site, I can publish one MCP server and be the default answer for every AI assistant in the world when a user asks about Hawaii.
The six tools
The server exposes six tools. Each maps to a real user intent I've seen in actual aloha.fyi chat conversations over the last year:
search_hawaii_tours— filter 2,464 bookable experiences by query, island, price, and source (Viator, GetYourGuide, Klook, Groupon)get_hawaii_deals— budget options under a price cap, cheapest firstsearch_hawaii_events— 495 events across 98 venues, updated weeklyget_hawaii_weather— current conditions plus 1-7 day forecast from Open-Meteo (free, no API key)find_hawaii_restaurants— 541 food spots across 18 categories (fine dining, casual, poke, ramen, food trucks, bakeries)plan_hawaii_day— generates a morning/lunch/afternoon/dinner itinerary for a given island and vibe (adventure, chill, cultural, romantic, family, budget)
Each tool has a carefully worded description. The description is the prompt the model reads to decide whether to call the tool. I learned quickly that descriptions matter more than parameter schemas — a tool with a vague description gets ignored, and a tool with a specific one gets called in conversations you didn't anticipate.
Compare these two descriptions for the deals tool:
Bad: "Returns Hawaii deals."
Good: "Find budget-friendly Hawaii tours and activities under a price cap. Cheapest first; well-reviewed Viator options preferred at similar prices. Use when users want affordable Hawaii experiences or budget travel tips."
The second version tells the model not just what the tool returns, but when to reach for it. "Budget," "affordable," "deals," "discounts" — those are the words a real user types. Claude picks up the pattern.
The tech stack
Nothing exotic. That's the point.
- TypeScript + the official
@modelcontextprotocol/sdk - Express 5 for the HTTP layer
- Postgres (the same one that powers the main aloha.fyi frontend) for the 2,464-row experience catalog
- Railway for hosting — one click, auto-deploy on
git push - Open-Meteo for weather (free, no key required, generous rate limits)
At the time of writing the whole server was a single ~900-line http.ts file (it has since grown past 2,000). Three tools are pure SQL against existing tables. One is an HTTP fetch with a 5-minute cache. The other two compose data from multiple tables.
The clever bit isn't the code — it's the transport choice.
Why I chose stateless Streamable HTTP
MCP supports two transport modes:
- stdio — one process per client session, bidirectional JSON-RPC over stdin/stdout. This is how Claude Desktop launches Python and Node servers locally. Great for local-only tools (filesystem, shell, developer IDE).
- Streamable HTTP — HTTPS POST endpoint, optionally with server-sent events for streaming responses. This is how remote/hosted servers work. You either maintain sessions with a server-generated session ID, or you run "stateless mode" where every POST is a fresh server instance.
I went stateless, and it's the right call for a mostly-read vertical catalog server. Every POST /mcp builds a fresh McpServer, a fresh StreamableHTTPServerTransport, handles the one JSON-RPC call (or batch), and cleans up. No session table, no TTL sweeping, no memory leaks.
The pattern looks like this:
app.post("/mcp", async (req, res) => {
const server = buildServer(logCtx);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless
});
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
Seven lines. No state. Every client, every request, completely independent.
The tradeoff is that I can't maintain a conversation across multiple tool calls — but for a tourism catalog, that's fine. The AI client is maintaining the conversation; my server is a stateless function call it makes when it needs data.
The attribution problem
Here's where it gets interesting.
When a tourist asks Claude "book me a helicopter tour on Kauai" and Claude calls my tool, my tool returns a response with a bookable URL. Claude shows the URL in its response. The tourist clicks the URL. The click goes to the affiliate network (Commission Junction, Viator's partner program, GYG's API). If the tourist books, the affiliate network eventually pays me a commission — weeks or months later, in a monthly report.
The question is: which AI client drove that commission?
Without attribution, I have zero visibility. Was it Claude Desktop? ChatGPT? Cursor? A custom agent someone built? I'm blind to my own funnel.
I built three layers of attribution, each complementing the next.
Layer 1: Request logging
Every POST to /mcp writes a row to a Postgres mcp_requests table with:
- Timestamp
- JSON-RPC method (
initialize,tools/list,tools/call) - Tool name (for calls)
- Client name (from
initialize.clientInfo.namewhen provided, otherwise pattern-matched from User-Agent) - Client version
- Hashed IP (sha256 with a salt, privacy-compliant)
- Query text (the actual search string)
- Row count returned
- Latency in ms
- Error, if any
The stateless fallback for client identification is the key trick. MCP's initialize handshake optionally includes a clientInfo object with the client name, but tools/call requests don't — because each POST is independent, I can't link them by session. So I pattern-match the HTTP User-Agent header. Claude Desktop sets a recognizable UA, as do Cursor, Cline, Continue, and the MCP Inspector. Everyone else falls through to a sanitized first-32-characters fallback.
One SQL query later I can answer: "How many calls did Claude Desktop make today?" "What's the average latency?" "Which tool is most popular?"
Layer 2: SID/UTM threading on affiliate URLs
For every affiliate URL I return, I stamp a client identifier onto the URL itself:
- Commission Junction (Groupon):
sid=mcp-claude-desktopappended to the CJ click-through URL. CJ captures it in their commission report, so when a booking clears 30-60 days later, I can see which AI assistant drove the dollar. - Viator / GetYourGuide / Klook:
utm_source=aloha-mcp,utm_medium=ai-assistant,utm_campaign=mcp-claude-desktopappended to the affiliate URL. These show up in each network's partner dashboard and in any Google Analytics / Plausible tracking the destination site has.
This is attribution that survives the redirect chain. The CJ URL redirects to Groupon which redirects to a product page, and the SID travels all the way through. No matter how many hops, CJ's commission report eventually shows which AI assistant converted.
Layer 3: Shortcode click-through
The problem with Layer 2 is the lag. Affiliate network reports come 30-60 days after the actual booking. I wanted real-time visibility — did someone actually click the link? How many clicks per tool? Which clients are converting?
So I added a shortcode hop. Instead of returning the raw affiliate URL in the tool response, I:
- Register the URL in a
mcp_click_targetstable, keyed by a deterministic 10-character base62 shortcode derived fromsha256(url + client) - Return
https://aloha.fyi/r/{code}instead of the raw URL
When the tourist clicks, they hit the /r/{code} handler, which:
- Looks up the code in
mcp_click_targets - Writes a row to
mcp_clickswith timestamp, hashed IP, user-agent, referer - 302-redirects to the real target URL (which still has the SID/UTM stamped on it)
End-to-end latency from MCP tool call to click row: 9 seconds in my production tests. Two parallel attribution paths: real-time click counts from my own database, plus the slower commission-clearing reports from the affiliate networks.
The shortcodes are deterministic — same (url, client) always produces the same code, so there are no duplicate entries when the same tool call returns the same result twice. I UPSERT on conflict and update last_seen_at.
Hardening for launch day
A vertical MCP server is fine at 5 requests per minute. A vertical MCP server on the front page of Hacker News is 500 requests per minute. I added two layers of defense before calling it done.
Rate limiting: an in-memory sliding-window counter keyed by hashed IP. 60 requests per minute per client, with a Retry-After header when exceeded. A MCP_BYPASS_TOKEN env var lets me load-test without triggering my own limiter. Not cluster-safe, but Railway runs one instance and the memory map is trivial.
Query caching: a 5-minute TTL cache keyed by (tool_name + serialized args), with LRU eviction at 500 entries. This is the big one. The queries "snorkeling in Oahu," "helicopter tours on Kauai," and "cheap luaus Waikiki" get hit thousands of times a day. Caching them means one DB hit every 5 minutes regardless of traffic. Attribution still runs on every request — the cache only skips the SQL query.
Both are cheap, both are well-understood, both work without external dependencies.
Publishing to the official registry
The MCP ecosystem has a canonical registry at registry.modelcontextprotocol.io, run by the protocol maintainers. When you publish there, every downstream directory — Glama, mcp.so, Smithery, Anthropic's own in-client directory — picks up your listing automatically. One publish, universal distribution.
The registry requires a server.json describing your server in a specific schema. For a remote Streamable HTTP server, it looks like this:
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.baphometnxg/aloha-fyi-mcp",
"title": "aloha.fyi Hawaii",
"description": "Hawaii MCP: tours, events, weather, restaurants, and day-plan itineraries across 4 islands.",
"version": "1.3.0",
"repository": {
"url": "https://github.com/baphometnxg/aloha-fyi-mcp",
"source": "github"
},
"remotes": [
{
"type": "streamable-http",
"url": "https://mcp.aloha.fyi/mcp"
}
]
}
The name field uses a reverse-DNS convention. For GitHub-owned servers it's io.github.<owner>/<repo>. Publishing requires a one-time GitHub device flow authentication via mcp-publisher login github, then mcp-publisher publish. Total time: 60 seconds.
A validation gotcha: the description must be 100 characters or fewer. I learned this by getting a 422 on my first publish attempt. Tightened it, republished, and the registry accepted it in under a second.
The result
It's been live for a few hours as I write this. So far:
- 79 requests across 13 distinct client types
- 0 errors
- 164 ms average latency, 616 ms p95
- Weather tool cached 8x (cache hit ratio climbing)
- First
/r/click logged at 9-second end-to-end latency - Registry entry visible at
registry.modelcontextprotocol.io/v0/servers?search=aloha-fyi
The daily digest (a npm run mcp:digest script I wrote to summarize the last 24 hours) is scheduled to run at 8:47am local time every morning and deliver a report. Mission Control — my minimal Node.js dashboard on port 8899 — has a new panel that polls the /stats endpoint every two minutes and shows the top clients, tools, and queries in real time.
Why this matters
The shift I see coming is this: vertical AI doesn't compete on "better answers." It competes on having access to real data that generic AI can't fabricate.
aloha.fyi has 2,464 bookable tours. It has affiliate relationships with every major OTA serving Hawaii. It has a Japanese-speaking agent for tourists from Tokyo and a Korean-speaking agent for tourists from Seoul. None of that is in Claude's training data. None of it can be conjured by a language model.
But all of it can be exposed as tools. And once it is, Claude becomes a distribution channel — not a competitor.
This is the $0 customer acquisition cost flywheel. I don't pay Google. I don't pay Meta. I pay Railway $20/month and the official MCP registry is free. When a tourist in Munich asks Claude about Hawaii, my tool gets called. When a family in Tokyo asks ChatGPT about a luau, eventually — when OpenAI wires MCP into ChatGPT, which they will — my tool gets called.
Distribution goes from push to pull. From paying for attention to earning access via tool quality.
Where this stands now
Added 2026-09-01. The post above was written the weekend it shipped; the launch-day numbers in it are from that weekend and I've left them as they were. Here is what actually happened over the following five months, including the parts that didn't work.
What changed in the build:
- The server outgrew its single file.
http.tswent from ~900 lines to over 2,000. /r/is no longer a Next.js API route. It moved to Express after the Next.js copy
spent a week pointing at a database it could not reach — the Vercel deployment's connection string resolved to a proxy that was switched off, so every shortcode resolution failed silently. Moving it next to the database it queries removed the class of bug entirely.
- The registry entry is now v1.3.0, served from
mcp.aloha.fyi/mcprather than the
raw Railway hostname. Same server, brandable URL, and one less thing to migrate later.
The honest scoreboard:
Being a tool works. Claude answers "what's going on in Honolulu tonight?" out of this server, names the venue and the price, and links through the tracked redirect. That whole path — expanded event feed, ticket links, click attribution — does what it was built to do.
Being a cited source does not, yet. A weekly canary runs ten real Hawaii travel queries through web search and checks whether aloha.fyi is cited. It has scored zero. The sites that win those queries are the ones with a decade of inbound links.
Those are different distribution channels and they fail independently, which is the thing I'd tell anyone building a vertical MCP server. Publishing to the registry earns you tool calls. It earns you nothing in the model's web answers, because those are still ranked on the same cross-source reputation that ranked the old web. An MCP server is a real distribution channel and it is not a shortcut around being worth citing.
The catalog today: 2,464 bookable tours and activities, 495 events across 98 venues, 541 restaurants. Every number in this section was queried the day it was written.
Try it
Add this to your Claude Desktop config at ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"aloha-fyi-hawaii": {
"url": "https://mcp.aloha.fyi/mcp"
}
}
}
Restart Claude. Ask it: "Plan me a family day on Oahu for $200 a person."
Watch it call plan_hawaii_day with your exact parameters and return a real morning/lunch/afternoon/dinner itinerary with real booking links. The same query from a Claude Desktop user in Berlin will route through the same server, hit the same Postgres, and return the same answer.
Source code: github.com/baphometnxg/aloha-fyi-mcp MCP registry: registry.modelcontextprotocol.io Website: aloha.fyi
Built with aloha. Ship with aloha. 🌺
Michael Crain is a cinematographer and founder of aloha.fyi, an AI-powered tourism concierge for Hawaii's visitor industry. Nani, the concierge agent, speaks five languages and has access to 2,464 bookable experiences across six affiliate networks. aloha.fyi is based in Honolulu.