feat(wizard): editable spec in step 2 — name, description, JSON schema, secrets

The wizard's confirm step is no longer read-only. Users can refine what Claude
parsed before committing to a build.

Backend:
- @bmm/types adds SpecEdit (tools[name,description,inputSchema] + requiredSecrets);
  CreateServerInput accepts an optional specEdit alongside previewId.
- Servers create endpoint: when specEdit is provided, loads cached spec from Redis,
  index-merges the edits in (keeping LLM-generated implementations untouched),
  re-validates via GeneratorSpec, re-runs the banned-pattern scan, overwrites the
  Redis cache so the worker reads the user's version. Refuses with
  preview_expired/tool_count_mismatch/banned_pattern on safety failures.
- New overwriteSpec() helper in preview-cache.

Frontend:
- Step 2 renders each tool as an editable card: name input, description textarea,
  JSON schema textarea with parse-on-keystroke validation (inline error if invalid).
- Required secrets list is editable: keys via uppercase-snake-case input, +Add /
  remove buttons, secret values kept in sync when keys are renamed.
- Reset-to-AI-suggestion button appears when edits are dirty.
- Pre-submit validation: schema must parse, secret keys must match UPPER_SNAKE_CASE,
  required secret values must be provided.
- Warning copy: 'Renaming parameters may require an Iterate after build — the
  existing impl references the original names.'

Verified end-to-end via browser smoke test: edited description + renamed tool
landed correctly in mcp_servers.tools_schema and in the live container at :4107.
Implementation field preserved from the original cached spec.
This commit is contained in:
Marco Sadjadi
2026-05-19 22:10:26 +02:00
parent 09688c1114
commit dda8f94de4
4 changed files with 367 additions and 75 deletions

View File

@@ -111,6 +111,23 @@ export type BuildEvent = z.infer<typeof BuildEvent>;
// ---- API request payloads ----
export const SpecEditTool = z.object({
name: z
.string()
.min(1)
.max(64)
.regex(/^[a-z][a-z0-9_]*$/, 'snake_case identifier required'),
description: z.string().min(1).max(2000),
inputSchema: z.record(z.string(), ToolParam),
});
export type SpecEditTool = z.infer<typeof SpecEditTool>;
export const SpecEdit = z.object({
tools: z.array(SpecEditTool).min(1).max(50),
requiredSecrets: z.array(z.string().regex(/^[A-Z][A-Z0-9_]*$/)).max(30).default([]),
});
export type SpecEdit = z.infer<typeof SpecEdit>;
export const CreateServerInput = z.object({
name: z.string().min(1).max(128),
slug: z
@@ -121,6 +138,7 @@ export const CreateServerInput = z.object({
prompt: z.string().min(10).max(8000),
secrets: z.record(z.string(), z.string()).default({}),
previewId: z.string().min(1).max(64).optional(),
specEdit: SpecEdit.optional(),
});
export type CreateServerInput = z.infer<typeof CreateServerInput>;