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:
Marco Sadjadi
2026-05-19 18:20:31 +02:00
parent 1c92964bbd
commit 09688c1114
20 changed files with 2055 additions and 75 deletions

View File

@@ -0,0 +1,90 @@
import {
DocsTitle,
DocsLead,
DocsH2,
DocsP,
DocsCode,
Mono,
} from '@/components/docs-page';
export const metadata = { title: 'API reference — BuildMyMCPServer docs' };
export default function ApiReference() {
return (
<>
<DocsTitle kicker="Reference">API reference</DocsTitle>
<DocsLead>
Every endpoint on the control plane. Authenticated routes use the session cookie set by
the magic-link verify call.
</DocsLead>
<DocsH2 id="auth">Auth</DocsH2>
<DocsP>
<Mono>POST /v1/auth/magic-link</Mono> body <Mono>{`{"email":"…"}`}</Mono> emails (or
prints in dev) a one-time link.
</DocsP>
<DocsP>
<Mono>POST /v1/auth/verify</Mono> body <Mono>{`{"token":"…"}`}</Mono> exchanges the
token for a session cookie.
</DocsP>
<DocsP><Mono>GET /v1/auth/me</Mono> returns the current session user + org.</DocsP>
<DocsP><Mono>POST /v1/auth/logout</Mono> destroys the session.</DocsP>
<DocsH2 id="servers">Servers</DocsH2>
<DocsP><Mono>GET /v1/servers</Mono> list servers in the current org.</DocsP>
<DocsP>
<Mono>POST /v1/servers/preview</Mono> body <Mono>{`{"prompt":"…"}`}</Mono> runs Claude
synchronously, validates the spec, caches it, returns <Mono>{`{ previewId, source, spec }`}</Mono>.
</DocsP>
<DocsP>
<Mono>POST /v1/servers</Mono> body <Mono>{`{name, slug, prompt, secrets, previewId?}`}</Mono>
creates the server, queues the build, returns the server + build records.
</DocsP>
<DocsP>
<Mono>GET /v1/servers/:id</Mono> server detail with the latest 10 build records.
</DocsP>
<DocsP>
<Mono>POST /v1/servers/:id/iterate</Mono> body <Mono>{`{prompt, secrets}`}</Mono>
queues a new version build.
</DocsP>
<DocsP><Mono>DELETE /v1/servers/:id</Mono> removes the server and tears down the container.</DocsP>
<DocsH2 id="builds">Builds</DocsH2>
<DocsP>
<Mono>GET /v1/builds/:id</Mono> build record + persisted logs.
</DocsP>
<DocsP>
<Mono>WS /v1/builds/:id/stream</Mono> live event stream of build events:
<Mono>status</Mono>, <Mono>log</Mono>, <Mono>done</Mono>, <Mono>error</Mono>.
</DocsP>
<DocsH2 id="oauth">OAuth (clients of generated servers, not dashboard)</DocsH2>
<DocsP>
<Mono>GET /oauth/.well-known/oauth-authorization-server</Mono> RFC 8414 metadata.
</DocsP>
<DocsP><Mono>GET /oauth/jwks</Mono> RS256 public key for verifying access tokens.</DocsP>
<DocsP><Mono>POST /oauth/register</Mono> RFC 7591 dynamic client registration.</DocsP>
<DocsP><Mono>GET /oauth/authorize</Mono> authorization code endpoint, requires session.</DocsP>
<DocsP><Mono>POST /oauth/token</Mono> code exchange + refresh.</DocsP>
<DocsH2 id="examples">Curl example</DocsH2>
<DocsCode
label="full lifecycle"
code={`# 1. magic link
curl -X POST http://localhost:4000/v1/auth/magic-link -d '{"email":"me@x.dev"}'
# (grab token from API console)
# 2. verify -> session
curl -c cookies.txt -X POST http://localhost:4000/v1/auth/verify -d '{"token":"…"}'
# 3. preview
curl -b cookies.txt -X POST http://localhost:4000/v1/servers/preview \\
-d '{"prompt":"echo server with one tool: echo(message)"}'
# 4. build
curl -b cookies.txt -X POST http://localhost:4000/v1/servers \\
-d '{"name":"Echo","slug":"echo","prompt":"…","secrets":{},"previewId":"…"}'`}
/>
</>
);
}

