fix(seo): canonical self-deindexing on /docs/*, truthful llms.txt, real sitemap dates, RSS feed, breadcrumbs, per-article OG images

- /docs subpages canonicalized to /docs and had no descriptions; each now
  uses pageMetadata with its own path (was: Google could index at most one)
- llms.txt claimed Team EUR 149, RBAC/SLA and BYO-cloud that do not exist;
  regenerated from lib/seo.ts truth + new llms-full.txt with docs content
- sitemap stamped lastModified=now on every request; now real per-route dates
  from new lib/articles.ts registry (single source for guides index/sitemap/RSS)
- new /feed.xml RSS 2.0 route + alternates link in root layout
- articleJsonLd: image (per-guide opengraph-image via lib/og-article.tsx),
  Person author, wordCount; new breadcrumbJsonLd on guides + docs
- GSC verification via NEXT_PUBLIC_GSC_VERIFICATION (documented in .env.example)
- dropped fabricated Enterprise EUR 499 offer from SoftwareApplication JSON-LD
- article-shell: OL/Table/Note primitives for upcoming articles

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:00:02 +02:00
parent 3dc65e4f4d
commit cba45402ce
25 changed files with 652 additions and 75 deletions

View File

@ -13,6 +13,9 @@ BETTER_AUTH_SECRET=replace-me-with-32-bytes-of-random-hex-1234567890abcdef
BETTER_AUTH_URL=http://localhost:3001 BETTER_AUTH_URL=http://localhost:3001
NEXT_PUBLIC_APP_URL=http://localhost:3001 NEXT_PUBLIC_APP_URL=http://localhost:3001
NEXT_PUBLIC_API_URL=http://localhost:4000 NEXT_PUBLIC_API_URL=http://localhost:4000
# Google Search Console HTML-tag verification token (content attribute only).
# Leave empty in dev; set in production, then submit /sitemap.xml in GSC.
NEXT_PUBLIC_GSC_VERIFICATION=
# ---- GitHub OAuth ("Continue with GitHub") ---- # ---- GitHub OAuth ("Continue with GitHub") ----
# Create at https://github.com/settings/applications/new # Create at https://github.com/settings/applications/new

View File

@ -0,0 +1,15 @@
import { pageMetadata } from '@/lib/seo';
import type { ReactNode } from 'react';
// The contact page itself is a client component ('use client'), which cannot
// export metadata — so the canonical/description live here.
export const metadata = pageMetadata({
title: 'Contact',
description:
'Get in touch with the BuildMyMCPServer team — support, sales and security questions answered by email.',
path: '/contact',
});
export default function ContactLayout({ children }: { children: ReactNode }) {
return children;
}

View File

@ -71,3 +71,33 @@ export function UL({ children }: { children: ReactNode }) {
export function Strong({ children }: { children: ReactNode }) { export function Strong({ children }: { children: ReactNode }) {
return <strong className="font-semibold text-[--color-fg]">{children}</strong>; return <strong className="font-semibold text-[--color-fg]">{children}</strong>;
} }
export function OL({ children }: { children: ReactNode }) {
return (
<ol className="mt-3 list-decimal space-y-1.5 pl-5 text-[14.5px] leading-relaxed text-[--color-fg-muted]">
{children}
</ol>
);
}
/** Comparison / feature tables. Pass fully-formed <thead>/<tbody> children;
* the wrapper provides the horizontal-scroll container so wide tables never
* break the mobile viewport. */
export function Table({ children }: { children: ReactNode }) {
return (
<div className="mt-4 overflow-x-auto rounded-lg border border-[--color-border]">
<table className="w-full min-w-[560px] border-collapse text-left text-[13.5px] leading-relaxed [&_td]:border-t [&_td]:border-[--color-border] [&_td]:px-3.5 [&_td]:py-2.5 [&_td]:align-top [&_td]:text-[--color-fg-muted] [&_th]:bg-[--color-bg-subtle] [&_th]:px-3.5 [&_th]:py-2.5 [&_th]:text-[12px] [&_th]:font-semibold [&_th]:uppercase [&_th]:tracking-wider [&_th]:text-[--color-fg]">
{children}
</table>
</div>
);
}
/** Callout for caveats and version-sensitive facts. */
export function Note({ children }: { children: ReactNode }) {
return (
<div className="mt-4 rounded-lg border border-[--color-border] bg-[--color-bg-subtle] px-4 py-3 text-[13.5px] leading-relaxed text-[--color-fg-muted]">
{children}
</div>
);
}

View File

@ -0,0 +1,13 @@
import { articleOgImage, OG_SIZE } from '@/lib/og-article';
export const runtime = 'edge';
export const alt = 'How to host a remote MCP server with OAuth (2026)';
export const size = OG_SIZE;
export const contentType = 'image/png';
export default function Image() {
return articleOgImage({
title: 'How to host a remote MCP server with OAuth (2026)',
tag: 'Guide',
});
}

View File

@ -1,5 +1,5 @@
import { JsonLd } from '@/components/json-ld'; import { JsonLd } from '@/components/json-ld';
import { articleJsonLd, pageMetadata } from '@/lib/seo'; import { articleJsonLd, breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
import Link from 'next/link'; import Link from 'next/link';
import { ArticleShell, H2, P, Strong, UL } from '../article-shell'; import { ArticleShell, H2, P, Strong, UL } from '../article-shell';
@ -21,6 +21,13 @@ export default function Page() {
datePublished: '2026-05-31', datePublished: '2026-05-31',
})} })}
/> />
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Guides', path: '/guides' },
{ name: TITLE, path: PATH },
])}
/>
<ArticleShell <ArticleShell
title={TITLE} 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." 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."

