feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
import { builds, createDb, eq, mcpServers } from '@bmm/db';
|
|
|
|
|
import { GeneratorSpec } from '@bmm/types';
|
2026-05-19 00:26:53 +02:00
|
|
|
import { Worker } from 'bullmq';
|
|
|
|
|
import { Redis } from 'ioredis';
|
|
|
|
|
import { config } from './config.js';
|
|
|
|
|
import { dockerBuild, prepareBuildContext, staticCheck } from './lib/build.js';
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
import { generateSpec } from './lib/claude.js';
|
2026-05-28 17:54:56 +02:00
|
|
|
import {
|
|
|
|
|
allocatePort,
|
|
|
|
|
computePublicUrl,
|
|
|
|
|
deployContainer,
|
|
|
|
|
dockerAvailable,
|
|
|
|
|
stopContainer,
|
|
|
|
|
} from './lib/deploy.js';
|
2026-05-19 00:26:53 +02:00
|
|
|
import { emitDone, emitError, emitLog, emitStatus } from './lib/emit.js';
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
import { renderServerCode } from './lib/render.js';
|
2026-05-19 00:26:53 +02:00
|
|
|
|
|
|
|
|
const db = createDb();
|
|
|
|
|
const connection = new Redis(config.REDIS_URL, { maxRetriesPerRequest: null });
|
feat(api,generator): preview endpoint + spec cache + audit-log writes
- POST /v1/servers/preview runs Claude synchronously, validates output, caches spec
in Redis under preview:<id> with 5min TTL, returns previewId+spec+detectedSecrets.
- POST /v1/servers accepts optional previewId; worker reuses the cached spec if
the entry is still present, otherwise regenerates fresh. Skips the second
Claude round-trip (~30s saved on the demoable path).
- audit() helper writes auth.login, auth.logout, server.create, server.iterate,
server.delete to audit_log with ip, metadata, resourceId.
- GET /v1/me/org returns organization + members list for the settings page.
- GET /v1/audit?limit=&action=&resourceType= returns scoped audit entries.
2026-05-19 18:08:29 +02:00
|
|
|
const cacheReader = new Redis(config.REDIS_URL, { maxRetriesPerRequest: null });
|
2026-05-19 00:26:53 +02:00
|
|
|
|
|
|
|
|
interface JobData {
|
|
|
|
|
buildId: string;
|
|
|
|
|
serverId: string;
|
|
|
|
|
orgId: string;
|
|
|
|
|
prompt: string;
|
|
|
|
|
version: number;
|
|
|
|
|
slug: string;
|
|
|
|
|
serverName: string;
|
|
|
|
|
secrets: Record<string, string>;
|
feat(api,generator): preview endpoint + spec cache + audit-log writes
- POST /v1/servers/preview runs Claude synchronously, validates output, caches spec
in Redis under preview:<id> with 5min TTL, returns previewId+spec+detectedSecrets.
- POST /v1/servers accepts optional previewId; worker reuses the cached spec if
the entry is still present, otherwise regenerates fresh. Skips the second
Claude round-trip (~30s saved on the demoable path).
- audit() helper writes auth.login, auth.logout, server.create, server.iterate,
server.delete to audit_log with ip, metadata, resourceId.
- GET /v1/me/org returns organization + members list for the settings page.
- GET /v1/audit?limit=&action=&resourceType= returns scoped audit entries.
2026-05-19 18:08:29 +02:00
|
|
|
previewId?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadCachedSpec(previewId: string): Promise<GeneratorSpec | null> {
|
|
|
|
|
const raw = await cacheReader.get(`preview:${previewId}`);
|
|
|
|
|
if (!raw) return null;
|
|
|
|
|
try {
|
|
|
|
|
const parsed = GeneratorSpec.safeParse(JSON.parse(raw));
|
|
|
|
|
return parsed.success ? parsed.data : null;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-05-19 00:26:53 +02:00
|
|
|
}
|
|
|
|
|
|
feat(marketplace): template publish + fork + voting/ranking + admin moderation
What this enables:
- A user builds an MCP server. If others would benefit, they click 'Publish as
template' on their server detail page. The spec + pre-rendered TypeScript
snapshot is preserved.
- Visitors browse /templates, filter by category, sort by trending/top/newest.
Each template card shows fork count + active deployment count as natural
manipulation-resistant popularity signal.
- /templates/[slug] shows the full plan: tool list with input schemas,
required-credential explanations (with 'how to get one' deep links), and a
collapsible code preview so users can audit before forking.
- Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and
pre-fills Step 2 with the template's parsed spec. Forker only fills in their
own credentials. mcp_servers.template_id is recorded; template.fork_count is
bumped atomically. Each fork gets its own isolated container with its own
port, its own AES-256 secrets — the template author has zero visibility into
the fork's traffic or data.
- Admin /admin/templates moderation: verify quality templates (shows shield
badge in marketplace), hide low-effort ones, takedown anything malicious.
Takedowns cascade-pause every fork container — owners must re-deploy.
Why template+fork instead of shared-container:
- Shared containers would mean the publisher's quota + their secrets + their
logs are exposed to forkers. Bad ergonomics, bad security, bad ownership.
- Templates/forks decouple the spec (shared, vouched-for) from the runtime
(isolated per user). Network-effect moat without the trust collapse.
Why no 5-star voting in v1:
- Manipulation-anfällig, empty lists without adoption. We use fork count +
active deploys + verified badge. Trending algorithm:
score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1)
Real signal, no brigading attack surface.
Backend:
- New schema: templates table (16 cols incl. tools_schema, generated_code,
required_secrets, allowedDomains, status enum, verified, fork_count).
- mcp_servers.template_id FK + idx for fork lookup.
- @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId.
- preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the
template's full rendered server.ts alongside the spec. Generator worker
detects this and skips the render step — uses the audited pre-built code
verbatim. Banned-pattern re-scan at publish time.
- routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern
re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via
SQL.
UI:
- /templates marketplace with trending/top/newest tabs, category filter, search.
Cards show forks + live count + author + verified badge.
- /templates/[slug] full detail with tools, credentials-with-hints, expandable
code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer.
- /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec
pre-filled, fork banner at top with link back to template.
- /servers/[id] new Publish tab with title, category, descriptions, per-secret
hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key).
- /admin/templates moderation with verify/hide/takedown actions.
- Marketing nav now includes /templates.
Verified end-to-end:
- Published Echo Demo Template from marco@test.local's live server
- Marketplace lists it correctly with stats
- Detail page renders with all sections
- Fork CTA navigates to wizard with ?template= param
- Wizard skips Step 1, shows fork banner, pre-fills spec
- Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND
render), container live on :4109 with proper OAuth 401 → token → 200 flow
- DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id
populated on the fork
- /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
|
|
|
async function loadPrebuiltCode(previewId: string): Promise<string | null> {
|
|
|
|
|
return (await cacheReader.get(`prebuilt:${previewId}`)) ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 00:26:53 +02:00
|
|
|
export const worker = new Worker<JobData>(
|
|
|
|
|
'build',
|
|
|
|
|
async (job) => {
|
feat(api,generator): preview endpoint + spec cache + audit-log writes
- POST /v1/servers/preview runs Claude synchronously, validates output, caches spec
in Redis under preview:<id> with 5min TTL, returns previewId+spec+detectedSecrets.
- POST /v1/servers accepts optional previewId; worker reuses the cached spec if
the entry is still present, otherwise regenerates fresh. Skips the second
Claude round-trip (~30s saved on the demoable path).
- audit() helper writes auth.login, auth.logout, server.create, server.iterate,
server.delete to audit_log with ip, metadata, resourceId.
- GET /v1/me/org returns organization + members list for the settings page.
- GET /v1/audit?limit=&action=&resourceType= returns scoped audit entries.
2026-05-19 18:08:29 +02:00
|
|
|
const { buildId, serverId, prompt, version, slug, secrets, previewId } = job.data;
|
2026-05-19 00:26:53 +02:00
|
|
|
const log = (level: 'info' | 'warn' | 'error', msg: string) => emitLog(buildId, level, msg);
|
|
|
|
|
|
2026-05-20 20:58:30 +02:00
|
|
|
// Capture the container currently serving this server (if any) BEFORE the
|
|
|
|
|
// build mutates the row. On an iterate (version > 1) we deploy the new
|
|
|
|
|
// container, then tear this old one down — rolling-deploy, no orphan.
|
|
|
|
|
const [priorState] = await db
|
|
|
|
|
.select({ containerId: mcpServers.containerId })
|
|
|
|
|
.from(mcpServers)
|
|
|
|
|
.where(eq(mcpServers.id, serverId))
|
|
|
|
|
.limit(1);
|
|
|
|
|
const oldContainerId = priorState?.containerId ?? null;
|
|
|
|
|
|
2026-05-19 00:26:53 +02:00
|
|
|
try {
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
await db
|
|
|
|
|
.update(builds)
|
|
|
|
|
.set({ status: 'generating', startedAt: new Date() })
|
|
|
|
|
.where(eq(builds.id, buildId));
|
|
|
|
|
await db
|
|
|
|
|
.update(mcpServers)
|
|
|
|
|
.set({ status: 'generating', updatedAt: new Date() })
|
|
|
|
|
.where(eq(mcpServers.id, serverId));
|
2026-05-19 00:26:53 +02:00
|
|
|
await emitStatus(buildId, 'generating');
|
|
|
|
|
|
feat(api,generator): preview endpoint + spec cache + audit-log writes
- POST /v1/servers/preview runs Claude synchronously, validates output, caches spec
in Redis under preview:<id> with 5min TTL, returns previewId+spec+detectedSecrets.
- POST /v1/servers accepts optional previewId; worker reuses the cached spec if
the entry is still present, otherwise regenerates fresh. Skips the second
Claude round-trip (~30s saved on the demoable path).
- audit() helper writes auth.login, auth.logout, server.create, server.iterate,
server.delete to audit_log with ip, metadata, resourceId.
- GET /v1/me/org returns organization + members list for the settings page.
- GET /v1/audit?limit=&action=&resourceType= returns scoped audit entries.
2026-05-19 18:08:29 +02:00
|
|
|
let spec: GeneratorSpec | null = null;
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
let source: 'claude' | 'glm' | 'mock' | 'cached' = 'mock';
|
feat(api,generator): preview endpoint + spec cache + audit-log writes
- POST /v1/servers/preview runs Claude synchronously, validates output, caches spec
in Redis under preview:<id> with 5min TTL, returns previewId+spec+detectedSecrets.
- POST /v1/servers accepts optional previewId; worker reuses the cached spec if
the entry is still present, otherwise regenerates fresh. Skips the second
Claude round-trip (~30s saved on the demoable path).
- audit() helper writes auth.login, auth.logout, server.create, server.iterate,
server.delete to audit_log with ip, metadata, resourceId.
- GET /v1/me/org returns organization + members list for the settings page.
- GET /v1/audit?limit=&action=&resourceType= returns scoped audit entries.
2026-05-19 18:08:29 +02:00
|
|
|
|
|
|
|
|
if (previewId) {
|
|
|
|
|
spec = await loadCachedSpec(previewId);
|
|
|
|
|
if (spec) {
|
|
|
|
|
source = 'cached';
|
|
|
|
|
await log('info', `Re-using preview spec ${previewId} (skipping Claude call)`);
|
|
|
|
|
} else {
|
|
|
|
|
await log('warn', `Preview ${previewId} cache miss — regenerating`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!spec) {
|
|
|
|
|
await log('info', 'Generating MCP server spec...');
|
|
|
|
|
const result = await generateSpec(prompt);
|
|
|
|
|
spec = result.spec;
|
|
|
|
|
source = result.source;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await log('info', `Spec ready via ${source} (${spec.tools.length} tool(s))`);
|
feat(marketplace): template publish + fork + voting/ranking + admin moderation
What this enables:
- A user builds an MCP server. If others would benefit, they click 'Publish as
template' on their server detail page. The spec + pre-rendered TypeScript
snapshot is preserved.
- Visitors browse /templates, filter by category, sort by trending/top/newest.
Each template card shows fork count + active deployment count as natural
manipulation-resistant popularity signal.
- /templates/[slug] shows the full plan: tool list with input schemas,
required-credential explanations (with 'how to get one' deep links), and a
collapsible code preview so users can audit before forking.
- Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and
pre-fills Step 2 with the template's parsed spec. Forker only fills in their
own credentials. mcp_servers.template_id is recorded; template.fork_count is
bumped atomically. Each fork gets its own isolated container with its own
port, its own AES-256 secrets — the template author has zero visibility into
the fork's traffic or data.
- Admin /admin/templates moderation: verify quality templates (shows shield
badge in marketplace), hide low-effort ones, takedown anything malicious.
Takedowns cascade-pause every fork container — owners must re-deploy.
Why template+fork instead of shared-container:
- Shared containers would mean the publisher's quota + their secrets + their
logs are exposed to forkers. Bad ergonomics, bad security, bad ownership.
- Templates/forks decouple the spec (shared, vouched-for) from the runtime
(isolated per user). Network-effect moat without the trust collapse.
Why no 5-star voting in v1:
- Manipulation-anfällig, empty lists without adoption. We use fork count +
active deploys + verified badge. Trending algorithm:
score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1)
Real signal, no brigading attack surface.
Backend:
- New schema: templates table (16 cols incl. tools_schema, generated_code,
required_secrets, allowedDomains, status enum, verified, fork_count).
- mcp_servers.template_id FK + idx for fork lookup.
- @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId.
- preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the
template's full rendered server.ts alongside the spec. Generator worker
detects this and skips the render step — uses the audited pre-built code
verbatim. Banned-pattern re-scan at publish time.
- routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern
re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via
SQL.
UI:
- /templates marketplace with trending/top/newest tabs, category filter, search.
Cards show forks + live count + author + verified badge.
- /templates/[slug] full detail with tools, credentials-with-hints, expandable
code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer.
- /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec
pre-filled, fork banner at top with link back to template.
- /servers/[id] new Publish tab with title, category, descriptions, per-secret
hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key).
- /admin/templates moderation with verify/hide/takedown actions.
- Marketing nav now includes /templates.
Verified end-to-end:
- Published Echo Demo Template from marco@test.local's live server
- Marketplace lists it correctly with stats
- Detail page renders with all sections
- Fork CTA navigates to wizard with ?template= param
- Wizard skips Step 1, shows fork banner, pre-fills spec
- Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND
render), container live on :4109 with proper OAuth 401 → token → 200 flow
- DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id
populated on the fork
- /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
|
|
|
|
|
|
|
|
// Forks supply pre-rendered code via Redis. If present, use it verbatim.
|
|
|
|
|
let generatedCode: string;
|
|
|
|
|
const prebuilt = previewId ? await loadPrebuiltCode(previewId) : null;
|
|
|
|
|
if (prebuilt) {
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
await log(
|
|
|
|
|
'info',
|
|
|
|
|
`Using pre-rendered template code (${prebuilt.length} chars) — skipping render`,
|
|
|
|
|
);
|
feat(marketplace): template publish + fork + voting/ranking + admin moderation
What this enables:
- A user builds an MCP server. If others would benefit, they click 'Publish as
template' on their server detail page. The spec + pre-rendered TypeScript
snapshot is preserved.
- Visitors browse /templates, filter by category, sort by trending/top/newest.
Each template card shows fork count + active deployment count as natural
manipulation-resistant popularity signal.
- /templates/[slug] shows the full plan: tool list with input schemas,
required-credential explanations (with 'how to get one' deep links), and a
collapsible code preview so users can audit before forking.
- Fork is one click → /servers/new?template=slug. The wizard skips Step 1 and
pre-fills Step 2 with the template's parsed spec. Forker only fills in their
own credentials. mcp_servers.template_id is recorded; template.fork_count is
bumped atomically. Each fork gets its own isolated container with its own
port, its own AES-256 secrets — the template author has zero visibility into
the fork's traffic or data.
- Admin /admin/templates moderation: verify quality templates (shows shield
badge in marketplace), hide low-effort ones, takedown anything malicious.
Takedowns cascade-pause every fork container — owners must re-deploy.
Why template+fork instead of shared-container:
- Shared containers would mean the publisher's quota + their secrets + their
logs are exposed to forkers. Bad ergonomics, bad security, bad ownership.
- Templates/forks decouple the spec (shared, vouched-for) from the runtime
(isolated per user). Network-effect moat without the trust collapse.
Why no 5-star voting in v1:
- Manipulation-anfällig, empty lists without adoption. We use fork count +
active deploys + verified badge. Trending algorithm:
score = (activeDeploys * 3 + forks) / sqrt(ageDays + 1)
Real signal, no brigading attack surface.
Backend:
- New schema: templates table (16 cols incl. tools_schema, generated_code,
required_secrets, allowedDomains, status enum, verified, fork_count).
- mcp_servers.template_id FK + idx for fork lookup.
- @bmm/types: SpecEdit unchanged, CreateServerInput accepts optional templateId.
- preview-cache.ts: new cachePrebuiltCode/loadPrebuiltCode for storing the
template's full rendered server.ts alongside the spec. Generator worker
detects this and skips the render step — uses the audited pre-built code
verbatim. Banned-pattern re-scan at publish time.
- routes/templates.ts: 5 public/auth routes + 2 admin routes. Banned-pattern
re-scan before publish. Slug auto-uniqued. forkCount atomic-increment via
SQL.
UI:
- /templates marketplace with trending/top/newest tabs, category filter, search.
Cards show forks + live count + author + verified badge.
- /templates/[slug] full detail with tools, credentials-with-hints, expandable
code preview, fork CTA, ownership + stats sidebar, 'forking is safe' explainer.
- /servers/new?template=slug — wizard auto-jumps to Step 2 with template spec
pre-filled, fork banner at top with link back to template.
- /servers/[id] new Publish tab with title, category, descriptions, per-secret
hint fields (description + howToGetUrl per UPPER_SNAKE_CASE key).
- /admin/templates moderation with verify/hide/takedown actions.
- Marketing nav now includes /templates.
Verified end-to-end:
- Published Echo Demo Template from marco@test.local's live server
- Marketplace lists it correctly with stats
- Detail page renders with all sections
- Fork CTA navigates to wizard with ?template= param
- Wizard skips Step 1, shows fork banner, pre-fills spec
- Build succeeds in ~10s (cached spec + prebuilt code path skips Claude AND
render), container live on :4109 with proper OAuth 401 → token → 200 flow
- DB: templates.fork_count=1, activeDeployments=1, mcp_servers.template_id
populated on the fork
- /admin/templates shows the new template with verify/hide/takedown controls
2026-05-19 23:22:35 +02:00
|
|
|
generatedCode = prebuilt;
|
|
|
|
|
} else {
|
|
|
|
|
generatedCode = renderServerCode(spec);
|
|
|
|
|
}
|
2026-05-19 00:26:53 +02:00
|
|
|
await db
|
|
|
|
|
.update(builds)
|
|
|
|
|
.set({ generatedSpec: spec, generatedCode })
|
|
|
|
|
.where(eq(builds.id, buildId));
|
|
|
|
|
|
|
|
|
|
await db.update(builds).set({ status: 'building' }).where(eq(builds.id, buildId));
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
await db
|
|
|
|
|
.update(mcpServers)
|
|
|
|
|
.set({ status: 'building', toolsSchema: spec.tools, updatedAt: new Date() })
|
|
|
|
|
.where(eq(mcpServers.id, serverId));
|
2026-05-19 00:26:53 +02:00
|
|
|
await emitStatus(buildId, 'building');
|
|
|
|
|
await log('info', 'Preparing build context...');
|
|
|
|
|
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
const { contextDir, imageTag } = await prepareBuildContext(
|
|
|
|
|
serverId,
|
|
|
|
|
version,
|
|
|
|
|
slug,
|
|
|
|
|
generatedCode,
|
|
|
|
|
spec,
|
|
|
|
|
);
|
2026-05-19 00:26:53 +02:00
|
|
|
await log('info', `Build context at ${contextDir}`);
|
|
|
|
|
|
|
|
|
|
await log('info', 'Running static checks...');
|
|
|
|
|
await staticCheck(contextDir);
|
|
|
|
|
await log('info', 'Static checks passed.');
|
|
|
|
|
|
|
|
|
|
const hasDocker = await dockerAvailable();
|
|
|
|
|
if (!hasDocker) {
|
|
|
|
|
await log('warn', 'Docker not available — skipping build/deploy. Server marked draft.');
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
await db
|
|
|
|
|
.update(builds)
|
|
|
|
|
.set({ status: 'failed', errorMessage: 'docker_unavailable', finishedAt: new Date() })
|
|
|
|
|
.where(eq(builds.id, buildId));
|
|
|
|
|
await db
|
|
|
|
|
.update(mcpServers)
|
|
|
|
|
.set({ status: 'failed', updatedAt: new Date() })
|
|
|
|
|
.where(eq(mcpServers.id, serverId));
|
2026-05-19 00:26:53 +02:00
|
|
|
await emitDone(buildId, 'failed', serverId, null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await log('info', `Building Docker image ${imageTag}...`);
|
|
|
|
|
await dockerBuild(contextDir, imageTag, (line) => {
|
|
|
|
|
emitLog(buildId, 'info', line).catch(() => undefined);
|
|
|
|
|
});
|
|
|
|
|
await log('info', 'Image built.');
|
|
|
|
|
|
|
|
|
|
await db.update(builds).set({ status: 'deploying' }).where(eq(builds.id, buildId));
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
await db
|
|
|
|
|
.update(mcpServers)
|
|
|
|
|
.set({ status: 'deploying', updatedAt: new Date() })
|
|
|
|
|
.where(eq(mcpServers.id, serverId));
|
2026-05-19 00:26:53 +02:00
|
|
|
await emitStatus(buildId, 'deploying');
|
|
|
|
|
|
|
|
|
|
const port = await allocatePort();
|
2026-05-28 17:54:56 +02:00
|
|
|
// The container's PUBLIC_URL must match what end-users (and Claude
|
|
|
|
|
// Desktop's DCR client) actually reach. When MCP_DOMAIN is set we
|
|
|
|
|
// route via https://<MCP_DOMAIN>/<slug>; the hardcoded loopback URL
|
|
|
|
|
// we used to inject caused the runner to advertise an unreachable
|
|
|
|
|
// resource_metadata URL in its WWW-Authenticate header, killing OAuth
|
|
|
|
|
// discovery from any external MCP client.
|
|
|
|
|
const publicUrl = computePublicUrl(slug, port);
|
2026-05-19 00:26:53 +02:00
|
|
|
const envVars: Record<string, string> = {
|
|
|
|
|
...secrets,
|
|
|
|
|
PUBLIC_URL: publicUrl,
|
|
|
|
|
CONTROL_PLANE_URL: config.CONTROL_PLANE_URL,
|
2026-05-19 00:57:23 +02:00
|
|
|
OAUTH_ISSUER: `${config.CONTROL_PLANE_PUBLIC_URL}/oauth`,
|
2026-05-19 00:26:53 +02:00
|
|
|
PORT: '3000',
|
|
|
|
|
SERVER_ID: serverId,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handle = await deployContainer({ serverId, slug, hostPort: port, imageTag, envVars });
|
feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate
limit on /preview, Opus default in the build worker, 5-min cache TTL that
made cache-miss the common case). This switches free users to GLM, paid
users to Claude tiers, and tightens every leak found in the audit.
Backend:
- @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel
+ pickBuildModel helpers, plan-aware ModelChoice
- preview-cache TTL 5min -> 24h (kills the cache-miss path)
- /v1/servers/preview: picks model from caller's plan, returns model name to UI
- /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds
- daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500)
- /v1/auth/me returns plan so the wizard can show the right model name
- generator worker: GLM default, Anthropic Sonnet fallback if GLM errors
Frontend:
- Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively,
upgrade hint for hobby users, friendly errors for 402 / 429
- Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus),
Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier
- Privacy + Security: explicit subprocessor disclosure for Anthropic (US) /
Zhipu (CN) and which tier uses which
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 23:50:00 +02:00
|
|
|
await log(
|
|
|
|
|
'info',
|
|
|
|
|
`Container ${handle.containerId.slice(0, 12)} running at ${handle.publicUrl}`,
|
|
|
|
|
);
|
2026-05-19 00:26:53 +02:00
|
|
|
|
@
fix(security): sovereign-audit hardening pass — RCE, multi-tenant, reliability
Reasoning-based audit fixes (all verified by typecheck, attack paths re-traced):
- build-time RCE: validate spec.dependencies to npm-registry semver only
(no git/url/file specifiers) + --ignore-scripts in runner Dockerfile.
- container hardening fail-CLOSED: harden unless RUNNER_DISABLE_HARDENING=1,
no longer gated on a fragile NODE_ENV string compare.
- secret env keys validated (UPPER_SNAKE, reject NODE_*/PATH/LD_*).
- cross-org image-tag collision: qualify tag with serverId.
- /iterate now enforces suspension + daily-build limits like /servers.
- preview SSE: clear keepalive in finally + on client close (timer/FD leak).
- SMS OTP: atomic attempt counter (lt(attempts,MAX) in UPDATE) — brute-force race.
- getSession orders membership by createdAt (deterministic primary org).
- template scopes aggregated from real tool scopes (was hardcoded mcp:read).
- template category filter pushed into WHERE (was applied after LIMIT).
- support admin reply/status: 404 on unknown ticket; status change now audited.
- build worker: queue defaultJobOptions, docker build/run/stop timeouts,
old-container teardown in finally (no orphan on post-deploy DB failure).
- nginx: HSTS, X-Frame-Options DENY, nosniff, Referrer-Policy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-05-29 20:56:30 +02:00
|
|
|
try {
|
|
|
|
|
await db
|
|
|
|
|
.update(builds)
|
|
|
|
|
.set({ status: 'success', finishedAt: new Date() })
|
|
|
|
|
.where(eq(builds.id, buildId));
|
|
|
|
|
await db
|
|
|
|
|
.update(mcpServers)
|
|
|
|
|
.set({
|
|
|
|
|
status: 'live',
|
|
|
|
|
currentVersion: version,
|
|
|
|
|
publicUrl: handle.publicUrl,
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})
|
|
|
|
|
.where(eq(mcpServers.id, serverId));
|
|
|
|
|
} finally {
|
|
|
|
|
// Rolling deploy: retire the previous container even if the success DB
|
|
|
|
|
// writes above threw — otherwise a DB hiccup after a healthy deploy
|
|
|
|
|
// leaves the old container orphaned, holding its host port. The new
|
|
|
|
|
// container is already live and its id is persisted in deployContainer. (GEN-007)
|
|
|
|
|
if (oldContainerId && oldContainerId !== handle.containerId) {
|
|
|
|
|
const stopped = await stopContainer(oldContainerId);
|
|
|
|
|
await log(
|
|
|
|
|
stopped.ok ? 'info' : 'warn',
|
|
|
|
|
stopped.ok
|
|
|
|
|
? `Retired previous container ${oldContainerId.slice(0, 12)}`
|
|
|
|
|
: `Could not stop previous container ${oldContainerId.slice(0, 12)}: ${stopped.detail}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-20 20:58:30 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-19 00:26:53 +02:00
|
|
|
await emitStatus(buildId, 'success');
|
|
|
|
|
await emitDone(buildId, 'success', serverId, handle.publicUrl);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
|
|
|
console.error('[worker] build failed:', err);
|
|
|
|
|
await db
|
|
|
|
|
.update(builds)
|
|
|
|
|
.set({ status: 'failed', errorMessage: msg, finishedAt: new Date() })
|
|
|
|
|
.where(eq(builds.id, buildId));
|
|
|
|
|
await db
|
|
|
|
|
.update(mcpServers)
|
|
|
|
|
.set({ status: 'failed', updatedAt: new Date() })
|
|
|
|
|
.where(eq(mcpServers.id, serverId));
|
|
|
|
|
await emitError(buildId, msg);
|
|
|
|
|
await emitDone(buildId, 'failed', serverId, null);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ connection, concurrency: 2 },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
worker.on('ready', () => console.log('[generator] worker ready'));
|
|
|
|
|
worker.on('failed', (job, err) => console.error('[generator] job failed', job?.id, err?.message));
|
|
|
|
|
worker.on('error', (err) => console.error('[generator] worker error', err.message));
|