Go to file
Marco Sadjadi 2ad4a7e34c fix(security): template integration sovereign audit + critical fixes
P0 — three critical issues found by tracing every attack vector on the template
publish + fork + render path. All three fixed and verified with attack tests.

FIX A — Takedown actually stops malicious containers
  PATCH /v1/admin/templates with status=takedown previously only updated
  mcp_servers.status to 'paused' in the DB. The Docker container kept running
  and serving traffic on its allocated port — takedown was cosmetic. Now the
  endpoint enumerates every fork's container, calls 'docker rm -f' on each,
  clears container_id/public_url/host_port in the DB, and returns the
  stoppedContainers count. New apps/api/src/lib/docker.ts owns the stop logic.
  Verified: takedown stopped container f5632962, port 4109 connection refused.

FIX B — Reject specEdit on fork
  A hand-crafted POST /v1/servers with {templateId, previewId, specEdit} would
  enter the spec-edit branch, merge edits into the cached spec, but the worker
  reads the pre-built template code (separate cache key), ignoring the merged
  spec entirely. User thinks they changed something; deployed container behaves
  as the original. Now returns 400 spec_edit_forbidden_on_fork with an explainer
  pointing to the Iterate flow.

FIX C — templateId validation via Redis fork-ref
  templateId on POST /v1/servers was user-controlled and unvalidated:
  fork_count of any template could be pumped, mcp_servers got garbage
  template_id rows, takedown cascade would miss the bogus rows. Fork endpoint
  now writes a Redis key fork-ref:<previewId> -> templateId (5min TTL).
  Server-create requires the ref to exist AND match the submitted templateId.
  Verified attack: fake templateId without fork-ref returns 410 fork_ref_expired.

