fix(mcp): apply_patch preserves schema-defaulted ids in multi-op batches
SceneBridge.applyPatch now threads the Zod-parsed node through to the apply phase instead of the raw input. Previously, when a caller sent a create patch without an `id` field, the schema's objectId default ran during dry-run parsing but only in `res.data`; the apply phase pushed the unparsed `p.node` (no id) to the store, so subsequent tools that walked `level.children` crashed on undefined entries (e.g. duplicate_level -> cloneLevelSubtree -> extractIdPrefix(undefined)). Also adds test-reports/ artefacts from live end-to-end testing: - t1-stdio: 21/21 tools pass via stdio (~106ms) - t2-http: connect/single-session behaviour (HTTP transport quirk documented) - t3-scenario: 2-bedroom apartment built end-to-end — 12/12 steps after this fix (24 final nodes, validate=true, apartment.json exported) - t4-errors: 24/24 invalid-input cases rejected with proper MCP errors - t5-resources-prompts: 4/4 resources, 3/3 prompts, dev server /api/health 200 OK Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a6f1c4140f
commit
b37e88cb83
@@ -283,6 +283,10 @@ export class SceneBridge {
|
||||
// earlier-created ids and reflect earlier-deleted ids.
|
||||
const simAvailable = new Set<string>(Object.keys(nodes))
|
||||
const simDeleted = new Set<string>()
|
||||
// Parsed create nodes keyed by patch index — so the apply phase can use the
|
||||
// Zod-normalised copy (which has a generated id if the caller omitted one)
|
||||
// instead of the unparsed input.
|
||||
const parsedCreateNodes = new Map<number, AnyNode>()
|
||||
|
||||
for (let i = 0; i < patches.length; i++) {
|
||||
const p = patches[i]
|
||||
@@ -297,7 +301,8 @@ export class SceneBridge {
|
||||
if (p.parentId !== undefined && !simAvailable.has(p.parentId)) {
|
||||
throw new Error(`invalid patch: patches[${i}] create parentId "${p.parentId}" not found`)
|
||||
}
|
||||
simAvailable.add(p.node.id)
|
||||
parsedCreateNodes.set(i, res.data)
|
||||
simAvailable.add(res.data.id)
|
||||
} else if (p.op === 'update') {
|
||||
if (!simAvailable.has(p.id) || simDeleted.has(p.id)) {
|
||||
throw new Error(`invalid patch: patches[${i}] update id "${p.id}" not found`)
|
||||
@@ -353,11 +358,13 @@ export class SceneBridge {
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of patches) {
|
||||
for (let i = 0; i < patches.length; i++) {
|
||||
const p = patches[i]!
|
||||
if (p.op === 'create') {
|
||||
flush('create')
|
||||
createOps.push({ node: p.node, parentId: p.parentId })
|
||||
createdIds.push(p.node.id as AnyNodeId)
|
||||
const parsedNode = parsedCreateNodes.get(i)!
|
||||
createOps.push({ node: parsedNode, parentId: p.parentId })
|
||||
createdIds.push(parsedNode.id as AnyNodeId)
|
||||
} else if (p.op === 'update') {
|
||||
flush('update')
|
||||
updateOps.push({ id: p.id, data: p.data })
|
||||
|
||||
Reference in New Issue
Block a user