View File

@@ -0,0 +1,94 @@
import {
DocsTitle,
DocsLead,
DocsH2,
DocsP,
DocsList,
DocsLi,
DocsCode,
Mono,
} from '@/components/docs-page';
export const metadata = { title: 'Authoring tools — BuildMyMCPServer docs' };
export default function Authoring() {
return (
<>
<DocsTitle kicker="Build">Authoring tools</DocsTitle>
<DocsLead>
What you write in the prompt is what Claude turns into TypeScript. Better prompts mean
better tools. These patterns cover 80% of the common asks.
</DocsLead>
<DocsH2 id="anatomy">Anatomy of a tool</DocsH2>
<DocsP>Each generated tool ends up looking like this:</DocsP>
<DocsCode
label="generated TypeScript"
code={`server.registerTool(
'search_pages',
{
title: 'search_pages',
description: 'Search Notion pages matching a query.',
inputSchema: {
query: z.string().describe('search terms'),
},
},
async (args) => {
try {
const res = await fetch('https://api.notion.com/v1/search', {
method: 'POST',
signal: AbortSignal.timeout(10000),
headers: {
'Authorization': \`Bearer \${process.env.NOTION_API_KEY}\`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: args.query }),
});
const data = await res.json();
return { content: [{ type: 'text', text: JSON.stringify(data.results) }] };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { content: [{ type: 'text', text: 'Error: ' + msg }], isError: true };
}
},
);`}
/>
<DocsH2 id="rules">Rules the generator enforces</DocsH2>
<DocsList>
<DocsLi>No <Mono>eval</Mono>, no <Mono>new Function</Mono>, no <Mono>child_process</Mono>. The static check rejects the build.</DocsLi>
<DocsLi>No <Mono>import</Mono> statements in tool bodies the runtime injects <Mono>fetch</Mono>, <Mono>pg</Mono>, <Mono>z</Mono>.</DocsLi>
<DocsLi>Secrets live in <Mono>process.env</Mono>. Never embedded literally.</DocsLi>
<DocsLi>External HTTP calls must use <Mono>AbortSignal.timeout</Mono>. Default 10s.</DocsLi>
<DocsLi>Database access via <Mono>pg</Mono> with parameterized queries only.</DocsLi>
<DocsLi>Errors return as MCP error-content, not thrown exceptions.</DocsLi>
</DocsList>
<DocsH2 id="patterns">Prompt patterns that work</DocsH2>
<DocsP>
<strong>Be explicit about tool names.</strong> "Tool: <Mono>search_pages(query)</Mono>"
beats "give me a search tool".
</DocsP>
<DocsP>
<strong>Name the credentials.</strong> "Auth: <Mono>NOTION_API_KEY</Mono>" tells the
generator what to put in <Mono>requiredSecrets</Mono>. Saves an iteration.
</DocsP>
<DocsP>
<strong>Say if a tool is destructive.</strong> "Tool: <Mono>delete_page(page_id)</Mono> —
destructive, permanently removes the page" surfaces the warning to the AI client.
</DocsP>
<DocsP>
<strong>One server per integration, not per tool.</strong> A Notion server with five
tools is cleaner than five Notion servers each with one tool.
</DocsP>
<DocsH2 id="iteration">Iterate on a live server</DocsH2>
<DocsP>
Open the server detail page, click the <Mono>Iterate</Mono> tab, describe what you want
to add. A new build version is queued, rolling-deployed, the old version stays live until
the new one is healthy.
</DocsP>
</>
);
}

View File

