diff --git a/apps/web/app/(marketing)/changelog/page.tsx b/apps/web/app/(marketing)/changelog/page.tsx
index e203302..33c8a4d 100644
--- a/apps/web/app/(marketing)/changelog/page.tsx
+++ b/apps/web/app/(marketing)/changelog/page.tsx
@@ -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;
diff --git a/apps/web/app/(marketing)/page.tsx b/apps/web/app/(marketing)/page.tsx
index c984cca..a5dd698 100644
--- a/apps/web/app/(marketing)/page.tsx
+++ b/apps/web/app/(marketing)/page.tsx
@@ -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 Let’s 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() {
{[
- { 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) => (
-
{s.n}
+
+ {s.n}
+
{s.t}
{s.d}
@@ -211,16 +202,23 @@ export default function Landing() {
-
Built for the work you actually have
+
+ Built for the work you actually have
+
Anything with an HTTP API or a database, in minutes.
diff --git a/apps/web/app/(marketing)/pricing/page.tsx b/apps/web/app/(marketing)/pricing/page.tsx
index 040f5fb..798bf24 100644
--- a/apps/web/app/(marketing)/pricing/page.tsx
+++ b/apps/web/app/(marketing)/pricing/page.tsx
@@ -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() {
{t.price}{t.tag}
-
{t.description}
+
+ {t.description}
+
{t.features.map((f) => (
— {f}
diff --git a/apps/web/app/(marketing)/privacy/page.tsx b/apps/web/app/(marketing)/privacy/page.tsx
index 85c6ece..16a0be3 100644
--- a/apps/web/app/(marketing)/privacy/page.tsx
+++ b/apps/web/app/(marketing)/privacy/page.tsx
@@ -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() {
Contact
Data controller: BuildMyMCPServer. Email{' '}
-
+
privacy@buildmymcpserver.com
{' '}
for any of the above.
diff --git a/apps/web/app/(marketing)/security/page.tsx b/apps/web/app/(marketing)/security/page.tsx
index a5e4d0b..51c76d1 100644
--- a/apps/web/app/(marketing)/security/page.tsx
+++ b/apps/web/app/(marketing)/security/page.tsx
@@ -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.
- 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.
@@ -67,7 +73,10 @@ export default function Security() {
Disclosure
Found a vulnerability? Email{' '}
-
+
security@buildmymcpserver.com
{' '}
with a clear reproduction. We respond within 48h. We do not run a paid bounty yet, but we
diff --git a/apps/web/app/(marketing)/status/layout.tsx b/apps/web/app/(marketing)/status/layout.tsx
new file mode 100644
index 0000000..7172d88
--- /dev/null
+++ b/apps/web/app/(marketing)/status/layout.tsx
@@ -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;
+}
diff --git a/apps/web/app/(marketing)/terms/page.tsx b/apps/web/app/(marketing)/terms/page.tsx
index 574a1af..d933c65 100644
--- a/apps/web/app/(marketing)/terms/page.tsx
+++ b/apps/web/app/(marketing)/terms/page.tsx
@@ -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 = [
{
diff --git a/apps/web/app/apple-icon.tsx b/apps/web/app/apple-icon.tsx
index 0101bca..3490682 100644
--- a/apps/web/app/apple-icon.tsx
+++ b/apps/web/app/apple-icon.tsx
@@ -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(
- (
-
-
-
- ),
+
+
+
,
{ ...size },
);
}
diff --git a/apps/web/app/docs/layout.tsx b/apps/web/app/docs/layout.tsx
index 6a27328..970f9cc 100644
--- a/apps/web/app/docs/layout.tsx
+++ b/apps/web/app/docs/layout.tsx
@@ -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 }[] }[] = [
{
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 1f41aab..e6b8402 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -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
>
+
{children}
diff --git a/apps/web/app/manifest.ts b/apps/web/app/manifest.ts
new file mode 100644
index 0000000..b82ac5f
--- /dev/null
+++ b/apps/web/app/manifest.ts
@@ -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' }],
+ };
+}
diff --git a/apps/web/app/opengraph-image.tsx b/apps/web/app/opengraph-image.tsx
new file mode 100644
index 0000000..75967c4
--- /dev/null
+++ b/apps/web/app/opengraph-image.tsx
@@ -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(
+
+
+
+ M
+
+
+ BuildMyMCPServer
+
+
+
+
+
+ Describe your tool.
+
+
+ We host the MCP server.
+
+
+ Prompt to a hosted, OAuth 2.1-protected MCP server in 60 seconds.
+