feat(web): real 3-step wizard, settings, audit, docs, marketing pages
Sprint 3.5: close every dead link and replace the single-step wizard with the spec-mandated 3-step flow. Wizard: - Step 1 collects prompt + name + slug, calls /v1/servers/preview. - Step 2 renders parsed tools (name, description, input schema as copyable JSON) + a credential field per requiredSecret Claude actually identified. Self-contained servers see 'No credentials needed' instead of generic Notion placeholders. - Step 3 streams the live build over WebSocket and shows install snippets. New dashboard pages: - /settings — org, plan/usage, members table, API keys + billing stubs (Sprint 4), encryption status. Reads /v1/me/org. - /audit — filterable table over /v1/audit with action pills, resource refs, IP, metadata JSON. Docs site (/docs + 6 sub-pages): - Sticky 240px sidebar, max-w-prose article column, shared DocsTitle/H2/Code primitives. - Quickstart, MCP concepts, OAuth 2.1 flow (full walkthrough with curl), Authoring tools, Self-hosting, API reference, FAQ. Marketing pages: - /changelog with tagged release timeline. - /security with 8 pillars + disclosure. - /privacy with GDPR-aware sections. - /terms (10 clauses). - /pricing full page (nav now points here instead of /#pricing anchor). - /status with live 10s probes against /api/health and /login. Footer 'system status' badge now links to /status. All 20 routes 200 OK in smoke crawl. Typecheck clean across packages.
This commit is contained in:
102
apps/web/app/(marketing)/changelog/page.tsx
Normal file
102
apps/web/app/(marketing)/changelog/page.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { CodeBlock } from '@/components/code-block';
|
||||
|
||||
export const metadata = { title: 'Changelog — BuildMyMCPServer' };
|
||||
|
||||
interface Release {
|
||||
version: string;
|
||||
date: string;
|
||||
tag: 'launch' | 'feature' | 'fix';
|
||||
title: string;
|
||||
items: string[];
|
||||
}
|
||||
|
||||
const RELEASES: Release[] = [
|
||||
{
|
||||
version: '0.2.0',
|
||||
date: '2026-05-19',
|
||||
tag: 'feature',
|
||||
title: '3-step wizard + filled-in pages',
|
||||
items: [
|
||||
'Real 3-step wizard: prompt → confirm parsed spec → build. Step 2 shows the tools Claude actually parsed and only asks for the credentials it identified.',
|
||||
'Preview endpoint: POST /v1/servers/preview runs Claude once, caches the spec 5 min, build worker reuses it. Saves a second Claude round-trip (~30s).',
|
||||
'Shared @bmm/llm package — system prompt + generateSpec live in one place, used by api and generator.',
|
||||
'Audit log: writes for login, logout, server.create, server.iterate, server.delete. /v1/audit endpoint + /audit page.',
|
||||
'Real /settings page (org info, plan & usage, members, encryption status).',
|
||||
'Full /docs site: quickstart, MCP concepts, OAuth flow, authoring, self-hosting, API reference, FAQ.',
|
||||
'Changelog, /security, /privacy, /terms, /pricing, /status pages — all marketing links work.',
|
||||
],
|
||||
},
|
||||
{
|
||||
version: '0.1.0',
|
||||
date: '2026-05-18',
|
||||
tag: 'launch',
|
||||
title: 'Sprints 1–3 — initial public dev build',
|
||||
items: [
|
||||
'Monorepo: Next.js 15 + Fastify + BullMQ generator + runner-template.',
|
||||
'Drizzle schema for orgs, servers, builds, secrets, oauth, metrics, audit.',
|
||||
'Magic-link auth, 30-day sessions, AES-256-GCM secret encryption.',
|
||||
'OAuth 2.1 Authorization Server: PKCE, RFC 7591 dynamic registration, RFC 8707 resource indicators, RS256 JWKS.',
|
||||
'Runner template with Streamable HTTP + OAuth 2.1 Resource Server.',
|
||||
'WebSocket build stream: queued → generating → building → deploying → live.',
|
||||
'Install snippet generator for Claude Desktop, Cursor, ChatGPT.',
|
||||
'docker-compose dev environment, pnpm dev bootstraps everything.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const tagStyle: Record<Release['tag'], string> = {
|
||||
launch: 'border-[--color-accent]/40 bg-[--color-accent]/10 text-[--color-accent]',
|
||||
feature: 'border-emerald-400/40 bg-emerald-400/10 text-emerald-300',
|
||||
fix: 'border-amber-400/40 bg-amber-400/10 text-amber-300',
|
||||
};
|
||||
|
||||
export default function Changelog() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<header className="mb-12">
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
|
||||
Release notes
|
||||
</div>
|
||||
<h1 className="mt-2 text-[32px] font-semibold tracking-tight">Changelog</h1>
|
||||
<p className="mt-3 text-[14px] leading-relaxed text-[--color-fg-muted]">
|
||||
What shipped, when, and why.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="space-y-12">
|
||||
{RELEASES.map((r) => (
|
||||
<article key={r.version} className="relative pl-6">
|
||||
<div className="absolute left-0 top-2 size-2 rounded-full border border-[--color-border-strong] bg-[--color-bg-elevated]" />
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="mono text-[13px] text-[--color-fg]">v{r.version}</span>
|
||||
<span className="text-[12px] text-[--color-fg-subtle]">{r.date}</span>
|
||||
<span
|
||||
className={`mono rounded-full border px-2 py-0.5 text-[10.5px] uppercase tracking-wider ${tagStyle[r.tag]}`}
|
||||
>
|
||||
{r.tag}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="mt-2 text-[16px] font-semibold tracking-tight">{r.title}</h2>
|
||||
<ul className="mt-3 space-y-1.5">
|
||||
{r.items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="relative pl-4 text-[13px] leading-relaxed text-[--color-fg-muted] before:absolute before:left-0 before:top-2 before:size-1 before:rounded-full before:bg-[--color-fg-subtle]"
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<footer className="mt-16">
|
||||
<CodeBlock
|
||||
label="subscribe"
|
||||
code={`Follow @buildmymcp on Mastodon — or watch the GitHub repo for tagged releases.`}
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export default function MarketingLayout({ children }: { children: React.ReactNod
|
||||
<Link href="/#how" className="transition-colors hover:text-[--color-fg]">
|
||||
How it works
|
||||
</Link>
|
||||
<Link href="/#pricing" className="transition-colors hover:text-[--color-fg]">
|
||||
<Link href="/pricing" className="transition-colors hover:text-[--color-fg]">
|
||||
Pricing
|
||||
</Link>
|
||||
<Link href="/docs" className="transition-colors hover:text-[--color-fg]">
|
||||
@@ -42,10 +42,10 @@ export default function MarketingLayout({ children }: { children: React.ReactNod
|
||||
<main className="flex-1">{children}</main>
|
||||
<footer className="border-t border-[--color-border] py-8">
|
||||
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-6 text-[12px] text-[--color-fg-subtle] md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/status" className="flex items-center gap-2 transition-colors hover:text-[--color-fg]">
|
||||
<span className="size-1.5 animate-pulse rounded-full bg-emerald-400" />
|
||||
<span>All systems operational</span>
|
||||
</div>
|
||||
<span>System status</span>
|
||||
</Link>
|
||||
<div className="flex flex-wrap gap-x-5 gap-y-1">
|
||||
<Link href="/docs" className="transition-colors hover:text-[--color-fg]">
|
||||
Docs
|
||||
|
||||
149
apps/web/app/(marketing)/pricing/page.tsx
Normal file
149
apps/web/app/(marketing)/pricing/page.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export const metadata = { title: 'Pricing — BuildMyMCPServer' };
|
||||
|
||||
const TIERS = [
|
||||
{
|
||||
name: 'Hobby',
|
||||
price: '€0',
|
||||
tag: 'Forever free',
|
||||
description: 'For trying things out and shipping single-user tools.',
|
||||
features: [
|
||||
'1 MCP server',
|
||||
'100,000 tool calls / month',
|
||||
'BuildMyMCP subdomain',
|
||||
'Community support',
|
||||
],
|
||||
cta: 'Start free',
|
||||
href: '/login',
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
price: '€49',
|
||||
tag: '/ month',
|
||||
description: 'For solo founders and small teams shipping production tools.',
|
||||
features: [
|
||||
'5 MCP servers',
|
||||
'1M tool calls / month',
|
||||
'Custom domain',
|
||||
'Priority build queue',
|
||||
'Email support, 1 business-day SLA',
|
||||
],
|
||||
cta: 'Start Pro',
|
||||
href: '/login',
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
name: 'Team',
|
||||
price: '€149',
|
||||
tag: '/ month',
|
||||
description: 'For teams with RBAC, audit, and 99.9% SLA needs.',
|
||||
features: [
|
||||
'25 MCP servers',
|
||||
'10M tool calls / month',
|
||||
'RBAC + extended audit log',
|
||||
'99.9% uptime SLA',
|
||||
'Shared Slack channel support',
|
||||
],
|
||||
cta: 'Start Team',
|
||||
href: '/login',
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
price: '€499+',
|
||||
tag: '/ month',
|
||||
description: 'For organizations bringing their own cloud, SSO and dedicated infra.',
|
||||
features: [
|
||||
'Unlimited servers',
|
||||
'BYOC (AWS, GCP, Azure, Hetzner)',
|
||||
'SSO / SAML',
|
||||
'Dedicated cluster',
|
||||
'Customer success manager',
|
||||
],
|
||||
cta: 'Contact sales',
|
||||
href: 'mailto:sales@buildmymcpserver.com',
|
||||
},
|
||||
];
|
||||
|
||||
const FAQ = [
|
||||
{
|
||||
q: 'What counts as a tool call?',
|
||||
a: 'Every successful invocation of a tool exposed by your MCP server. Failed calls do not count.',
|
||||
},
|
||||
{
|
||||
q: 'What happens if I exceed my quota?',
|
||||
a: 'Hobby: 429 with a hint to upgrade. Pro/Team: overage at €0.02 per 1000 calls, billed the following month. Soft caps configurable.',
|
||||
},
|
||||
{
|
||||
q: 'Annual billing?',
|
||||
a: 'Yes — save 20% on Pro and Team paying annually. Enterprise is annual by default.',
|
||||
},
|
||||
{
|
||||
q: 'Plan changes?',
|
||||
a: 'Upgrade any time, pro-rated. Downgrade at end of billing period.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Pricing() {
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl px-6 py-16">
|
||||
<header className="mb-12 max-w-2xl">
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
|
||||
Pricing
|
||||
</div>
|
||||
<h1 className="mt-2 text-[36px] font-semibold leading-tight tracking-tight">
|
||||
Pay for tool calls, not boilerplate.
|
||||
</h1>
|
||||
<p className="mt-4 text-[15px] leading-relaxed text-[--color-fg-muted]">
|
||||
Build infinite servers, pay for the traffic they actually serve. Cancel any time, export
|
||||
everything, no lock-in.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
{TIERS.map((t) => (
|
||||
<div
|
||||
key={t.name}
|
||||
className={`panel flex h-full flex-col p-5 ${t.highlight ? 'border-[--color-accent]/40' : ''}`}
|
||||
>
|
||||
<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-[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>
|
||||
<ul className="mt-4 space-y-1.5 text-[12.5px] text-[--color-fg-muted]">
|
||||
{t.features.map((f) => (
|
||||
<li key={f}>— {f}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link
|
||||
href={t.href}
|
||||
className={`mt-6 inline-flex h-8 items-center justify-center rounded-md px-3 text-[12.5px] font-medium transition-colors duration-200 ${
|
||||
t.highlight
|
||||
? 'bg-[--color-accent] text-white hover:bg-[#5557e8]'
|
||||
: 'border border-[--color-border] bg-[--color-bg-elevated] text-[--color-fg] hover:bg-[--color-bg-subtle]'
|
||||
}`}
|
||||
>
|
||||
{t.cta}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="mt-20">
|
||||
<h2 className="text-[22px] font-semibold tracking-tight">Pricing FAQ</h2>
|
||||
<div className="mt-6 grid gap-x-12 gap-y-6 md:grid-cols-2">
|
||||
{FAQ.map((f) => (
|
||||
<div key={f.q}>
|
||||
<h3 className="text-[14px] font-semibold tracking-tight">{f.q}</h3>
|
||||
<p className="mt-1.5 text-[13px] leading-relaxed text-[--color-fg-muted]">{f.a}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
apps/web/app/(marketing)/privacy/page.tsx
Normal file
103
apps/web/app/(marketing)/privacy/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
export const metadata = { title: 'Privacy — BuildMyMCPServer' };
|
||||
|
||||
const SECTIONS = [
|
||||
{
|
||||
h: 'What we collect',
|
||||
p: [
|
||||
'Account: email, organization name, the IP address of your sign-in.',
|
||||
'Workspace: the prompts you send to the generator, the generated server specifications, build logs, server names and slugs you choose.',
|
||||
'Tool-call metrics: counts, latencies, status codes — never the request or response payloads. We only know that a tool was called, how long it took, and whether it succeeded.',
|
||||
'Billing: Stripe customer id + subscription metadata for paid plans. Card details never touch our servers.',
|
||||
],
|
||||
},
|
||||
{
|
||||
h: 'What we do NOT collect',
|
||||
p: [
|
||||
'Plaintext credentials. Every secret you save is AES-256-GCM encrypted before it hits storage. We have no ability to read them; only your container at runtime can.',
|
||||
'Tool-call payloads. The arguments your AI client sends and the responses our server returns are not stored or logged.',
|
||||
'Browsing data, cross-site tracking, advertising identifiers.',
|
||||
],
|
||||
},
|
||||
{
|
||||
h: 'Where it lives',
|
||||
p: [
|
||||
'EU region by default (Hetzner Falkenstein, Germany).',
|
||||
'Postgres + Redis + container hosts are all in the same region.',
|
||||
'Backups encrypted at rest in Backblaze B2 EU.',
|
||||
],
|
||||
},
|
||||
{
|
||||
h: 'Subprocessors',
|
||||
p: [
|
||||
'Anthropic (generation) — only the prompt text you send. Anthropic\'s data-retention policy applies.',
|
||||
'Hetzner (compute).',
|
||||
'Backblaze (encrypted backups).',
|
||||
'Stripe (billing).',
|
||||
'Cloudflare (DNS + DDoS).',
|
||||
],
|
||||
},
|
||||
{
|
||||
h: 'Retention',
|
||||
p: [
|
||||
'Active account: all workspace data kept while subscribed.',
|
||||
'Cancelled account: 30-day grace period, then full deletion (servers, builds, logs, audit).',
|
||||
'Audit log: 1 year on Team+, 30 days on Pro/Hobby.',
|
||||
'Tool-call metrics: 30 days, then aggregated to daily counts.',
|
||||
],
|
||||
},
|
||||
{
|
||||
h: 'Your rights (GDPR)',
|
||||
p: [
|
||||
'Access — export everything in JSON via the settings page (Sprint 4) or by email.',
|
||||
'Deletion — delete your organization in settings; everything goes within 30 days.',
|
||||
'Rectification — change name and email yourself; everything else is editable on request.',
|
||||
'Portability — the generated TypeScript source of every server is yours, downloadable.',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Privacy() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<header className="mb-12">
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
|
||||
Privacy policy
|
||||
</div>
|
||||
<h1 className="mt-2 text-[32px] font-semibold tracking-tight">Privacy</h1>
|
||||
<p className="mt-3 text-[14px] leading-relaxed text-[--color-fg-muted]">
|
||||
Plain language. What we collect, where it lives, how to get it deleted. Last updated
|
||||
2026-05-19.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="space-y-9">
|
||||
{SECTIONS.map((s) => (
|
||||
<section key={s.h}>
|
||||
<h2 className="text-[16px] font-semibold tracking-tight">{s.h}</h2>
|
||||
<ul className="mt-3 space-y-1.5">
|
||||
{s.p.map((p) => (
|
||||
<li
|
||||
key={p}
|
||||
className="relative pl-4 text-[13.5px] leading-relaxed text-[--color-fg-muted] before:absolute before:left-0 before:top-2 before:size-1 before:rounded-full before:bg-[--color-fg-subtle]"
|
||||
>
|
||||
{p}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section>
|
||||
<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">
|
||||
privacy@buildmymcpserver.com
|
||||
</a>{' '}
|
||||
for any of the above.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
97
apps/web/app/(marketing)/security/page.tsx
Normal file
97
apps/web/app/(marketing)/security/page.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import Link from 'next/link';
|
||||
import { CodeBlock } from '@/components/code-block';
|
||||
|
||||
export const metadata = { title: 'Security — BuildMyMCPServer' };
|
||||
|
||||
const PILLARS = [
|
||||
{
|
||||
title: 'Per-server isolation',
|
||||
body: 'Every customer MCP server runs in its own Docker container. No shared process, no shared filesystem, no shared memory. One server compromised does not affect any other.',
|
||||
},
|
||||
{
|
||||
title: 'Encrypted secrets',
|
||||
body: 'API keys and credentials are AES-256-GCM encrypted at rest in Postgres with a 32-byte key sourced from env. Decryption happens only at the moment of container ENV injection. Plaintext is never logged.',
|
||||
},
|
||||
{
|
||||
title: 'OAuth 2.1, no API keys for end users',
|
||||
body: 'Every generated server is an OAuth 2.1 Resource Server. PKCE, Dynamic Client Registration (RFC 7591), Resource Indicators (RFC 8707), RS256-signed JWTs. Short-lived, audience-bound, replay-resistant.',
|
||||
},
|
||||
{
|
||||
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.',
|
||||
},
|
||||
{
|
||||
title: 'Static security checks',
|
||||
body: 'Every LLM-generated tool body is scanned for banned patterns (eval, new Function, child_process) before Docker build. Prompt-injection markers like "ignore previous instructions" also trip the check. Build fails fast, no risky code ships.',
|
||||
},
|
||||
{
|
||||
title: 'Container hardening',
|
||||
body: 'Production containers run with --read-only, --cap-drop=ALL, --security-opt=no-new-privileges, CPU and memory limits. Network egress can be restricted to whitelisted domains per server.',
|
||||
},
|
||||
{
|
||||
title: 'Audit log',
|
||||
body: 'Every privileged action — login, logout, server create/iterate/delete — is recorded with IP, timestamp, user, and metadata. Available via /audit for Team-and-above orgs.',
|
||||
},
|
||||
{
|
||||
title: 'Rate limiting',
|
||||
body: 'Default 100 requests/min/IP per tool, enforced at the Traefik layer before traffic ever reaches your container.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Security() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<header className="mb-12">
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
|
||||
Security posture
|
||||
</div>
|
||||
<h1 className="mt-2 text-[32px] font-semibold tracking-tight">
|
||||
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.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
{PILLARS.map((p) => (
|
||||
<section key={p.title} className="panel p-5">
|
||||
<h2 className="text-[14px] font-semibold tracking-tight">{p.title}</h2>
|
||||
<p className="mt-2 text-[13px] leading-relaxed text-[--color-fg-muted]">{p.body}</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="mt-12">
|
||||
<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">
|
||||
security@buildmymcpserver.com
|
||||
</a>{' '}
|
||||
with a clear reproduction. We respond within 48h. We do not run a paid bounty yet, but we
|
||||
will credit you publicly in the changelog (or anonymously if you prefer).
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<CodeBlock
|
||||
label="pgp"
|
||||
code={`Key fingerprint published at buildmymcpserver.com/.well-known/security.txt`}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-[18px] font-semibold tracking-tight">Compliance roadmap</h2>
|
||||
<p className="mt-2 text-[13.5px] leading-relaxed text-[--color-fg-muted]">
|
||||
SOC 2 Type I is targeted for Q4 2026. The GDPR posture is described in our{' '}
|
||||
<Link href="/privacy" className="text-[--color-accent] underline">
|
||||
privacy policy
|
||||
</Link>
|
||||
. If you need a DPA or other contracts ahead of SOC 2, reach out — Team and Enterprise
|
||||
customers get one on request.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
139
apps/web/app/(marketing)/status/page.tsx
Normal file
139
apps/web/app/(marketing)/status/page.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
interface Probe {
|
||||
name: string;
|
||||
url: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const PROBES: Probe[] = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
url: '/login',
|
||||
description: 'Web frontend (Next.js)',
|
||||
},
|
||||
{
|
||||
name: 'Control plane API',
|
||||
url: '/api/health',
|
||||
description: 'Fastify control plane via /api proxy',
|
||||
},
|
||||
];
|
||||
|
||||
type State = 'pending' | 'ok' | 'down';
|
||||
|
||||
interface Result {
|
||||
state: State;
|
||||
latencyMs: number | null;
|
||||
detail: string | null;
|
||||
}
|
||||
|
||||
export default function Status() {
|
||||
const [results, setResults] = useState<Record<string, Result>>(() =>
|
||||
Object.fromEntries(PROBES.map((p) => [p.name, { state: 'pending', latencyMs: null, detail: null }])),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function probe() {
|
||||
const next: Record<string, Result> = {};
|
||||
for (const p of PROBES) {
|
||||
const start = performance.now();
|
||||
try {
|
||||
const res = await fetch(p.url, { cache: 'no-store' });
|
||||
const latency = Math.round(performance.now() - start);
|
||||
next[p.name] = {
|
||||
state: res.ok ? 'ok' : 'down',
|
||||
latencyMs: latency,
|
||||
detail: res.ok ? `HTTP ${res.status}` : `HTTP ${res.status}`,
|
||||
};
|
||||
} catch (e) {
|
||||
next[p.name] = {
|
||||
state: 'down',
|
||||
latencyMs: null,
|
||||
detail: (e as Error).message,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!cancelled) setResults(next);
|
||||
}
|
||||
probe();
|
||||
const interval = setInterval(probe, 10_000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const anyDown = Object.values(results).some((r) => r.state === 'down');
|
||||
const allPending = Object.values(results).every((r) => r.state === 'pending');
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<header className="mb-10">
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
|
||||
System status
|
||||
</div>
|
||||
<h1 className="mt-2 text-[32px] font-semibold tracking-tight">
|
||||
{allPending ? 'Checking…' : anyDown ? 'Partial outage' : 'All systems operational'}
|
||||
</h1>
|
||||
<p className="mt-3 text-[14px] leading-relaxed text-[--color-fg-muted]">
|
||||
Live health probes against each service. Refreshes every 10 seconds.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="panel divide-y divide-[--color-border]">
|
||||
{PROBES.map((p) => {
|
||||
const r = results[p.name]!;
|
||||
return (
|
||||
<div key={p.name} className="flex items-center justify-between px-4 py-3">
|
||||
<div>
|
||||
<div className="text-[13.5px] font-medium">{p.name}</div>
|
||||
<div className="text-[12px] text-[--color-fg-subtle]">{p.description}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-[12px]">
|
||||
{r.latencyMs !== null && (
|
||||
<span className="mono text-[--color-fg-subtle]">{r.latencyMs}ms</span>
|
||||
)}
|
||||
<Dot state={r.state} />
|
||||
<span className={cn(stateClass(r.state), 'font-medium tabular-nums')}>
|
||||
{stateLabel(r.state)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="mt-8 text-[12px] text-[--color-fg-subtle]">
|
||||
Production status board: BetterStack-hosted at status.buildmymcpserver.com (set up
|
||||
post-launch).
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Dot({ state }: { state: State }) {
|
||||
const color =
|
||||
state === 'ok' ? 'bg-emerald-400' : state === 'down' ? 'bg-red-400' : 'bg-zinc-400';
|
||||
return (
|
||||
<span
|
||||
className={cn('size-2 rounded-full', color)}
|
||||
style={state === 'ok' ? { animation: 'pulse-dot 1.6s ease-in-out infinite' } : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function stateLabel(state: State): string {
|
||||
if (state === 'ok') return 'Operational';
|
||||
if (state === 'down') return 'Down';
|
||||
return 'Checking';
|
||||
}
|
||||
|
||||
function stateClass(state: State): string {
|
||||
if (state === 'ok') return 'text-emerald-300';
|
||||
if (state === 'down') return 'text-red-300';
|
||||
return 'text-[--color-fg-subtle]';
|
||||
}
|
||||
77
apps/web/app/(marketing)/terms/page.tsx
Normal file
77
apps/web/app/(marketing)/terms/page.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
export const metadata = { title: 'Terms — BuildMyMCPServer' };
|
||||
|
||||
const SECTIONS = [
|
||||
{
|
||||
h: '1. The service',
|
||||
p: 'BuildMyMCPServer ("we", "us") lets you turn natural-language prompts into hosted Model Context Protocol (MCP) servers. You ("you") are the customer.',
|
||||
},
|
||||
{
|
||||
h: '2. Acceptable use',
|
||||
p: 'You will not use the service to build servers that violate applicable law, infringe third-party rights, or facilitate abuse. You retain responsibility for what your servers do with the credentials you provide to them.',
|
||||
},
|
||||
{
|
||||
h: '3. Generated code',
|
||||
p: 'Code we generate on your behalf is yours. You may export, modify, or self-host it. You also accept that LLM-generated code may contain bugs, and you are responsible for reviewing it before relying on it for critical workloads.',
|
||||
},
|
||||
{
|
||||
h: '4. Your credentials',
|
||||
p: 'Any API keys or secrets you upload are encrypted at rest and used only to run your servers. You warrant you have the right to use the credentials you upload.',
|
||||
},
|
||||
{
|
||||
h: '5. Service availability',
|
||||
p: 'Free and Pro plans are best-effort. Team plan carries a 99.9% monthly uptime SLA with service credits as the sole remedy. Enterprise SLAs are negotiated separately.',
|
||||
},
|
||||
{
|
||||
h: '6. Billing',
|
||||
p: 'Paid plans bill monthly via Stripe in advance. Metered overage bills the month following accrual. You can cancel any time; refunds are pro-rated only for service outages we acknowledge.',
|
||||
},
|
||||
{
|
||||
h: '7. Termination',
|
||||
p: 'Either party may terminate for material breach with 30 days written notice. We may suspend without notice for security incidents, payment failures over 14 days, or violations of section 2.',
|
||||
},
|
||||
{
|
||||
h: '8. Liability',
|
||||
p: 'To the maximum extent permitted by law, our aggregate liability is capped at the fees you paid us in the 12 months preceding the event. Neither party is liable for indirect or consequential damages.',
|
||||
},
|
||||
{
|
||||
h: '9. Changes',
|
||||
p: 'We may update these terms; we will notify you 30 days before a material change takes effect. Your continued use after that date constitutes acceptance.',
|
||||
},
|
||||
{
|
||||
h: '10. Governing law',
|
||||
p: 'These terms are governed by the laws of Germany. Exclusive jurisdiction is Berlin.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Terms() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-6 py-16">
|
||||
<header className="mb-12">
|
||||
<div className="text-[11px] uppercase tracking-[0.16em] text-[--color-fg-subtle]">
|
||||
Terms of service
|
||||
</div>
|
||||
<h1 className="mt-2 text-[32px] font-semibold tracking-tight">Terms</h1>
|
||||
<p className="mt-3 text-[14px] leading-relaxed text-[--color-fg-muted]">
|
||||
Boring on purpose. Last updated 2026-05-19.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="space-y-7">
|
||||
{SECTIONS.map((s) => (
|
||||
<section key={s.h}>
|
||||
<h2 className="text-[15px] font-semibold tracking-tight">{s.h}</h2>
|
||||
<p className="mt-2 text-[13.5px] leading-relaxed text-[--color-fg-muted]">{s.p}</p>
|
||||
</section>
|
||||
))}
|
||||
<section>
|
||||
<h2 className="text-[15px] font-semibold tracking-tight">Questions</h2>
|
||||
<p className="mt-2 text-[13.5px] leading-relaxed text-[--color-fg-muted]">
|
||||
<a className="text-[--color-accent] underline" href="mailto:legal@buildmymcpserver.com">
|
||||
legal@buildmymcpserver.com
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user