feat(web): SEO — server-rendered template pages + /guides articles

- templates/[slug] converted from client to server component: per-template
  generateMetadata (title/description/canonical/OG) + SoftwareApplication
  JSON-LD; code-audit toggle split into a client island; missing/non-public
  templates now return a real 404.
- sitemap.ts pulls public template slugs live from the API (best-effort) +
  the new /guides routes.
- new /guides section: 3 server-rendered SEO articles (host MCP with OAuth,
  hosted-platforms comparison, MintMCP alternative) with TechArticle JSON-LD;
  Guides link added to the marketing nav.
- lib/seo.ts: articleJsonLd + templateJsonLd builders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
This commit is contained in:
Marco Sadjadi
2026-05-31 12:08:05 +02:00
parent 21a5cf5762
commit 1349dc1dc0
11 changed files with 734 additions and 141 deletions

View File

@@ -0,0 +1,73 @@
import Link from 'next/link';
import type { ReactNode } from 'react';
// Shared layout + typographic primitives for /guides/* SEO articles. Server
// component (no client JS) so each article page can export its own metadata.
export function ArticleShell({
title,
subtitle,
updated,
children,
}: {
title: string;
subtitle?: string;
updated?: string;
children: ReactNode;
}) {
return (
<article className="mx-auto max-w-3xl px-6 py-14">
<Link
href="/guides"
className="text-[12px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
>
Guides
</Link>
<h1 className="mt-4 text-[30px] font-semibold leading-tight tracking-tight text-[--color-fg]">
{title}
</h1>
{subtitle && <p className="mt-3 text-[15px] leading-relaxed text-[--color-fg-muted]">{subtitle}</p>}
{updated && <p className="mt-2 text-[12px] text-[--color-fg-subtle]">Updated {updated}</p>}
<div className="mt-8">{children}</div>
<div className="mt-14 rounded-lg border border-[--color-border] bg-[--color-bg-subtle] p-5">
<p className="text-[14px] font-medium text-[--color-fg]">
Skip the boilerplate describe your tool, get a hosted MCP server.
</p>
<p className="mt-1 text-[13px] text-[--color-fg-muted]">
BuildMyMCPServer generates the TypeScript server, wraps it in OAuth 2.1 and deploys it to a
public Streamable HTTP URL for Claude, Cursor and ChatGPT. Free tier, source export, no
lock-in.
</p>
<Link
href="/login"
className="mt-3 inline-flex h-9 items-center rounded-md bg-[--color-accent] px-4 text-[13px] font-medium text-white transition-colors hover:bg-[#5557e8]"
>
Start building
</Link>
</div>
</article>
);
}
export function H2({ children }: { children: ReactNode }) {
return (
<h2 className="mt-10 text-[19px] font-semibold tracking-tight text-[--color-fg]">{children}</h2>
);
}
export function P({ children }: { children: ReactNode }) {
return <p className="mt-3 text-[14.5px] leading-relaxed text-[--color-fg-muted]">{children}</p>;
}
export function UL({ children }: { children: ReactNode }) {
return (
<ul className="mt-3 list-disc space-y-1.5 pl-5 text-[14.5px] leading-relaxed text-[--color-fg-muted]">
{children}
</ul>
);
}
export function Strong({ children }: { children: ReactNode }) {
return <strong className="font-semibold text-[--color-fg]">{children}</strong>;
}

View File