@@ -0,0 +1,86 @@
import {
DocsTitle,
DocsLead,
DocsH2,
DocsP,
DocsList,
DocsLi,
DocsCode,
Mono,
} from '@/components/docs-page';
export const metadata = { title: 'MCP concepts — BuildMyMCPServer docs' };
export default function Concepts() {
return (
<>
<DocsTitle kicker="Get started">MCP concepts</DocsTitle>
<DocsLead>
Model Context Protocol is an open standard from Anthropic for connecting AI assistants to
external tools, data, and APIs. Three primitives, one transport.
</DocsLead>
<DocsH2 id="primitives">The three primitives</DocsH2>
<DocsList>
<DocsLi>
<strong className="text-[--color-fg]">Tools</strong> functions the AI can invoke.
Each has a name, a description, an input schema (JSON Schema or Zod), and a server-side
implementation. The AI decides when to call them based on the description.
</DocsLi>
<DocsLi>
<strong className="text-[--color-fg]">Resources</strong> read-only data the AI can
fetch. URI-addressed. Think files, documents, database records.
</DocsLi>
<DocsLi>
<strong className="text-[--color-fg]">Prompts</strong> parameterized prompt templates
the server exposes to the client. Used for orchestration patterns the server author
wants to encourage.
</DocsLi>
</DocsList>
<DocsH2 id="transport">Transport: Streamable HTTP</DocsH2>
<DocsP>
Every generated server speaks <Mono>Streamable HTTP</Mono> (MCP spec 2025-11-25). The
previous SSE transport was deprecated in June 2025 and is not supported here. One HTTP
endpoint at <Mono>/mcp</Mono>, optionally negotiating a long-lived stream via
<Mono>text/event-stream</Mono> when the server wants to push updates.
</DocsP>
<DocsCode
label="single request"
code={`POST /mcp HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json
Accept: application/json, text/event-stream
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}`}
/>
<DocsH2 id="session">Session lifecycle</DocsH2>
<DocsList>
<DocsLi>
<Mono>initialize</Mono> client sends protocol version + capabilities, server returns
its info and assigns a session id via the <Mono>mcp-session-id</Mono> header.
</DocsLi>
<DocsLi>
<Mono>notifications/initialized</Mono> client confirms readiness. Server is now free
to push notifications.
</DocsLi>
<DocsLi>
<Mono>tools/list</Mono>, <Mono>tools/call</Mono>, <Mono>resources/list</Mono>,
<Mono>prompts/list</Mono> the work.
</DocsLi>
</DocsList>
<DocsH2 id="why-mcp">Why MCP and not just REST</DocsH2>
<DocsP>
REST APIs need bespoke OpenAPI integration per client. MCP standardizes the discovery,
invocation, auth and streaming so any spec-compliant client picks up any spec-compliant
server with zero glue code. That&apos;s the entire point.
</DocsP>
</>
);
}

View File

