chore: bootstrap monorepo (turbo, biome, docker-compose, env, CHOICES)
This commit is contained in:
commit
fe4800e2c8
46
.env.example
Normal file
46
.env.example
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# ---- Core ----
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# ---- Database ----
|
||||||
|
DATABASE_URL=postgresql://bmm:bmm@localhost:5432/bmm
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
|
||||||
|
# ---- Auth (Better-Auth) ----
|
||||||
|
BETTER_AUTH_SECRET=replace-me-with-32-bytes-of-random-hex-1234567890abcdef
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||||
|
|
||||||
|
# ---- GitHub OAuth (optional in dev) ----
|
||||||
|
GITHUB_OAUTH_ID=
|
||||||
|
GITHUB_OAUTH_SECRET=
|
||||||
|
|
||||||
|
# ---- Anthropic ----
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
|
||||||
|
# ---- Crypto ----
|
||||||
|
# 32-byte hex for AES-256-GCM; generate with: openssl rand -hex 32
|
||||||
|
SECRETS_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
|
||||||
|
# ---- OAuth signing (RS256 JWKS) ----
|
||||||
|
# Path to PEM keypair; auto-generated on api boot if missing
|
||||||
|
OAUTH_KEY_DIR=./keys
|
||||||
|
|
||||||
|
# ---- Runner / Generator ----
|
||||||
|
# Where MCP runtime containers bind (host machine reachable from API)
|
||||||
|
RUNNER_HOST=localhost
|
||||||
|
# Range of host ports used for generated MCP containers
|
||||||
|
RUNNER_PORT_RANGE_START=4100
|
||||||
|
RUNNER_PORT_RANGE_END=4999
|
||||||
|
# Public URL template — $SLUG and $PORT are interpolated
|
||||||
|
RUNNER_PUBLIC_URL_TEMPLATE=http://localhost:$PORT
|
||||||
|
# Control plane URL reachable from runner containers
|
||||||
|
CONTROL_PLANE_URL=http://host.docker.internal:4000
|
||||||
|
|
||||||
|
# ---- Stripe (Sprint 4) ----
|
||||||
|
STRIPE_SECRET_KEY=
|
||||||
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
|
||||||
|
# ---- Observability (optional) ----
|
||||||
|
SENTRY_DSN=
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||||
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
.turbo/
|
||||||
|
.next/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
!.env.example
|
||||||
|
drizzle/meta/_journal.json.bak
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
coverage/
|
||||||
|
build-context/
|
||||||
|
*.tsbuildinfo
|
||||||
|
generated/
|
||||||
|
keys/
|
||||||
740
BuildMyMCPServer_MASTER_PROMPT.md
Normal file
740
BuildMyMCPServer_MASTER_PROMPT.md
Normal file
@ -0,0 +1,740 @@
|
|||||||
|
# 🎯 BUILD: BuildMyMCPServer — Production-Ready SaaS
|
||||||
|
|
||||||
|
> **Master Prompt für Claude Code**
|
||||||
|
> Mission: Baue eine komplette, deploy-ready SaaS-Plattform die aus einem natürlichsprachigen Prompt einen vollständig gehosteten, OAuth-2.1-geschützten MCP-Server (Model Context Protocol) erzeugt, deployed und verwaltet. Endkunden konfigurieren ihn in Claude Desktop, Cursor oder ChatGPT mit einem Klick.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. PROJECT IDENTITY & POSITIONING
|
||||||
|
|
||||||
|
**Produktname:** BuildMyMCPServer
|
||||||
|
**Domain:** buildmymcpserver.com (bzw. die spätere gewählte Variante)
|
||||||
|
**Tagline:** *"Describe your tool. We host the server. AI uses it."*
|
||||||
|
**Untertitel:** *"From prompt to production MCP server in 60 seconds. OAuth 2.1, Streamable HTTP, ready for Claude, Cursor & ChatGPT."*
|
||||||
|
|
||||||
|
**Zielgruppe primär:** B2B Mid-Market — Operations Leads, Solo-Founder, Tech-Teams in KMUs, Agenturen die für Kunden MCP-Server bauen, interne Tooling-Teams in Enterprise.
|
||||||
|
|
||||||
|
**Pricing-Anker (im UI sichtbar):**
|
||||||
|
- **Hobby:** €0 — 1 Server, 100k Tool-Calls/Mo, geteilte Infra, BMM-Subdomain
|
||||||
|
- **Pro:** €49/Mo — 5 Server, 1M Calls, Custom Domain, Priority Build
|
||||||
|
- **Team:** €149/Mo — 25 Server, 10M Calls, RBAC, Audit-Logs, SLA 99.9%
|
||||||
|
- **Enterprise:** ab €499/Mo — Unlimited, BYOC, SSO/SAML, dedicated Cluster
|
||||||
|
|
||||||
|
**Brand-Voice:** Technisch, präzise, ohne Marketing-BS. "It's infrastructure, not magic." Dunkel & monochrom mit einem präzisen Akzent. Kein lila Gradient, kein "AI sparkle". Inspiration: Linear, Vercel, Resend, Railway. **Native-feel, kein generisches AI-Wrapper-UI.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. TECH-STACK (fest, nicht verhandelbar)
|
||||||
|
|
||||||
|
### Monorepo (Turborepo + pnpm)
|
||||||
|
|
||||||
|
```
|
||||||
|
buildmymcpserver/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Next.js 15 (App Router) — Marketing + Dashboard
|
||||||
|
│ ├── api/ # Fastify Backend (Control Plane)
|
||||||
|
│ ├── generator/ # Generation-Worker (BullMQ-Consumer)
|
||||||
|
│ └── runner-template/ # Template für gehostete MCP-Server-Container
|
||||||
|
├── packages/
|
||||||
|
│ ├── db/ # Drizzle ORM + PostgreSQL Schema
|
||||||
|
│ ├── mcp-templates/ # Code-Templates für generierte Server
|
||||||
|
│ ├── ui/ # Shared shadcn/ui components
|
||||||
|
│ ├── auth/ # Better-Auth wrapper
|
||||||
|
│ └── types/ # Shared TypeScript types (Zod)
|
||||||
|
├── infra/
|
||||||
|
│ ├── docker/ # Dockerfiles
|
||||||
|
│ ├── coolify/ # Coolify-Konfigurationen
|
||||||
|
│ └── traefik/ # Reverse-Proxy-Config
|
||||||
|
└── turbo.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stack-Entscheidungen (final)
|
||||||
|
|
||||||
|
**Frontend Web:**
|
||||||
|
- Next.js 15 (App Router, RSC, Server Actions)
|
||||||
|
- TypeScript (strict mode)
|
||||||
|
- Tailwind CSS v4
|
||||||
|
- shadcn/ui (registry-mode, nur was wir brauchen)
|
||||||
|
- Lucide-React (icons)
|
||||||
|
- Framer Motion (sparingly — nur für state-transitions)
|
||||||
|
- next-themes (dark default, light optional)
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- Fastify (statt Express — schneller, besseres TS)
|
||||||
|
- Drizzle ORM
|
||||||
|
- PostgreSQL 16
|
||||||
|
- Redis 7 (BullMQ-Queue + Sessions)
|
||||||
|
- Better-Auth (Auth-Layer — User-Auth NICHT MCP-OAuth)
|
||||||
|
- Stripe (Subscriptions + Metered-Billing)
|
||||||
|
|
||||||
|
**MCP-Generierung:**
|
||||||
|
- Anthropic Claude API (claude-opus-4-7 für Generation, claude-haiku-4-5 für Fixes)
|
||||||
|
- `@modelcontextprotocol/sdk` v1.x (TypeScript) als Basis-Library für generierte Server
|
||||||
|
- Zod für Tool-Input-Validation
|
||||||
|
- esbuild für Build-Pipeline
|
||||||
|
|
||||||
|
**Hosting / Runtime:**
|
||||||
|
- Coolify (auf Hetzner Dedicated AX52) — Container-Orchestrator
|
||||||
|
- Docker (jeder Kunden-MCP-Server = eigener Container)
|
||||||
|
- Traefik (Reverse-Proxy + automatic SSL via Let's Encrypt + per-customer subdomains)
|
||||||
|
- Cloudflare (DNS + DDoS + Cache vor Web)
|
||||||
|
|
||||||
|
**Transport für generierte MCP-Server:**
|
||||||
|
- **Streamable HTTP** (Stand 2025-11-25 spec) — der einzige Standard heute
|
||||||
|
- KEIN SSE (deprecated seit 2025-06-18)
|
||||||
|
- Stateless-ready (Sessions in Redis falls nötig, default stateless)
|
||||||
|
- OAuth 2.1 + PKCE + Dynamic Client Registration + Resource Indicators (RFC 8707)
|
||||||
|
- `.well-known/oauth-protected-resource` Discovery Endpoint
|
||||||
|
- `.well-known/oauth-authorization-server` (OAuth Authorization Server Metadata, RFC 8414)
|
||||||
|
|
||||||
|
**Observability:**
|
||||||
|
- OpenTelemetry (OTel Collector → Grafana/Loki/Tempo Self-hosted)
|
||||||
|
- BetterStack für Uptime-Pings
|
||||||
|
- Sentry für Frontend + Backend Errors
|
||||||
|
|
||||||
|
**CI/CD:**
|
||||||
|
- GitHub Actions
|
||||||
|
- Docker-Build → Push zu GHCR
|
||||||
|
- Coolify Webhook für Auto-Deploy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. SYSTEM-ARCHITEKTUR (so funktioniert das Ganze)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ END USER │
|
||||||
|
│ (uses Claude Desktop / Cursor / ChatGPT to call MCP tools) │
|
||||||
|
└──────────────────────────┬──────────────────────────────────┘
|
||||||
|
│ Streamable HTTP + OAuth 2.1
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ TRAEFIK (per-customer routing) │
|
||||||
|
│ {slug}.mcp.buildmymcpserver.com → container │
|
||||||
|
└──────────────────────────┬──────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ KUNDEN-MCP-SERVER (Docker Container, 1 pro Server) │
|
||||||
|
│ - Generierter Code aus Template + Claude-Output │
|
||||||
|
│ - @modelcontextprotocol/sdk │
|
||||||
|
│ - OAuth 2.1 Resource Server │
|
||||||
|
│ - Connects to: Customer's APIs, DBs, etc. │
|
||||||
|
└──────────────────────────┬──────────────────────────────────┘
|
||||||
|
│ (logs, metrics)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ CONTROL PLANE (Fastify API) │
|
||||||
|
│ - User-Auth (Better-Auth) │
|
||||||
|
│ - Server-CRUD │
|
||||||
|
│ - Generation-Job-Queue (BullMQ) │
|
||||||
|
│ - Container-Orchestrierung (Coolify API) │
|
||||||
|
│ - Billing (Stripe webhooks) │
|
||||||
|
│ - Tool-Call-Metering (für Usage-based-Billing) │
|
||||||
|
└──────────────────────────┬──────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────┼────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────┐ ┌──────────┐ ┌──────────┐
|
||||||
|
│Postgres│ │ Redis │ │ Coolify │
|
||||||
|
└────────┘ └──────────┘ └──────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ CUSTOMER (Marco's User) — uses Web Dashboard │
|
||||||
|
│ Next.js 15 Dashboard: │
|
||||||
|
│ - Prompt to build a new MCP server │
|
||||||
|
│ - Watch generation stream in real-time │
|
||||||
|
│ - Manage existing servers (logs, restart, edit) │
|
||||||
|
│ - Install instructions for Claude/Cursor/ChatGPT │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Kritischer Flow — "Prompt to Live Server":**
|
||||||
|
|
||||||
|
1. User loggt sich ins Dashboard ein, klickt "New MCP Server"
|
||||||
|
2. Gibt Prompt ein: *"Create an MCP server that queries my Postgres DB at db.example.com with read-only access to the users and orders tables"*
|
||||||
|
3. UI fragt strukturiert nach: API endpoints, secrets needed, scopes, tool names
|
||||||
|
4. Backend pusht Job in BullMQ Queue
|
||||||
|
5. Generator-Worker:
|
||||||
|
- Lädt MCP-Template
|
||||||
|
- Ruft Claude API mit System-Prompt + User-Spezifikation
|
||||||
|
- Parst generierte Tools/Resources/Prompts (Zod-validated)
|
||||||
|
- Schreibt fertigen TypeScript-Code
|
||||||
|
- Baut Docker-Image via `docker buildx`
|
||||||
|
- Pusht zu interner Registry
|
||||||
|
- Triggert Coolify-Deployment auf `{slug}.mcp.buildmymcpserver.com`
|
||||||
|
- Streamt Status (queued → generating → building → deploying → live) via Server-Sent-Events ans Dashboard
|
||||||
|
6. User sieht Live-URL + Copy-Paste-Snippets für:
|
||||||
|
- **Claude Desktop:** `claude_desktop_config.json` Eintrag
|
||||||
|
- **Cursor:** `mcp.json` Eintrag
|
||||||
|
- **ChatGPT Custom Connector:** URL + OAuth-Setup
|
||||||
|
7. Erster Tool-Call vom End-User → Traefik → Container → Tool → Response → Metric in Redis → später aggregiert in Postgres für Billing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. DATABASE SCHEMA (Drizzle, vollständig)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// packages/db/schema.ts
|
||||||
|
|
||||||
|
import { pgTable, uuid, varchar, text, timestamp, jsonb, boolean, integer, bigint, index, pgEnum } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
|
export const planEnum = pgEnum('plan', ['hobby', 'pro', 'team', 'enterprise']);
|
||||||
|
export const serverStatusEnum = pgEnum('server_status', ['draft', 'queued', 'generating', 'building', 'deploying', 'live', 'failed', 'paused']);
|
||||||
|
export const buildStatusEnum = pgEnum('build_status', ['queued', 'running', 'success', 'failed', 'cancelled']);
|
||||||
|
|
||||||
|
// Tenant/Org-Modell — auch wenn Single-User, baust du es Org-fähig (kein Refactor später)
|
||||||
|
export const organizations = pgTable('organizations', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
slug: varchar('slug', { length: 64 }).notNull().unique(),
|
||||||
|
name: varchar('name', { length: 128 }).notNull(),
|
||||||
|
plan: planEnum('plan').default('hobby').notNull(),
|
||||||
|
stripeCustomerId: varchar('stripe_customer_id', { length: 128 }),
|
||||||
|
stripeSubscriptionId: varchar('stripe_subscription_id', { length: 128 }),
|
||||||
|
monthlyCallQuota: bigint('monthly_call_quota', { mode: 'number' }).default(100_000),
|
||||||
|
callsThisPeriod: bigint('calls_this_period', { mode: 'number' }).default(0),
|
||||||
|
periodStartsAt: timestamp('period_starts_at').defaultNow(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const users = pgTable('users', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||||
|
name: varchar('name', { length: 128 }),
|
||||||
|
avatarUrl: text('avatar_url'),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const memberships = pgTable('memberships', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
orgId: uuid('org_id').references(() => organizations.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
role: varchar('role', { length: 32 }).default('owner').notNull(), // owner | admin | member | viewer
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mcpServers = pgTable('mcp_servers', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
orgId: uuid('org_id').references(() => organizations.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
slug: varchar('slug', { length: 64 }).notNull(), // becomes subdomain
|
||||||
|
name: varchar('name', { length: 128 }).notNull(),
|
||||||
|
description: text('description'),
|
||||||
|
status: serverStatusEnum('status').default('draft').notNull(),
|
||||||
|
currentVersion: integer('current_version').default(0),
|
||||||
|
containerId: varchar('container_id', { length: 128 }), // Coolify ref
|
||||||
|
publicUrl: text('public_url'), // https://{slug}.mcp.buildmymcpserver.com
|
||||||
|
toolsSchema: jsonb('tools_schema'), // generated tools metadata
|
||||||
|
oauthEnabled: boolean('oauth_enabled').default(true),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
|
}, (table) => ({
|
||||||
|
orgSlugIdx: index('idx_servers_org_slug').on(table.orgId, table.slug),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const builds = pgTable('builds', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
serverId: uuid('server_id').references(() => mcpServers.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
version: integer('version').notNull(),
|
||||||
|
prompt: text('prompt').notNull(),
|
||||||
|
generatedCode: text('generated_code'),
|
||||||
|
status: buildStatusEnum('status').default('queued').notNull(),
|
||||||
|
errorMessage: text('error_message'),
|
||||||
|
startedAt: timestamp('started_at'),
|
||||||
|
finishedAt: timestamp('finished_at'),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const secrets = pgTable('secrets', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
serverId: uuid('server_id').references(() => mcpServers.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
key: varchar('key', { length: 128 }).notNull(),
|
||||||
|
encryptedValue: text('encrypted_value').notNull(), // AES-256-GCM, key in env
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Per-customer OAuth clients (für Dynamic Client Registration)
|
||||||
|
export const oauthClients = pgTable('oauth_clients', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
serverId: uuid('server_id').references(() => mcpServers.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
clientId: varchar('client_id', { length: 128 }).notNull().unique(),
|
||||||
|
clientSecretHash: text('client_secret_hash'), // bcrypt
|
||||||
|
redirectUris: jsonb('redirect_uris').notNull(), // string[]
|
||||||
|
metadata: jsonb('metadata'), // RFC 7591 client metadata
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const oauthTokens = pgTable('oauth_tokens', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
clientId: uuid('client_id').references(() => oauthClients.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
accessTokenHash: text('access_token_hash').notNull(),
|
||||||
|
refreshTokenHash: text('refresh_token_hash'),
|
||||||
|
scope: text('scope'),
|
||||||
|
expiresAt: timestamp('expires_at').notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toolCallMetrics = pgTable('tool_call_metrics', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
serverId: uuid('server_id').references(() => mcpServers.id).notNull(),
|
||||||
|
toolName: varchar('tool_name', { length: 128 }).notNull(),
|
||||||
|
durationMs: integer('duration_ms'),
|
||||||
|
success: boolean('success').notNull(),
|
||||||
|
errorCode: varchar('error_code', { length: 64 }),
|
||||||
|
timestamp: timestamp('timestamp').defaultNow().notNull(),
|
||||||
|
}, (table) => ({
|
||||||
|
serverTimeIdx: index('idx_metrics_server_time').on(table.serverId, table.timestamp),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const auditLog = pgTable('audit_log', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
orgId: uuid('org_id').references(() => organizations.id),
|
||||||
|
userId: uuid('user_id').references(() => users.id),
|
||||||
|
action: varchar('action', { length: 128 }).notNull(),
|
||||||
|
resourceType: varchar('resource_type', { length: 64 }),
|
||||||
|
resourceId: varchar('resource_id', { length: 128 }),
|
||||||
|
metadata: jsonb('metadata'),
|
||||||
|
ipAddress: varchar('ip_address', { length: 64 }),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI/UX — DAS MUSS NATIVE-FEEL HABEN
|
||||||
|
|
||||||
|
### Design-Prinzipien
|
||||||
|
|
||||||
|
1. **Monochrom mit Akzent.** Background `#0A0A0B` (fast schwarz aber nicht). Foreground `#FAFAFA`. Border `#1F1F22`. Akzentfarbe: ein präzises **Electric Indigo `#6366F1`** sparingly für CTAs und Status-Live. Kein Lila-Gradient.
|
||||||
|
2. **Geometric Sans + Mono.** `Geist` für UI, `Geist Mono` für Code/IDs/URLs.
|
||||||
|
3. **Tight Density.** Padding 12-16px nicht 24-32px. Compact-feeling wie Linear/Vercel-Dashboard.
|
||||||
|
4. **Reduced motion.** Nur Skeleton-Loads und State-Changes animieren (200ms ease-out). Keine "hero-spinners".
|
||||||
|
5. **Code als Hero.** Jede Page hat irgendwo einen Code-Block (Copy-Button immer rechts oben).
|
||||||
|
6. **Status-Pills.** `live` = green dot pulsing, `building` = amber spinner, `failed` = red, `draft` = grey.
|
||||||
|
|
||||||
|
### Pages (komplett)
|
||||||
|
|
||||||
|
**Marketing (apps/web/app):**
|
||||||
|
|
||||||
|
1. **`/` (Landing):**
|
||||||
|
- Hero: Großes Heading *"Describe your tool. We host the server."*, Subline, ein Code-Block der einen Prompt zeigt + Output-Snippet (animiertes Typing-Demo, nicht Video, in HTML).
|
||||||
|
- "How it works" — 3 Steps mit jeweils einem kleinen Screenshot/Code-Block.
|
||||||
|
- "Works with" — Logos: Claude, Cursor, ChatGPT, VS Code Copilot, Continue.dev.
|
||||||
|
- "Examples" — Grid mit 6 Use-Cases (Postgres, Salesforce, Notion, GitHub, Stripe, Custom REST).
|
||||||
|
- Pricing (4 Tiers, transparent, kein "Contact us" außer Enterprise).
|
||||||
|
- FAQ (12 Fragen: Was ist MCP, Brauche ich OAuth, Welche AI-Tools, Kann ich self-hosten, Was passiert wenn ich kündige, Custom Domain, etc.).
|
||||||
|
- Footer: Status, Docs, GitHub (wenn open-source-Teile), Security/SOC2-roadmap, Privacy, Terms.
|
||||||
|
|
||||||
|
2. **`/docs/*`** — MDX-basiert, schmaler 280px Sidebar, max-w-prose, Search via Pagefind. Bereiche: Quickstart, Concepts (Tools, Resources, Prompts, OAuth), Recipes, API-Reference, Self-Hosting-Guide.
|
||||||
|
|
||||||
|
3. **`/changelog`** — MDX timeline.
|
||||||
|
|
||||||
|
4. **`/pricing`** — wenn aus Landing rausgezogen werden soll.
|
||||||
|
|
||||||
|
**Dashboard (apps/web/app/(dashboard)):**
|
||||||
|
|
||||||
|
5. **`/dashboard`** — Übersicht. Cards: Servers (Anzahl + Status), Calls this period (Progress-Bar vs Quota), Recent Builds, Quick-Action "New Server".
|
||||||
|
|
||||||
|
6. **`/servers`** — Liste aller MCP-Server. Tabelle mit Name, Status, Calls (24h), Last Build, Actions. Filter + Search.
|
||||||
|
|
||||||
|
7. **`/servers/new`** — DER WIZARD. Drei-Step:
|
||||||
|
- Step 1: Prompt-Eingabe (Textarea mit Beispiel-Prompts als Chips drumherum: *"Postgres reader"*, *"Notion search"*, *"Custom REST API"*).
|
||||||
|
- Step 2: Strukturierte Confirmation. Claude hat parsed, zeigt: Tool-Liste, Required Secrets (Input-Felder, encrypted save), Optional Scopes.
|
||||||
|
- Step 3: Deploy. Live-Stream der Build-Logs (Server-Sent Events von der API). Bei Success: Live-URL + 3 Tabs (Claude Desktop, Cursor, ChatGPT) mit copy-ready Config-Snippets.
|
||||||
|
|
||||||
|
8. **`/servers/[id]`** — Server-Detail mit Tabs:
|
||||||
|
- **Overview:** Status, URL, OAuth-Status, Quick-Install-Configs
|
||||||
|
- **Tools:** Liste der exposed Tools mit Schemas
|
||||||
|
- **Logs:** Streaming-Logs (Server-Sent Events, last 1000 lines)
|
||||||
|
- **Metrics:** Charts (Calls/h, Latency P50/P95/P99, Error-Rate) — Recharts
|
||||||
|
- **Secrets:** Manage env-vars (verschlüsselt)
|
||||||
|
- **Iterate:** Neuer Prompt um Server zu erweitern (= neuer Build, neue Version)
|
||||||
|
- **Settings:** Rename, Custom Domain (Pro+), Pause, Delete
|
||||||
|
|
||||||
|
9. **`/settings`** — Org/User-Settings, Billing (Stripe Customer Portal embed), API Keys (für CLI), Team Members (Team+).
|
||||||
|
|
||||||
|
10. **`/audit`** — Audit Log (Team+).
|
||||||
|
|
||||||
|
### Komponenten-Library
|
||||||
|
|
||||||
|
Use shadcn/ui registry, install only: `button`, `card`, `dialog`, `dropdown-menu`, `input`, `label`, `select`, `separator`, `sheet`, `table`, `tabs`, `toast`, `tooltip`, `badge`, `skeleton`, `progress`, `command` (für CMD+K). Custom: `<CodeBlock>` mit Shiki highlighting + Copy-Button, `<StatusPill>` mit Animations, `<StreamingLogs>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. MCP-GENERIERUNGS-PIPELINE (das Kernstück)
|
||||||
|
|
||||||
|
### Template (apps/runner-template/src/server.ts)
|
||||||
|
|
||||||
|
Ein parametrisiertes Template das die Generation füllt:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// THIS IS A TEMPLATE — values in {{ }} get replaced by generator
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import Fastify from "fastify";
|
||||||
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
||||||
|
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "{{SERVER_NAME}}",
|
||||||
|
version: "{{SERVER_VERSION}}",
|
||||||
|
});
|
||||||
|
|
||||||
|
// {{TOOL_REGISTRATIONS}}
|
||||||
|
// Generated tools go here, e.g.:
|
||||||
|
// server.tool("query_users", "...", { id: z.string() }, async ({ id }) => { ... });
|
||||||
|
|
||||||
|
// {{RESOURCE_REGISTRATIONS}}
|
||||||
|
// {{PROMPT_REGISTRATIONS}}
|
||||||
|
|
||||||
|
const app = Fastify({ logger: { level: 'info' } });
|
||||||
|
|
||||||
|
// OAuth 2.1 Protected Resource Metadata (RFC 9728)
|
||||||
|
app.get('/.well-known/oauth-protected-resource', async () => ({
|
||||||
|
resource: process.env.PUBLIC_URL,
|
||||||
|
authorization_servers: [`${process.env.PUBLIC_URL}/oauth`],
|
||||||
|
bearer_methods_supported: ['header'],
|
||||||
|
resource_documentation: `${process.env.PUBLIC_URL}/docs`,
|
||||||
|
scopes_supported: {{SCOPES_JSON}},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// OAuth Authorization Server Metadata (RFC 8414)
|
||||||
|
app.get('/.well-known/oauth-authorization-server', async () => ({
|
||||||
|
issuer: `${process.env.PUBLIC_URL}/oauth`,
|
||||||
|
authorization_endpoint: `${process.env.PUBLIC_URL}/oauth/authorize`,
|
||||||
|
token_endpoint: `${process.env.PUBLIC_URL}/oauth/token`,
|
||||||
|
registration_endpoint: `${process.env.PUBLIC_URL}/oauth/register`,
|
||||||
|
response_types_supported: ['code'],
|
||||||
|
grant_types_supported: ['authorization_code', 'refresh_token'],
|
||||||
|
code_challenge_methods_supported: ['S256'],
|
||||||
|
token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post', 'none'],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// OAuth endpoints (delegated to control-plane via internal RPC, or local for stateless)
|
||||||
|
// {{OAUTH_HANDLERS}}
|
||||||
|
|
||||||
|
// MCP Streamable HTTP endpoint
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: undefined, // stateless mode
|
||||||
|
});
|
||||||
|
|
||||||
|
app.all('/mcp', async (request, reply) => {
|
||||||
|
// Validate bearer token (OAuth 2.1 Resource Server)
|
||||||
|
const auth = request.headers.authorization;
|
||||||
|
if (!auth?.startsWith('Bearer ')) {
|
||||||
|
return reply
|
||||||
|
.code(401)
|
||||||
|
.header('WWW-Authenticate', `Bearer resource_metadata="${process.env.PUBLIC_URL}/.well-known/oauth-protected-resource"`)
|
||||||
|
.send({ error: 'unauthorized' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = auth.slice(7);
|
||||||
|
try {
|
||||||
|
const JWKS = createRemoteJWKSet(new URL(`${process.env.CONTROL_PLANE_URL}/oauth/jwks`));
|
||||||
|
const { payload } = await jwtVerify(token, JWKS, {
|
||||||
|
issuer: `${process.env.PUBLIC_URL}/oauth`,
|
||||||
|
audience: process.env.PUBLIC_URL,
|
||||||
|
});
|
||||||
|
// RFC 8707 Resource Indicators check
|
||||||
|
if (payload.aud !== process.env.PUBLIC_URL) {
|
||||||
|
return reply.code(403).send({ error: 'invalid_audience' });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return reply.code(401).send({ error: 'invalid_token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to MCP transport
|
||||||
|
await transport.handleRequest(request.raw, reply.raw, request.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.connect(transport);
|
||||||
|
await app.listen({ port: parseInt(process.env.PORT || '3000'), host: '0.0.0.0' });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generator-Worker Logic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// apps/generator/src/worker.ts (Pseudocode-Skelett)
|
||||||
|
|
||||||
|
import { Worker } from 'bullmq';
|
||||||
|
import Anthropic from '@anthropic-ai/sdk';
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT = `You are an expert TypeScript engineer generating production MCP servers.
|
||||||
|
|
||||||
|
Output ONLY a JSON object with this exact schema:
|
||||||
|
{
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "snake_case_name",
|
||||||
|
"description": "Clear description for the AI client",
|
||||||
|
"inputSchema": { /* JSON Schema, strict types */ },
|
||||||
|
"implementation": "async ({ args }) => { /* full TypeScript body */ }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [...],
|
||||||
|
"prompts": [...],
|
||||||
|
"requiredSecrets": ["API_KEY", "DATABASE_URL"],
|
||||||
|
"scopes": ["read:users", "write:orders"],
|
||||||
|
"dependencies": { "pg": "^8.13.0" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Use Zod for parameter schemas, NOT JSON Schema directly
|
||||||
|
- All implementations MUST be async, MUST handle errors, MUST return MCP content blocks
|
||||||
|
- NEVER use eval, exec, fs writes outside /tmp
|
||||||
|
- NEVER hardcode secrets — use process.env.{SECRET_NAME}
|
||||||
|
- Validate all inputs with Zod before use
|
||||||
|
- Return errors as { content: [{ type: "text", text: "Error: ..." }], isError: true }
|
||||||
|
- For databases: use parameterized queries ONLY
|
||||||
|
- For external APIs: implement retry with exponential backoff
|
||||||
|
- Each tool must be idempotent OR clearly state it is destructive in description
|
||||||
|
`;
|
||||||
|
|
||||||
|
new Worker('build', async (job) => {
|
||||||
|
const { serverId, prompt, orgId } = job.data;
|
||||||
|
|
||||||
|
// 1. Generate with Claude
|
||||||
|
const anthropic = new Anthropic();
|
||||||
|
const response = await anthropic.messages.create({
|
||||||
|
model: 'claude-opus-4-7',
|
||||||
|
max_tokens: 8192,
|
||||||
|
system: SYSTEM_PROMPT,
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Parse + Validate output (Zod schema for the generator output itself)
|
||||||
|
const spec = parseAndValidate(response.content[0].text);
|
||||||
|
|
||||||
|
// 3. Inject into template
|
||||||
|
const code = renderTemplate(spec);
|
||||||
|
|
||||||
|
// 4. Run static checks (tsc --noEmit on generated code, lint, basic sandbox-test)
|
||||||
|
await runStaticChecks(code);
|
||||||
|
|
||||||
|
// 5. Build Docker image
|
||||||
|
const imageTag = `mcp-${serverId}:v${version}`;
|
||||||
|
await dockerBuild(code, imageTag, spec.dependencies);
|
||||||
|
|
||||||
|
// 6. Deploy via Coolify API
|
||||||
|
await coolify.deploy({
|
||||||
|
image: imageTag,
|
||||||
|
subdomain: `${serverSlug}.mcp.buildmymcpserver.com`,
|
||||||
|
env: secrets,
|
||||||
|
healthcheck: '/health',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Update DB, emit SSE to dashboard
|
||||||
|
await db.update(mcpServers).set({ status: 'live', publicUrl: '...' }).where(...);
|
||||||
|
await redis.publish(`build:${serverId}`, JSON.stringify({ status: 'live' }));
|
||||||
|
}, { connection: redis });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sandboxing & Sicherheit (NON-NEGOTIABLE)
|
||||||
|
|
||||||
|
- Container laufen mit `--read-only` Filesystem (außer `/tmp` mit Quota)
|
||||||
|
- `--cap-drop=ALL`, `--security-opt=no-new-privileges`
|
||||||
|
- CPU-Limit 0.5 cores, Memory 512MB pro Container default
|
||||||
|
- Network-Egress nur zu whitelisted Domains (definiert via Tool-Spec)
|
||||||
|
- Keine SSH, kein Shell in den Containern
|
||||||
|
- Secrets via Docker Secrets / Coolify Env-Vars (verschlüsselt at rest mit AES-256-GCM in Postgres, KMS-Key in Hetzner Vault oder Doppler)
|
||||||
|
- Rate-Limit pro Tool-Call (default 100/min/IP, configurable)
|
||||||
|
- Prompt-Injection-Defense: Generierte Tool-Descriptions werden auf gefährliche Patterns gescannt (z.B. "ignore previous instructions" → reject build)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. PFLICHT-FEATURES FÜR PRODUCTION
|
||||||
|
|
||||||
|
### MUST-HAVE (MVP-Cut)
|
||||||
|
|
||||||
|
- [ ] Auth (Email-Magic-Link + GitHub-OAuth via Better-Auth)
|
||||||
|
- [ ] Org-Management mit RBAC
|
||||||
|
- [ ] Server-CRUD
|
||||||
|
- [ ] Prompt-to-Server Wizard mit Live-Build-Stream
|
||||||
|
- [ ] Secret-Management (encrypted)
|
||||||
|
- [ ] OAuth-2.1-Server-Implementation für jeden generierten MCP
|
||||||
|
- [ ] Streamable HTTP Transport in jedem Generated Server
|
||||||
|
- [ ] Stripe-Billing (Pro/Team-Subs + Metered Overage)
|
||||||
|
- [ ] Tool-Call-Metering + Quota-Enforcement
|
||||||
|
- [ ] Server Detail mit Logs/Metrics
|
||||||
|
- [ ] Install-Snippets für Claude Desktop, Cursor, ChatGPT
|
||||||
|
- [ ] Marketing-Landing
|
||||||
|
- [ ] Docs-Site (10+ Pages)
|
||||||
|
- [ ] Status-Page
|
||||||
|
- [ ] Audit-Log
|
||||||
|
- [ ] Health-Checks + Auto-Restart bei Failure
|
||||||
|
- [ ] Backups (DB + Server-Configs daily zu Backblaze B2)
|
||||||
|
|
||||||
|
### SHOULD-HAVE (Phase 2, nach erstem zahlenden Kunden)
|
||||||
|
|
||||||
|
- [ ] CLI (`buildmymcp init`, `deploy`, `logs`)
|
||||||
|
- [ ] GitHub-Repo-Sync (Server-Code in Customer's Repo pushen für Transparenz/Audit)
|
||||||
|
- [ ] Template-Marketplace (Pre-Built MCP-Server für gängige Tools)
|
||||||
|
- [ ] Custom-Domain per Server (CNAME-Validation)
|
||||||
|
- [ ] Team-Member-Invites
|
||||||
|
- [ ] Webhooks für Build-Events
|
||||||
|
- [ ] SSO/SAML für Enterprise
|
||||||
|
|
||||||
|
### NICE-TO-HAVE (Phase 3)
|
||||||
|
|
||||||
|
- [ ] BYOC (Bring Your Own Cloud — Deploy in Customer's AWS/GCP/Azure)
|
||||||
|
- [ ] MCP-Registry-Publishing (Servers public listen)
|
||||||
|
- [ ] AI-Powered Test-Generation (Tests für jeden generierten Tool)
|
||||||
|
- [ ] Multi-Region Deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. DEPLOYMENT (production-ready ab Tag 1)
|
||||||
|
|
||||||
|
### Hetzner-Setup
|
||||||
|
|
||||||
|
- **1× AX52 Dedicated** (€59/Mo): 16 Cores / 64GB RAM — running Coolify + Postgres + Redis + Generator + Customer-Containers (capacity: ~300-500 small MCP-Server)
|
||||||
|
- **1× CX22 Cloud** (€5/Mo) als Backup-Target + Status-Page
|
||||||
|
- **Cloudflare Free Plan** für DNS + DDoS
|
||||||
|
- **Backblaze B2** für DB-Backups (~€1/Mo bis 10GB)
|
||||||
|
|
||||||
|
### Domains-Plan
|
||||||
|
|
||||||
|
- `buildmymcpserver.com` — Marketing + Dashboard
|
||||||
|
- `api.buildmymcpserver.com` — Control-Plane API
|
||||||
|
- `*.mcp.buildmymcpserver.com` — Customer MCP-Server (Wildcard SSL via Let's Encrypt DNS challenge mit Cloudflare API)
|
||||||
|
- `docs.buildmymcpserver.com` (oder /docs auf Hauptdomain)
|
||||||
|
- `status.buildmymcpserver.com` (BetterStack hosted)
|
||||||
|
|
||||||
|
### Environment-Variables (.env.example)
|
||||||
|
|
||||||
|
```
|
||||||
|
# Control Plane
|
||||||
|
DATABASE_URL=postgresql://...
|
||||||
|
REDIS_URL=redis://...
|
||||||
|
BETTER_AUTH_SECRET=
|
||||||
|
BETTER_AUTH_URL=https://buildmymcpserver.com
|
||||||
|
GITHUB_OAUTH_ID=
|
||||||
|
GITHUB_OAUTH_SECRET=
|
||||||
|
STRIPE_SECRET_KEY=
|
||||||
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
STRIPE_PRICE_PRO=
|
||||||
|
STRIPE_PRICE_TEAM=
|
||||||
|
ANTHROPIC_API_KEY=
|
||||||
|
SECRETS_ENCRYPTION_KEY= # 32 bytes hex, AES-256-GCM
|
||||||
|
COOLIFY_API_URL=https://coolify.internal/...
|
||||||
|
COOLIFY_API_TOKEN=
|
||||||
|
TRAEFIK_API_URL=
|
||||||
|
CLOUDFLARE_API_TOKEN=
|
||||||
|
SENTRY_DSN=
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||||
|
|
||||||
|
# Per-MCP-Container (injected by generator/coolify)
|
||||||
|
PUBLIC_URL=https://{slug}.mcp.buildmymcpserver.com
|
||||||
|
CONTROL_PLANE_URL=https://api.buildmymcpserver.com
|
||||||
|
SERVER_ID=
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD (GitHub Actions)
|
||||||
|
|
||||||
|
- `pnpm typecheck` (alle Pakete, strict)
|
||||||
|
- `pnpm test` (Vitest)
|
||||||
|
- `pnpm lint` (eslint + biome)
|
||||||
|
- `pnpm build` (turbo)
|
||||||
|
- Docker-Build mit cache
|
||||||
|
- Push zu GHCR
|
||||||
|
- Coolify-Webhook trigger
|
||||||
|
- Smoke-Test gegen Staging
|
||||||
|
- Promote zu Prod
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. WAS DU IN DIESER REIHENFOLGE BAUST
|
||||||
|
|
||||||
|
**Sprint 1 (Woche 1): Foundation**
|
||||||
|
1. Monorepo-Setup (turbo, pnpm, tsconfig, biome, prettier)
|
||||||
|
2. DB-Schema + Drizzle-Migrations
|
||||||
|
3. Better-Auth-Setup mit Magic-Link + GitHub
|
||||||
|
4. Web-App-Shell (Layout, Theme, Auth-Pages, leeres Dashboard)
|
||||||
|
5. Marketing-Landing (Hero, Pricing, FAQ — kann statisch)
|
||||||
|
|
||||||
|
**Sprint 2 (Woche 2): Generation-Pipeline**
|
||||||
|
6. MCP-Template (apps/runner-template) — fertig lauffähig stdio + Streamable HTTP
|
||||||
|
7. Generator-Worker mit BullMQ + Claude-API-Call
|
||||||
|
8. Static-Checks-Pipeline (tsc, lint, security-scan)
|
||||||
|
9. Docker-Build-Pipeline (lokal mit buildx)
|
||||||
|
10. Coolify-API-Integration zum Deploy
|
||||||
|
|
||||||
|
**Sprint 3 (Woche 3): OAuth + Dashboard**
|
||||||
|
11. OAuth-2.1-Implementation (Control-Plane als Authorization Server, Per-Server-Container als Resource Server)
|
||||||
|
12. JWKS Endpoint + Token-Signing (asymmetric, RS256)
|
||||||
|
13. Dashboard: New-Server-Wizard mit Live-SSE-Stream
|
||||||
|
14. Server-Detail-Page mit Logs/Metrics
|
||||||
|
15. Install-Snippets-Generator (Claude/Cursor/ChatGPT-Configs)
|
||||||
|
|
||||||
|
**Sprint 4 (Woche 4): Billing + Production**
|
||||||
|
16. Stripe-Integration (Subscriptions, Webhooks, Metered Usage)
|
||||||
|
17. Quota-Enforcement
|
||||||
|
18. Audit-Log
|
||||||
|
19. Docs-Site (10+ MDX-Pages)
|
||||||
|
20. Status-Page-Integration
|
||||||
|
21. Backup-Cron
|
||||||
|
22. End-to-End Smoke-Test
|
||||||
|
23. Production-Deploy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. ACCEPTANCE CRITERIA — WANN BIST DU FERTIG
|
||||||
|
|
||||||
|
✅ User kann sich registrieren, einloggen, sieht ein leeres Dashboard.
|
||||||
|
✅ User klickt "New Server", gibt Prompt ein wie *"Create an MCP server that exposes a 'search_docs' tool which queries my Notion via API"*, gibt Notion-API-Key ein, sieht Build-Stream live.
|
||||||
|
✅ Nach <90 Sekunden: Live-URL `xyz.mcp.buildmymcpserver.com/mcp` ist erreichbar, return 401 ohne Token, 200 mit gültigem Token.
|
||||||
|
✅ User kopiert Snippet in `claude_desktop_config.json`, startet Claude Desktop neu, OAuth-Flow geht durch, Tool ist verfügbar in Claude.
|
||||||
|
✅ User stellt Frage in Claude die `search_docs` triggert, Result kommt zurück, Call wird in Dashboard-Metrics gezählt.
|
||||||
|
✅ User kann den Server iterieren ("Add a 'create_page' tool"), neue Version wird gebaut, ohne dass die alte runtergeht (rolling deploy).
|
||||||
|
✅ Stripe-Checkout für Pro-Plan funktioniert, Quota wird angehoben.
|
||||||
|
✅ Bei Quota-Überschreitung: HTTP 429 + Hinweis im Dashboard.
|
||||||
|
✅ Logs streamen in Echtzeit ins Dashboard.
|
||||||
|
✅ Audit-Log zeigt alle Aktionen.
|
||||||
|
✅ Lighthouse: 95+ auf allen Marketing-Pages, 90+ auf Dashboard.
|
||||||
|
✅ TypeScript strict, 0 `any`, 0 unused imports, biome-clean.
|
||||||
|
✅ Sentry erfasst Errors, OTel-Traces gehen durch.
|
||||||
|
✅ DB-Backup läuft täglich automatisch.
|
||||||
|
✅ Generated Server bestehen das offizielle MCP-Test-Suite (`mcp-inspector` + Anthropic's Reference-Client).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. WAS DU NIEMALS TUN DARFST
|
||||||
|
|
||||||
|
❌ KEIN `eval()` oder `Function()` im Generator-Worker für generierten Code — immer durch Static-Build-Pipeline.
|
||||||
|
❌ KEIN Sharing von Containern zwischen Kunden — 1 Server = 1 Container = 1 Isolation-Boundary.
|
||||||
|
❌ KEINE Plain-Text-Secrets in DB — immer AES-256-GCM verschlüsselt.
|
||||||
|
❌ KEINE Verwendung von deprecated SSE-Transport — nur Streamable HTTP.
|
||||||
|
❌ KEINE Token-Pass-Through (Spec verbietet es ausdrücklich) — du musst Token-Exchange machen wenn der MCP-Server downstream-APIs aufruft.
|
||||||
|
❌ KEINE Storage von OAuth-Access-Tokens im Klartext — nur Hashes für Verification.
|
||||||
|
❌ KEIN `*` als CORS — explizit pro Server konfigurieren.
|
||||||
|
❌ KEIN automatisches Deploy von generated Code ohne Static-Checks — wenn Checks failen, Build = failed, klare Error-Message.
|
||||||
|
❌ KEIN Branding wie "Powered by Claude" oder ähnliches im Marketing — du bist eine eigenständige Plattform, kein Anthropic-Wrapper.
|
||||||
|
❌ KEIN "AI-Sparkle-Design" — Linear/Vercel-Style. Boring on the outside, magic on the inside.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. REFERENCES (FÜR DICH, CLAUDE CODE, IM ZWEIFELSFALL CHECKEN)
|
||||||
|
|
||||||
|
- MCP Spec: https://modelcontextprotocol.io/specification/2025-11-25/
|
||||||
|
- MCP Authorization: https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
|
||||||
|
- TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
|
||||||
|
- FastMCP (alternative wrapper, optional): https://github.com/punkpeye/fastmcp
|
||||||
|
- OAuth 2.1: https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/
|
||||||
|
- RFC 8414 (Auth Server Metadata): https://datatracker.ietf.org/doc/html/rfc8414
|
||||||
|
- RFC 9728 (Protected Resource Metadata): https://datatracker.ietf.org/doc/html/rfc9728
|
||||||
|
- RFC 8707 (Resource Indicators): https://datatracker.ietf.org/doc/html/rfc8707
|
||||||
|
- RFC 7591 (Dynamic Client Registration): https://datatracker.ietf.org/doc/html/rfc7591
|
||||||
|
- Coolify Docs: https://coolify.io/docs
|
||||||
|
- Drizzle ORM: https://orm.drizzle.team/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. START
|
||||||
|
|
||||||
|
Lies diese Spec komplett durch. Stelle Rückfragen wenn etwas mehrdeutig ist. Beginne dann mit Sprint 1, Task 1: Monorepo-Setup. Commit nach jedem abgeschlossenen Task mit conventional-commits-Format (`feat:`, `fix:`, `chore:`, `docs:`).
|
||||||
|
|
||||||
|
Wenn du an einem Punkt unsicher bist über die MCP-Spec, **fetche die offizielle Doku, rate nicht aus dem Speicher.** Die Spec ändert sich noch häufig.
|
||||||
|
|
||||||
|
**Build it like infrastructure, not like a demo. Production-grade from line 1.**
|
||||||
|
|
||||||
|
Let's go.
|
||||||
79
CHOICES.md
Normal file
79
CHOICES.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# CHOICES.md
|
||||||
|
|
||||||
|
Decisions made during the autonomous Sprints 1–3 build where the spec was ambiguous or
|
||||||
|
where production-quality required a concrete pick.
|
||||||
|
|
||||||
|
## Stream transport: WebSocket over SSE
|
||||||
|
Spec lists both in different sections. The user's goal explicitly says
|
||||||
|
"WebSocket-streamed build flow", so the dashboard subscribes to build events via
|
||||||
|
`/v1/builds/:id/stream` WebSocket on the control-plane API. Generator-worker publishes
|
||||||
|
events to Redis pub/sub; API fans out per-WebSocket subscriber.
|
||||||
|
|
||||||
|
## Local container deploy (no Coolify in dev)
|
||||||
|
Spec targets Coolify on Hetzner for production. In dev the generator spawns containers
|
||||||
|
directly via the local Docker daemon (Docker Engine API on `/var/run/docker.sock` or
|
||||||
|
`npipe:////./pipe/docker_engine` on Windows). Each generated server gets a host port from
|
||||||
|
the `RUNNER_PORT_RANGE_*` window and is exposed at `http://localhost:<port>`. The
|
||||||
|
`coolify` adapter is interface-compatible so swapping in for production is a single import.
|
||||||
|
|
||||||
|
## Local addressing: ports not wildcard subdomains
|
||||||
|
`*.mcp.buildmymcpserver.com` requires DNS + Traefik + wildcard SSL — pointless on
|
||||||
|
localhost. Dev uses `http://localhost:<port>` for each generated container. Production
|
||||||
|
Traefik routing is wired but unused by `pnpm dev`.
|
||||||
|
|
||||||
|
## OAuth 2.1 Authorization Server lives in the control plane
|
||||||
|
The runner-template is a Resource Server only. The control-plane API is the AS:
|
||||||
|
issues codes, exchanges tokens, signs RS256 JWTs, exposes JWKS. Each generated server
|
||||||
|
verifies tokens against `${CONTROL_PLANE_URL}/oauth/jwks`. This matches the spec's
|
||||||
|
"token exchange not pass-through" mandate.
|
||||||
|
|
||||||
|
## Better-Auth: email magic link via console transport in dev
|
||||||
|
We wire Better-Auth with the email/password + magic-link plugin. In dev, magic-link
|
||||||
|
emails are written to the API stdout (so the developer can click them); production
|
||||||
|
plugs in Resend. GitHub OAuth is configured but only used if env vars are populated.
|
||||||
|
|
||||||
|
## Generator: Claude API call gated by env, mock fallback for offline dev
|
||||||
|
If `ANTHROPIC_API_KEY` is set, the worker calls the real Claude API
|
||||||
|
(`claude-opus-4-7`). If not, a deterministic mock returns a small "echo" tool so the
|
||||||
|
end-to-end build flow stays demoable without burning credits. The choice is logged in
|
||||||
|
build logs so users always know which path ran.
|
||||||
|
|
||||||
|
## Static checks: tsc + targeted regex scan
|
||||||
|
- `tsc --noEmit` on the rendered server code in a tmp dir.
|
||||||
|
- Regex scan for banned tokens: `eval(`, `Function(`, `child_process`, `fs.unlink`,
|
||||||
|
prompt-injection markers (`ignore previous instructions`, `disregard the above`).
|
||||||
|
- esbuild bundle produced for the runner so the image is one file + deps.
|
||||||
|
|
||||||
|
## Sandboxing: dev relaxed, prod flags documented
|
||||||
|
Dev containers run without `--read-only` / `--cap-drop=ALL` because Windows Docker
|
||||||
|
Desktop refuses some combinations. The production launch flags are baked into
|
||||||
|
`apps/generator/src/lib/deploy.ts` constants but commented `// prod-only` for clarity.
|
||||||
|
|
||||||
|
## ChatGPT install snippet
|
||||||
|
ChatGPT supports MCP via "Custom GPTs → Actions → Add MCP server" or the
|
||||||
|
research "Custom Connectors" surface. We render a copy block with `URL`,
|
||||||
|
`Auth: OAuth 2.1`, and a link to the install-flow page on chat.openai.com. If the
|
||||||
|
official deep link path changes, only `apps/web/lib/install-snippets.ts` needs editing.
|
||||||
|
|
||||||
|
## Tailwind v4 + shadcn registry
|
||||||
|
Tailwind v4 uses CSS-first config. shadcn components are vendored into
|
||||||
|
`apps/web/components/ui/*` — only the primitives the spec lists. No drop-in defaults
|
||||||
|
left untouched: every component has been re-styled against the design tokens.
|
||||||
|
|
||||||
|
## Stripe / metering / quotas: scaffolded only in Sprint 1–3
|
||||||
|
The schema + types are complete. Wired Stripe checkout / webhooks are Sprint 4
|
||||||
|
per spec, so we leave a clean seam in `apps/api/src/routes/billing.ts` but do not
|
||||||
|
implement the flow yet. Quotas are read-only in the dashboard.
|
||||||
|
|
||||||
|
## Drizzle: `migrate` strategy
|
||||||
|
Schema lives in `packages/db/src/schema.ts`. `pnpm db:push` is dev-only and pushes
|
||||||
|
schema directly. Migrations are generated via `pnpm db:generate` and applied via
|
||||||
|
`pnpm db:migrate` on first API boot. Local Postgres comes from docker-compose.
|
||||||
|
|
||||||
|
## Node 20 LTS, ESM everywhere
|
||||||
|
All packages are `"type": "module"`. `tsx` runs ts directly in dev. esbuild bundles
|
||||||
|
for prod. No CJS.
|
||||||
|
|
||||||
|
## Conventional commits per task, single branch
|
||||||
|
Single `main` branch, conventional-commits messages. No PR flow because this is one
|
||||||
|
autonomous run.
|
||||||
46
README.md
Normal file
46
README.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# BuildMyMCPServer
|
||||||
|
|
||||||
|
> Describe your tool. We host the server. AI uses it.
|
||||||
|
|
||||||
|
Prompt-to-production MCP servers with OAuth 2.1 and Streamable HTTP.
|
||||||
|
|
||||||
|
## Run locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install deps
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 2. Copy env and fill ANTHROPIC_API_KEY if you want real generation
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 3. Boot everything (postgres, redis, web, api, generator)
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open:
|
||||||
|
|
||||||
|
- Dashboard: <http://localhost:3000>
|
||||||
|
- API: <http://localhost:4000>
|
||||||
|
|
||||||
|
Sign in with the magic link printed to the API stdout, click **New Server**, paste a
|
||||||
|
prompt, watch the build stream live over WebSocket.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
See `BuildMyMCPServer_MASTER_PROMPT.md` for the full spec and `CHOICES.md` for the
|
||||||
|
decisions made during Sprints 1–3.
|
||||||
|
|
||||||
|
## Workspace layout
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/
|
||||||
|
web/ Next.js 15 dashboard + marketing
|
||||||
|
api/ Fastify control plane
|
||||||
|
generator/ BullMQ worker — Claude → build → deploy
|
||||||
|
runner-template/ Hosted MCP server template
|
||||||
|
packages/
|
||||||
|
db/ Drizzle schema + client
|
||||||
|
auth/ Better-Auth wrapper
|
||||||
|
types/ Shared Zod types
|
||||||
|
ui/ Shared React primitives
|
||||||
|
```
|
||||||
55
biome.json
Normal file
55
biome.json
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||||
|
"files": {
|
||||||
|
"ignore": [
|
||||||
|
"**/node_modules",
|
||||||
|
"**/.next",
|
||||||
|
"**/dist",
|
||||||
|
"**/.turbo",
|
||||||
|
"**/drizzle",
|
||||||
|
"**/.pnpm-store",
|
||||||
|
"**/build-context"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineWidth": 100
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true,
|
||||||
|
"suspicious": {
|
||||||
|
"noExplicitAny": "error",
|
||||||
|
"noConsoleLog": "off"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"useImportType": "off",
|
||||||
|
"useNodejsImportProtocol": "warn"
|
||||||
|
},
|
||||||
|
"complexity": {
|
||||||
|
"noBannedTypes": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "single",
|
||||||
|
"semicolons": "always",
|
||||||
|
"trailingCommas": "all",
|
||||||
|
"arrowParentheses": "always"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
"formatter": {
|
||||||
|
"indentWidth": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: bmm-postgres
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: bmm
|
||||||
|
POSTGRES_PASSWORD: bmm
|
||||||
|
POSTGRES_DB: bmm
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- bmm_pg:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U bmm -d bmm"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: bmm-redis
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- bmm_redis:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
bmm_pg:
|
||||||
|
bmm_redis:
|
||||||
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "buildmymcpserver",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"packageManager": "pnpm@9.12.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.11.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "docker compose up -d postgres redis && turbo run dev --parallel",
|
||||||
|
"dev:no-docker": "turbo run dev --parallel",
|
||||||
|
"build": "turbo run build",
|
||||||
|
"typecheck": "turbo run typecheck",
|
||||||
|
"lint": "biome check .",
|
||||||
|
"lint:fix": "biome check --write .",
|
||||||
|
"db:generate": "pnpm --filter @bmm/db run generate",
|
||||||
|
"db:migrate": "pnpm --filter @bmm/db run migrate",
|
||||||
|
"db:push": "pnpm --filter @bmm/db run push",
|
||||||
|
"stop": "docker compose down"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "1.9.4",
|
||||||
|
"turbo": "2.3.0",
|
||||||
|
"typescript": "5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
packages:
|
||||||
|
- "apps/*"
|
||||||
|
- "packages/*"
|
||||||
20
tsconfig.base.json
Normal file
20
tsconfig.base.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
"incremental": true
|
||||||
|
}
|
||||||
|
}
|
||||||
21
turbo.json
Normal file
21
turbo.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"globalDependencies": [".env", ".env.local"],
|
||||||
|
"tasks": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"typecheck": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"outputs": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user