View File

@ -0,0 +1,13 @@
import { articleOgImage, OG_SIZE } from '@/lib/og-article';
export const runtime = 'edge';
export const alt = 'Hosted MCP platforms compared: Cloudflare, Smithery, Composio & generating your own';
export const size = OG_SIZE;
export const contentType = 'image/png';
export default function Image() {
return articleOgImage({
title: 'Hosted MCP platforms compared: Cloudflare, Smithery, Composio & more',
tag: 'Comparison',
});
}

View File

@ -1,5 +1,5 @@
import { JsonLd } from '@/components/json-ld'; import { JsonLd } from '@/components/json-ld';
import { articleJsonLd, pageMetadata } from '@/lib/seo'; import { articleJsonLd, breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
import Link from 'next/link'; import Link from 'next/link';
import { ArticleShell, H2, P, Strong, UL } from '../article-shell'; import { ArticleShell, H2, P, Strong, UL } from '../article-shell';
@ -22,6 +22,13 @@ export default function Page() {
datePublished: '2026-05-31', datePublished: '2026-05-31',
})} })}
/> />
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Guides', path: '/guides' },
{ name: TITLE, path: PATH },
])}
/>
<ArticleShell <ArticleShell
title={TITLE} 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." 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."

View File

@ -0,0 +1,13 @@
import { articleOgImage, OG_SIZE } from '@/lib/og-article';
export const runtime = 'edge';
export const alt = 'MintMCP alternative: generate and host a custom MCP server';
export const size = OG_SIZE;
export const contentType = 'image/png';
export default function Image() {
return articleOgImage({
title: 'MintMCP alternative: generate and host a custom MCP server',
tag: 'Alternative',
});
}

View File

@ -1,5 +1,5 @@
import { JsonLd } from '@/components/json-ld'; import { JsonLd } from '@/components/json-ld';
import { articleJsonLd, pageMetadata } from '@/lib/seo'; import { articleJsonLd, breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
import Link from 'next/link'; import Link from 'next/link';
import { ArticleShell, H2, P, Strong, UL } from '../article-shell'; import { ArticleShell, H2, P, Strong, UL } from '../article-shell';
@ -21,6 +21,13 @@ export default function Page() {
datePublished: '2026-05-31', datePublished: '2026-05-31',
})} })}
/> />
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Guides', path: '/guides' },
{ name: TITLE, path: PATH },
])}
/>
<ArticleShell <ArticleShell
title={TITLE} 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." 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."

View File