@@ -0,0 +1,116 @@
import { JsonLd } from '@/components/json-ld';
import { articleJsonLd, pageMetadata } from '@/lib/seo';
import Link from 'next/link';
import { ArticleShell, H2, P, Strong, UL } from '../article-shell';
const PATH = '/guides/host-mcp-server-with-oauth';
const TITLE = 'How to host a remote MCP server with OAuth (2026)';
const DESCRIPTION =
'What it actually takes to put a remote MCP server in production: Streamable HTTP transport, OAuth 2.1 with PKCE and Resource Indicators, and the shortcuts.';
export const metadata = pageMetadata({ title: TITLE, description: DESCRIPTION, path: PATH });
export default function Page() {
return (
<>
<JsonLd
data={articleJsonLd({
title: TITLE,
description: DESCRIPTION,
path: PATH,
datePublished: '2026-05-31',
})}
/>
<ArticleShell
title={TITLE}
subtitle="Local STDIO servers are easy. A remote MCP server that Claude, Cursor and ChatGPT can install over the internet — without leaving it open to the world — is where the real work is. Here's the whole picture."
updated="May 2026"
>
<H2>Local vs remote: why this is harder than it looks</H2>
<P>
A local MCP server talks to one client over STDIO on your machine no network, no auth.
The moment you want a server that lives at a URL and any MCP client can connect to, you
inherit a full web-service problem: a public transport, TLS, identity, authorization, and
isolation between callers. The MCP spec settled on <Strong>Streamable HTTP</Strong> as the
remote transport (it replaced the older HTTP+SSE pairing), and on{' '}
<Strong>OAuth 2.1</Strong> as the auth model. Both are non-negotiable if you want the
server installable from Claude Desktop or ChatGPT.
</P>
<H2>The OAuth 2.1 pieces you can't skip</H2>
<P>
MCP authorization is OAuth 2.1, and for remote servers it leans on a few RFCs that older
OAuth tutorials don't cover:
</P>
<UL>
<li>
<Strong>PKCE (RFC 7636)</Strong> on every authorization-code exchange mandatory in
OAuth 2.1, no exceptions for &quot;confidential&quot; clients.
</li>
<li>
<Strong>Dynamic Client Registration (RFC 7591)</Strong> clients like Claude Desktop
register themselves at runtime; you can't pre-provision a client_id for every user.
</li>
<li>
<Strong>Resource Indicators (RFC 8707)</Strong> — the token has to be bound to the
specific MCP server (the <code>resource</code>), so a token minted for one server can't
be replayed against another.
</li>
<li>
<Strong>Protected-resource metadata</Strong> your server returns a{' '}
<code>WWW-Authenticate</code> header pointing at the authorization server so clients can
discover where to get a token.
</li>
</UL>
<P>
Get any of these wrong and the symptom is the same: the client either can't complete the
handshake, or it silently fails to discover your auth server. This is the single most
common reason a &quot;working&quot; MCP server won't install from Claude.
</P>
<H2>Option A roll your own on generic infra</H2>
<P>
You can deploy a remote MCP server to <Strong>Cloudflare Workers</Strong> (the most common
production choice, edge-global), or to <Strong>Render, Fly, or Cloud Run</Strong> as a
normal container. Cloudflare even ships an OAuth provider library for Workers-based MCP
servers. This path gives you full control and is the right call if you have engineers and
want to own the runtime.
</P>
<P>
The cost is everything around the code: standing up the authorization server (or wiring a
third-party IdP correctly for the RFCs above), per-tenant secret storage, TLS, rate
limiting, and keeping the transport spec-current as MCP evolves. Budget days, not hours,
for the auth layer alone.
</P>
<H2>Option B a platform that wraps it for you</H2>
<P>
If you already have a server, tools like <Strong>MintMCP</Strong> take a local STDIO
server and expose it as a remote one with OAuth wrapping. If you{' '}
<Strong>don't have a server yet</Strong>, that's where BuildMyMCPServer fits: you describe
the tool in plain language, it generates the TypeScript MCP server, runs static checks,
builds a container, and deploys it behind a full OAuth 2.1 authorization server PKCE,
DCR and Resource Indicators included with copy-paste install snippets for each client.
</P>
<H2>A practical checklist before you ship</H2>
<UL>
<li>Transport is Streamable HTTP, served over TLS at a stable public URL.</li>
<li>Unauthenticated request returns 401 + a <code>WWW-Authenticate</code> pointing at your AS.</li>
<li>Authorization code flow enforces PKCE (S256), exact redirect-URI match, single-use codes.</li>
<li>Issued access tokens are audience-bound to the specific server (RFC 8707).</li>
<li>Per-caller secrets are encrypted at rest and injected only at runtime, never logged.</li>
<li>You've actually installed it from Claude Desktop end-to-end — not just curl'd it.</li>
</UL>
<P>
Whichever route you take, test the real install path in a real client early. See the{' '}
<Link href="/guides/hosted-mcp-platforms-compared" className="text-[--color-accent] hover:underline">
platform comparison
</Link>{' '}
for which option fits your situation.
</P>
</ArticleShell>
</>
);
}

