Most MCP write-ups are a tutorial: install this gem, copy this handler, ship a toy server against an in-memory dataset. This isn't that. TailyX runs a production MCP server in front of a real Rails 8 application on Heroku, backed by Postgres and Redis, serving live lead-qualification data to AI agents right now. Here's what we actually decided, and where those decisions cost us.
Engineering TailyX — Part 1 of 4
TailyX's core product is a lead-qualification widget: visitors interact with structured intent buttons and a deterministic scoring engine — we call it the FlowResolver — rather than a freeform chat box. That design choice, made for reasons that had nothing to do with agents, turned out to be exactly what MCP wants. Deterministic, schema-shaped outputs are what an agent can reason over reliably. Conversational transcripts are not.
The decision that made our qualification results explainable to a sales team also made them consumable by an agent. That wasn't foresight — auditability and machine-readability are the same requirement wearing different clothes.
Building the MCP server was mostly exposing an interface we'd already built for humans to a new class of caller — not a bolt-on integration project.
Agent client (ChatGPT / Claude / custom)
│
▼
MCP transport layer (JSON-RPC over HTTP)
│
▼
MCP::Server (Rails service object, not a parallel app)
│
├─▶ lookup_widget_by_domain → resolves tenant from a domain
├─▶ get_agent_schema → returns the qualification schema for that tenant
├─▶ submit_agent_lead → runs the FlowResolver scoring engine
└─▶ get_lead_status → polls result state
│
▼
Same Postgres-backed models the web widget uses
│
▼
Sidekiq jobs for anything async (routing, notification, CRM sync)
The deliberate constraint: the MCP layer is a thin adapter, not a second brain. Every tool call hits the same ActiveRecord models and the same FlowResolver logic the browser widget uses. No separate qualification logic exists for agents — that's the line between "supporting MCP" and "bolting MCP onto a demo."
Four tools, each doing one narrow job:
The first version of this interface didn't look like four narrow tools. An earlier draft accepted a single broader payload — resolve the tenant, validate the schema, and score the lead in one call, on the theory that fewer round trips meant a simpler integration. In practice that tool became harder to validate (one failure could originate from three different causes), harder to document (the parameter object had optional fields whose meaning depended on which "mode" you were in), and easier for a client to call with an incomplete or malformed payload that partially matched what it expected. Splitting it into four narrowly scoped tools — each with one job and one failure mode — made both testing and integration simpler, at the cost of an extra round trip per lead. That trade-off held up; the assumption that fewer calls beats clearer calls didn't.
The sequence a client actually walks through, end to end:
1. lookup_widget_by_domain(domain: "example.com")
→ { tenant_id: "...", widget_config: {...} }
2. get_agent_schema(tenant_id: "...")
→ { fields: [...], required: [...] }
3. submit_agent_lead(tenant_id: "...", answers: {...})
→ { qualification_result: "...", lead_id: "..." }
4. get_lead_status(lead_id: "...")
→ { status: "routed" | "pending" | "rejected" }
Discovery ("how does an agent learn my server exists") and authentication ("should this caller be allowed to call this tool") are separate problems we were careful not to conflate. Discovery is largely solved by publishing a well-formed schema and pointing to it from llms.txt. Authentication is a standard API auth problem wearing an MCP costume.
Standard token-based auth sits on the transport layer. Tenant resolution happens as the first step inside the tool call, not baked into the transport. No state-changing tool trusts a discovery-layer claim without re-validating tenant scope server-side. The failure mode we were most paranoid about wasn't "agent can't find our server" — it was "agent finds our server and submits a lead against a tenant it shouldn't touch."
MCP tooling is immature relative to standard API testing in mid-2026. The test suite runs three layers:
get_agent_schema's output still matches what submit_agent_lead accepts. These two silently drifting apart is the single most common MCP bug class we've hit.Nothing exotic, which is the point: the MCP server ships as part of the same Heroku release as the rest of the app. No separate service, no separate pipeline, no separate on-call surface. An MCP layer with its own uptime tier makes your data available to agents less reliably than to humans — which defeats "agent-native" before it starts.
Current as of July 2026. MCP tooling and client behavior are evolving quickly — details here reflect our production setup at time of writing, not a permanent spec.