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:
90
apps/web/app/docs/api-reference/page.tsx
Normal file
90
apps/web/app/docs/api-reference/page.tsx
Normal 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":"…"}'`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
94
apps/web/app/docs/authoring/page.tsx
Normal file
94
apps/web/app/docs/authoring/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
86
apps/web/app/docs/concepts/page.tsx
Normal file
86
apps/web/app/docs/concepts/page.tsx
Normal 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's the entire point.
|
||||
</DocsP>
|
||||
</>
|
||||
);
|
||||
}
|
||||
71
apps/web/app/docs/faq/page.tsx
Normal file
71
apps/web/app/docs/faq/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
85
apps/web/app/docs/layout.tsx
Normal file
85
apps/web/app/docs/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
125
apps/web/app/docs/oauth/page.tsx
Normal file
125
apps/web/app/docs/oauth/page.tsx
Normal 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's JWKS, checks the <Mono>iss</Mono>, the <Mono>aud</Mono>
|
||||
(RFC 8707 — must match the runner's own public URL), and the expiry. No token
|
||||
passthrough; the runner never forwards the client's token to a downstream API.
|
||||
</DocsP>
|
||||
|
||||
<DocsH2 id="security">Why this matters</DocsH2>
|
||||
<DocsP>
|
||||
Without audience binding, a token issued for one customer's MCP server could be
|
||||
replayed against another customer'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
89
apps/web/app/docs/page.tsx
Normal file
89
apps/web/app/docs/page.tsx
Normal 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'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'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
82
apps/web/app/docs/self-hosting/page.tsx
Normal file
82
apps/web/app/docs/self-hosting/page.tsx
Normal 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't ship dev defaults to
|
||||
prod.
|
||||
</DocsP>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user