@ -1,4 +1,6 @@
import { pageMetadata } from '@/lib/seo'; import { JsonLd } from '@/components/json-ld';
import { articlesNewestFirst } from '@/lib/articles';
import { breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
import Link from 'next/link'; import Link from 'next/link';
export const metadata = pageMetadata({ export const metadata = pageMetadata({
@ -8,48 +10,40 @@ export const metadata = pageMetadata({
path: '/guides', 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() { export default function GuidesIndex() {
const guides = articlesNewestFirst();
return ( return (
<div className="mx-auto max-w-3xl px-6 py-14"> <div className="mx-auto max-w-3xl px-6 py-14">
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Guides', path: '/guides' },
])}
/>
<h1 className="text-[28px] font-semibold tracking-tight text-[--color-fg]">MCP guides</h1> <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]"> <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 Hosting, auth and shipping for Model Context Protocol servers written for people building
real tools, not demos. real tools, not demos.
</p> </p>
<div className="mt-8 space-y-3"> <div className="mt-8 space-y-3">
{GUIDES.map((g) => ( {guides.map((g) => (
<Link <Link
key={g.slug} key={g.slug}
href={`/guides/${g.slug}`} href={`/guides/${g.slug}`}
className="block rounded-lg border border-[--color-border] p-4 transition-colors hover:bg-[--color-bg-subtle]" className="block rounded-lg border border-[--color-border] p-4 transition-colors hover:bg-[--color-bg-subtle]"
> >
<div className="flex items-center gap-2.5">
<span className="mono text-[10.5px] uppercase tracking-wider text-[--color-fg-subtle]"> <span className="mono text-[10.5px] uppercase tracking-wider text-[--color-fg-subtle]">
{g.tag} {g.tag}
</span> </span>
<span className="text-[10.5px] text-[--color-fg-subtle]">
{new Date(g.dateModified ?? g.datePublished).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
</span>
</div>
<h2 className="mt-1 text-[16px] font-semibold tracking-tight text-[--color-fg]"> <h2 className="mt-1 text-[16px] font-semibold tracking-tight text-[--color-fg]">
{g.title} {g.title}
</h2> </h2>

View File

@ -6,12 +6,26 @@ import {
DocsCode, DocsCode,
Mono, Mono,
} from '@/components/docs-page'; } from '@/components/docs-page';
import { JsonLd } from '@/components/json-ld';
import { breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
export const metadata = { title: 'API reference — BuildMyMCPServer docs' }; export const metadata = pageMetadata({
title: 'API reference',
description:
'REST API reference for the BuildMyMCPServer control plane — auth, server CRUD, build streaming, templates and the OAuth 2.1 endpoints.',
path: '/docs/api-reference',
});
export default function ApiReference() { export default function ApiReference() {
return ( return (
<> <>
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Docs', path: '/docs' },
{ name: 'API reference', path: '/docs/api-reference' },
])}
/>
<DocsTitle kicker="Reference">API reference</DocsTitle> <DocsTitle kicker="Reference">API reference</DocsTitle>
<DocsLead> <DocsLead>
Every endpoint on the control plane. Authenticated routes use the session cookie set by Every endpoint on the control plane. Authenticated routes use the session cookie set by

View File

@ -8,12 +8,26 @@ import {
DocsCode, DocsCode,
Mono, Mono,
} from '@/components/docs-page'; } from '@/components/docs-page';
import { JsonLd } from '@/components/json-ld';
import { breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
export const metadata = { title: 'Authoring tools — BuildMyMCPServer docs' }; export const metadata = pageMetadata({
title: 'Authoring tools',
description:
'How to write prompts that generate good MCP tools — naming, input schemas, credentials, and how the generated TypeScript is checked before it ships.',
path: '/docs/authoring',
});
export default function Authoring() { export default function Authoring() {
return ( return (
<> <>
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Docs', path: '/docs' },
{ name: 'Authoring tools', path: '/docs/authoring' },
])}
/>
<DocsTitle kicker="Build">Authoring tools</DocsTitle> <DocsTitle kicker="Build">Authoring tools</DocsTitle>
<DocsLead> <DocsLead>
What you write in the prompt is what Claude turns into TypeScript. Better prompts mean What you write in the prompt is what Claude turns into TypeScript. Better prompts mean

View File

@ -8,12 +8,26 @@ import {
DocsCode, DocsCode,
Mono, Mono,
} from '@/components/docs-page'; } from '@/components/docs-page';
import { JsonLd } from '@/components/json-ld';
import { breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
export const metadata = { title: 'MCP concepts — BuildMyMCPServer docs' }; export const metadata = pageMetadata({
title: 'MCP concepts',
description:
'What Model Context Protocol is: tools, resources and prompts, the Streamable HTTP transport, and how a client discovers and calls a server.',
path: '/docs/concepts',
});
export default function Concepts() { export default function Concepts() {
return ( return (
<> <>
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Docs', path: '/docs' },
{ name: 'MCP concepts', path: '/docs/concepts' },
])}
/>
<DocsTitle kicker="Get started">MCP concepts</DocsTitle> <DocsTitle kicker="Get started">MCP concepts</DocsTitle>
<DocsLead> <DocsLead>
Model Context Protocol is an open standard from Anthropic for connecting AI assistants to Model Context Protocol is an open standard from Anthropic for connecting AI assistants to

View File

@ -1,6 +1,13 @@
import { DocsTitle, DocsLead, DocsH2, DocsP, Mono } from '@/components/docs-page'; import { DocsTitle, DocsLead, DocsH2, DocsP, Mono } from '@/components/docs-page';
import { JsonLd } from '@/components/json-ld';
import { breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
export const metadata = { title: 'FAQ — BuildMyMCPServer docs' }; export const metadata = pageMetadata({
title: 'Docs FAQ',
description:
'Answers on generated-code safety, secrets handling, build failures, quotas and self-hosting for BuildMyMCPServer.',
path: '/docs/faq',
});
const ITEMS: { q: string; a: React.ReactNode }[] = [ const ITEMS: { q: string; a: React.ReactNode }[] = [
{ {
@ -56,6 +63,13 @@ const ITEMS: { q: string; a: React.ReactNode }[] = [
export default function Faq() { export default function Faq() {
return ( return (
<> <>
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Docs', path: '/docs' },
{ name: 'FAQ', path: '/docs/faq' },
])}
/>
<DocsTitle kicker="Reference">FAQ</DocsTitle> <DocsTitle kicker="Reference">FAQ</DocsTitle>
<DocsLead>Common questions, direct answers.</DocsLead> <DocsLead>Common questions, direct answers.</DocsLead>
<div className="space-y-7"> <div className="space-y-7">

View File

@ -8,12 +8,26 @@ import {
DocsCode, DocsCode,
Mono, Mono,
} from '@/components/docs-page'; } from '@/components/docs-page';
import { JsonLd } from '@/components/json-ld';
import { breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
export const metadata = { title: 'OAuth 2.1 flow — BuildMyMCPServer docs' }; export const metadata = pageMetadata({
title: 'OAuth 2.1 flow',
description:
'How every generated MCP server is protected: OAuth 2.1 with PKCE, Dynamic Client Registration (RFC 7591) and Resource Indicators (RFC 8707), walked through request by request.',
path: '/docs/oauth',
});
export default function OAuthDocs() { export default function OAuthDocs() {
return ( return (
<> <>
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Docs', path: '/docs' },
{ name: 'OAuth 2.1 flow', path: '/docs/oauth' },
])}
/>
<DocsTitle kicker="Auth">OAuth 2.1 flow</DocsTitle> <DocsTitle kicker="Auth">OAuth 2.1 flow</DocsTitle>
<DocsLead> <DocsLead>
Every generated server is an OAuth 2.1 Resource Server. The control plane is the Every generated server is an OAuth 2.1 Resource Server. The control plane is the

View File

@ -9,8 +9,14 @@ import {
DocsCode, DocsCode,
Mono, Mono,
} from '@/components/docs-page'; } from '@/components/docs-page';
import { pageMetadata } from '@/lib/seo';
export const metadata = { title: 'Quickstart — BuildMyMCPServer docs' }; export const metadata = pageMetadata({
title: 'Quickstart',
description:
'From first prompt to a live OAuth-protected MCP server in five minutes — sign in, describe your tool, confirm the plan, watch the build stream, install in your client.',
path: '/docs',
});
export default function Quickstart() { export default function Quickstart() {
return ( return (

View File

@ -8,12 +8,26 @@ import {
DocsCode, DocsCode,
Mono, Mono,
} from '@/components/docs-page'; } from '@/components/docs-page';
import { JsonLd } from '@/components/json-ld';
import { breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
export const metadata = { title: 'Self-hosting — BuildMyMCPServer docs' }; export const metadata = pageMetadata({
title: 'Self-hosting',
description:
'Run the BuildMyMCPServer control plane yourself — bring your own Postgres, Redis and Docker host, plus the production sandboxing flags for generated containers.',
path: '/docs/self-hosting',
});
export default function SelfHosting() { export default function SelfHosting() {
return ( return (
<> <>
<JsonLd
data={breadcrumbJsonLd([
{ name: 'Home', path: '/' },
{ name: 'Docs', path: '/docs' },
{ name: 'Self-hosting', path: '/docs/self-hosting' },
])}
/>
<DocsTitle kicker="Build">Self-hosting</DocsTitle> <DocsTitle kicker="Build">Self-hosting</DocsTitle>
<DocsLead> <DocsLead>
The control plane and generator are open. Bring your own Postgres, Redis, Docker host and The control plane and generator are open. Bring your own Postgres, Redis, Docker host and

View File

@ -0,0 +1,54 @@
import { articlesNewestFirst } from '@/lib/articles';
import { SITE_DESCRIPTION, SITE_NAME, SITE_URL } from '@/lib/seo';
export const dynamic = 'force-static';
function escapeXml(s: string): string {
return s
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
}
export function GET(): Response {
const articles = articlesNewestFirst();
const lastBuildDate = new Date(
articles[0]?.dateModified ?? articles[0]?.datePublished ?? '2026-05-31',
).toUTCString();
const items = articles
.map((a) => {
const url = `${SITE_URL}/guides/${a.slug}`;
return ` <item>
<title>${escapeXml(a.title)}</title>
<link>${url}</link>
<guid isPermaLink="true">${url}</guid>
<description>${escapeXml(a.description)}</description>
<pubDate>${new Date(a.datePublished).toUTCString()}</pubDate>
</item>`;
})
.join('\n');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${escapeXml(`${SITE_NAME} — MCP guides`)}</title>
<link>${SITE_URL}/guides</link>
<atom:link href="${SITE_URL}/feed.xml" rel="self" type="application/rss+xml"/>
<description>${escapeXml(SITE_DESCRIPTION)}</description>
<language>en</language>
<lastBuildDate>${lastBuildDate}</lastBuildDate>
${items}
</channel>
</rss>
`;
return new Response(xml, {
headers: {
'content-type': 'application/rss+xml; charset=utf-8',
'cache-control': 'public, max-age=3600',
},
});
}

View File

@ -26,7 +26,15 @@ export const metadata: Metadata = {
authors: [{ name: SITE_NAME }], authors: [{ name: SITE_NAME }],
creator: SITE_NAME, creator: SITE_NAME,
publisher: SITE_NAME, publisher: SITE_NAME,
alternates: { canonical: '/' }, alternates: {
canonical: '/',
types: { 'application/rss+xml': [{ url: '/feed.xml', title: `${SITE_NAME} — MCP guides` }] },
},
// Google Search Console ownership token. Unset in dev; set in production so
// the sitemap can be submitted and indexing monitored.
...(process.env.NEXT_PUBLIC_GSC_VERIFICATION
? { verification: { google: process.env.NEXT_PUBLIC_GSC_VERIFICATION } }
: {}),
openGraph: { openGraph: {
type: 'website', type: 'website',
locale: 'en_US', locale: 'en_US',

View File

@ -1,3 +1,4 @@
import { ARTICLES } from '@/lib/articles';
import { SITE_URL } from '@/lib/seo'; import { SITE_URL } from '@/lib/seo';
import { fetchPublicTemplateSlugs } from '@/lib/templates-server'; import { fetchPublicTemplateSlugs } from '@/lib/templates-server';
import type { MetadataRoute } from 'next'; import type { MetadataRoute } from 'next';
@ -6,48 +7,67 @@ type Entry = {
path: string; path: string;
priority: number; priority: number;
changeFrequency: MetadataRoute.Sitemap[number]['changeFrequency']; changeFrequency: MetadataRoute.Sitemap[number]['changeFrequency'];
/** Real last-substantive-change date. Bump when the page meaningfully changes. */
lastModified: string;
}; };
// lastModified must reflect actual content changes — Google discounts sitemaps
// whose lastmod is always "now". Bump a route's date when you edit its page.
const ROUTES: Entry[] = [ const ROUTES: Entry[] = [
{ path: '/', priority: 1.0, changeFrequency: 'weekly' }, { path: '/', priority: 1.0, changeFrequency: 'weekly', lastModified: '2026-07-08' },
{ path: '/pricing', priority: 0.9, changeFrequency: 'weekly' }, { path: '/pricing', priority: 0.9, changeFrequency: 'weekly', lastModified: '2026-07-08' },
{ path: '/templates', priority: 0.9, changeFrequency: 'daily' }, { path: '/templates', priority: 0.9, changeFrequency: 'daily', lastModified: '2026-07-08' },
{ path: '/guides', priority: 0.8, changeFrequency: 'weekly' }, { path: '/guides', priority: 0.8, changeFrequency: 'weekly', lastModified: '2026-07-08' },
{ path: '/guides/host-mcp-server-with-oauth', priority: 0.8, changeFrequency: 'monthly' }, { path: '/docs', priority: 0.8, changeFrequency: 'weekly', lastModified: '2026-07-08' },
{ path: '/guides/hosted-mcp-platforms-compared', priority: 0.8, changeFrequency: 'monthly' }, { path: '/docs/concepts', priority: 0.7, changeFrequency: 'monthly', lastModified: '2026-07-08' },
{ path: '/guides/mintmcp-alternative', priority: 0.7, changeFrequency: 'monthly' }, { path: '/docs/oauth', priority: 0.7, changeFrequency: 'monthly', lastModified: '2026-07-08' },
{ path: '/docs', priority: 0.8, changeFrequency: 'weekly' }, { path: '/docs/authoring', priority: 0.7, changeFrequency: 'monthly', lastModified: '2026-07-08' },
{ path: '/docs/concepts', priority: 0.7, changeFrequency: 'monthly' }, {
{ path: '/docs/oauth', priority: 0.7, changeFrequency: 'monthly' }, path: '/docs/api-reference',
{ path: '/docs/authoring', priority: 0.7, changeFrequency: 'monthly' }, priority: 0.7,
{ path: '/docs/api-reference', priority: 0.7, changeFrequency: 'monthly' }, changeFrequency: 'monthly',
{ path: '/docs/self-hosting', priority: 0.7, changeFrequency: 'monthly' }, lastModified: '2026-07-08',
{ path: '/docs/faq', priority: 0.6, changeFrequency: 'monthly' }, },
{ path: '/changelog', priority: 0.6, changeFrequency: 'weekly' }, {
{ path: '/security', priority: 0.5, changeFrequency: 'monthly' }, path: '/docs/self-hosting',
{ path: '/status', priority: 0.4, changeFrequency: 'weekly' }, priority: 0.7,
{ path: '/privacy', priority: 0.3, changeFrequency: 'yearly' }, changeFrequency: 'monthly',
{ path: '/terms', priority: 0.3, changeFrequency: 'yearly' }, lastModified: '2026-07-08',
},
{ path: '/docs/faq', priority: 0.6, changeFrequency: 'monthly', lastModified: '2026-07-08' },
{ path: '/changelog', priority: 0.6, changeFrequency: 'weekly', lastModified: '2026-06-11' },
{ path: '/security', priority: 0.5, changeFrequency: 'monthly', lastModified: '2026-06-04' },
{ path: '/contact', priority: 0.4, changeFrequency: 'yearly', lastModified: '2026-06-04' },
{ path: '/status', priority: 0.4, changeFrequency: 'weekly', lastModified: '2026-06-04' },
{ path: '/privacy', priority: 0.3, changeFrequency: 'yearly', lastModified: '2026-06-29' },
{ path: '/terms', priority: 0.3, changeFrequency: 'yearly', lastModified: '2026-06-29' },
]; ];
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const now = new Date();
const staticEntries: MetadataRoute.Sitemap = ROUTES.map((r) => ({ const staticEntries: MetadataRoute.Sitemap = ROUTES.map((r) => ({
url: `${SITE_URL}${r.path}`, url: `${SITE_URL}${r.path}`,
lastModified: now, lastModified: new Date(r.lastModified),
changeFrequency: r.changeFrequency, changeFrequency: r.changeFrequency,
priority: r.priority, priority: r.priority,
})); }));
// Guide articles come from the single registry, with their true dates.
const guideEntries: MetadataRoute.Sitemap = ARTICLES.map((a) => ({
url: `${SITE_URL}/guides/${a.slug}`,
lastModified: new Date(a.dateModified ?? a.datePublished),
changeFrequency: 'monthly',
priority: 0.8,
}));
// Marketplace templates — each public template is its own indexable page. // Marketplace templates — each public template is its own indexable page.
// Best-effort: if the API is unreachable the static entries still ship. // Best-effort: if the API is unreachable the static entries still ship.
const slugs = await fetchPublicTemplateSlugs(); const slugs = await fetchPublicTemplateSlugs();
const templateEntries: MetadataRoute.Sitemap = slugs.map((slug) => ({ const templateEntries: MetadataRoute.Sitemap = slugs.map((slug) => ({
url: `${SITE_URL}/templates/${slug}`, url: `${SITE_URL}/templates/${slug}`,
lastModified: now, lastModified: new Date('2026-07-08'),
changeFrequency: 'weekly', changeFrequency: 'weekly',
priority: 0.6, priority: 0.6,
})); }));
return [...staticEntries, ...templateEntries]; return [...staticEntries, ...guideEntries, ...templateEntries];
} }

53
apps/web/lib/articles.ts Normal file
View File

@ -0,0 +1,53 @@
// Single registry for all /guides/* articles. The guides index, sitemap and
// RSS feed all render from this list, so adding an article here is the only
// bookkeeping step a new guide page needs besides its own directory.
export interface Article {
slug: string;
title: string;
description: string;
/** Short badge shown on the index card, e.g. "Guide", "Comparison". */
tag: string;
/** ISO date the article first shipped. */
datePublished: string;
/** ISO date of last substantive edit. Defaults to datePublished. */
dateModified?: string;
}
export const ARTICLES: Article[] = [
{
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',
datePublished: '2026-05-31',
},
{
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',
datePublished: '2026-05-31',
},
{
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',
datePublished: '2026-05-31',
},
];
export function articleBySlug(slug: string): Article | undefined {
return ARTICLES.find((a) => a.slug === slug);
}
/** Articles newest-first, for the index page and the RSS feed. */
export function articlesNewestFirst(): Article[] {
return [...ARTICLES].sort((a, b) =>
(b.dateModified ?? b.datePublished).localeCompare(a.dateModified ?? a.datePublished),
);
}

118
apps/web/lib/og-article.tsx Normal file
View File

@ -0,0 +1,118 @@
import { ImageResponse } from 'next/og';
// Shared Open Graph card for /guides/* articles. Each guide directory ships a
// tiny opengraph-image.tsx that calls this with its title + tag — the design
// stays consistent and Google Discover gets a ≥1200px image per article.
export const OG_SIZE = { width: 1200, height: 630 };
export function articleOgImage(opts: { title: string; tag: string }): ImageResponse {
return new ImageResponse(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: '#0a0a0b',
padding: '72px',
fontFamily: 'sans-serif',
position: 'relative',
}}
>
{/* Indigo→cyan accent bar along the top edge */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '1200px',
height: '8px',
background: 'linear-gradient(90deg, #6366f1 0%, #22d3ee 100%)',
}}
/>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div
style={{
width: '46px',
height: '46px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '2px solid #fafafa',
borderRadius: '9px',
color: '#fafafa',
fontSize: '26px',
fontWeight: 700,
}}
>
M
</div>
<div
style={{
color: '#fafafa',
fontSize: '28px',
fontWeight: 600,
letterSpacing: '-0.02em',
}}
>
BuildMyMCPServer
</div>
</div>
<div
style={{
color: '#22d3ee',
fontSize: '20px',
textTransform: 'uppercase',
letterSpacing: '0.18em',
border: '1px solid #164e63',
borderRadius: '999px',
padding: '8px 22px',
}}
>
{opts.tag}
</div>
</div>
<div
style={{
color: '#fafafa',
fontSize: opts.title.length > 60 ? '56px' : '66px',
fontWeight: 700,
lineHeight: 1.08,
letterSpacing: '-0.03em',
maxWidth: '1000px',
}}
>
{opts.title}
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '11px',
color: '#71717a',
fontSize: '22px',
}}
>
<div
style={{
width: '9px',
height: '9px',
borderRadius: '9px',
backgroundColor: '#6366f1',
}}
/>
MCP guides
</div>
<div style={{ color: '#71717a', fontSize: '22px' }}>buildmymcpserver.com/guides</div>
</div>
</div>,
{ ...OG_SIZE },
);
}

View File

@ -97,6 +97,8 @@ const SOFTWARE_FEATURES = [
'Self-hostable control plane with BYO Postgres and Redis', 'Self-hostable control plane with BYO Postgres and Redis',
]; ];
// Enterprise is deliberately absent: its price is "Custom" and schema.org
// Offers with fabricated numeric prices are exactly what commit ee4713f purged.
const OFFERS = [ const OFFERS = [
{ {
name: 'Hobby', name: 'Hobby',
@ -114,12 +116,6 @@ const OFFERS = [
price: '199', price: '199',
description: '25 servers, 10M tool calls/month, audit log, shared Slack support.', description: '25 servers, 10M tool calls/month, audit log, shared Slack support.',
}, },
{
name: 'Enterprise',
price: '499',
description:
'Unlimited servers; custom infrastructure and data residency on request; dedicated hosting and SSO/SAML scoped per contract; customer success.',
},
]; ];
/** Organization + WebSite + SoftwareApplication graph — emitted sitewide. */ /** Organization + WebSite + SoftwareApplication graph — emitted sitewide. */
@ -191,7 +187,17 @@ export function articleJsonLd(opts: {
path: string; path: string;
datePublished: string; datePublished: string;
dateModified?: string; dateModified?: string;
/**
* Absolute or site-relative image URL. Defaults to the route's generated
* opengraph-image (1200×630), which satisfies Google Discover's large-image
* requirement once the route ships an opengraph-image.tsx.
*/
image?: string;
/** Named human author. Falls back to the Organization. */
authorName?: string;
wordCount?: number;
}): object { }): object {
const image = opts.image ?? `${SITE_URL}${opts.path}/opengraph-image`;
return { return {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'TechArticle', '@type': 'TechArticle',
@ -199,11 +205,29 @@ export function articleJsonLd(opts: {
description: opts.description, description: opts.description,
url: `${SITE_URL}${opts.path}`, url: `${SITE_URL}${opts.path}`,
mainEntityOfPage: { '@type': 'WebPage', '@id': `${SITE_URL}${opts.path}` }, mainEntityOfPage: { '@type': 'WebPage', '@id': `${SITE_URL}${opts.path}` },
image: image.startsWith('http') ? image : `${SITE_URL}${image}`,
datePublished: opts.datePublished, datePublished: opts.datePublished,
dateModified: opts.dateModified ?? opts.datePublished, dateModified: opts.dateModified ?? opts.datePublished,
inLanguage: 'en', inLanguage: 'en',
author: { '@id': `${SITE_URL}/#organization` }, author: opts.authorName
? { '@type': 'Person', name: opts.authorName }
: { '@id': `${SITE_URL}/#organization` },
publisher: { '@id': `${SITE_URL}/#organization` }, publisher: { '@id': `${SITE_URL}/#organization` },
...(opts.wordCount ? { wordCount: opts.wordCount } : {}),
};
}
/** BreadcrumbList structured data. Pass the trail from root to current page. */
export function breadcrumbJsonLd(trail: { name: string; path: string }[]): object {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: trail.map((crumb, i) => ({
'@type': 'ListItem',
position: i + 1,
name: crumb.name,
item: `${SITE_URL}${crumb.path}`,
})),
}; };
} }

View File

@ -0,0 +1,91 @@
# BuildMyMCPServer — full documentation
> Turn a natural-language prompt into a hosted, OAuth 2.1-protected Model Context Protocol (MCP) server in about 60 seconds. This file condenses the full documentation at https://buildmymcpserver.com/docs for LLM consumption. See https://buildmymcpserver.com/llms.txt for the short index.
## Quickstart (https://buildmymcpserver.com/docs)
Describe the tool you want, paste in any credentials, watch the build stream, copy a snippet into your AI client. Five minutes from first prompt to a live OAuth-protected MCP server.
Prerequisites: an MCP-capable AI client (Claude Desktop, Cursor, ChatGPT Custom Connectors, VS Code Copilot, Continue.dev) and API credentials for whatever the server should access — or pick the echo example to skip credentials.
1. **Sign in** to the dashboard.
2. **Describe your tool** in plain language. Example prompt: "Search and read pages from our Notion workspace via the Notion API. Tools: search_pages(query), get_page_content(page_id). Auth: NOTION_API_KEY."
3. **Confirm the plan.** The wizard shows which tools were parsed from the prompt, the input schemas, and which credentials are needed. Everything is editable before the build starts.
4. **Watch the build stream** over WebSocket through five states: queued → generating (Claude returns the spec) → building (TypeScript rendered, static checks, Docker image) → deploying (container boot) → live (endpoint responds, OAuth gate active).
5. **Install in your client.** The Done screen shows copy-ready snippets for Claude Desktop, Cursor and ChatGPT. The OAuth handshake runs automatically on the first tool call.
Example client config:
{
"mcpServers": {
"notion-reader": {
"url": "https://<your-server>.mcp.buildmymcpserver.com/mcp",
"auth": "oauth2"
}
}
}
## MCP concepts (https://buildmymcpserver.com/docs/concepts)
Model Context Protocol is an open standard from Anthropic for connecting AI assistants to external tools, data and APIs. Three primitives, one transport:
- **Tools** — functions the AI can invoke. Each has a name, description, input schema (JSON Schema/Zod) and a server-side implementation. The AI decides when to call them based on the description.
- **Resources** — read-only, URI-addressed data the AI can fetch (files, documents, database records).
- **Prompts** — parameterized prompt templates the server exposes to encourage specific orchestration patterns.
**Transport: Streamable HTTP.** Every generated server speaks Streamable HTTP (MCP spec 2025-11-25). The older SSE transport was deprecated in June 2025 and is not supported. One HTTP endpoint at /mcp, optionally negotiating a long-lived text/event-stream when the server pushes updates.
**Session lifecycle:** initialize (client sends protocol version + capabilities; server assigns a session id via the mcp-session-id header) → notifications/initialized → tools/list, tools/call, resources/list, prompts/list.
**Why MCP and not just REST:** REST APIs need bespoke OpenAPI integration per client. MCP standardizes discovery, invocation, auth and streaming, so any spec-compliant client picks up any spec-compliant server with zero glue code.
## OAuth 2.1 flow (https://buildmymcpserver.com/docs/oauth)
Every generated server is an OAuth 2.1 Resource Server; the control plane is the Authorization Server. Standards implemented:
- OAuth 2.1 draft (draft-ietf-oauth-v2-1) — no implicit flow, mandatory PKCE
- RFC 8414 — Authorization Server Metadata
- RFC 9728 — Protected Resource Metadata
- RFC 8707 — Resource Indicators (audience binding)
- RFC 7591 — Dynamic Client Registration
End-to-end: the first unauthenticated request gets a 401 with a WWW-Authenticate header pointing at the server's protected-resource metadata. The client fetches it, discovers the authorization server, registers itself dynamically (no human in the loop; each AI surface gets its own ephemeral client identity), then runs Authorization Code + PKCE. The user consents, the client exchanges the code for an RS256-signed JWT bound to the specific server's resource URL. The runner verifies signature (against the AS JWKS), issuer, audience and expiry on every call. No token passthrough — the runner never forwards the client's token to a downstream API.
Why audience binding matters: without RFC 8707, a token issued for one customer's MCP server could be replayed against another customer's server.
## Authoring tools (https://buildmymcpserver.com/docs/authoring)
What you write in the prompt is what Claude turns into TypeScript. Rules the generator enforces on generated code:
- No eval, no new Function, no child_process — static checks reject the build.
- No import statements in tool bodies; the runtime injects fetch, pg and z (Zod).
- Secrets live in process.env, never embedded literally.
- External HTTP calls must use AbortSignal.timeout (default 10s).
- Database access via pg with parameterized queries only.
- Errors return as MCP error-content, not thrown exceptions.
Prompt patterns that work: be explicit about tool names ("Tool: search_pages(query)"); name the credentials ("Auth: NOTION_API_KEY"); flag destructive tools so clients can warn; one server per integration rather than one server per tool.
Iteration: open the server's Iterate tab, describe the change, a new build version is queued and rolling-deployed — the old version stays live until the new one is healthy.
## Self-hosting (https://buildmymcpserver.com/docs/self-hosting)
The control plane and generator are open. Requirements: Node.js 20+, pnpm 9+, a Docker engine reachable from the generator, Postgres 16+, Redis 7+, and an Anthropic API key (optional — a mock generator covers offline dev).
Key environment variables: DATABASE_URL, REDIS_URL, ANTHROPIC_API_KEY, SECRETS_ENCRYPTION_KEY (32-byte hex AES-256-GCM key), CONTROL_PLANE_PUBLIC_URL (OAuth issuer), OAUTH_KEY_DIR (RS256 keypair), RUNNER_PORT_RANGE_START/END (host-port window for generated containers).
Production container sandboxing flags: --read-only, --cap-drop=ALL, --security-opt=no-new-privileges, --cpus=0.5 --memory=512m.
## FAQ highlights (https://buildmymcpserver.com/docs/faq)
- **How does LLM-generated code stay safe?** Three layers: strict Zod validation of the JSON spec, a regex scan for banned tokens (eval, child_process, prompt-injection markers), and a static check on the rendered TypeScript before the Docker build. Any failure stops the deploy.
- **What if Claude generates a broken tool?** The build fails at static-check or Docker-build stage; the user sees the exact error in the live log, refines the prompt and rebuilds. No invalid server serves traffic.
- **Do secrets leave the environment?** No. AES-256-GCM encrypted at rest, decrypted only when injected into the container at boot. Never in audit logs, build logs, or prompts sent to Claude.
- **No API key?** The generator falls back to a deterministic mock spec (echo + now tools) so the full pipeline can be verified without credits.
## Pricing (https://buildmymcpserver.com/pricing)
- Hobby: €0 — 1 server, 100,000 tool calls/month, community support.
- Pro: €49/month — 5 servers, 1M tool calls/month, priority build queue, email support. Custom domains on the roadmap.
- Team: €199/month — 25 servers, 10M tool calls/month, audit log, Slack support. RBAC on the roadmap.
- Enterprise: custom — unlimited servers; infrastructure, data residency, SSO/SAML scoped per contract.

View File

@ -19,22 +19,39 @@ The workflow:
- **OAuth 2.1 authorization server** — PKCE, Dynamic Client Registration (RFC 7591), Resource Indicators (RFC 8707), RS256 JWKS. - **OAuth 2.1 authorization server** — PKCE, Dynamic Client Registration (RFC 7591), Resource Indicators (RFC 8707), RS256 JWKS.
- **Streamable HTTP transport** — the modern MCP transport, compatible with every major MCP client. - **Streamable HTTP transport** — the modern MCP transport, compatible with every major MCP client.
- **Per-server isolation** — each generated server runs in its own Docker container. - **Per-server isolation** — each generated server runs in its own Docker container.
- **Encrypted secrets** — customer credentials are stored with AES-256-GCM envelope encryption and injected only at runtime; never logged. - **Encrypted secrets** — customer credentials are stored with AES-256-GCM encryption and injected only at runtime; never logged.
- **Template marketplace** — publish a server you built as a template, or fork one someone else published and add your own credentials. - **Template marketplace** — publish a server you built as a template, or fork one someone else published and add your own credentials.
- **Source export** — export the full TypeScript source of any server. No vendor lock-in. - **Source export** — export the full TypeScript source of any server. No vendor lock-in.
- **Self-hostable** — the runner is a plain Docker container; the control plane runs against your own Postgres and Redis. - **Self-hostable** — the runner is a plain Docker container; the control plane runs against your own Postgres and Redis.
## Pricing ## Pricing
- **Hobby** — free. 1 server, 100k tool calls/month. - **Hobby** — €0, forever free. 1 MCP server, 100,000 tool calls/month, BuildMyMCP subdomain, community support.
- **Pro** — €49/month. 5 servers, 1M tool calls/month, custom domain, priority build queue. - **Pro** — €49/month. 5 MCP servers, 1M tool calls/month, priority build queue, email support. Custom domains are on the roadmap (not shipped yet).
- **Team** — €149/month. 25 servers, 10M tool calls/month, RBAC, audit log, 99.9% SLA. - **Team** — €199/month. 25 MCP servers, 10M tool calls/month, audit log, shared Slack channel support. RBAC is on the roadmap (not shipped yet).
- **Enterprise** — from €499/month. Unlimited servers, bring-your-own-cloud, SSO/SAML. - **Enterprise** — custom pricing. Unlimited servers; custom infrastructure, data residency, dedicated hosting and SSO/SAML scoped per contract.
Full details: https://buildmymcpserver.com/pricing
## Docs ## Docs
- Quickstart: https://buildmymcpserver.com/docs
- Concepts: https://buildmymcpserver.com/docs/concepts - Concepts: https://buildmymcpserver.com/docs/concepts
- OAuth: https://buildmymcpserver.com/docs/oauth - OAuth: https://buildmymcpserver.com/docs/oauth
- Authoring servers: https://buildmymcpserver.com/docs/authoring - Authoring servers: https://buildmymcpserver.com/docs/authoring
- API reference: https://buildmymcpserver.com/docs/api-reference - API reference: https://buildmymcpserver.com/docs/api-reference
- Self-hosting: https://buildmymcpserver.com/docs/self-hosting - Self-hosting: https://buildmymcpserver.com/docs/self-hosting
- FAQ: https://buildmymcpserver.com/docs/faq
## Guides
- How to host a remote MCP server with OAuth (2026): https://buildmymcpserver.com/guides/host-mcp-server-with-oauth
- Hosted MCP platforms compared: https://buildmymcpserver.com/guides/hosted-mcp-platforms-compared
- MintMCP alternative: https://buildmymcpserver.com/guides/mintmcp-alternative
## Optional
- Full docs content in one file: https://buildmymcpserver.com/llms-full.txt
- Template marketplace: https://buildmymcpserver.com/templates
- Security posture: https://buildmymcpserver.com/security
- System status: https://buildmymcpserver.com/status