buildmymcpserver/apps/web/app/(dashboard)/servers/[id]/page.tsx

668 lines
23 KiB
TypeScript
Raw Normal View History

'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { apiFetch } from '@/lib/api';
import { StatusPill } from '@/components/status-pill';
import { CodeBlock } from '@/components/code-block';
import { InstallSnippets } from '@/components/install-snippets';
import { StreamingLogs } from '@/components/streaming-logs';
import { Button } from '@/components/ui/button';
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
import { Input, Textarea, Label } from '@/components/input';
import { cn } from '@/lib/cn';
import type { ToolSpec } from '@bmm/types';
interface ServerDetail {
id: string;
name: string;
slug: string;
status: string;
publicUrl: string | null;
toolsSchema: ToolSpec[] | null;
currentVersion: number;
oauthEnabled: boolean;
createdAt: string;
updatedAt: string;
}
interface BuildSummary {
id: string;
version: number;
status: string;
startedAt: string | null;
finishedAt: string | null;
errorMessage: string | null;
}
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
type Tab = 'overview' | 'tools' | 'logs' | 'metrics' | 'secrets' | 'iterate' | 'publish';
export default function ServerDetailPage() {
const params = useParams<{ id: string }>();
const router = useRouter();
const [server, setServer] = useState<ServerDetail | null>(null);
const [builds, setBuilds] = useState<BuildSummary[]>([]);
const [tab, setTab] = useState<Tab>('overview');
const [iteratePrompt, setIteratePrompt] = useState('');
const [latestBuildId, setLatestBuildId] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
const [deleteError, setDeleteError] = useState<string | null>(null);
async function refresh() {
const r = await apiFetch<{ server: ServerDetail; builds: BuildSummary[] }>(
`/v1/servers/${params.id}`,
);
setServer(r.server);
setBuilds(r.builds);
if (r.builds.length > 0 && !latestBuildId) {
setLatestBuildId(r.builds[0]!.id);
}
}
useEffect(() => {
refresh();
const t = setInterval(refresh, 4000);
return () => clearInterval(t);
}, [params.id]);
async function onIterate() {
if (!server) return;
const res = await apiFetch<{ build: { id: string } }>(
`/v1/servers/${server.id}/iterate`,
{ method: 'POST', body: JSON.stringify({ prompt: iteratePrompt, secrets: {} }) },
);
setLatestBuildId(res.build.id);
setIteratePrompt('');
setTab('logs');
}
async function onDelete() {
if (!server) return;
// Destructive — the running container is torn down and the row is gone.
// Browser confirm is enough at this scope (single operator, no users yet);
// upgrade to a typed-confirmation dialog once we have customer-tier data.
const sure = window.confirm(
`Delete "${server.name}" (${server.slug})? The running container is stopped and the server row is removed. This cannot be undone.`,
);
if (!sure) return;
setDeleting(true);
setDeleteError(null);
try {
await apiFetch(`/v1/servers/${server.id}`, { method: 'DELETE' });
router.push('/servers');
} catch (e) {
const detail = (e as { detail?: { error?: string; detail?: string } }).detail;
setDeleteError(detail?.detail ?? detail?.error ?? (e as Error).message);
setDeleting(false);
}
}
if (!server) {
return (
<div className="mx-auto max-w-7xl px-6 py-8 text-[12.5px] text-[--color-fg-muted]">Loading</div>
);
}
const tabs: { id: Tab; label: string }[] = [
{ id: 'overview', label: 'Overview' },
{ id: 'tools', label: 'Tools' },
{ id: 'logs', label: 'Logs' },
{ id: 'metrics', label: 'Metrics' },
{ id: 'secrets', label: 'Secrets' },
{ id: 'iterate', label: 'Iterate' },
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
{ id: 'publish', label: 'Publish' },
];
return (
<div className="mx-auto max-w-7xl px-6 py-8">
<div className="flex items-baseline justify-between gap-4">
<div>
<div className="flex items-center gap-2.5">
<h1 className="text-[22px] font-semibold tracking-tight">{server.name}</h1>
<StatusPill status={server.status as never} />
</div>
<div className="mt-1 flex items-center gap-3 text-[12.5px] text-[--color-fg-muted]">
<span className="mono">{server.slug}</span>
<span>·</span>
<span>v{server.currentVersion}</span>
{server.publicUrl && (
<>
<span>·</span>
<a
href={`${server.publicUrl}/mcp`}
target="_blank"
rel="noreferrer"
className="mono hover:text-[--color-fg]"
>
{server.publicUrl}/mcp
</a>
</>
)}
</div>
{deleteError && (
<div className="mt-2 text-[12px] text-[--color-danger]">
Delete failed: {deleteError}
</div>
)}
</div>
<div className="flex shrink-0 items-center gap-2">
<Button variant="danger" size="sm" onClick={onDelete} disabled={deleting}>
{deleting ? 'Deleting…' : 'Delete server'}
</Button>
</div>
</div>
<div className="mt-6 flex gap-1 border-b border-[--color-border]">
{tabs.map((t) => (
<button
key={t.id}
type="button"
onClick={() => setTab(t.id)}
className={cn(
'relative px-3 py-2 text-[12.5px] transition-colors duration-200 ease-out',
tab === t.id
? 'text-[--color-fg]'
: 'text-[--color-fg-muted] hover:text-[--color-fg]',
)}
>
{t.label}
{tab === t.id && (
<span className="absolute inset-x-0 -bottom-px h-px bg-[--color-fg]" />
)}
</button>
))}
</div>
<div className="mt-6">
{tab === 'overview' && (
<div className="grid gap-4 md:grid-cols-2">
<div className="panel p-4">
<div className="text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">
Endpoint
</div>
<div className="mt-3">
{server.publicUrl ? (
<CodeBlock code={`${server.publicUrl}/mcp`} />
) : (
<div className="text-[12.5px] text-[--color-fg-muted]">Not deployed yet.</div>
)}
</div>
<div className="mt-4 flex items-center gap-2 text-[12px] text-[--color-fg-muted]">
<span className="size-1.5 rounded-full bg-emerald-400" />
OAuth 2.1 enforced · Streamable HTTP
</div>
</div>
<div className="panel p-4">
<div className="text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">
Builds
</div>
<div className="mt-3 space-y-2">
{builds.slice(0, 5).map((b) => (
<div key={b.id} className="flex items-center justify-between text-[12.5px]">
<span className="mono text-[--color-fg-muted]">v{b.version}</span>
<StatusPill status={b.status as never} />
<span className="mono text-[11px] text-[--color-fg-subtle]">
{b.startedAt ? new Date(b.startedAt).toLocaleString() : '—'}
</span>
</div>
))}
</div>
</div>
{server.publicUrl && (
<div className="panel p-4 md:col-span-2">
<div className="text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">
Install
</div>
<div className="mt-3">
<InstallSnippets
input={{ name: server.name, slug: server.slug, publicUrl: server.publicUrl }}
/>
</div>
</div>
)}
</div>
)}
{tab === 'tools' && (
<div className="panel p-4">
{!server.toolsSchema || server.toolsSchema.length === 0 ? (
<div className="text-[12.5px] text-[--color-fg-muted]">No tools yet.</div>
) : (
<div className="space-y-3">
{server.toolsSchema.map((tool) => (
<div key={tool.name} className="rounded-md border border-[--color-border] p-3">
<div className="flex items-baseline gap-2">
<span className="mono text-[13px] font-semibold">{tool.name}</span>
</div>
<p className="mt-1 text-[12.5px] text-[--color-fg-muted]">{tool.description}</p>
{Object.keys(tool.inputSchema ?? {}).length > 0 && (
<div className="mt-2">
<CodeBlock
label="input schema"
code={JSON.stringify(tool.inputSchema, null, 2)}
/>
</div>
)}
</div>
))}
</div>
)}
</div>
)}
{tab === 'logs' && (
latestBuildId ? (
<StreamingLogs buildId={latestBuildId} />
) : (
<div className="panel p-4 text-[12.5px] text-[--color-fg-muted]">
No builds to stream yet.
</div>
)
)}
{tab === 'metrics' && (
<div className="panel p-4">
<div className="grid gap-4 md:grid-cols-3">
<Metric label="Calls (24h)" value="0" />
<Metric label="P95 latency" value="—" />
<Metric label="Error rate" value="0%" />
</div>
<p className="mt-4 text-[12px] text-[--color-fg-subtle]">
Live metrics begin streaming after the first tool call.
</p>
</div>
)}
{tab === 'secrets' && (
<div className="panel p-4 text-[12.5px] text-[--color-fg-muted]">
Secrets are AES-256-GCM encrypted at rest. They are injected as environment variables
into your container and are never echoed back. Use the wizard or CLI to rotate them.
</div>
)}
{tab === 'iterate' && (
<div className="panel p-4">
<Label hint="A new build will be queued; rolling deploy keeps current version live until ready.">
Extend the server
</Label>
<Textarea
rows={4}
className="mt-2"
value={iteratePrompt}
onChange={(e) => setIteratePrompt(e.target.value)}
placeholder="Add a tool: create_page that POSTs to /v1/pages with title and body."
/>
<div className="mt-3 flex justify-end">
<Button
variant="primary"
size="md"
disabled={iteratePrompt.length < 10}
onClick={onIterate}
>
Queue rebuild
</Button>
</div>
</div>
)}
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
{tab === 'publish' && <PublishPanel serverId={server.id} serverStatus={server.status} />}
</div>
</div>
);
}
const CATEGORIES = [
'productivity',
'developer-tools',
'data',
'communication',
'finance',
'crm',
'analytics',
'devops',
'demo',
'other',
];
interface SecretHint {
key: string;
description: string;
howToGetUrl: string;
}
feat(marketplace): default-on share in wizard + owner unshare anytime Goal: maximize template volume without a dark pattern and without leaking data. Wizard Done-page Share panel: - 'Share as template in the marketplace (recommended)' checkbox, default ON, rendered inline in the build-success flow where every user lands. - Honest copy — corrected a draft that claimed 'only abstracted code pattern is shared'. That is false: the FULL generated code becomes publicly viewable on the template detail page (by design, for pre-fork audit). The panel now says: 'Your secrets stay private ... but your generated code becomes publicly viewable so others can audit it before forking. Unshare anytime.' - When checked: inline minimal form — short description (prefilled from the spec), category select, optional per-secret credential hints. One 'Publish to marketplace' click. Not auto-published silently — that would be a consent dark pattern; one visible deliberate click keeps it clean. - Forked servers don't show the panel (re-publishing a fork is an edge case). Owner unshare/reshare: - GET /v1/servers/:id/template — owner lookup, drives the Publish tab UI. - PATCH /v1/templates/:slug/visibility { shared } — owner-only toggle between public and hidden. 403 for non-owners, 409 if an admin took it down (owner cannot resurrect an admin takedown). Audit-logged as template.unshare / template.reshare. - Server-detail Publish tab now detects an existing template and shows the shared status (public/hidden/takedown badge), fork count, a marketplace link and an Unshare/Re-share button — instead of the publish form. Why this is safe to default ON: - Secrets are architecturally bound to mcp_servers, never copied into templates. Publish reads tools_schema + generated_code only; the secrets table is never touched. Data leak is structurally impossible, not policy-dependent. - Publish re-scans the generated code for banned patterns AND hardcoded credentials (sovereign-audit hardening) before it can reach the marketplace. - The user sees a visible, pre-ticked checkbox and reads one honest sentence before publishing. Privacy-conscious users untick; everyone else contributes volume. Informed consent, GDPR-clean. Verified end-to-end via API: GET server/:id/template -> null (unpublished) POST /v1/templates -> published, slug share-test-server GET server/:id/template -> status public PATCH visibility {shared:false} -> hidden, drops out of public list PATCH visibility {shared:true} -> public again UI: Publish tab renders the shared-status panel with View + Unshare (screenshot confirmed). Also: hero badge date set to 2026-05-20. Changed 'MCP spec 2025-11-25' to 'updated 2026-05-20' — claiming an MCP spec dated today would be factually wrong (no such spec release exists); 'updated' is accurate and gives the requested fresh date. The real spec date is still cited correctly in /docs.
2026-05-20 17:04:46 +02:00
interface ExistingTemplate {
id: string;
slug: string;
title: string;
status: 'draft' | 'public' | 'hidden' | 'takedown';
verified: boolean;
forkCount: number;
}
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
function PublishPanel({ serverId, serverStatus }: { serverId: string; serverStatus: string }) {
const [title, setTitle] = useState('');
const [category, setCategory] = useState('other');
const [shortDescription, setShortDescription] = useState('');
const [longDescription, setLongDescription] = useState('');
const [secretHints, setSecretHints] = useState<SecretHint[]>([]);
const [state, setState] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
const [error, setError] = useState<string | null>(null);
const [publishedSlug, setPublishedSlug] = useState<string | null>(null);
feat(marketplace): default-on share in wizard + owner unshare anytime Goal: maximize template volume without a dark pattern and without leaking data. Wizard Done-page Share panel: - 'Share as template in the marketplace (recommended)' checkbox, default ON, rendered inline in the build-success flow where every user lands. - Honest copy — corrected a draft that claimed 'only abstracted code pattern is shared'. That is false: the FULL generated code becomes publicly viewable on the template detail page (by design, for pre-fork audit). The panel now says: 'Your secrets stay private ... but your generated code becomes publicly viewable so others can audit it before forking. Unshare anytime.' - When checked: inline minimal form — short description (prefilled from the spec), category select, optional per-secret credential hints. One 'Publish to marketplace' click. Not auto-published silently — that would be a consent dark pattern; one visible deliberate click keeps it clean. - Forked servers don't show the panel (re-publishing a fork is an edge case). Owner unshare/reshare: - GET /v1/servers/:id/template — owner lookup, drives the Publish tab UI. - PATCH /v1/templates/:slug/visibility { shared } — owner-only toggle between public and hidden. 403 for non-owners, 409 if an admin took it down (owner cannot resurrect an admin takedown). Audit-logged as template.unshare / template.reshare. - Server-detail Publish tab now detects an existing template and shows the shared status (public/hidden/takedown badge), fork count, a marketplace link and an Unshare/Re-share button — instead of the publish form. Why this is safe to default ON: - Secrets are architecturally bound to mcp_servers, never copied into templates. Publish reads tools_schema + generated_code only; the secrets table is never touched. Data leak is structurally impossible, not policy-dependent. - Publish re-scans the generated code for banned patterns AND hardcoded credentials (sovereign-audit hardening) before it can reach the marketplace. - The user sees a visible, pre-ticked checkbox and reads one honest sentence before publishing. Privacy-conscious users untick; everyone else contributes volume. Informed consent, GDPR-clean. Verified end-to-end via API: GET server/:id/template -> null (unpublished) POST /v1/templates -> published, slug share-test-server GET server/:id/template -> status public PATCH visibility {shared:false} -> hidden, drops out of public list PATCH visibility {shared:true} -> public again UI: Publish tab renders the shared-status panel with View + Unshare (screenshot confirmed). Also: hero badge date set to 2026-05-20. Changed 'MCP spec 2025-11-25' to 'updated 2026-05-20' — claiming an MCP spec dated today would be factually wrong (no such spec release exists); 'updated' is accurate and gives the requested fresh date. The real spec date is still cited correctly in /docs.
2026-05-20 17:04:46 +02:00
const [existing, setExisting] = useState<ExistingTemplate | null | undefined>(undefined);
async function reloadExisting() {
try {
const r = await apiFetch<{ template: ExistingTemplate | null }>(
`/v1/servers/${serverId}/template`,
);
setExisting(r.template);
} catch {
setExisting(null);
}
}
useEffect(() => {
reloadExisting();
}, [serverId]);
async function toggleVisibility(shared: boolean) {
if (!existing) return;
await apiFetch(`/v1/templates/${existing.slug}/visibility`, {
method: 'PATCH',
body: JSON.stringify({ shared }),
});
reloadExisting();
}
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
if (serverStatus !== 'live') {
return (
<div className="panel p-4">
<p className="text-[13px] text-[--color-fg-muted]">
Server must be live before publishing as a template. Current status:{' '}
<span className="mono">{serverStatus}</span>.
</p>
</div>
);
}
feat(marketplace): default-on share in wizard + owner unshare anytime Goal: maximize template volume without a dark pattern and without leaking data. Wizard Done-page Share panel: - 'Share as template in the marketplace (recommended)' checkbox, default ON, rendered inline in the build-success flow where every user lands. - Honest copy — corrected a draft that claimed 'only abstracted code pattern is shared'. That is false: the FULL generated code becomes publicly viewable on the template detail page (by design, for pre-fork audit). The panel now says: 'Your secrets stay private ... but your generated code becomes publicly viewable so others can audit it before forking. Unshare anytime.' - When checked: inline minimal form — short description (prefilled from the spec), category select, optional per-secret credential hints. One 'Publish to marketplace' click. Not auto-published silently — that would be a consent dark pattern; one visible deliberate click keeps it clean. - Forked servers don't show the panel (re-publishing a fork is an edge case). Owner unshare/reshare: - GET /v1/servers/:id/template — owner lookup, drives the Publish tab UI. - PATCH /v1/templates/:slug/visibility { shared } — owner-only toggle between public and hidden. 403 for non-owners, 409 if an admin took it down (owner cannot resurrect an admin takedown). Audit-logged as template.unshare / template.reshare. - Server-detail Publish tab now detects an existing template and shows the shared status (public/hidden/takedown badge), fork count, a marketplace link and an Unshare/Re-share button — instead of the publish form. Why this is safe to default ON: - Secrets are architecturally bound to mcp_servers, never copied into templates. Publish reads tools_schema + generated_code only; the secrets table is never touched. Data leak is structurally impossible, not policy-dependent. - Publish re-scans the generated code for banned patterns AND hardcoded credentials (sovereign-audit hardening) before it can reach the marketplace. - The user sees a visible, pre-ticked checkbox and reads one honest sentence before publishing. Privacy-conscious users untick; everyone else contributes volume. Informed consent, GDPR-clean. Verified end-to-end via API: GET server/:id/template -> null (unpublished) POST /v1/templates -> published, slug share-test-server GET server/:id/template -> status public PATCH visibility {shared:false} -> hidden, drops out of public list PATCH visibility {shared:true} -> public again UI: Publish tab renders the shared-status panel with View + Unshare (screenshot confirmed). Also: hero badge date set to 2026-05-20. Changed 'MCP spec 2025-11-25' to 'updated 2026-05-20' — claiming an MCP spec dated today would be factually wrong (no such spec release exists); 'updated' is accurate and gives the requested fresh date. The real spec date is still cited correctly in /docs.
2026-05-20 17:04:46 +02:00
// Already published — show shared status + view + unshare/reshare.
if (existing) {
const isTakedown = existing.status === 'takedown';
const isShared = existing.status === 'public';
return (
<div className="panel p-4">
<div className="flex items-baseline justify-between">
<h3 className="text-[14px] font-semibold tracking-tight">Marketplace</h3>
<span
className={`mono rounded-full border px-2 py-0.5 text-[11px] ${
isTakedown
? 'border-red-400/40 bg-red-400/10 text-red-300'
: isShared
? 'border-emerald-400/40 bg-emerald-400/10 text-emerald-300'
: 'border-amber-400/40 bg-amber-400/10 text-amber-300'
}`}
>
{existing.status}
</span>
</div>
<p className="mt-1 text-[12.5px] text-[--color-fg-muted]">
Published as <span className="mono">{existing.slug}</span> ·{' '}
{existing.forkCount} fork{existing.forkCount === 1 ? '' : 's'}
{existing.verified && ' · verified'}
</p>
{isTakedown && (
<p className="mt-2 text-[12px] text-[--color-danger]">
An admin removed this template from the marketplace. You can&apos;t re-share it.
</p>
)}
<div className="mt-3 flex gap-2">
<a
href={`/templates/${existing.slug}`}
target="_blank"
rel="noreferrer"
className="inline-flex h-8 items-center rounded-md border border-[--color-border] bg-[--color-bg-elevated] px-3 text-[12.5px] text-[--color-fg] transition-colors hover:bg-[--color-bg-subtle]"
>
View in marketplace
</a>
{!isTakedown && isShared && (
<Button variant="danger" size="md" onClick={() => toggleVisibility(false)}>
Unshare
</Button>
)}
{!isTakedown && !isShared && (
<Button variant="primary" size="md" onClick={() => toggleVisibility(true)}>
Re-share
</Button>
)}
</div>
</div>
);
}
if (existing === undefined) {
return <div className="panel p-4 text-[12.5px] text-[--color-fg-muted]">Loading</div>;
}
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
function addHint() {
setSecretHints((h) => [...h, { key: '', description: '', howToGetUrl: '' }]);
}
function updateHint(i: number, patch: Partial<SecretHint>) {
setSecretHints((h) => {
const next = [...h];
const cur = next[i];
if (!cur) return h;
next[i] = { ...cur, ...patch };
return next;
});
}
function removeHint(i: number) {
setSecretHints((h) => h.filter((_, j) => j !== i));
}
async function submit() {
setError(null);
if (title.length < 3) {
setError('Title needs at least 3 characters.');
return;
}
if (shortDescription.length < 10) {
setError('Short description needs at least 10 characters.');
return;
}
// Validate secret hints
for (const h of secretHints) {
if (!h.key) {
setError('Empty secret key — remove the row or fill it in.');
return;
}
if (!/^[A-Z][A-Z0-9_]*$/.test(h.key)) {
setError(`Secret key "${h.key}" must be UPPER_SNAKE_CASE.`);
return;
}
if (h.description.length < 1) {
setError(`Add a description for ${h.key}.`);
return;
}
}
setState('submitting');
try {
const res = await apiFetch<{ template: { slug: string } }>('/v1/templates', {
method: 'POST',
body: JSON.stringify({
serverId,
title,
category,
shortDescription,
longDescription: longDescription || undefined,
secretHints: secretHints.map((h) => ({
key: h.key,
description: h.description,
...(h.howToGetUrl ? { howToGetUrl: h.howToGetUrl } : {}),
})),
}),
});
setPublishedSlug(res.template.slug);
setState('success');
} catch (e) {
const detail = (e as { detail?: { error?: string; detail?: string } }).detail;
setError(detail?.detail ?? detail?.error ?? (e as Error).message);
setState('error');
}
}
if (state === 'success' && publishedSlug) {
return (
<div className="panel p-4">
<div className="text-[14px] font-semibold tracking-tight">Published 🎉</div>
<p className="mt-2 text-[12.5px] text-[--color-fg-muted]">
Your template is live on the marketplace.
</p>
<div className="mt-3 flex gap-2">
<a
href={`/templates/${publishedSlug}`}
className="inline-flex h-8 items-center rounded-md bg-[--color-accent] px-3 text-[12.5px] font-medium text-white transition-colors hover:bg-[#5557e8]"
>
Open in marketplace
</a>
</div>
</div>
);
}
return (
<div className="panel p-4 space-y-4">
<div>
<h3 className="text-[14px] font-semibold tracking-tight">Publish as template</h3>
<p className="mt-1 text-[12px] text-[--color-fg-muted]">
Share this server&apos;s spec on the public marketplace. Others fork in one click they
run their own container with their own credentials. Your secrets are never shared.
</p>
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
<p className="mt-2 rounded-md border border-amber-400/30 bg-amber-400/5 p-2 text-[11.5px] text-amber-200/90">
Publishing re-scans your generated code for banned patterns (eval, child_process,
dynamic import, setTimeout-eval) AND for hardcoded credentials (API keys, tokens,
private keys). Templates with detected leaks are rejected before they reach the
marketplace. Network egress allowlisting is on the roadmap for now any template
can reach any URL its code names.
</p>
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
</div>
<div className="grid gap-3 md:grid-cols-[1fr_220px]">
<div className="space-y-1.5">
<Label htmlFor="t-title">Title</Label>
<Input
id="t-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Notion Reader"
maxLength={128}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="t-cat">Category</Label>
<select
id="t-cat"
value={category}
onChange={(e) => setCategory(e.target.value)}
className="h-8 w-full rounded-md border border-[--color-border] bg-[--color-bg-subtle] px-2.5 text-[13px] focus:border-[--color-accent] focus:outline-none"
>
{CATEGORIES.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="t-short" hint={`${shortDescription.length}/280`}>
Short description
</Label>
<Input
id="t-short"
value={shortDescription}
onChange={(e) => setShortDescription(e.target.value)}
placeholder="Search and read pages from a Notion workspace."
maxLength={280}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="t-long">Long description (optional)</Label>
<Textarea
id="t-long"
rows={4}
value={longDescription}
onChange={(e) => setLongDescription(e.target.value)}
placeholder="What does it do, what doesn't it do, what should the user know before forking?"
/>
</div>
<div className="space-y-2">
<Label hint="Help future users understand which credentials they need and where to get them">
Credential hints
</Label>
{secretHints.length === 0 && (
<p className="text-[11.5px] text-[--color-fg-muted]">
None. This server doesn&apos;t need any credentials.
</p>
)}
{secretHints.map((h, i) => (
<div key={i} className="panel-subtle p-2.5 space-y-2">
<div className="grid grid-cols-[200px_1fr_auto] gap-2">
<Input
placeholder="NOTION_API_KEY"
value={h.key}
onChange={(e) => updateHint(i, { key: e.target.value.toUpperCase() })}
className="mono"
/>
<Input
placeholder="What is this credential? One sentence."
value={h.description}
onChange={(e) => updateHint(i, { description: e.target.value })}
/>
<Button variant="ghost" size="md" onClick={() => removeHint(i)}>
Remove
</Button>
</div>
<Input
placeholder="https://notion.so/my-integrations (optional 'How to get one' link)"
value={h.howToGetUrl}
onChange={(e) => updateHint(i, { howToGetUrl: e.target.value })}
/>
</div>
))}
<Button variant="ghost" size="sm" onClick={addHint}>
+ Add credential hint
</Button>
</div>
{error && <p className="text-[12.5px] text-[--color-danger]">{error}</p>}
<div className="flex items-center justify-between border-t border-[--color-border] pt-3">
<p className="text-[11px] text-[--color-fg-subtle]">
By publishing, you agree the generated code may be inspected by others.
</p>
<Button
variant="primary"
size="md"
onClick={submit}
disabled={state === 'submitting'}
>
{state === 'submitting' ? 'Publishing…' : 'Publish'}
</Button>
</div>
</div>
);
}
function Metric({ label, value }: { label: string; value: string }) {
return (
<div>
<div className="text-[11px] uppercase tracking-wider text-[--color-fg-subtle]">{label}</div>
<div className="mt-1 text-[22px] font-semibold tracking-tight">{value}</div>
</div>
);
}