commit fe4800e2c8609b1a9cf509e390b0ba4569e4dc8f Author: Marco Sadjadi Date: Tue May 19 00:20:15 2026 +0200 chore: bootstrap monorepo (turbo, biome, docker-compose, env, CHOICES) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..79c6128 --- /dev/null +++ b/.env.example @@ -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= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..450c43f --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/BuildMyMCPServer_MASTER_PROMPT.md b/BuildMyMCPServer_MASTER_PROMPT.md new file mode 100644 index 0000000..26565dd --- /dev/null +++ b/BuildMyMCPServer_MASTER_PROMPT.md @@ -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: `` mit Shiki highlighting + Copy-Button, `` mit Animations, ``. + +--- + +## 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. diff --git a/CHOICES.md b/CHOICES.md new file mode 100644 index 0000000..eaf4294 --- /dev/null +++ b/CHOICES.md @@ -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:`. 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:` 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a31bbdb --- /dev/null +++ b/README.md @@ -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: +- API: + +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 +``` diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..863120c --- /dev/null +++ b/biome.json @@ -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 + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..340f482 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/package.json b/package.json new file mode 100644 index 0000000..1f682df --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..3ff5faa --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*" diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..bbb9e4d --- /dev/null +++ b/tsconfig.base.json @@ -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 + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..4d7ac73 --- /dev/null +++ b/turbo.json @@ -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": [] + } + } +}