fix(review): loop-2 findings — login fallback, proof-band confidence, contrast, content hedges

Code review: login no longer strands visitors with zero sign-in methods when
the providers fetch fails (falls back to SMS + error notice); skeleton while
providers load instead of a blank card.

Design re-audit: proof band flipped from apology to flex ('Don't take our
word for it / Every claim links to its proof') and promoted to the primary
heading tier; unverifiable '60 seconds'/'in minutes' speed claims dropped;
fg-subtle body text bumped to fg-muted (AA contrast); marketplace section
differentiated via border-y + elevated bg (adjacent hairlines removed);
preview frame traffic lights on-palette; header h-12 -> h-14 (hero svh calc
adjusted); emerald-400 -> --color-success token.

Content QA: rest-api article description drift between page and registry
resolved; ChatGPT plan-gating table and Atlassian Rovo SSE-cutoff claims now
carry dated hedges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXUwmPVRTD8AKQtio6gCN5
This commit is contained in:
Marco Sadjadi 2026-07-08 23:13:53 +02:00
parent a08f5f05b1
commit c4f0db5719
6 changed files with 57 additions and 32 deletions

View File

@ -38,7 +38,9 @@ export default function Page() {
<H2>The plan limits, first because they decide everything</H2> <H2>The plan limits, first because they decide everything</H2>
<P> <P>
Before writing a single prompt or line of code, check what your ChatGPT plan allows. Before writing a single prompt or line of code, check what your ChatGPT plan allows.
OpenAI gates custom MCP connectors by workspace type: As of mid-2026, OpenAI gates custom MCP connectors by workspace type verify against
OpenAI&apos;s current help docs before committing to a plan, since these limits have
shifted before:
</P> </P>
<Table> <Table>
<thead> <thead>

View File

@ -129,7 +129,8 @@ export default function Page() {
Spec revision <Strong>2025-03-26</Strong> replaced HTTP+SSE with Streamable HTTP. The old Spec revision <Strong>2025-03-26</Strong> replaced HTTP+SSE with Streamable HTTP. The old
transport still works where both sides keep supporting it, but the direction is one-way: transport still works where both sides keep supporting it, but the direction is one-way:
platforms have been announcing removal dates through 2026 Atlassian's Rovo MCP server, platforms have been announcing removal dates through 2026 Atlassian's Rovo MCP server,
for example, set its HTTP+SSE cutoff for June 30, 2026. New servers should not ship it. for example, announced an HTTP+SSE cutoff of June 30, 2026. New servers should not ship
it.
</P> </P>
<H2>Streamable HTTP: the current standard</H2> <H2>Streamable HTTP: the current standard</H2>

View File

@ -8,7 +8,7 @@ export default function MarketingLayout({ children }: { children: React.ReactNod
return ( return (
<div className="flex min-h-screen flex-col"> <div className="flex min-h-screen flex-col">
<header className="sticky top-0 z-50 border-b border-[--color-border] bg-[--color-bg]/80 backdrop-blur-md"> <header className="sticky top-0 z-50 border-b border-[--color-border] bg-[--color-bg]/80 backdrop-blur-md">
<div className="mx-auto flex h-12 max-w-6xl items-center justify-between px-5 sm:px-6"> <div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-5 sm:px-6">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<Logo /> <Logo />
<nav className="hidden items-center gap-5 text-[13px] text-[--color-fg-muted] md:flex"> <nav className="hidden items-center gap-5 text-[13px] text-[--color-fg-muted] md:flex">
@ -45,15 +45,15 @@ export default function MarketingLayout({ children }: { children: React.ReactNod
{/* Brand column — positioning line + live status. */} {/* Brand column — positioning line + live status. */}
<div className="sm:col-span-2 md:col-span-1"> <div className="sm:col-span-2 md:col-span-1">
<Logo /> <Logo />
<p className="mt-3 max-w-xs text-[12.5px] leading-relaxed text-[--color-fg-subtle]"> <p className="mt-3 max-w-xs text-[12.5px] leading-relaxed text-[--color-fg-muted]">
From a prompt to a hosted, OAuth-protected MCP server for Claude, Cursor and From a prompt to a hosted, OAuth-protected MCP server for Claude, Cursor and
ChatGPT. ChatGPT.
</p> </p>
<Link <Link
href="/status" href="/status"
className="mt-4 flex items-center gap-2 text-[12px] text-[--color-fg-subtle] transition-colors hover:text-[--color-fg]" className="mt-4 flex items-center gap-2 text-[12px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
> >
<span className="size-1.5 animate-pulse rounded-full bg-emerald-400" /> <span className="size-1.5 animate-pulse rounded-full bg-[--color-success]" />
<span>System status</span> <span>System status</span>
</Link> </Link>
</div> </div>
@ -107,7 +107,7 @@ function FooterColumn({
<h3 className="text-[11px] font-semibold uppercase tracking-[0.16em] text-[--color-fg-muted]"> <h3 className="text-[11px] font-semibold uppercase tracking-[0.16em] text-[--color-fg-muted]">
{title} {title}
</h3> </h3>
<ul className="mt-3 space-y-2 text-[12.5px] text-[--color-fg-subtle]"> <ul className="mt-3 space-y-2 text-[12.5px] text-[--color-fg-muted]">
{links.map((l) => ( {links.map((l) => (
<li key={l.href}> <li key={l.href}>
<Link href={l.href} className="transition-colors hover:text-[--color-fg]"> <Link href={l.href} className="transition-colors hover:text-[--color-fg]">

View File

@ -164,7 +164,7 @@ export default function Landing() {
CTAs stay interactive. */} CTAs stay interactive. */}
<section <section
className="relative flex items-center overflow-hidden border-b border-[--color-border]" className="relative flex items-center overflow-hidden border-b border-[--color-border]"
style={{ minHeight: 'calc(100svh - 3rem)' }} style={{ minHeight: 'calc(100svh - 3.5rem)' }}
> >
<ParticleHero /> <ParticleHero />
<div className="relative z-10 mx-auto grid w-full max-w-6xl gap-10 px-6 py-14 sm:py-20 md:grid-cols-[1.05fr_1fr] md:items-center md:gap-12"> <div className="relative z-10 mx-auto grid w-full max-w-6xl gap-10 px-6 py-14 sm:py-20 md:grid-cols-[1.05fr_1fr] md:items-center md:gap-12">
@ -177,8 +177,8 @@ export default function Landing() {
<span className="text-[--color-fg-muted]">AI uses it.</span> <span className="text-[--color-fg-muted]">AI uses it.</span>
</h1> </h1>
<p className="mt-5 max-w-md text-[15px] leading-relaxed text-[--color-fg-muted] sm:text-[16px]"> <p className="mt-5 max-w-md text-[15px] leading-relaxed text-[--color-fg-muted] sm:text-[16px]">
From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready From a prompt to a production MCP server OAuth 2.1, Streamable HTTP, ready for
for Claude, Cursor and ChatGPT. Claude, Cursor and ChatGPT.
</p> </p>
<div className="mt-8 flex flex-wrap items-center gap-3"> <div className="mt-8 flex flex-wrap items-center gap-3">
<PulseLink <PulseLink
@ -191,21 +191,21 @@ export default function Landing() {
href="#flow" href="#flow"
className="inline-flex h-11 items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] px-5 text-[14px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]" className="inline-flex h-11 items-center justify-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] px-5 text-[14px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
> >
See a live build Watch a build
</PulseLink> </PulseLink>
</div> </div>
<div className="mt-8 flex flex-wrap gap-x-6 gap-y-2 text-[12px] text-[--color-fg-subtle]"> <div className="mt-8 flex flex-wrap gap-x-6 gap-y-2 text-[12px] text-[--color-fg-muted]">
<span className="inline-flex items-center gap-1.5"> <span className="inline-flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-emerald-400" /> OAuth 2.1 + PKCE <span className="size-1.5 rounded-full bg-[--color-success]" /> OAuth 2.1 + PKCE
</span> </span>
<span className="inline-flex items-center gap-1.5"> <span className="inline-flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-emerald-400" /> Streamable HTTP <span className="size-1.5 rounded-full bg-[--color-success]" /> Streamable HTTP
</span> </span>
<span className="inline-flex items-center gap-1.5"> <span className="inline-flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-emerald-400" /> AES-256 secrets <span className="size-1.5 rounded-full bg-[--color-success]" /> AES-256 secrets
</span> </span>
<span className="inline-flex items-center gap-1.5"> <span className="inline-flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-emerald-400" /> Per-server isolation <span className="size-1.5 rounded-full bg-[--color-success]" /> Per-server isolation
</span> </span>
</div> </div>
</div> </div>
@ -290,13 +290,13 @@ export default function Landing() {
{/* Proof by specificity three verifiable claims, each one click from {/* Proof by specificity three verifiable claims, each one click from
its evidence. Replaces the old pseudo-logo row: no invented marks, its evidence. Replaces the old pseudo-logo row: no invented marks,
just a plain-text compatibility line. */} just a plain-text compatibility line. */}
<section className="border-b border-[--color-border] py-16 sm:py-24"> <section className="py-16 sm:py-24">
<div className="mx-auto max-w-6xl px-6"> <div className="mx-auto max-w-6xl px-6">
<Kicker>verify_it_yourself</Kicker> <Kicker>verify_it_yourself</Kicker>
<h2 className="mt-3 max-w-2xl text-[28px] font-semibold leading-[1.1] tracking-tight sm:text-[32px]"> <h2 className="mt-3 max-w-2xl text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
No testimonials yet we're new. Don&apos;t take our word for it.
<br /> <br />
<span className="text-[--color-fg-muted]">So check the claims instead.</span> <span className="text-[--color-fg-muted]">Every claim links to its proof.</span>
</h2> </h2>
<div className="mt-10 grid gap-4 md:grid-cols-3"> <div className="mt-10 grid gap-4 md:grid-cols-3">
{PROOF_POINTS.map((p) => { {PROOF_POINTS.map((p) => {
@ -319,15 +319,16 @@ export default function Landing() {
); );
})} })}
</div> </div>
<p className="mt-10 text-center text-[13px] text-[--color-fg-subtle]"> <p className="mt-10 text-center text-[13px] text-[--color-fg-muted]">
Works with Claude Desktop, Cursor, ChatGPT, VS Code Copilot and Continue.dev anything Works with Claude Desktop, Cursor, ChatGPT, VS Code Copilot and Continue.dev anything
that speaks MCP. that speaks MCP.
</p> </p>
</div> </div>
</section> </section>
{/* Use cases — brand-coloured integration grid. */} {/* Use cases brand-coloured integration grid. No bottom hairline:
<section className="border-b border-[--color-border] py-20 sm:py-28"> the marketplace section below brings its own border-y. */}
<section className="py-20 sm:py-28">
<div className="mx-auto max-w-6xl px-6"> <div className="mx-auto max-w-6xl px-6">
<div className="mb-12 grid gap-6 md:grid-cols-[1fr_auto] md:items-end md:gap-12"> <div className="mb-12 grid gap-6 md:grid-cols-[1fr_auto] md:items-end md:gap-12">
<div> <div>
@ -335,11 +336,11 @@ export default function Landing() {
<h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]"> <h2 className="mt-3 text-[32px] font-semibold leading-[1.1] tracking-tight sm:text-[40px]">
Wrap any HTTP API. Wrap any HTTP API.
<br /> <br />
<span className="text-[--color-fg-muted]">In minutes.</span> <span className="text-[--color-fg-muted]">From one prompt.</span>
</h2> </h2>
</div> </div>
<p className="max-w-xs text-[14px] leading-relaxed text-[--color-fg-muted]"> <p className="max-w-xs text-[14px] leading-relaxed text-[--color-fg-muted]">
Ship integrations like these from one prompt each. Each shipped from a single prompt.
</p> </p>
</div> </div>
@ -375,8 +376,9 @@ export default function Landing() {
</section> </section>
{/* Marketplace split layout: selling points left, honest preview {/* Marketplace split layout: selling points left, honest preview
frame right. */} frame right. Elevated background (no hairline) so the page reads
<section className="border-b border-[--color-border] py-20 sm:py-28"> in blocks instead of one continuously ruled ledger. */}
<section className="border-y border-[--color-border] bg-[--color-bg-elevated] py-20 sm:py-28">
<div className="mx-auto grid max-w-6xl gap-14 px-6 md:grid-cols-[5fr_6fr] md:items-center md:gap-16"> <div className="mx-auto grid max-w-6xl gap-14 px-6 md:grid-cols-[5fr_6fr] md:items-center md:gap-16">
<div> <div>
<Kicker>marketplace</Kicker> <Kicker>marketplace</Kicker>
@ -602,9 +604,9 @@ function MarketplacePreview() {
{/* Browser chrome */} {/* Browser chrome */}
<div className="flex items-center gap-3 border-b border-[--color-border] bg-[--color-bg-subtle] px-4 py-3"> <div className="flex items-center gap-3 border-b border-[--color-border] bg-[--color-bg-subtle] px-4 py-3">
<div className="flex gap-1.5"> <div className="flex gap-1.5">
<span className="size-2.5 rounded-full bg-zinc-700" /> <span className="size-2.5 rounded-full bg-[#ff5f57]/80" />
<span className="size-2.5 rounded-full bg-zinc-700" /> <span className="size-2.5 rounded-full bg-[#febc2e]/80" />
<span className="size-2.5 rounded-full bg-zinc-700" /> <span className="size-2.5 rounded-full bg-[#28c840]/80" />
</div> </div>
<div className="mono flex-1 truncate rounded-md border border-[--color-border] bg-[--color-bg] px-3 py-1 text-[11px] text-[--color-fg-subtle]"> <div className="mono flex-1 truncate rounded-md border border-[--color-border] bg-[--color-bg] px-3 py-1 text-[11px] text-[--color-fg-subtle]">
buildmymcpserver.com/templates buildmymcpserver.com/templates

View File

@ -208,6 +208,7 @@ export default function LoginPage() {
// product should never see a phone-number field as the front door. It only // product should never see a phone-number field as the front door. It only
// renders expanded when SMS is the sole configured provider. // renders expanded when SMS is the sole configured provider.
const [phoneOpen, setPhoneOpen] = useState(false); const [phoneOpen, setPhoneOpen] = useState(false);
const [providersState, setProvidersState] = useState<'loading' | 'ready' | 'failed'>('loading');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Email magic-link // Email magic-link
@ -228,11 +229,21 @@ export default function LoginPage() {
) )
.then((p) => { .then((p) => {
setProviders(p); setProviders(p);
setProvidersState('ready');
// SMS as the only provider → show the phone form directly (no point // SMS as the only provider → show the phone form directly (no point
// hiding the sole sign-in method behind a toggle). // hiding the sole sign-in method behind a toggle).
if (p.sms && !p.email && !p.google && !p.github) setPhoneOpen(true); if (p.sms && !p.email && !p.google && !p.github) setPhoneOpen(true);
}) })
.catch(() => undefined); .catch(() => {
// If the providers lookup fails, the page must not strand the visitor
// with zero sign-in methods — fall back to SMS + phone form expanded
// (the one flow that has no provider-specific client config) and say
// why the other options are missing.
setProviders({ google: false, github: false, sms: true, email: false });
setPhoneOpen(true);
setProvidersState('failed');
setError('Some sign-in options could not be loaded. Reload the page to try again.');
});
const err = new URLSearchParams(window.location.search).get('error'); const err = new URLSearchParams(window.location.search).get('error');
if (err) setError(ERROR_COPY[err] ?? 'Sign-in failed. Please try again.'); if (err) setError(ERROR_COPY[err] ?? 'Sign-in failed. Please try again.');
}, []); }, []);
@ -296,6 +307,15 @@ export default function LoginPage() {
Passwordless pick whichever is easiest. Passwordless pick whichever is easiest.
</p> </p>
{/* Providers still loading neutral skeleton instead of a blank card
(previously nothing rendered until the fetch resolved). */}
{providersState === 'loading' && (
<div aria-hidden className="mt-7 space-y-2">
<div className="h-10 w-full animate-pulse rounded-md bg-[--color-bg-elevated]" />
<div className="h-10 w-full animate-pulse rounded-md bg-[--color-bg-elevated]" />
</div>
)}
{hasOAuth && ( {hasOAuth && (
<div className="mt-7 space-y-2"> <div className="mt-7 space-y-2">
{providers.google && ( {providers.google && (

View File

@ -43,7 +43,7 @@ export const ARTICLES: Article[] = [
slug: 'rest-api-to-mcp-server', slug: 'rest-api-to-mcp-server',
title: 'Wrap any REST API as an MCP server', title: 'Wrap any REST API as an MCP server',
description: description:
'How to turn a REST API into an MCP server your AI clients can use: designing the tool surface, handling auth headers with encrypted secrets, and respecting upstream rate limits.', 'How to turn a REST API into an MCP server your AI clients can use: designing the tool surface (fewer, better tools beat 1:1 endpoint mapping), handling auth headers with encrypted secrets, and respecting upstream rate limits.',
tag: 'Guide', tag: 'Guide',
datePublished: '2026-07-08', datePublished: '2026-07-08',
}, },