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:
parent
a08f5f05b1
commit
c4f0db5719
@ -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's current help docs before committing to a plan, since these limits have
|
||||||
|
shifted before:
|
||||||
</P>
|
</P>
|
||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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]">
|
||||||
|
|||||||
@ -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'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
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user