ADR-0003: Capture leads through an MCP tool and an OpenAPI action, not a web form alone
If the thesis is that buyers increasingly research inside an LLM (ADR-0001), then a conventional contact form is the wrong last mile: it requires the user to leave the chat, find t...
ADR-0003: Capture leads through an MCP tool and an OpenAPI action, not a web form alone
- Status: Accepted
- Date: 2026-07-30 (recorded; implemented earlier)
- Deciders: Junaid Siddiqui (owner)
- Related:
../integrations/mcp-server.md,../integrations/lead-capture.md, ADR-0001, ADR-0004
Context
If the thesis is that buyers increasingly research inside an LLM (ADR-0001), then a conventional contact form is the wrong last mile: it requires the user to leave the chat, find the site, and retype what they already told the assistant. Every step loses people.
Options considered:
- Web form only — simplest, but invisible to agents and unusable mid-conversation.
- MCP tool only — native for MCP-capable hosts, but excludes ChatGPT Custom GPTs and Actions-style integrations that speak OpenAPI.
- OpenAPI action only — broad host coverage, but not the native path for MCP clients.
- Both agent surfaces, plus keep the web form for humans arriving via the site.
Option 4 costs little extra because validation and delivery can be shared.
Decision
Expose lead capture on three surfaces, with the two agent-facing ones sharing a single implementation:
| Surface | Path | Recorded source |
|---|---|---|
MCP tool request_dev_consultation |
/api/mcp/sse, /api/mcp/mcp |
"mcp" |
OpenAPI action submitDevConsultationLead |
POST /api/leads/submit |
"openapi" |
| On-site contact form | POST /api/contact |
— (separate payload and path) |
Both agent surfaces converge on submitConsultationLead() in lib/leads.ts, sharing one Zod schema, one error taxonomy, and one delivery path. The MCP endpoint runs stateless Streamable HTTP (sessionIdGenerator: undefined) so it works on serverless and multi-instance deployments with no session store.
Both surfaces are public, unauthenticated, and CORS-open, because the point is that arbitrary AI hosts can reach them.
Consequences
- Positive:
- Covers MCP-native clients and OpenAPI/Actions hosts with one validation contract, so the two cannot drift apart.
- Statelessness removes Redis and any session infrastructure.
- Structured error responses (
validation_errorwith per-field issues) let an agent re-prompt the user instead of failing opaquely. sourceon every lead shows which channel actually produces business.- Doubles as the live demo (ADR-0007) and as proof-of-capability for prospects.
- Trade-offs / costs:
- Public, unauthenticated, no rate limiting — the endpoint can be flooded, writing junk into the destination.
- Two specs to maintain: the MCP tool schema in
lib/mcp-server.tsandlib/openapi-spec.ts(served with CORS at/docs/openapi.jsonand/api/openapi.json). A field change means editing both. - Per-request server construction adds a little latency.
- Stateless mode forecloses features needing cross-call state (progress notifications, subscriptions).
- The success message promises human follow-up within one hour, made unattended by an agent — an operational commitment with no notification behind it (
../operations/client-engagement-sop.md).
- Follow-ups:
- Add rate limiting or abuse protection to both public endpoints.
- Add notification on lead arrival to make the 1-hour promise credible.
- Tier 3 sells "multi-endpoint MCP tools"; exactly one tool exists. Build or reword (
../sales/pricing-tiers.md).