The llm package called the user-supplied onSpec/onError handlers
without awaiting them. In the /preview/stream route onSpec is async
(it does `await cacheSpec(...)` then writes the SSE `spec` event), so
the api handler's `await streamSpecFromAnthropic(...)` returned BEFORE
the terminal event had been written. The route's finally block then
ran `reply.raw.end()`, the queued `send('spec', ...)` hit a closed
stream and silently no-op'd, and the browser saw zero terminal
events — frontend ran into the "Spec generation failed." fallback
even though Anthropic had delivered a perfectly valid spec.
Verified against prod log: req-8 ran 66s with 200 and produced no
preview_spec_* log line, which is exactly the success-but-event-lost
signature.
Fix:
- StreamHandlers.onSpec / onError typed as Promise<void> | void
- Both call sites in streamSpecFromAnthropic now `await` them
- /preview/stream sets `resolved = true` at the END of each handler
(after the SSE write completes) so the post-stream "unresolved"
fallback only fires on a genuine programming bug
- Added preview_spec_ready info log on the happy path so future
diagnosis doesn't have to infer success from the absence of error
logs
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:
- Load
.env. docker compose up -d --waitpostgres + redis.- Push the Drizzle schema (
drizzle-kit push --force). - Start the full stack in parallel: web (Next.js, :3000), api (Fastify, :4000), generator (BullMQ worker).
Then open:
- Dashboard: http://localhost:3000
- API: http://localhost:4000/health
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 1–3 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:3000renders the landing page.http://localhost:4000/healthreturns{ "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>/mcpresponds 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 lintsnoExplicitAnyas error). - ESM-only, Node 20 LTS.
- Conventional commits.
- Tailwind v4 (
@import 'tailwindcss'). - Geist + Geist Mono.