@@ -0,0 +1,71 @@
import { DocsTitle, DocsLead, DocsH2, DocsP, Mono } from '@/components/docs-page';
export const metadata = { title: 'FAQ — BuildMyMCPServer docs' };
const ITEMS: { q: string; a: React.ReactNode }[] = [
{
q: 'How does the LLM-generated code stay safe?',
a: 'Three layers: strict Zod validation of the JSON spec, regex scan for banned tokens (eval, child_process, prompt-injection markers), and a static check on the rendered TypeScript before Docker build. If any layer trips, the build fails with a clear error and nothing is deployed.',
},
{
q: 'What happens if Claude hallucinates a broken tool?',
a: 'The build fails at the static-check or Docker-build stage. The user sees the exact error in the live log and can refine the prompt and rebuild. No invalid server ever serves traffic.',
},
{
q: 'Do my secrets ever leave my environment?',
a: 'No. Secrets are AES-256-GCM encrypted at rest in your Postgres, decrypted only when injecting into your container at boot. They never appear in audit logs, build logs, or the prompt sent to Claude.',
},
{
q: 'Why MCP and not OpenAPI?',
a: 'MCP standardizes the discovery, invocation, auth, and streaming surface in a way OpenAPI never did. The point is that any spec-compliant client picks up any spec-compliant server with zero per-API integration work. OpenAPI requires custom glue for every client.',
},
{
q: 'Can I use my own Claude API key?',
a: 'Yes — set ANTHROPIC_API_KEY in .env. On self-hosted control planes you can also wire a separate per-org key (Sprint 4).',
},
{
q: 'What if I don\'t set ANTHROPIC_API_KEY?',
a: <>The generator falls back to a deterministic mock spec (two tools: <Mono>echo</Mono>, <Mono>now</Mono>) so you can verify the full pipeline without burning credits.</>,
},
{
q: 'Cold-start latency?',
a: 'Generated containers stay warm. After first boot, /mcp responds in sub-50ms in-region.',
},
{
q: 'Rate limits?',
a: 'Default 100 requests/min/IP per tool. Configurable per server. Quota enforced before hitting your container.',
},
{
q: 'How is OAuth different from API keys?',
a: 'OAuth 2.1 with PKCE + Dynamic Client Registration + Resource Indicators means the AI client gets a short-lived, audience-bound token. Compromised tokens expire and can\'t be replayed against other servers. API keys are static and replayable forever.',
},
{
q: 'Can the AI client itself get phished into using a malicious server?',
a: 'The MCP spec mandates user consent on initial server addition. Beyond that, each server\'s scope is opaque to other servers — there\'s no cross-server token leakage because of audience binding.',
},
{
q: 'How do I export my server\'s code?',
a: 'Every build record stores the rendered TypeScript in Postgres. The /servers/:id detail page exposes it for download (Sprint 4 UI; available now via API).',
},
{
q: 'What about ChatGPT specifically?',
a: 'ChatGPT supports MCP via Custom Connectors. The wizard\'s install tab gives you the URL + OAuth setting; the handshake runs automatically on first call.',
},
];
export default function Faq() {
return (
<>
<DocsTitle kicker="Reference">FAQ</DocsTitle>
<DocsLead>Common questions, direct answers.</DocsLead>
<div className="space-y-7">
{ITEMS.map((item) => (
<div key={item.q}>
<DocsH2>{item.q}</DocsH2>
<DocsP>{item.a}</DocsP>
</div>
))}
</div>
</>
);
}

View File

@@ -0,0 +1,85 @@
import Link from 'next/link';
import { Logo } from '@/components/logo';
const SECTIONS: { heading: string; items: { href: string; label: string }[] }[] = [
{
heading: 'Get started',
items: [
{ href: '/docs', label: 'Quickstart' },
{ href: '/docs/concepts', label: 'MCP concepts' },
],
},
{
heading: 'Auth',
items: [{ href: '/docs/oauth', label: 'OAuth 2.1 flow' }],
},
{
heading: 'Build',
items: [
{ href: '/docs/authoring', label: 'Authoring tools' },
{ href: '/docs/self-hosting', label: 'Self-hosting' },
],
},
{
heading: 'Reference',
items: [
{ href: '/docs/api-reference', label: 'API reference' },
{ href: '/docs/faq', label: 'FAQ' },
],
},
];
export default function DocsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-screen flex-col">
<header className="sticky top-0 z-50 border-b border-[--color-border] bg-[--color-bg]/85 backdrop-blur-md">
<div className="mx-auto flex h-12 max-w-6xl items-center justify-between px-6">
<div className="flex items-center gap-3">
<Logo />
<span className="text-[12.5px] text-[--color-fg-subtle]">/ docs</span>
</div>
<nav className="flex items-center gap-2">
<Link
href="/"
className="text-[12.5px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
>
Home
</Link>
<Link
href="/login"
className="rounded-md bg-[--color-accent] px-3 py-1.5 text-[12.5px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]"
>
Start building
</Link>
</nav>
</div>
</header>
<div className="mx-auto flex w-full max-w-6xl flex-1 gap-12 px-6 py-10">
<aside className="w-[240px] shrink-0">
<nav className="sticky top-20 space-y-5">
{SECTIONS.map((section) => (
<div key={section.heading}>
<div className="text-[10.5px] uppercase tracking-[0.14em] text-[--color-fg-subtle]">
{section.heading}
</div>
<ul className="mt-2 space-y-0.5">
{section.items.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className="block rounded-sm px-1 py-1 text-[12.5px] text-[--color-fg-muted] transition-colors hover:text-[--color-fg]"
>
{item.label}
</Link>
</li>
))}
</ul>
</div>
))}
</nav>
</aside>
<article className="prose prose-invert max-w-2xl flex-1">{children}</article>
</div>
</div>
);
}