View File

@@ -0,0 +1,110 @@
import { JsonLd } from '@/components/json-ld';
import { articleJsonLd, pageMetadata } from '@/lib/seo';
import Link from 'next/link';
import { ArticleShell, H2, P, Strong, UL } from '../article-shell';
const PATH = '/guides/hosted-mcp-platforms-compared';
const TITLE =
'Hosted MCP platforms compared: Cloudflare, Smithery, Composio & generating your own';
const DESCRIPTION =
'The MCP hosting landscape splits into four categories — registries, connector platforms, hosting infra, and generators. Here is which one fits which job.';
export const metadata = pageMetadata({ title: TITLE, description: DESCRIPTION, path: PATH });
export default function Page() {
return (
<>
<JsonLd
data={articleJsonLd({
title: TITLE,
description: DESCRIPTION,
path: PATH,
datePublished: '2026-05-31',
})}
/>
<ArticleShell
title={TITLE}
subtitle="There are 14,000+ MCP servers out there and a dozen platforms claiming to host them. They are not competing for the same job. Sort them into four buckets and the choice gets obvious."
updated="May 2026"
>
<H2>1. Registries &amp; directories</H2>
<P>
<Strong>Smithery</Strong>, <Strong>Glama</Strong> and <Strong>PulseMCP</Strong> are about{' '}
<em>discovery</em> finding and listing existing servers (Glama indexes thousands). Some
add light hosting on top, but the core value is the catalog and the traffic. Use them to
publish a server people can find, or to find one that already does what you need.
</P>
<H2>2. Connector platforms</H2>
<P>
<Strong>Composio</Strong>, <Strong>Nango</Strong>, <Strong>Klavis</Strong>,{' '}
<Strong>Zapier</Strong> and <Strong>Pipedream</Strong> expose <em>their</em> catalog of
hundreds of pre-built SaaS integrations as MCP, with managed auth. If your need is
&quot;let my agent touch Gmail / Slack / Salesforce,&quot; these are the fastest path
you're buying breadth of pre-built connectors, not building your own logic.
</P>
<H2>3. Hosting infrastructure</H2>
<P>
<Strong>Cloudflare Workers</Strong> is the default for hosting a remote MCP server in
production — edge-global, with an OAuth provider library. <Strong>Vercel</Strong>,{' '}
<Strong>Render</Strong> and <Strong>Cloud Run</Strong> host custom Node containers too.{' '}
<Strong>MintMCP</Strong> sits slightly higher up: one-click wrap of an existing STDIO
server into a remote one with auto-OAuth, and it leads on compliance (SOC 2 Type II,
GDPR/HIPAA-formatted audit logs). All of these assume <Strong>you bring the code.</Strong>
</P>
<H2>4. Generators (the gap most lists miss)</H2>
<P>
The first three categories all assume you already have a server, or that a pre-built
connector covers your case. Neither is true when you need a <em>bespoke</em> tool — a
wrapper around your own internal API, a niche workflow, a one-off integration nobody has
built. That's the generator category: describe the tool, get a custom MCP server hosted
for you. It's the youngest and least crowded slice, and it's where{' '}
<Strong>BuildMyMCPServer</Strong> plays.
</P>
<H2>So which do you pick?</H2>
<UL>
<li>
<Strong>Need a popular SaaS connector?</Strong> A connector platform (Composio / Klavis /
Zapier) don't rebuild what they maintain.
</li>
<li>
<Strong>Have a server and engineers?</Strong> Host it on Cloudflare Workers; wrap your
own OAuth or use MintMCP if you want the compliance posture done for you.
</li>
<li>
<Strong>Just browsing for something that exists?</Strong> Smithery or Glama.
</li>
<li>
<Strong>Need a custom tool and don't want to write or host a server?</Strong> A generator
describe it, ship it. Export the source later if you outgrow it.
</li>
</UL>
<H2>Where BuildMyMCPServer fits honestly</H2>
<P>
We're not trying to out-scale Cloudflare's edge or out-catalog Composio. The job we do is
the bespoke one: <Strong>prompt a hosted, OAuth-protected MCP server</Strong>, with
install snippets for Claude, Cursor and ChatGPT, an EU/US data-residency choice for teams
that care, full TypeScript source export, and a template marketplace to fork from. If your
tool is custom and your time is the constraint, that's the wedge. If you need a vetted
enterprise SOC 2 host for an existing server today, MintMCP or your own Cloudflare setup is
the more honest answer.
</P>
<P>
Next:{' '}
<Link
href="/guides/host-mcp-server-with-oauth"
className="text-[--color-accent] hover:underline"
>
what hosting a remote MCP server with OAuth actually involves
</Link>
.
</P>
</ArticleShell>
</>
);
}

