feat(web): full SEO stack — metadata, JSON-LD, sitemap, robots, OG image
Some checks failed
Deploy to Production / deploy (push) Failing after 46s

Ported and adapted from the BuildMyDiscord SEO setup:

- lib/seo.ts — single source for site constants, the FAQ data (shared by
  the rendered FAQ and the FAQPage schema so they never drift) and JSON-LD
  builders.
- Rich root metadata: title template, keywords, Open Graph, Twitter card,
  robots directives, canonical.
- JSON-LD: Organization + WebSite + SoftwareApplication sitewide, FAQPage
  on the landing page. No AggregateRating — there are no real reviews yet.
- app/robots.ts — allow all, explicit allow-list for AI answer-engine
  crawlers (GPTBot, ClaudeBot, PerplexityBot, …), disallow private routes.
- app/sitemap.ts — every public marketing + docs route.
- app/opengraph-image.tsx — monochrome on-brand 1200x630 share card.
- app/manifest.ts + public/llms.txt.
- Per-page metadata for pricing, changelog, security, privacy, terms,
  docs, templates and status.
- opengraph-image + apple-icon pinned to the edge runtime — next/og
  crashes during a Node-runtime prerender.

Verified: next build passes; /robots.txt, /sitemap.xml,
/manifest.webmanifest and /opengraph-image all generate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marco Sadjadi 2026-05-21 19:16:40 +02:00
parent 617886352c
commit b843394d0f
18 changed files with 689 additions and 112 deletions

View File