View File

@@ -0,0 +1,125 @@
import {
DocsTitle,
DocsLead,
DocsH2,
DocsP,
DocsList,
DocsLi,
DocsCode,
Mono,
} from '@/components/docs-page';
export const metadata = { title: 'OAuth 2.1 flow — BuildMyMCPServer docs' };
export default function OAuthDocs() {
return (
<>
<DocsTitle kicker="Auth">OAuth 2.1 flow</DocsTitle>
<DocsLead>
Every generated server is an OAuth 2.1 Resource Server. The control plane is the
Authorization Server. Dynamic Client Registration, PKCE, and Resource Indicators per the
2025 MCP authorization spec.
</DocsLead>
<DocsH2 id="rfcs">Standards we follow</DocsH2>
<DocsList>
<DocsLi>OAuth 2.1 draft (<Mono>draft-ietf-oauth-v2-1</Mono>) no implicit, mandatory PKCE</DocsLi>
<DocsLi>RFC 8414 Authorization Server Metadata at <Mono>/.well-known/oauth-authorization-server</Mono></DocsLi>
<DocsLi>RFC 9728 Protected Resource Metadata at <Mono>/.well-known/oauth-protected-resource</Mono></DocsLi>
<DocsLi>RFC 8707 Resource Indicators (audience binding)</DocsLi>
<DocsLi>RFC 7591 Dynamic Client Registration</DocsLi>
</DocsList>
<DocsH2 id="walkthrough">End-to-end walkthrough</DocsH2>
<DocsP>
First request from a fresh client to a fresh server is unauthenticated. The server
replies with a <Mono>401</Mono> plus a <Mono>WWW-Authenticate</Mono> header pointing to
its resource metadata.
</DocsP>
<DocsCode
label="step 1 — 401 challenge"
code={`$ curl -i http://localhost:4103/mcp -d '{}' -H 'content-type: application/json'
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer resource_metadata="http://localhost:4103/.well-known/oauth-protected-resource"
content-type: application/json
{"error":"unauthorized"}`}
/>
<DocsP>
The client fetches that resource metadata, sees the authorization server, then fetches the
AS metadata to discover registration, authorize, token and JWKS endpoints.
</DocsP>
<DocsCode
label="step 2 — resource metadata"
code={`$ curl http://localhost:4103/.well-known/oauth-protected-resource
{
"resource": "http://localhost:4103",
"authorization_servers": ["http://localhost:4000/oauth"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:read"]
}`}
/>
<DocsP>
The client registers itself dynamically. No human in the loop, no preconfigured client
IDs. Each AI surface gets its own ephemeral identity.
</DocsP>
<DocsCode
label="step 3 — dynamic registration"
code={`POST /oauth/register HTTP/1.1
{
"client_name": "Claude Desktop",
"redirect_uris": ["claude://oauth/callback"],
"token_endpoint_auth_method": "none",
"resource": "http://localhost:4103"
}
201 Created
{ "client_id": "bmm_8aee2fe0…", "redirect_uris": […] }`}
/>
<DocsP>
Authorization Code with PKCE. The user gives consent, the AS returns a one-time code,
the client exchanges it for an RS256-signed JWT bound to the resource (audience).
</DocsP>
<DocsCode
label="step 4 — token exchange"
code={`POST /oauth/token HTTP/1.1
{
"grant_type": "authorization_code",
"code": "4uNk_SCU8…",
"code_verifier": "riSU-w1DT…",
"client_id": "bmm_8aee2fe0…",
"redirect_uri": "claude://oauth/callback",
"resource": "http://localhost:4103"
}
200 OK
{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "pQR…"
}`}
/>
<DocsP>
Subsequent <Mono>/mcp</Mono> calls carry the JWT. The runner verifies the signature
against the AS&apos;s JWKS, checks the <Mono>iss</Mono>, the <Mono>aud</Mono>
(RFC 8707 must match the runner&apos;s own public URL), and the expiry. No token
passthrough; the runner never forwards the client&apos;s token to a downstream API.
</DocsP>
<DocsH2 id="security">Why this matters</DocsH2>
<DocsP>
Without audience binding, a token issued for one customer&apos;s MCP server could be
replayed against another customer&apos;s server. RFC 8707 closes that. Without PKCE, a
public OAuth client on a desktop is exposed to interception of the authorization code.
OAuth 2.1 closes that.
</DocsP>
</>
);
}