View File

@@ -0,0 +1,96 @@
import { JsonLd } from '@/components/json-ld';
import { articleJsonLd, pageMetadata } from '@/lib/seo';
import Link from 'next/link';
import { ArticleShell, H2, P, Strong, UL } from '../article-shell';
const PATH = '/guides/mintmcp-alternative';
const TITLE = 'MintMCP alternative: generate and host a custom MCP server';
const DESCRIPTION =
'MintMCP wraps an existing STDIO server into a remote one with OAuth. If you do not have a server yet, here is the generate-from-a-prompt alternative — and where MintMCP still wins.';
export const metadata = pageMetadata({ title: TITLE, description: DESCRIPTION, path: PATH });
export default function Page() {
return (
<>
<JsonLd
data={articleJsonLd({
title: TITLE,
description: DESCRIPTION,
path: PATH,
datePublished: '2026-05-31',
})}
/>
<ArticleShell
title={TITLE}
subtitle="MintMCP and BuildMyMCPServer both get you to a hosted, OAuth-protected MCP server — but they start from opposite ends. The right pick depends entirely on whether you already have server code."
updated="May 2026"
>
<H2>What MintMCP does well</H2>
<P>
MintMCP takes a local <Strong>STDIO-based MCP server you already wrote</Strong> and turns
it into a production remote deployment one-click, with automatic OAuth wrapping. Its
headline strength is <Strong>compliance</Strong>: SOC 2 Type II, with audit logs in SOC 2,
HIPAA and GDPR-friendly formats. For an enterprise that already has a server and needs the
certifications signed off, that's a strong, honest fit.
</P>
<H2>Where it leaves a gap</H2>
<P>
The model assumes the hard part — designing and writing the server — is already done. If
you're starting from <em>&quot;I need a tool that does X&quot;</em> and there's no code
yet, a wrapper doesn't help. You still have to learn the MCP SDK, write and test the tool
logic, then bring it over.
</P>
<H2>The alternative: start from the prompt</H2>
<P>
<Strong>BuildMyMCPServer</Strong> covers the step before the wrap. You describe the tool in
plain language; it generates the TypeScript MCP server, runs static checks against banned
patterns, builds a container, and deploys it behind a full OAuth 2.1 authorization server
(PKCE, Dynamic Client Registration, Resource Indicators). You get copy-paste install
snippets for Claude Desktop, Cursor and ChatGPT, and the full source to export whenever you
want.
</P>
<H2>Pick by your starting point</H2>
<UL>
<li>
<Strong>You have a working STDIO server + need SOC 2/HIPAA today</Strong> MintMCP is
the more honest fit. We don't claim those certifications.
</li>
<li>
<Strong>You have an idea, not a server</Strong> → generate it here, ship in minutes, and
export the TypeScript if you later move it onto your own infra.
</li>
<li>
<Strong>You're an agency building one-off tools for clients repeatedly</Strong>
generation + a fork-able template marketplace removes the per-client boilerplate.
</li>
<li>
<Strong>You're in the EU/DACH and care where prompts go</Strong> → we expose the provider
and offer a data-residency choice rather than defaulting everything to one region.
</li>
</UL>
<H2>What's the same either way</H2>
<P>
Both deliver a remote, OAuth-protected MCP server at a stable URL that real clients can
install neither leaves you hand-rolling the auth handshake. The difference is purely{' '}
<Strong>where you start</Strong>: with code, or with a sentence.
</P>
<P>
More on the landscape:{' '}
<Link
href="/guides/hosted-mcp-platforms-compared"
className="text-[--color-accent] hover:underline"
>
hosted MCP platforms compared
</Link>
.
</P>
</ArticleShell>
</>
);
}