@ -1,6 +1,12 @@
import { CodeBlock } from '@/components/code-block';
import { pageMetadata } from '@/lib/seo';
export const metadata = { title: 'Changelog — BuildMyMCPServer' };
export const metadata = pageMetadata({
title: 'Changelog',
description:
'Product updates and release notes for BuildMyMCPServer — new features, fixes and improvements to the MCP server platform.',
path: '/changelog',
});
interface Release {
version: string;

View File

@ -1,5 +1,7 @@
import Link from 'next/link';
import { CodeBlock } from '@/components/code-block';
import { JsonLd } from '@/components/json-ld';
import { FAQ, faqJsonLd } from '@/lib/seo';
import Link from 'next/link';
const PROMPT_EXAMPLE = `Create an MCP server that searches our Notion workspace.
Tools: search_pages, get_page_content.
@ -45,62 +47,37 @@ const MARKETPLACE_POINTS: { t: string; d: string }[] = [
},
];
const FAQ: { q: string; a: string }[] = [
{
q: 'What is MCP?',
a: 'Model Context Protocol — an open standard from Anthropic for connecting AI assistants to external tools, data and APIs over a transport like Streamable HTTP.',
},
{
q: 'Do I need to write code?',
a: 'No. You describe the tool in natural language. We generate the TypeScript server, run static checks, build a Docker image and deploy it to a public OAuth-protected URL.',
},
{
q: 'Which clients work?',
a: 'Claude Desktop, Cursor, ChatGPT Custom Connectors, VS Code Copilot, Continue.dev — anything that speaks the MCP spec.',
},
{
q: 'How is auth handled?',
a: 'Every generated server is an OAuth 2.1 Resource Server. Our control plane is the Authorization Server (PKCE + Dynamic Client Registration + Resource Indicators per RFC 8707).',
},
{
q: 'Can I self-host?',
a: 'Yes. The runner is a plain Docker container; the control plane is open to BYO Postgres + Redis. See the self-hosting guide in docs.',
},
{
q: 'What about secrets?',
a: 'AES-256-GCM at rest in Postgres, injected as environment variables into the runtime container. Never logged, never echoed back.',
},
{
q: 'Cold starts?',
a: 'No cold starts. Containers stay warm. Sub-50ms tool-call overhead on average for in-region requests.',
},
{
q: 'Rate limits?',
a: 'Default 100 requests/min/IP per tool. Configurable per server. Quota enforced at the Traefik layer before hitting your container.',
},
{
q: 'How fast is generation?',
a: 'Spec → image → live URL typically completes in 45-90 seconds.',
},
{
q: 'Logs and metrics?',
a: 'Live log streaming to the dashboard, structured tool-call metrics (P50/P95/P99 latency, error rate, per-tool throughput) — all retained for 30 days.',
},
{
q: 'What if I cancel?',
a: 'You can export the full TypeScript source of every server you built. No vendor lock-in.',
},
{
q: 'Custom domain?',
a: 'Pro plan and above. Add a CNAME, we provision Lets Encrypt automatically.',
},
];
const TIERS = [
{ name: 'Hobby', price: '€0', tag: 'Forever free', features: ['1 server', '100k calls/mo', 'BMM subdomain', 'Community support'] },
{ name: 'Pro', price: '€49', tag: '/ month', features: ['5 servers', '1M calls/mo', 'Custom domain', 'Priority build queue', 'Email support'] },
{ name: 'Team', price: '€149', tag: '/ month', features: ['25 servers', '10M calls/mo', 'RBAC + audit log', 'SLA 99.9%', 'Slack support'] },
{ name: 'Enterprise', price: '€499+', tag: '/ month', features: ['Unlimited', 'BYOC', 'SSO / SAML', 'Dedicated cluster', 'Customer success'] },
{
name: 'Hobby',
price: '€0',
tag: 'Forever free',
features: ['1 server', '100k calls/mo', 'BMM subdomain', 'Community support'],
},
{
name: 'Pro',
price: '€49',
tag: '/ month',
features: [
'5 servers',
'1M calls/mo',
'Custom domain',
'Priority build queue',
'Email support',
],
},
{
name: 'Team',
price: '€149',
tag: '/ month',
features: ['25 servers', '10M calls/mo', 'RBAC + audit log', 'SLA 99.9%', 'Slack support'],
},
{
name: 'Enterprise',
price: '€499+',
tag: '/ month',
features: ['Unlimited', 'BYOC', 'SSO / SAML', 'Dedicated cluster', 'Customer success'],
},
];
export default function Landing() {
@ -176,12 +153,26 @@ export default function Landing() {
</div>
<div className="grid gap-6 md:grid-cols-3">
{[
{ n: '01', t: 'Describe your tool', d: 'A sentence is enough. List your secrets and which APIs to call.' },
{ n: '02', t: 'We generate, check, deploy', d: 'Claude writes the spec. We render TypeScript, run static checks, build a container, deploy to your subdomain.' },
{ n: '03', t: 'Install in your client', d: 'Copy the snippet into Claude Desktop, Cursor or ChatGPT. OAuth flow on first use.' },
{
n: '01',
t: 'Describe your tool',
d: 'A sentence is enough. List your secrets and which APIs to call.',
},
{
n: '02',
t: 'We generate, check, deploy',
d: 'Claude writes the spec. We render TypeScript, run static checks, build a container, deploy to your subdomain.',
},
{
n: '03',
t: 'Install in your client',
d: 'Copy the snippet into Claude Desktop, Cursor or ChatGPT. OAuth flow on first use.',
},
].map((s) => (
<div key={s.n} className="panel p-5">
<div className="mono text-[11px] tracking-widest text-[--color-fg-subtle]">{s.n}</div>
<div className="mono text-[11px] tracking-widest text-[--color-fg-subtle]">
{s.n}
</div>
<h3 className="mt-4 text-[15px] font-semibold tracking-tight">{s.t}</h3>
<p className="mt-2 text-[13px] leading-relaxed text-[--color-fg-muted]">{s.d}</p>
</div>
@ -211,16 +202,23 @@ export default function Landing() {
<section className="border-b border-[--color-border] py-20">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-10 max-w-2xl">
<h2 className="text-[28px] font-semibold tracking-tight">Built for the work you actually have</h2>
<h2 className="text-[28px] font-semibold tracking-tight">
Built for the work you actually have
</h2>
<p className="mt-2 text-[14px] text-[--color-fg-muted]">
Anything with an HTTP API or a database, in minutes.
</p>
</div>
<div className="grid gap-3 md:grid-cols-3">
{EXAMPLES.map((e) => (
<div key={e.title} className="panel p-4 transition-colors hover:border-[--color-border-strong]">
<div
key={e.title}
className="panel p-4 transition-colors hover:border-[--color-border-strong]"
>
<div className="text-[13px] font-semibold tracking-tight">{e.title}</div>
<p className="mt-1 text-[12.5px] leading-relaxed text-[--color-fg-muted]">{e.desc}</p>
<p className="mt-1 text-[12.5px] leading-relaxed text-[--color-fg-muted]">
{e.desc}
</p>
</div>
))}
</div>
@ -273,7 +271,9 @@ export default function Landing() {
key={t.name}
className={`panel p-5 ${i === 1 ? 'border-[--color-accent]/40' : ''}`}
>
<div className="text-[12px] uppercase tracking-wider text-[--color-fg-subtle]">{t.name}</div>
<div className="text-[12px] uppercase tracking-wider text-[--color-fg-subtle]">
{t.name}
</div>
<div className="mt-2 flex items-baseline gap-1">
<span className="text-[26px] font-semibold tracking-tight">{t.price}</span>
<span className="text-[12px] text-[--color-fg-subtle]">{t.tag}</span>
@ -291,6 +291,7 @@ export default function Landing() {
{/* FAQ */}
<section className="py-20">
<JsonLd data={faqJsonLd()} />
<div className="mx-auto max-w-6xl px-6">
<h2 className="text-[28px] font-semibold tracking-tight">FAQ</h2>
<div className="mt-8 grid gap-x-12 gap-y-6 md:grid-cols-2">

View File

@ -1,6 +1,12 @@
import { pageMetadata } from '@/lib/seo';
import Link from 'next/link';
export const metadata = { title: 'Pricing — BuildMyMCPServer' };
export const metadata = pageMetadata({
title: 'Pricing',
description:
'BuildMyMCPServer pricing — start free with one hosted MCP server, scale to Pro, Team and Enterprise. Pay for tool calls, not boilerplate.',
path: '/pricing',
});
const TIERS = [
{
@ -113,7 +119,9 @@ export default function Pricing() {
<span className="text-[28px] font-semibold tracking-tight">{t.price}</span>
<span className="text-[12px] text-[--color-fg-subtle]">{t.tag}</span>
</div>
<p className="mt-2 text-[12px] leading-relaxed text-[--color-fg-muted]">{t.description}</p>
<p className="mt-2 text-[12px] leading-relaxed text-[--color-fg-muted]">
{t.description}
</p>
<ul className="mt-4 space-y-1.5 text-[12.5px] text-[--color-fg-muted]">
{t.features.map((f) => (
<li key={f}> {f}</li>

View File

@ -1,4 +1,11 @@
export const metadata = { title: 'Privacy — BuildMyMCPServer' };
import { pageMetadata } from '@/lib/seo';
export const metadata = pageMetadata({
title: 'Privacy',
description:
'BuildMyMCPServer privacy policy — what data we collect, how it is used, and the rights you have over it.',
path: '/privacy',
});
const SECTIONS = [
{
@ -29,7 +36,7 @@ const SECTIONS = [
{
h: 'Subprocessors',
p: [
'Anthropic (generation) — only the prompt text you send. Anthropic\'s data-retention policy applies.',
"Anthropic (generation) — only the prompt text you send. Anthropic's data-retention policy applies.",
'Hetzner (compute).',
'Backblaze (encrypted backups).',
'Stripe (billing).',
@ -91,7 +98,10 @@ export default function Privacy() {
<h2 className="text-[16px] font-semibold tracking-tight">Contact</h2>
<p className="mt-3 text-[13.5px] leading-relaxed text-[--color-fg-muted]">
Data controller: BuildMyMCPServer. Email{' '}
<a className="text-[--color-accent] underline" href="mailto:privacy@buildmymcpserver.com">
<a
className="text-[--color-accent] underline"
href="mailto:privacy@buildmymcpserver.com"
>
privacy@buildmymcpserver.com
</a>{' '}
for any of the above.

View File

@ -1,7 +1,13 @@
import Link from 'next/link';
import { CodeBlock } from '@/components/code-block';
import { pageMetadata } from '@/lib/seo';
import Link from 'next/link';
export const metadata = { title: 'Security — BuildMyMCPServer' };
export const metadata = pageMetadata({
title: 'Security',
description:
'How BuildMyMCPServer secures your MCP servers — per-server Docker isolation, AES-256-GCM encrypted secrets, OAuth 2.1 and a hardened control plane.',
path: '/security',
});
const PILLARS = [
{
@ -18,7 +24,7 @@ const PILLARS = [
},
{
title: 'No token passthrough',
body: 'When a tool calls a downstream API, it uses its own server-side credentials — not the user\'s OAuth token. Tokens never leak across trust boundaries. This is mandated by the MCP authorization spec.',
body: "When a tool calls a downstream API, it uses its own server-side credentials — not the user's OAuth token. Tokens never leak across trust boundaries. This is mandated by the MCP authorization spec.",
},
{
title: 'Static security checks',
@ -49,8 +55,8 @@ export default function Security() {
Built like infrastructure.
</h1>
<p className="mt-3 text-[14px] leading-relaxed text-[--color-fg-muted]">
We host code generated by an LLM, on behalf of customers, that exposes their internal
APIs to AI clients. The threat model is real. Here is what we do about it.
We host code generated by an LLM, on behalf of customers, that exposes their internal APIs
to AI clients. The threat model is real. Here is what we do about it.
</p>
</header>
@ -67,7 +73,10 @@ export default function Security() {
<h2 className="text-[18px] font-semibold tracking-tight">Disclosure</h2>
<p className="mt-2 text-[13.5px] leading-relaxed text-[--color-fg-muted]">
Found a vulnerability? Email{' '}
<a className="text-[--color-accent] underline" href="mailto:security@buildmymcpserver.com">
<a
className="text-[--color-accent] underline"
href="mailto:security@buildmymcpserver.com"
>
security@buildmymcpserver.com
</a>{' '}
with a clear reproduction. We respond within 48h. We do not run a paid bounty yet, but we

View File

@ -0,0 +1,14 @@
import { pageMetadata } from '@/lib/seo';
// status/page.tsx is a client component and cannot export metadata itself —
// this layout carries it.
export const metadata = pageMetadata({
title: 'Status',
description:
'Live operational status of BuildMyMCPServer — control plane, build pipeline and hosted MCP servers.',
path: '/status',
});
export default function StatusLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@ -1,4 +1,11 @@
export const metadata = { title: 'Terms — BuildMyMCPServer' };
import { pageMetadata } from '@/lib/seo';
export const metadata = pageMetadata({
title: 'Terms',
description:
'BuildMyMCPServer terms of service — the agreement that governs use of the platform.',
path: '/terms',
});
const SECTIONS = [
{

View File

@ -1,40 +1,37 @@
import { ImageResponse } from 'next/og';
// Edge runtime — see opengraph-image.tsx: avoids the next/og fileURLToPath
// crash during a Node-runtime prerender (notably on Windows builds).
export const runtime = 'edge';
export const size = { width: 180, height: 180 };
export const contentType = 'image/png';
export default function AppleIcon() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
background: '#6366F1',
borderRadius: 38,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<svg
width="120"
height="120"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<title>BuildMyMCPServer</title>
<path
d="M8.5 22.5V9.5L16 16L23.5 9.5V22.5"
stroke="#FFFFFF"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
</div>
),
<div
style={{
width: '100%',
height: '100%',
background: '#6366F1',
borderRadius: 38,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<svg width="120" height="120" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<title>BuildMyMCPServer</title>
<path
d="M8.5 22.5V9.5L16 16L23.5 9.5V22.5"
stroke="#FFFFFF"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
</div>,
{ ...size },
);
}

View File

@ -1,5 +1,13 @@
import Link from 'next/link';
import { Logo } from '@/components/logo';
import { pageMetadata } from '@/lib/seo';
import Link from 'next/link';
export const metadata = pageMetadata({
title: 'Docs',
description:
'BuildMyMCPServer documentation — quickstart, MCP concepts, the OAuth 2.1 flow, authoring tools, self-hosting and the API reference.',
path: '/docs',
});
const SECTIONS: { heading: string; items: { href: string; label: string }[] }[] = [
{

View File

@ -1,14 +1,57 @@
import { JsonLd } from '@/components/json-ld';
import { SiteBanner } from '@/components/site-banner';
import {
SEO_KEYWORDS,
SITE_DESCRIPTION,
SITE_NAME,
SITE_TAGLINE,
SITE_URL,
siteJsonLd,
} from '@/lib/seo';
import { GeistMono } from 'geist/font/mono';
import { GeistSans } from 'geist/font/sans';
import type { Metadata } from 'next';
import './globals.css';
const TITLE = `${SITE_NAME}${SITE_TAGLINE}`;
export const metadata: Metadata = {
title: 'BuildMyMCPServer — Describe your tool. We host the server.',
description:
'From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready for Claude, Cursor & ChatGPT.',
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3001'),
metadataBase: new URL(SITE_URL),
title: {
default: TITLE,
template: `%s | ${SITE_NAME}`,
},
description: SITE_DESCRIPTION,
applicationName: SITE_NAME,
keywords: SEO_KEYWORDS,
authors: [{ name: SITE_NAME }],
creator: SITE_NAME,
publisher: SITE_NAME,
alternates: { canonical: '/' },
openGraph: {
type: 'website',
locale: 'en_US',
url: SITE_URL,
siteName: SITE_NAME,
title: TITLE,
description: SITE_DESCRIPTION,
},
twitter: {
card: 'summary_large_image',
title: TITLE,
description: SITE_DESCRIPTION,
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-snippet': -1,
'max-video-preview': -1,
},
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
@ -19,6 +62,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
suppressHydrationWarning
>
<body>
<JsonLd data={siteJsonLd()} />
<SiteBanner />
{children}
</body>

15
apps/web/app/manifest.ts Normal file
View File

@ -0,0 +1,15 @@
import { SITE_DESCRIPTION, SITE_NAME } from '@/lib/seo';
import type { MetadataRoute } from 'next';
export default function manifest(): MetadataRoute.Manifest {
return {
name: SITE_NAME,
short_name: 'BuildMyMCP',
description: SITE_DESCRIPTION,
start_url: '/',
display: 'standalone',
background_color: '#0a0a0b',
theme_color: '#0a0a0b',
icons: [{ src: '/icon.svg', sizes: 'any', type: 'image/svg+xml' }],
};
}

View File

@ -0,0 +1,107 @@
import { ImageResponse } from 'next/og';
// Edge runtime: next/og loads its assets via fileURLToPath, which throws an
// "Invalid URL" during a Node-runtime prerender. The edge runtime sidesteps
// that and is how the OG image route builds reliably.
export const runtime = 'edge';
// Sitewide Open Graph / social-share card. Monochrome to match the brand —
// flat colours only, a single indigo accent, no gradients.
export const alt = 'BuildMyMCPServer — Describe your tool. We host the MCP server.';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default function Image() {
return new ImageResponse(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
backgroundColor: '#0a0a0b',
padding: '72px',
fontFamily: 'sans-serif',
}}
>
<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={{ display: 'flex', flexDirection: 'column' }}>
<div
style={{
color: '#fafafa',
fontSize: '74px',
fontWeight: 700,
lineHeight: 1.05,
letterSpacing: '-0.03em',
}}
>
Describe your tool.
</div>
<div
style={{
color: '#fafafa',
fontSize: '74px',
fontWeight: 700,
lineHeight: 1.05,
letterSpacing: '-0.03em',
}}
>
We host the MCP server.
</div>
<div
style={{
color: '#a1a1aa',
fontSize: '27px',
marginTop: '26px',
letterSpacing: '-0.01em',
}}
>
Prompt to a hosted, OAuth 2.1-protected MCP server in 60 seconds.
</div>
</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' }}
/>
OAuth 2.1 · Streamable HTTP · Docker-isolated
</div>
<div style={{ color: '#71717a', fontSize: '22px' }}>buildmymcpserver.com</div>
</div>
</div>,
{ ...size },
);
}

38
apps/web/app/robots.ts Normal file
View File

@ -0,0 +1,38 @@
import { SITE_URL } from '@/lib/seo';
import type { MetadataRoute } from 'next';
// AI search/answer crawlers — explicitly allowed so the product surfaces in
// ChatGPT, Claude, Perplexity, Google AI and similar answer engines.
const AI_CRAWLERS = [
'GPTBot',
'OAI-SearchBot',
'ChatGPT-User',
'ClaudeBot',
'anthropic-ai',
'Claude-Web',
'PerplexityBot',
'Perplexity-User',
'Google-Extended',
'CCBot',
'Applebot-Extended',
'Amazonbot',
'meta-externalagent',
'DuckAssistBot',
];
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin', '/dashboard', '/login'],
},
// Aggressive scraper with no search value.
{ userAgent: 'Bytespider', disallow: '/' },
{ userAgent: AI_CRAWLERS, allow: '/', disallow: ['/api/', '/admin', '/dashboard'] },
],
sitemap: `${SITE_URL}/sitemap.xml`,
host: SITE_URL,
};
}

36
apps/web/app/sitemap.ts Normal file
View File

@ -0,0 +1,36 @@
import { SITE_URL } from '@/lib/seo';
import type { MetadataRoute } from 'next';
type Entry = {
path: string;
priority: number;
changeFrequency: MetadataRoute.Sitemap[number]['changeFrequency'];
};
const ROUTES: Entry[] = [
{ path: '/', priority: 1.0, changeFrequency: 'weekly' },
{ path: '/pricing', priority: 0.9, changeFrequency: 'weekly' },
{ path: '/templates', priority: 0.9, changeFrequency: 'daily' },
{ path: '/docs', priority: 0.8, changeFrequency: 'weekly' },
{ path: '/docs/concepts', priority: 0.7, changeFrequency: 'monthly' },
{ path: '/docs/oauth', priority: 0.7, changeFrequency: 'monthly' },
{ path: '/docs/authoring', priority: 0.7, changeFrequency: 'monthly' },
{ path: '/docs/api-reference', priority: 0.7, changeFrequency: 'monthly' },
{ path: '/docs/self-hosting', priority: 0.7, changeFrequency: 'monthly' },
{ path: '/docs/faq', priority: 0.6, changeFrequency: 'monthly' },
{ path: '/changelog', priority: 0.6, changeFrequency: 'weekly' },
{ path: '/security', priority: 0.5, changeFrequency: 'monthly' },
{ path: '/status', priority: 0.4, changeFrequency: 'weekly' },
{ path: '/privacy', priority: 0.3, changeFrequency: 'yearly' },
{ path: '/terms', priority: 0.3, changeFrequency: 'yearly' },
];
export default function sitemap(): MetadataRoute.Sitemap {
const now = new Date();
return ROUTES.map((r) => ({
url: `${SITE_URL}${r.path}`,
lastModified: now,
changeFrequency: r.changeFrequency,
priority: r.priority,
}));
}

View File

@ -0,0 +1,14 @@
import { pageMetadata } from '@/lib/seo';
// templates/page.tsx is a client component and cannot export metadata itself —
// this layout carries it.
export const metadata = pageMetadata({
title: 'Template marketplace',
description:
'Browse the BuildMyMCPServer template marketplace — fork a ready-made MCP server, add your own credentials and deploy in seconds.',
path: '/templates',
});
export default function TemplatesLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@ -0,0 +1,13 @@
// Renders a JSON-LD structured-data script. The `<` escape prevents a
// `</script>` sequence in the data from breaking out of the tag.
export function JsonLd({ data }: { data: object }) {
return (
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD must be injected as a raw script.
dangerouslySetInnerHTML={{
__html: JSON.stringify(data).replace(/</g, '\\u003c'),
}}
/>
);
}

210
apps/web/lib/seo.ts Normal file
View File

@ -0,0 +1,210 @@
// Central SEO source of truth — site constants, FAQ data, JSON-LD builders.
// The FAQ array here is rendered on the landing page AND emitted as FAQPage
// structured data, so the two never drift (Google requires them to match).
import type { Metadata } from 'next';
export const SITE_URL = process.env.NEXT_PUBLIC_APP_URL ?? 'https://buildmymcpserver.com';
export const SITE_NAME = 'BuildMyMCPServer';
export const SITE_TAGLINE = 'Describe your tool. We host the MCP server.';
export const SITE_DESCRIPTION =
'From a natural-language prompt to a hosted, OAuth 2.1-protected MCP server in 60 seconds. Streamable HTTP, ready for Claude, Cursor and ChatGPT.';
export const SEO_KEYWORDS = [
'MCP server',
'Model Context Protocol',
'hosted MCP server',
'MCP server hosting',
'MCP server builder',
'create MCP server',
'deploy MCP server',
'MCP server generator',
'OAuth MCP server',
'Streamable HTTP MCP',
'Claude MCP server',
'Cursor MCP',
'ChatGPT connector',
'MCP template marketplace',
];
export interface FaqItem {
q: string;
a: string;
}
// Rendered on the landing page and emitted as FAQPage JSON-LD — keep in sync
// by virtue of being a single export.
export const FAQ: FaqItem[] = [
{
q: 'What is MCP?',
a: 'Model Context Protocol — an open standard from Anthropic for connecting AI assistants to external tools, data and APIs over a transport like Streamable HTTP.',
},
{
q: 'Do I need to write code?',
a: 'No. You describe the tool in natural language. We generate the TypeScript server, run static checks, build a Docker image and deploy it to a public OAuth-protected URL.',
},
{
q: 'Which clients work?',
a: 'Claude Desktop, Cursor, ChatGPT Custom Connectors, VS Code Copilot, Continue.dev — anything that speaks the MCP spec.',
},
{
q: 'How is auth handled?',
a: 'Every generated server is an OAuth 2.1 Resource Server. Our control plane is the Authorization Server (PKCE + Dynamic Client Registration + Resource Indicators per RFC 8707).',
},
{
q: 'Can I self-host?',
a: 'Yes. The runner is a plain Docker container; the control plane is open to BYO Postgres + Redis. See the self-hosting guide in docs.',
},
{
q: 'What about secrets?',
a: 'AES-256-GCM at rest in Postgres, injected as environment variables into the runtime container. Never logged, never echoed back.',
},
{
q: 'Cold starts?',
a: 'No cold starts. Containers stay warm. Sub-50ms tool-call overhead on average for in-region requests.',
},
{
q: 'Rate limits?',
a: 'Default 100 requests/min/IP per tool. Configurable per server. Quota enforced at the proxy layer before hitting your container.',
},
{
q: 'How fast is generation?',
a: 'Spec to image to live URL typically completes in 45-90 seconds.',
},
{
q: 'Logs and metrics?',
a: 'Live log streaming to the dashboard, structured tool-call metrics (P50/P95/P99 latency, error rate, per-tool throughput) — all retained for 30 days.',
},
{
q: 'What if I cancel?',
a: 'You can export the full TypeScript source of every server you built. No vendor lock-in.',
},
{
q: 'Custom domain?',
a: 'Pro plan and above. Add a CNAME, we provision a TLS certificate automatically.',
},
];
const SOFTWARE_FEATURES = [
'Prompt to a deployed MCP server in under 60 seconds',
'OAuth 2.1 authorization server — PKCE, Dynamic Client Registration (RFC 7591), Resource Indicators (RFC 8707)',
'Streamable HTTP transport, compatible with Claude Desktop, Cursor, ChatGPT, VS Code Copilot and Continue.dev',
'Every generated server runs in an isolated Docker container',
'Customer secrets encrypted with AES-256-GCM, injected only at runtime',
'Live build-log streaming to the dashboard',
'Template marketplace — publish your server or fork someone elses',
'Full TypeScript source export, no vendor lock-in',
'Self-hostable control plane with BYO Postgres and Redis',
];
const OFFERS = [
{
name: 'Hobby',
price: '0',
description: '1 server, 100k tool calls/month, BMM subdomain, community support.',
},
{
name: 'Pro',
price: '49',
description:
'5 servers, 1M tool calls/month, custom domain, priority build queue, email support.',
},
{
name: 'Team',
price: '149',
description: '25 servers, 10M tool calls/month, RBAC + audit log, 99.9% SLA, Slack support.',
},
{
name: 'Enterprise',
price: '499',
description:
'Unlimited servers, bring-your-own-cloud, SSO/SAML, dedicated cluster, customer success.',
},
];
/** Organization + WebSite + SoftwareApplication graph — emitted sitewide. */
export function siteJsonLd(): object {
return {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'Organization',
'@id': `${SITE_URL}/#organization`,
name: SITE_NAME,
url: SITE_URL,
logo: { '@type': 'ImageObject', url: `${SITE_URL}/icon.svg` },
foundingDate: '2026',
foundingLocation: {
'@type': 'Place',
address: { '@type': 'PostalAddress', addressCountry: 'CH' },
},
},
{
'@type': 'WebSite',
'@id': `${SITE_URL}/#website`,
url: SITE_URL,
name: SITE_NAME,
publisher: { '@id': `${SITE_URL}/#organization` },
description: SITE_DESCRIPTION,
inLanguage: 'en',
},
{
'@type': 'SoftwareApplication',
'@id': `${SITE_URL}/#software`,
name: SITE_NAME,
applicationCategory: 'DeveloperApplication',
operatingSystem: 'Web Browser',
url: SITE_URL,
inLanguage: 'en',
description:
'BuildMyMCPServer turns a natural-language prompt into a hosted, OAuth 2.1-protected Model Context Protocol (MCP) server. Describe the tools you need; the platform generates a TypeScript server, runs static checks, builds a Docker image and deploys it to a public Streamable HTTP endpoint that Claude, Cursor and ChatGPT can connect to. A template marketplace lets users publish and fork ready-made servers.',
featureList: SOFTWARE_FEATURES,
offers: OFFERS.map((o) => ({
'@type': 'Offer',
name: o.name,
price: o.price,
priceCurrency: 'EUR',
description: o.description,
})),
},
],
};
}
/** FAQPage structured data — emitted on the page that displays the FAQ. */
export function faqJsonLd(items: FaqItem[] = FAQ): object {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: items.map((item) => ({
'@type': 'Question',
name: item.q,
acceptedAnswer: { '@type': 'Answer', text: item.a },
})),
};
}
/**
* Per-page metadata. `title` is a bare string so the root layout's
* "%s | BuildMyMCPServer" template appends the brand exactly once.
*/
export function pageMetadata(opts: { title: string; description: string; path: string }): Metadata {
const fullTitle = `${opts.title} | ${SITE_NAME}`;
return {
title: opts.title,
description: opts.description,
alternates: { canonical: opts.path },
openGraph: {
type: 'website',
siteName: SITE_NAME,
title: fullTitle,
description: opts.description,
url: `${SITE_URL}${opts.path}`,
},
twitter: {
card: 'summary_large_image',
title: fullTitle,
description: opts.description,
},
};
}