View File

@@ -0,0 +1,89 @@
import {
DocsTitle,
DocsLead,
DocsH2,
DocsH3,
DocsP,
DocsList,
DocsLi,
DocsCode,
Mono,
} from '@/components/docs-page';
export const metadata = { title: 'Quickstart — BuildMyMCPServer docs' };
export default function Quickstart() {
return (
<>
<DocsTitle kicker="Get started">Quickstart</DocsTitle>
<DocsLead>
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.
</DocsLead>
<DocsH2 id="prereqs">Prerequisites</DocsH2>
<DocsList>
<DocsLi>An AI client that speaks MCP Claude Desktop, Cursor, ChatGPT Custom Connectors, VS Code Copilot, or Continue.dev.</DocsLi>
<DocsLi>API credentials for whatever you want your server to access (Notion, your DB, etc.). Or pick the echo example to skip this.</DocsLi>
</DocsList>
<DocsH2 id="step-1">1. Sign in</DocsH2>
<DocsP>Hit the dashboard and enter your email. We send a magic link no password.</DocsP>
<DocsCode label="dev mode" code={`The link is printed to the API console output.\nCheck the terminal where you ran \`pnpm dev\`.`} />
<DocsH2 id="step-2">2. Describe your tool</DocsH2>
<DocsP>
Click <Mono>+ New server</Mono> and write what you want in plain language. The clearer
you are about which APIs and which tool names, the better the spec.
</DocsP>
<DocsCode
label="prompt.txt"
code={`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.`}
/>
<DocsH2 id="step-3">3. Confirm the plan</DocsH2>
<DocsP>
Step 2 of the wizard shows you exactly which tools Claude parsed from your prompt, the
input schemas, and which credentials we need from you. Fill them in. Skip the step
entirely for self-contained demo servers like the <Mono>echo</Mono> template.
</DocsP>
<DocsH2 id="step-4">4. Watch the build stream</DocsH2>
<DocsP>The build goes through five states live over WebSocket:</DocsP>
<DocsList>
<DocsLi><Mono>queued</Mono> spec validated, job in BullMQ</DocsLi>
<DocsLi><Mono>generating</Mono> Claude returns spec (or cached preview is reused)</DocsLi>
<DocsLi><Mono>building</Mono> TypeScript rendered, static checks, Docker image built</DocsLi>
<DocsLi><Mono>deploying</Mono> container booted on an allocated host port</DocsLi>
<DocsLi><Mono>live</Mono> endpoint responds, OAuth gate is active</DocsLi>
</DocsList>
<DocsH2 id="step-5">5. Install in your client</DocsH2>
<DocsP>
The Done screen shows three tabs Claude Desktop, Cursor, ChatGPT each with a copy-ready
snippet. Paste the JSON into your client&apos;s MCP config and restart. The OAuth handshake
runs automatically on first tool call.
</DocsP>
<DocsCode
label="claude_desktop_config.json"
code={`{
"mcpServers": {
"notion-reader": {
"url": "http://localhost:4103/mcp",
"auth": "oauth2"
}
}
}`}
/>
<DocsH3>What&apos;s next</DocsH3>
<DocsP>
Read about the <a href="/docs/concepts" className="text-[--color-accent] underline">underlying MCP concepts</a>,
learn how the <a href="/docs/oauth" className="text-[--color-accent] underline">OAuth 2.1 flow</a> protects each server,
or jump to <a href="/docs/authoring" className="text-[--color-accent] underline">authoring custom tools</a>.
</DocsP>
</>
);
}