DEFENSE-IN-DEPTH — Hardened static checks

  Banned patterns (added):
    Function\s*\(['"`]    — Function('code')() form, no 'new' needed
    \bimport\s*\(           — dynamic import escapes bundle scope
    \bsetTimeout\s*\(['"`] — setTimeout('code', ms) eval form
    \bsetInterval\s*\(['"`]
    \bfs\s*\.\s*(unlink|rmdir|rm)\b
    \bprocess\s*\.\s*kill\b
    you are now in (developer|jailbreak|dan) mode — extra jailbreak markers

  Hardcoded-credential patterns (new — scanForLeakedSecrets):
    sk-ant-(api|sid)…  — Anthropic
    sk-…               — OpenAI
    sk_(live|test)_…   — Stripe
    ghp_…              — GitHub PAT
    github_pat_…       — GitHub fine-grained
    xox[bpoasr]-…      — Slack
    AKIA[0-9A-Z]{16}   — AWS
    -----BEGIN…PRIVATE KEY----- — RSA / SSH / GPG
  Triggered when a publisher pasted their key into the prompt and Claude
  embedded it literally in the generated code. Publish-blocking.
  Verified attack: smuggled 'Function("return 1")' into a build's
  generated_code, attempted publish → 422 publish_blocked.

  Slug regex tightened — fork + detail routes now require
  ^[a-z0-9][a-z0-9-]{0,63}$ (was loose min(1).max(64) — letting through
  '../admin', long strings, mixed case).

  UI warning — Publish-as-template form now shows an amber callout listing
  what's scanned and explicitly stating egress allowlisting is roadmap, not
  enforced today (was misleading: the field was collected, never enforced).

  TEMPLATE_SECURITY_AUDIT.md added — documents all 20 audited vectors with
  severity, status, and rationale for what's deferred.

UI polish
  globals.css — select/input/textarea/button get color-scheme: dark + custom
  chevron + option styling so Chrome's native popdown stops rendering as a
  white OS-themed widget on dark pages. The /templates category dropdown was
  the immediate trigger; same rule applies system-wide.
2026-05-19 23:35:45 +02:00
apps fix(security): template integration sovereign audit + critical fixes 2026-05-19 23:35:45 +02:00
packages feat(marketplace): template publish + fork + voting/ranking + admin moderation 2026-05-19 23:22:35 +02:00
scripts chore(dev): bootstrap script wires docker + drizzle push + turbo dev 2026-05-19 00:35:27 +02:00
.env.example feat(admin): password-auth admin panel with 8 pages + 15 API endpoints 2026-05-19 23:01:26 +02:00
.gitignore chore: bootstrap monorepo (turbo, biome, docker-compose, env, CHOICES) 2026-05-19 00:20:15 +02:00
biome.json chore: bootstrap monorepo (turbo, biome, docker-compose, env, CHOICES) 2026-05-19 00:20:15 +02:00
BuildMyMCPServer_MASTER_PROMPT.md chore: bootstrap monorepo (turbo, biome, docker-compose, env, CHOICES) 2026-05-19 00:20:15 +02:00
CHOICES.md feat(web): real 3-step wizard, settings, audit, docs, marketing pages 2026-05-19 18:20:31 +02:00
docker-compose.yml fix: live-run wiring (SDK 1.29, zod 3.25, OAUTH_ISSUER split, alt host ports, web on 3001, log level cast, pino transport) 2026-05-19 00:57:23 +02:00
package.json chore(dev): bootstrap script wires docker + drizzle push + turbo dev 2026-05-19 00:35:27 +02:00
pnpm-lock.yaml feat(llm): extract Claude SYSTEM_PROMPT + generateSpec into shared @bmm/llm package 2026-05-19 18:05:31 +02:00
pnpm-workspace.yaml chore: bootstrap monorepo (turbo, biome, docker-compose, env, CHOICES) 2026-05-19 00:20:15 +02:00
README.md chore(dev): bootstrap script wires docker + drizzle push + turbo dev 2026-05-19 00:35:27 +02:00
TEMPLATE_SECURITY_AUDIT.md fix(security): template integration sovereign audit + critical fixes 2026-05-19 23:35:45 +02:00
tsconfig.base.json chore: bootstrap monorepo (turbo, biome, docker-compose, env, CHOICES) 2026-05-19 00:20:15 +02:00
turbo.json chore: bootstrap monorepo (turbo, biome, docker-compose, env, CHOICES) 2026-05-19 00:20:15 +02:00

BuildMyMCPServer

Describe your tool. We host the server. AI uses it.

Prompt-to-production MCP servers with OAuth 2.1 and Streamable HTTP. Production-grade infrastructure for hosting Model Context Protocol servers your AI clients (Claude Desktop, Cursor, ChatGPT) can install with a copy-paste snippet.

Quick start

# 1. Install
pnpm install

# 2. Copy env. Defaults work for local dev. Set ANTHROPIC_API_KEY if you want real generation.
cp .env.example .env

# 3. Boot everything
pnpm dev

pnpm dev will:

  1. Load .env.
  2. docker compose up -d --wait postgres + redis.
  3. Push the Drizzle schema (drizzle-kit push --force).
  4. Start the full stack in parallel: web (Next.js, :3000), api (Fastify, :4000), generator (BullMQ worker).

Then open:

Click Start building, enter your email, copy the magic-link URL printed to the api terminal output, paste it in your browser. You land on /dashboard. Click New server, paste a prompt, and watch the build stream live over WebSocket.

If ANTHROPIC_API_KEY is unset, the generator returns a deterministic mock spec (an echo and a now tool) so the full end-to-end flow stays demoable.

If Docker is unavailable, the build will fail at the deploy step with a clear error. Otherwise: a fresh container is launched on a host port from RUNNER_PORT_RANGE_START…RUNNER_PORT_RANGE_END, the server is marked live, and the dashboard renders install snippets for Claude Desktop, Cursor and ChatGPT.

Architecture

See BuildMyMCPServer_MASTER_PROMPT.md for the full specification and CHOICES.md for decisions made during this Sprints 13 build.

apps/
  web/              Next.js 15 dashboard + marketing landing
  api/              Fastify control plane (auth, server CRUD, OAuth 2.1 AS, JWKS, WS stream)
  generator/        BullMQ worker — Claude → spec → render → docker build → local deploy
  runner-template/  Hosted MCP server template (Streamable HTTP + OAuth 2.1 RS)
packages/
  db/               Drizzle schema + client
  auth/             Magic-link + session
  types/            Shared Zod contracts

Scripts

Command Effect
pnpm dev Bootstrap + parallel dev for web, api, generator
pnpm dev:no-docker Skip docker-compose (assumes postgres + redis already up)
pnpm build Turbo build all apps
pnpm typecheck Turbo typecheck all apps
pnpm lint Biome check
pnpm lint:fix Biome check --write
pnpm db:push Push schema to postgres (drizzle-kit)
pnpm db:generate Generate SQL migration files
pnpm db:migrate Apply pending migrations
pnpm stop docker compose down

Acceptance check

After pnpm dev is up:

  • http://localhost:3000 renders the landing page.
  • http://localhost:4000/health returns { "ok": true }.
  • Sign in via magic link (URL printed in the api terminal).
  • New Server → paste prompt → live WebSocket stream queued → generating → building → deploying → live.
  • If Docker is running, a container is launched and http://localhost:<port>/mcp responds 401 + WWW-Authenticate without a token, 200 with a valid token issued by /oauth/token.
  • Install snippets render with copy buttons for Claude Desktop, Cursor, ChatGPT.

Repo conventions

  • TypeScript strict, zero any (Biome lints noExplicitAny as error).
  • ESM-only, Node 20 LTS.
  • Conventional commits.
  • Tailwind v4 (@import 'tailwindcss').
  • Geist + Geist Mono.