40
apps/web/public/llms.txt Normal file
View File

@ -0,0 +1,40 @@
# BuildMyMCPServer
> Turn a natural-language prompt into a hosted, OAuth 2.1-protected Model Context Protocol (MCP) server in about 60 seconds. The platform generates a TypeScript server, runs static checks, builds a Docker image and deploys it to a public Streamable HTTP endpoint that Claude, Cursor and ChatGPT can connect to.
## What it is
BuildMyMCPServer is a platform for creating and hosting MCP servers without writing code. You describe the tools you want in plain language; the platform produces a working MCP server and runs it for you.
The workflow:
1. **Describe.** Write what the tool should do and which APIs or secrets it needs.
2. **Review.** The platform shows the generated spec — tool names, input schemas, required credentials — and lets you edit it before building.
3. **Build & deploy.** It renders the TypeScript server, runs static checks, builds a Docker image and deploys it as an isolated container behind a public OAuth-protected URL.
4. **Connect.** Drop the endpoint into any MCP client — Claude Desktop, Cursor, ChatGPT Custom Connectors, VS Code Copilot, Continue.dev.
## Key capabilities
- **Prompt-to-server generation** — no MCP boilerplate, no Docker management.
- **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.
- **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.
- **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.
- **Self-hostable** — the runner is a plain Docker container; the control plane runs against your own Postgres and Redis.
## Pricing
- **Hobby** — free. 1 server, 100k tool calls/month.
- **Pro** — €49/month. 5 servers, 1M tool calls/month, custom domain, priority build queue.
- **Team** — €149/month. 25 servers, 10M tool calls/month, RBAC, audit log, 99.9% SLA.
- **Enterprise** — from €499/month. Unlimited servers, bring-your-own-cloud, SSO/SAML.
## Docs
- Concepts: https://buildmymcpserver.com/docs/concepts
- OAuth: https://buildmymcpserver.com/docs/oauth
- Authoring servers: https://buildmymcpserver.com/docs/authoring
- API reference: https://buildmymcpserver.com/docs/api-reference
- Self-hosting: https://buildmymcpserver.com/docs/self-hosting