View File

@@ -0,0 +1,82 @@
import {
DocsTitle,
DocsLead,
DocsH2,
DocsP,
DocsList,
DocsLi,
DocsCode,
Mono,
} from '@/components/docs-page';
export const metadata = { title: 'Self-hosting — BuildMyMCPServer docs' };
export default function SelfHosting() {
return (
<>
<DocsTitle kicker="Build">Self-hosting</DocsTitle>
<DocsLead>
The control plane and generator are open. Bring your own Postgres, Redis, Docker host and
Anthropic API key. Production uses Hetzner + Coolify + Traefik; the seams are the same.
</DocsLead>
<DocsH2 id="requirements">Requirements</DocsH2>
<DocsList>
<DocsLi>Node.js 20+</DocsLi>
<DocsLi>pnpm 9+</DocsLi>
<DocsLi>Docker engine reachable from the generator process</DocsLi>
<DocsLi>Postgres 16+ and Redis 7+ (docker-compose for dev)</DocsLi>
<DocsLi>Anthropic API key (optional mock fallback for offline dev)</DocsLi>
</DocsList>
<DocsH2 id="dev">Local dev</DocsH2>
<DocsCode
label="bash"
code={`git clone <repo>
cd buildmymcpserver
pnpm install
cp .env.example .env
pnpm dev`}
/>
<DocsP>
<Mono>pnpm dev</Mono> loads <Mono>.env</Mono>, brings up Postgres and Redis via
docker-compose, pushes the Drizzle schema, and starts web (<Mono>:3001</Mono>), api
(<Mono>:4000</Mono>) and generator concurrently.
</DocsP>
<DocsH2 id="env">Environment variables</DocsH2>
<DocsList>
<DocsLi><Mono>DATABASE_URL</Mono> Postgres connection string</DocsLi>
<DocsLi><Mono>REDIS_URL</Mono> Redis (BullMQ + pubsub + preview cache)</DocsLi>
<DocsLi><Mono>ANTHROPIC_API_KEY</Mono> unset = mock generator</DocsLi>
<DocsLi><Mono>SECRETS_ENCRYPTION_KEY</Mono> 32-byte hex, AES-256-GCM key</DocsLi>
<DocsLi><Mono>CONTROL_PLANE_PUBLIC_URL</Mono> issuer for OAuth tokens</DocsLi>
<DocsLi><Mono>OAUTH_KEY_DIR</Mono> where RS256 keypair lives (auto-generated on boot)</DocsLi>
<DocsLi><Mono>RUNNER_PORT_RANGE_START/END</Mono> host port window for generated containers</DocsLi>
</DocsList>
<DocsH2 id="prod">Production deployment</DocsH2>
<DocsP>
The intended production setup is a Hetzner AX52 running Coolify, Traefik for wildcard SSL
on <Mono>*.mcp.yourdomain.com</Mono>, and Cloudflare for DNS+DDoS. The runner-deploy
adapter is the only environment-specific seam swap the Docker-CLI implementation in
<Mono>apps/generator/src/lib/deploy.ts</Mono> for the Coolify HTTP API.
</DocsP>
<DocsH2 id="sandboxing">Container sandboxing</DocsH2>
<DocsP>
Production flags (commented in deploy.ts):
</DocsP>
<DocsList>
<DocsLi><Mono>--read-only</Mono></DocsLi>
<DocsLi><Mono>--cap-drop=ALL</Mono></DocsLi>
<DocsLi><Mono>--security-opt=no-new-privileges</Mono></DocsLi>
<DocsLi><Mono>--cpus=0.5 --memory=512m</Mono></DocsLi>
</DocsList>
<DocsP>
Dev relaxes these for Docker Desktop on Windows compat. Don&apos;t ship dev defaults to
prod.
</DocsP>
</>
);
}