View File

@@ -0,0 +1,64 @@
import { pageMetadata } from '@/lib/seo';
import Link from 'next/link';
export const metadata = pageMetadata({
title: 'MCP guides',
description:
'Practical guides on hosting, securing and shipping Model Context Protocol (MCP) servers — OAuth 2.1, remote transport, platform comparisons.',
path: '/guides',
});
const GUIDES = [
{
slug: 'host-mcp-server-with-oauth',
title: 'How to host a remote MCP server with OAuth (2026)',
description:
'Streamable HTTP, OAuth 2.1, PKCE and Resource Indicators — what it actually takes to put a remote MCP server in production, and the shortcuts.',
tag: 'Guide',
},
{
slug: 'hosted-mcp-platforms-compared',
title: 'Hosted MCP platforms compared: Cloudflare, Smithery, Composio & generating your own',
description:
'The MCP hosting landscape splits into four categories. Which one fits depends on whether you have a server already, need a catalog, or need bespoke logic.',
tag: 'Comparison',
},
{
slug: 'mintmcp-alternative',
title: 'MintMCP alternative: generate and host a custom MCP server',
description:
'MintMCP wraps an existing STDIO server into a remote one. If you do not have a server yet, here is the generate-from-a-prompt route — and where MintMCP still wins.',
tag: 'Alternative',
},
];
export default function GuidesIndex() {
return (
<div className="mx-auto max-w-3xl px-6 py-14">
<h1 className="text-[28px] font-semibold tracking-tight text-[--color-fg]">MCP guides</h1>
<p className="mt-2 text-[14.5px] leading-relaxed text-[--color-fg-muted]">
Hosting, auth and shipping for Model Context Protocol servers written for people building
real tools, not demos.
</p>
<div className="mt-8 space-y-3">
{GUIDES.map((g) => (
<Link
key={g.slug}
href={`/guides/${g.slug}`}
className="block rounded-lg border border-[--color-border] p-4 transition-colors hover:bg-[--color-bg-subtle]"
>
<span className="mono text-[10.5px] uppercase tracking-wider text-[--color-fg-subtle]">
{g.tag}
</span>
<h2 className="mt-1 text-[16px] font-semibold tracking-tight text-[--color-fg]">
{g.title}
</h2>
<p className="mt-1.5 text-[13px] leading-relaxed text-[--color-fg-muted]">
{g.description}
</p>
</Link>
))}
</div>
</div>
);
}