feat(mcp,editor): Option A+B storage + 10 agent deliverables (Phase 7)
Ships the combined filesystem/Supabase storage adapter + MCP scene lifecycle tools + Next.js API routes + editor /scene/[id] route, so an MCP save is directly openable at /scene/<id> without any injection hack. End-to-end verified: 10/10 e2e steps pass. Storage (A1/A2/A3): - SceneStore interface + error classes + slug helpers - FilesystemSceneStore at $PASCAL_DATA_DIR (defaults XDG/~/.pascal) with atomic writes, .index sidecar, optimistic locking - SupabaseSceneStore with scenes + scene_revisions tables, RLS migration SQL, mock-backed unit tests - createSceneStore(env) auto-selects based on SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY MCP tools (A4, A8, A9, A10): - save_scene / load_scene / list_scenes / delete_scene / rename_scene - list_templates / create_from_template (3 seed templates: empty-studio, two-bedroom, garden-house) - generate_variants (7 mutation kinds, seeded RNG, save=true|false) - photo_to_scene (vision sampling → scene graph → save) Editor (A5, A6): - /api/scenes + /api/scenes/[id] with RFC 7232 If-Match locking - /scene/[id] and /scenes route pages with save button, SceneLoader - Removed the window.__pascalScene dev injection hack Security + UX edges (A7, A8): - AssetUrl Zod validator: asset:// blob: data:image/ /path https: (http://localhost for dev) + PASCAL_ALLOWED_ASSET_ORIGINS env allowlist. Hardens scan.url, guide.url, item.asset.src, material.texture.url, MaterialMaps.*Map - Auto-frame camera on empty→non-empty scene transition (camera-controls:fit-scene emitter event) Shared utilities: - rehydrateSiteChildren() extracted to packages/mcp/src/lib/ and used by both create-from-template and generate-variants to work around the SiteNode.children-as-objects vs. ids inconsistency (CROSS_CUTTING §2) - Storage + MCP subpath exports added to packages/mcp/package.json (CROSS_CUTTING §4) Tests: 293 pass / 0 fail across 40 files (was 142 pre-Phase-7). Biome: clean. Phase-7 e2e script at packages/mcp/test-reports/phase7-e2e.ts: MCP HTTP + editor Next.js both point at $PASCAL_DATA_DIR = /tmp/pascal-e2e, save_scene from MCP, GET /api/scenes/<id> from editor server, /scenes list page renders all saved scenes, scene page renders SceneLoader, delete_scene works. 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
42bd05db9c
commit
e8d0b13ff5
@@ -19,12 +19,12 @@
|
||||
* bun packages/mcp/test-reports/casa-sol/build.ts
|
||||
*/
|
||||
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const SERVER_URL = 'http://localhost:3917/mcp'
|
||||
@@ -234,7 +234,14 @@ const OPENINGS: OpeningSpec[] = [
|
||||
},
|
||||
{ wallDesignId: 2, kind: 'door', position: 0.65, width: 0.9, height: 2.1, label: 'kitchen-back' },
|
||||
{ wallDesignId: 6, kind: 'door', position: 0.3, width: 0.8, height: 2.1, label: 'master-door' },
|
||||
{ wallDesignId: 7, kind: 'door', position: 0.5, width: 0.8, height: 2.1, label: 'bedroom-2-door' },
|
||||
{
|
||||
wallDesignId: 7,
|
||||
kind: 'door',
|
||||
position: 0.5,
|
||||
width: 0.8,
|
||||
height: 2.1,
|
||||
label: 'bedroom-2-door',
|
||||
},
|
||||
{ wallDesignId: 9, kind: 'door', position: 0.5, width: 0.7, height: 2.0, label: 'bath2-door' },
|
||||
{
|
||||
wallDesignId: 1,
|
||||
@@ -329,8 +336,12 @@ async function main(): Promise<void> {
|
||||
log(`[casa] falling back to in-memory MCP server (same tool surface)`)
|
||||
// Load the in-process MCP server to keep the build moving. This preserves
|
||||
// the tool contract; the only thing we lose is the HTTP wire test.
|
||||
const { SceneBridge } = await import('/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/src/bridge/scene-bridge.ts')
|
||||
const { createPascalMcpServer } = await import('/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/src/server.ts')
|
||||
const { SceneBridge } = await import(
|
||||
'/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/src/bridge/scene-bridge.ts'
|
||||
)
|
||||
const { createPascalMcpServer } = await import(
|
||||
'/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/src/server.ts'
|
||||
)
|
||||
|
||||
const bridge = new SceneBridge()
|
||||
bridge.loadDefault()
|
||||
@@ -530,7 +541,9 @@ async function main(): Promise<void> {
|
||||
})
|
||||
}
|
||||
}
|
||||
const ids = openingResults.filter((r) => r.ok && r.openingId).map((r) => r.openingId!) as string[]
|
||||
const ids = openingResults
|
||||
.filter((r) => r.ok && r.openingId)
|
||||
.map((r) => r.openingId!) as string[]
|
||||
return {
|
||||
summary: `${doors} doors, ${windows} windows, ${failures.length} failures`,
|
||||
nodeIds: ids,
|
||||
@@ -691,9 +704,8 @@ async function main(): Promise<void> {
|
||||
|
||||
// ----- Step 12: Final validate + summary counts -----
|
||||
const finalValid = await runValidate(client, 'final')
|
||||
const allNodes = (
|
||||
await callTool<{ nodes: Array<{ type: string }> }>(client, 'find_nodes', {})
|
||||
).nodes
|
||||
const allNodes = (await callTool<{ nodes: Array<{ type: string }> }>(client, 'find_nodes', {}))
|
||||
.nodes
|
||||
const tally: Record<string, number> = {}
|
||||
for (const n of allNodes) {
|
||||
tally[n.type] = (tally[n.type] ?? 0) + 1
|
||||
@@ -790,7 +802,9 @@ async function main(): Promise<void> {
|
||||
|
||||
lines.push('## Validation')
|
||||
lines.push('')
|
||||
lines.push(`- Final \`validate_scene\`: valid=\`${finalValid.valid}\`, errors=${finalValid.errors.length}`)
|
||||
lines.push(
|
||||
`- Final \`validate_scene\`: valid=\`${finalValid.valid}\`, errors=${finalValid.errors.length}`,
|
||||
)
|
||||
if (!finalValid.valid && finalValid.errors.length > 0) {
|
||||
lines.push('')
|
||||
lines.push('Errors (verbatim):')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Phase 7 end-to-end: prove MCP save_scene → editor /scene/[id] renders the scene
|
||||
* without any window.__pascalScene injection.
|
||||
*
|
||||
* Run: PASCAL_DATA_DIR=/tmp/pascal-e2e bun run packages/mcp/test-reports/phase7-e2e.ts
|
||||
*/
|
||||
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
|
||||
const MCP_URL = 'http://localhost:3917/mcp'
|
||||
const EDITOR_URL = 'http://localhost:3002'
|
||||
|
||||
async function main() {
|
||||
console.log('---- Phase 7 e2e ----')
|
||||
|
||||
// 1. Connect to MCP over HTTP
|
||||
const transport = new StreamableHTTPClientTransport(new URL(MCP_URL))
|
||||
const client = new Client({ name: 'e2e', version: '0.0.0' })
|
||||
await client.connect(transport)
|
||||
console.log('OK 1 connect MCP HTTP')
|
||||
|
||||
// 2. Build a scene from a template
|
||||
const created = await client.callTool({
|
||||
name: 'create_from_template',
|
||||
arguments: { id: 'two-bedroom', name: 'e2e-two-bedroom' },
|
||||
})
|
||||
if (created.isError) throw new Error(`create_from_template: ${JSON.stringify(created)}`)
|
||||
console.log('OK 2 create_from_template two-bedroom')
|
||||
|
||||
// 3. Save it
|
||||
const saved = await client.callTool({
|
||||
name: 'save_scene',
|
||||
arguments: { name: 'e2e test house' },
|
||||
})
|
||||
if (saved.isError) throw new Error(`save_scene: ${JSON.stringify(saved)}`)
|
||||
const savedData = JSON.parse((saved.content as Array<{ text: string }>)[0]!.text)
|
||||
const sceneId = savedData.id as string
|
||||
console.log(`OK 3 save_scene -> id=${sceneId}, version=${savedData.version}`)
|
||||
|
||||
// 4. list_scenes
|
||||
const list = await client.callTool({ name: 'list_scenes', arguments: {} })
|
||||
if (list.isError) throw new Error(`list_scenes: ${JSON.stringify(list)}`)
|
||||
const listData = JSON.parse((list.content as Array<{ text: string }>)[0]!.text)
|
||||
console.log(`OK 4 list_scenes -> ${listData.scenes.length} scenes`)
|
||||
|
||||
// 5. Fetch via editor's API (proves A5 works against the same store)
|
||||
const apiRes = await fetch(`${EDITOR_URL}/api/scenes/${sceneId}`)
|
||||
if (!apiRes.ok) throw new Error(`GET /api/scenes/${sceneId} → ${apiRes.status}`)
|
||||
const apiBody = await apiRes.json()
|
||||
const nodeCount = Object.keys(apiBody.graph.nodes).length
|
||||
console.log(`OK 5 editor /api/scenes/${sceneId} → ${nodeCount} nodes`)
|
||||
|
||||
// 6. Fetch editor's /scenes list page (HTML)
|
||||
const listHtmlRes = await fetch(`${EDITOR_URL}/scenes`)
|
||||
if (!listHtmlRes.ok) throw new Error(`GET /scenes → ${listHtmlRes.status}`)
|
||||
const listHtml = await listHtmlRes.text()
|
||||
const hasSceneLink = listHtml.includes(`/scene/${sceneId}`)
|
||||
console.log(`OK 6 /scenes renders, links scene: ${hasSceneLink}`)
|
||||
|
||||
// 7. Fetch /scene/[id] page
|
||||
const sceneHtmlRes = await fetch(`${EDITOR_URL}/scene/${sceneId}`)
|
||||
if (!sceneHtmlRes.ok) throw new Error(`GET /scene/${sceneId} → ${sceneHtmlRes.status}`)
|
||||
console.log(`OK 7 /scene/${sceneId} renders (${sceneHtmlRes.status})`)
|
||||
|
||||
// 8. generate_variants — 3 variants, save=true
|
||||
const variants = await client.callTool({
|
||||
name: 'generate_variants',
|
||||
arguments: { count: 3, vary: ['wall-thickness', 'wall-height'], save: true, seed: 42 },
|
||||
})
|
||||
if (variants.isError) throw new Error(`generate_variants: ${JSON.stringify(variants)}`)
|
||||
const variantsData = JSON.parse((variants.content as Array<{ text: string }>)[0]!.text)
|
||||
console.log(`OK 8 generate_variants -> ${variantsData.variants.length} variants`)
|
||||
|
||||
// 9. list_scenes again — should be > 1
|
||||
const list2 = await client.callTool({ name: 'list_scenes', arguments: {} })
|
||||
const list2Data = JSON.parse((list2.content as Array<{ text: string }>)[0]!.text)
|
||||
console.log(`OK 9 list_scenes now shows ${list2Data.scenes.length} scenes`)
|
||||
|
||||
// 10. delete_scene
|
||||
const deleted = await client.callTool({ name: 'delete_scene', arguments: { id: sceneId } })
|
||||
if (deleted.isError) throw new Error(`delete_scene: ${JSON.stringify(deleted)}`)
|
||||
const deletedData = JSON.parse((deleted.content as Array<{ text: string }>)[0]!.text)
|
||||
console.log(`OK 10 delete_scene -> deleted=${deletedData.deleted}`)
|
||||
|
||||
await client.close()
|
||||
console.log(`\nSceneId to open in browser: ${EDITOR_URL}/scenes`)
|
||||
console.log(`Direct: ${EDITOR_URL}/scene/${variantsData.variants[0].sceneId}`)
|
||||
console.log('\n✅ Phase 7 e2e PASSED\n')
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('\n❌ e2e failed:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
# Phase 7 plan — A+B storage + edge cases + ideas
|
||||
|
||||
## Shared SceneStore contract (every agent reuses this)
|
||||
|
||||
```ts
|
||||
// packages/mcp/src/storage/types.ts (Agent 1 owns)
|
||||
|
||||
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph'
|
||||
|
||||
export type SceneId = string // slug-safe (a-z0-9-), ≤ 64 chars
|
||||
|
||||
export interface SceneMeta {
|
||||
id: SceneId
|
||||
name: string
|
||||
projectId: string | null
|
||||
thumbnailUrl: string | null
|
||||
version: number // monotonic, incremented on every save
|
||||
createdAt: string // ISO 8601
|
||||
updatedAt: string
|
||||
ownerId: string | null
|
||||
sizeBytes: number
|
||||
nodeCount: number
|
||||
}
|
||||
|
||||
export interface SceneWithGraph extends SceneMeta {
|
||||
graph: SceneGraph
|
||||
}
|
||||
|
||||
export interface SceneStore {
|
||||
readonly backend: 'filesystem' | 'supabase'
|
||||
save(opts: {
|
||||
id?: SceneId
|
||||
name: string
|
||||
projectId?: string | null
|
||||
ownerId?: string | null
|
||||
graph: SceneGraph
|
||||
thumbnailUrl?: string | null
|
||||
expectedVersion?: number // 409 on mismatch
|
||||
}): Promise<SceneMeta>
|
||||
load(id: SceneId): Promise<SceneWithGraph | null>
|
||||
list(opts?: { projectId?: string; ownerId?: string; limit?: number }): Promise<SceneMeta[]>
|
||||
delete(id: SceneId, opts?: { expectedVersion?: number }): Promise<boolean>
|
||||
rename(id: SceneId, newName: string, opts?: { expectedVersion?: number }): Promise<SceneMeta>
|
||||
}
|
||||
|
||||
export class SceneNotFoundError extends Error { code = 'not_found' as const }
|
||||
export class SceneVersionConflictError extends Error { code = 'version_conflict' as const }
|
||||
export class SceneInvalidError extends Error { code = 'invalid' as const }
|
||||
export class SceneTooLargeError extends Error { code = 'too_large' as const }
|
||||
|
||||
export function createSceneStore(env?: NodeJS.ProcessEnv): SceneStore { /* factory */ }
|
||||
```
|
||||
|
||||
## Agent scope map
|
||||
|
||||
| Agent | Scope | File ownership |
|
||||
|---|---|---|
|
||||
| A1 | Storage interface + types + factory | `packages/mcp/src/storage/types.ts`, `packages/mcp/src/storage/index.ts`, `packages/mcp/src/storage/store.test.ts` |
|
||||
| A2 | Filesystem impl | `packages/mcp/src/storage/filesystem-scene-store.ts` + tests |
|
||||
| A3 | Supabase impl + migration SQL | `packages/mcp/src/storage/supabase-scene-store.ts`, `packages/mcp/sql/migrations/0001_scenes.sql` + tests |
|
||||
| A4 | MCP scene-lifecycle tools | `packages/mcp/src/tools/scene-lifecycle/*.ts` + index wiring |
|
||||
| A5 | Next.js API routes | `apps/editor/app/api/scenes/route.ts`, `apps/editor/app/api/scenes/[id]/route.ts`, `apps/editor/lib/scene-store-server.ts` |
|
||||
| A6 | Editor routes + kill dev hook | `apps/editor/app/scene/[id]/page.tsx`, `apps/editor/app/scenes/page.tsx`, edit `apps/editor/app/page.tsx` |
|
||||
| A7 | URL hardening in core schemas | `packages/core/src/schema/nodes/{scan,guide,item}.ts`, `packages/core/src/schema/material.ts` + migration |
|
||||
| A8 | Auto-frame camera + scene templates | `packages/editor/src/hooks/use-auto-frame.ts`, `packages/mcp/src/templates/*`, `packages/mcp/src/tools/scene-lifecycle/list-templates.ts` |
|
||||
| A9 | Multi-variant generation | `packages/mcp/src/tools/variants/*` + tests |
|
||||
| A10 | Photo → scene + example | `packages/mcp/src/tools/photo-to-scene/*` (orchestrator), update `README.md`, new `examples/photo-to-scene.md` |
|
||||
|
||||
## Global coordination rules
|
||||
- Agent A1 drops first (interface only). A2, A3, A4, A5 read from `packages/mcp/src/storage/types.ts`; if it doesn't exist when they start, they should **inline a copy of the types above** and the integrator fixes up the import later.
|
||||
- All MCP tools use `StreamableHTTPClientTransport`-compatible input/output Zod schemas.
|
||||
- Every tool uses the shared `SceneStore` via `createSceneStore()` — never instantiates concrete stores.
|
||||
- Tests are `bun:test`, colocated.
|
||||
- Biome 2-space, single quote, no semicolons, trailing commas all.
|
||||
- Do NOT run `bun install` — already done.
|
||||
- Do NOT modify files outside your ownership.
|
||||
|
||||
## Acceptance
|
||||
- `bun test --cwd packages/mcp` green.
|
||||
- `bunx biome check packages/mcp apps/editor/app` green.
|
||||
- `bun run --cwd packages/mcp build` green.
|
||||
- `MCP save_scene → list_scenes → editor opens /scene/<id>` works without `window.__pascalScene`.
|
||||
@@ -168,18 +168,21 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
// ---- 1. get_scene ------------------------------------------------------
|
||||
const sceneResult = await run('get_scene', {}, {
|
||||
describe: (r) => {
|
||||
const s = r.structuredContent as any
|
||||
const nodeCount = s?.nodes ? Object.keys(s.nodes).length : 0
|
||||
const rootCount = s?.rootNodeIds?.length ?? 0
|
||||
return `${nodeCount} nodes, ${rootCount} roots`
|
||||
const sceneResult = await run(
|
||||
'get_scene',
|
||||
{},
|
||||
{
|
||||
describe: (r) => {
|
||||
const s = r.structuredContent as any
|
||||
const nodeCount = s?.nodes ? Object.keys(s.nodes).length : 0
|
||||
const rootCount = s?.rootNodeIds?.length ?? 0
|
||||
return `${nodeCount} nodes, ${rootCount} roots`
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// Discover key node ids from the scene snapshot.
|
||||
const sceneNodes: Record<string, any> =
|
||||
(sceneResult?.structuredContent as any)?.nodes ?? {}
|
||||
const sceneNodes: Record<string, any> = (sceneResult?.structuredContent as any)?.nodes ?? {}
|
||||
const sceneRoots: string[] = (sceneResult?.structuredContent as any)?.rootNodeIds ?? []
|
||||
|
||||
const findFirst = (type: string): any | null => {
|
||||
@@ -198,30 +201,41 @@ async function main(): Promise<void> {
|
||||
)
|
||||
|
||||
// ---- 2. get_node -------------------------------------------------------
|
||||
await run('get_node', { id: siteNode?.id ?? sceneRoots[0] ?? '' }, {
|
||||
describe: (r) => {
|
||||
const n = (r.structuredContent as any)?.node
|
||||
return `node type=${n?.type}, id=${n?.id}`
|
||||
await run(
|
||||
'get_node',
|
||||
{ id: siteNode?.id ?? sceneRoots[0] ?? '' },
|
||||
{
|
||||
describe: (r) => {
|
||||
const n = (r.structuredContent as any)?.node
|
||||
return `node type=${n?.type}, id=${n?.id}`
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// ---- 3. describe_node --------------------------------------------------
|
||||
await run('describe_node', { id: siteNode?.id ?? sceneRoots[0] ?? '' }, {
|
||||
describe: (r) => {
|
||||
const s = r.structuredContent as any
|
||||
return `type=${s?.type}, ${s?.childrenIds?.length ?? 0} children`
|
||||
await run(
|
||||
'describe_node',
|
||||
{ id: siteNode?.id ?? sceneRoots[0] ?? '' },
|
||||
{
|
||||
describe: (r) => {
|
||||
const s = r.structuredContent as any
|
||||
return `type=${s?.type}, ${s?.childrenIds?.length ?? 0} children`
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// ---- 4. find_nodes -----------------------------------------------------
|
||||
const findLevels = await run('find_nodes', { type: 'level' }, {
|
||||
describe: (r) => `${(r.structuredContent as any)?.nodes?.length ?? 0} level node(s)`,
|
||||
})
|
||||
const findLevels = await run(
|
||||
'find_nodes',
|
||||
{ type: 'level' },
|
||||
{
|
||||
describe: (r) => `${(r.structuredContent as any)?.nodes?.length ?? 0} level node(s)`,
|
||||
},
|
||||
)
|
||||
|
||||
// Refresh levelNode from find_nodes output (most current).
|
||||
const foundLevels = (findLevels?.structuredContent as any)?.nodes ?? []
|
||||
const groundLevelId: string | undefined =
|
||||
foundLevels[0]?.id ?? levelNode?.id ?? undefined
|
||||
const groundLevelId: string | undefined = foundLevels[0]?.id ?? levelNode?.id ?? undefined
|
||||
console.log(`[t1] groundLevelId=${groundLevelId}`)
|
||||
|
||||
// ---- 5. measure --------------------------------------------------------
|
||||
@@ -462,38 +476,58 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
// ---- 14. undo ----------------------------------------------------------
|
||||
await run('undo', {}, {
|
||||
describe: (r) => `undone=${(r.structuredContent as any)?.undone}`,
|
||||
})
|
||||
await run(
|
||||
'undo',
|
||||
{},
|
||||
{
|
||||
describe: (r) => `undone=${(r.structuredContent as any)?.undone}`,
|
||||
},
|
||||
)
|
||||
|
||||
// ---- 15. redo ----------------------------------------------------------
|
||||
await run('redo', {}, {
|
||||
describe: (r) => `redone=${(r.structuredContent as any)?.redone}`,
|
||||
})
|
||||
await run(
|
||||
'redo',
|
||||
{},
|
||||
{
|
||||
describe: (r) => `redone=${(r.structuredContent as any)?.redone}`,
|
||||
},
|
||||
)
|
||||
|
||||
// ---- 16. export_json ---------------------------------------------------
|
||||
await run('export_json', { pretty: true }, {
|
||||
describe: (r) => {
|
||||
const s = r.structuredContent as any
|
||||
return `${s?.json?.length ?? 0} chars JSON`
|
||||
await run(
|
||||
'export_json',
|
||||
{ pretty: true },
|
||||
{
|
||||
describe: (r) => {
|
||||
const s = r.structuredContent as any
|
||||
return `${s?.json?.length ?? 0} chars JSON`
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// ---- 17. export_glb ----------------------------------------------------
|
||||
await run('export_glb', {})
|
||||
|
||||
// ---- 18. validate_scene ------------------------------------------------
|
||||
await run('validate_scene', {}, {
|
||||
describe: (r) => {
|
||||
const s = r.structuredContent as any
|
||||
return `valid=${s?.valid}, errors=${s?.errors?.length ?? 0}`
|
||||
await run(
|
||||
'validate_scene',
|
||||
{},
|
||||
{
|
||||
describe: (r) => {
|
||||
const s = r.structuredContent as any
|
||||
return `valid=${s?.valid}, errors=${s?.errors?.length ?? 0}`
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// ---- 19. check_collisions ----------------------------------------------
|
||||
await run('check_collisions', {}, {
|
||||
describe: (r) => `${(r.structuredContent as any)?.collisions?.length ?? 0} collision(s)`,
|
||||
})
|
||||
await run(
|
||||
'check_collisions',
|
||||
{},
|
||||
{
|
||||
describe: (r) => `${(r.structuredContent as any)?.collisions?.length ?? 0} collision(s)`,
|
||||
},
|
||||
)
|
||||
|
||||
// ---- 20. analyze_floorplan_image — expected sampling_unavailable -------
|
||||
await run(
|
||||
|
||||
@@ -20,13 +20,14 @@
|
||||
* best-effort report.
|
||||
*/
|
||||
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
|
||||
const TARGET_URL = 'http://localhost:3917/mcp'
|
||||
const OUT_DIR = '/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/test-reports/t2-http'
|
||||
const OUT_DIR =
|
||||
'/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/test-reports/t2-http'
|
||||
|
||||
type ToolResult = {
|
||||
name: string
|
||||
@@ -106,9 +107,9 @@ function getStructured<T>(
|
||||
const r = result.result as { structuredContent?: unknown; content?: unknown }
|
||||
if (r.structuredContent !== undefined) return r.structuredContent as T
|
||||
if (Array.isArray(r.content)) {
|
||||
const textBlock = r.content.find(
|
||||
(c) => (c as { type?: string }).type === 'text',
|
||||
) as { text?: string } | undefined
|
||||
const textBlock = r.content.find((c) => (c as { type?: string }).type === 'text') as
|
||||
| { text?: string }
|
||||
| undefined
|
||||
if (textBlock?.text) {
|
||||
try {
|
||||
return JSON.parse(textBlock.text) as T
|
||||
@@ -430,26 +431,24 @@ async function main() {
|
||||
|
||||
// ---- cut_opening ------------------------------------------------------
|
||||
let openingId: string | null = null
|
||||
{
|
||||
if (!wallId) {
|
||||
record('cut_opening', false, 'skipped — no wall id to cut')
|
||||
} else {
|
||||
const r = await callTool(clientA, 'cut_opening', {
|
||||
wallId,
|
||||
type: 'door',
|
||||
position: 0.5,
|
||||
width: 0.9,
|
||||
height: 2,
|
||||
})
|
||||
const struct = getStructured<{ openingId: string }>(r)
|
||||
openingId = struct?.openingId ?? null
|
||||
record(
|
||||
'cut_opening',
|
||||
r.ok && typeof struct?.openingId === 'string',
|
||||
r.ok ? `cut opening ${struct?.openingId}` : `error: ${String(r.error)}`,
|
||||
r.latencyMs,
|
||||
)
|
||||
}
|
||||
if (!wallId) {
|
||||
record('cut_opening', false, 'skipped — no wall id to cut')
|
||||
} else {
|
||||
const r = await callTool(clientA, 'cut_opening', {
|
||||
wallId,
|
||||
type: 'door',
|
||||
position: 0.5,
|
||||
width: 0.9,
|
||||
height: 2,
|
||||
})
|
||||
const struct = getStructured<{ openingId: string }>(r)
|
||||
openingId = struct?.openingId ?? null
|
||||
record(
|
||||
'cut_opening',
|
||||
r.ok && typeof struct?.openingId === 'string',
|
||||
r.ok ? `cut opening ${struct?.openingId}` : `error: ${String(r.error)}`,
|
||||
r.latencyMs,
|
||||
)
|
||||
}
|
||||
|
||||
// ---- set_zone ---------------------------------------------------------
|
||||
@@ -475,59 +474,45 @@ async function main() {
|
||||
r.latencyMs,
|
||||
)
|
||||
}
|
||||
|
||||
// ---- duplicate_level --------------------------------------------------
|
||||
{
|
||||
if (!extraLevelId) {
|
||||
record('duplicate_level', false, 'skipped — no extra level to duplicate')
|
||||
} else {
|
||||
const r = await callTool(clientA, 'duplicate_level', { levelId: extraLevelId })
|
||||
const struct = getStructured<{ newLevelId: string; newNodeIds: string[] }>(r)
|
||||
record(
|
||||
'duplicate_level',
|
||||
r.ok && typeof struct?.newLevelId === 'string',
|
||||
r.ok
|
||||
? `duplicated → new level ${struct?.newLevelId} (${struct?.newNodeIds?.length ?? 0} nodes)`
|
||||
: `error: ${String(r.error)}`,
|
||||
r.latencyMs,
|
||||
)
|
||||
}
|
||||
if (!extraLevelId) {
|
||||
record('duplicate_level', false, 'skipped — no extra level to duplicate')
|
||||
} else {
|
||||
const r = await callTool(clientA, 'duplicate_level', { levelId: extraLevelId })
|
||||
const struct = getStructured<{ newLevelId: string; newNodeIds: string[] }>(r)
|
||||
record(
|
||||
'duplicate_level',
|
||||
r.ok && typeof struct?.newLevelId === 'string',
|
||||
r.ok
|
||||
? `duplicated → new level ${struct?.newLevelId} (${struct?.newNodeIds?.length ?? 0} nodes)`
|
||||
: `error: ${String(r.error)}`,
|
||||
r.latencyMs,
|
||||
)
|
||||
}
|
||||
|
||||
// ---- apply_patch ------------------------------------------------------
|
||||
{
|
||||
if (!zoneId) {
|
||||
record('apply_patch', false, 'skipped — no zone to patch')
|
||||
} else {
|
||||
const r = await callTool(clientA, 'apply_patch', {
|
||||
patches: [{ op: 'update', id: zoneId, data: { name: 'T2-zone-renamed' } }],
|
||||
})
|
||||
const struct = getStructured<{ appliedOps: number }>(r)
|
||||
record(
|
||||
'apply_patch',
|
||||
r.ok && struct?.appliedOps === 1,
|
||||
r.ok ? `appliedOps=${struct?.appliedOps}` : `error: ${String(r.error)}`,
|
||||
r.latencyMs,
|
||||
)
|
||||
}
|
||||
if (!zoneId) {
|
||||
record('apply_patch', false, 'skipped — no zone to patch')
|
||||
} else {
|
||||
const r = await callTool(clientA, 'apply_patch', {
|
||||
patches: [{ op: 'update', id: zoneId, data: { name: 'T2-zone-renamed' } }],
|
||||
})
|
||||
const struct = getStructured<{ appliedOps: number }>(r)
|
||||
record(
|
||||
'apply_patch',
|
||||
r.ok && struct?.appliedOps === 1,
|
||||
r.ok ? `appliedOps=${struct?.appliedOps}` : `error: ${String(r.error)}`,
|
||||
r.latencyMs,
|
||||
)
|
||||
}
|
||||
|
||||
// ---- delete_node ------------------------------------------------------
|
||||
{
|
||||
if (!openingId) {
|
||||
record('delete_node', false, 'skipped — no opening to delete')
|
||||
} else {
|
||||
const r = await callTool(clientA, 'delete_node', { id: openingId, cascade: true })
|
||||
const struct = getStructured<{ deletedIds: string[] }>(r)
|
||||
record(
|
||||
'delete_node',
|
||||
r.ok && Array.isArray(struct?.deletedIds) && (struct?.deletedIds?.length ?? 0) >= 1,
|
||||
r.ok
|
||||
? `deleted ${struct?.deletedIds?.length ?? 0} nodes`
|
||||
: `error: ${String(r.error)}`,
|
||||
r.latencyMs,
|
||||
)
|
||||
}
|
||||
if (!openingId) {
|
||||
record('delete_node', false, 'skipped — no opening to delete')
|
||||
} else {
|
||||
const r = await callTool(clientA, 'delete_node', { id: openingId, cascade: true })
|
||||
const struct = getStructured<{ deletedIds: string[] }>(r)
|
||||
record(
|
||||
'delete_node',
|
||||
r.ok && Array.isArray(struct?.deletedIds) && (struct?.deletedIds?.length ?? 0) >= 1,
|
||||
r.ok ? `deleted ${struct?.deletedIds?.length ?? 0} nodes` : `error: ${String(r.error)}`,
|
||||
r.latencyMs,
|
||||
)
|
||||
}
|
||||
|
||||
// ---- undo -------------------------------------------------------------
|
||||
@@ -695,7 +680,9 @@ async function main() {
|
||||
distinctSessions = sidA !== sidB
|
||||
const toolsListB = await clientB.listTools()
|
||||
toolCountB = toolsListB.tools.length
|
||||
console.log(`Session B listTools() → ${toolCountB} tools; sessions distinct: ${distinctSessions}`)
|
||||
console.log(
|
||||
`Session B listTools() → ${toolCountB} tools; sessions distinct: ${distinctSessions}`,
|
||||
)
|
||||
|
||||
const sceneB = await callTool(clientB, 'get_scene', {})
|
||||
const sceneBstruct = getStructured<{ nodes: Record<string, unknown> }>(sceneB)
|
||||
|
||||
@@ -9,24 +9,7 @@
|
||||
"metadata": {},
|
||||
"polygon": {
|
||||
"type": "polygon",
|
||||
"points": [
|
||||
[
|
||||
-15,
|
||||
-15
|
||||
],
|
||||
[
|
||||
15,
|
||||
-15
|
||||
],
|
||||
[
|
||||
15,
|
||||
15
|
||||
],
|
||||
[
|
||||
-15,
|
||||
15
|
||||
]
|
||||
]
|
||||
"points": [[-15, -15], [15, -15], [15, 15], [-15, 15]]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -36,19 +19,9 @@
|
||||
"parentId": null,
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"children": [
|
||||
"level_wyuoxj87czq3v0re"
|
||||
],
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
"children": ["level_wyuoxj87czq3v0re"],
|
||||
"position": [0, 0, 0],
|
||||
"rotation": [0, 0, 0]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -59,19 +32,9 @@
|
||||
"parentId": null,
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"children": [
|
||||
"level_wyuoxj87czq3v0re"
|
||||
],
|
||||
"position": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
]
|
||||
"children": ["level_wyuoxj87czq3v0re"],
|
||||
"position": [0, 0, 0],
|
||||
"rotation": [0, 0, 0]
|
||||
},
|
||||
"level_wyuoxj87czq3v0re": {
|
||||
"object": "node",
|
||||
@@ -106,20 +69,11 @@
|
||||
"parentId": "level_wyuoxj87czq3v0re",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"children": [
|
||||
"window_47x40mtv2l4ca9p4",
|
||||
"window_n09awmg5m3ct4fvn"
|
||||
],
|
||||
"children": ["window_47x40mtv2l4ca9p4", "window_n09awmg5m3ct4fvn"],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"end": [
|
||||
10,
|
||||
0
|
||||
],
|
||||
"start": [0, 0],
|
||||
"end": [10, 0],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -133,14 +87,8 @@
|
||||
"children": [],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
10,
|
||||
0
|
||||
],
|
||||
"end": [
|
||||
10,
|
||||
8
|
||||
],
|
||||
"start": [10, 0],
|
||||
"end": [10, 8],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -151,19 +99,11 @@
|
||||
"parentId": "level_wyuoxj87czq3v0re",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"children": [
|
||||
"window_xlta3f3f0cnmbti3"
|
||||
],
|
||||
"children": ["window_xlta3f3f0cnmbti3"],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
10,
|
||||
8
|
||||
],
|
||||
"end": [
|
||||
0,
|
||||
8
|
||||
],
|
||||
"start": [10, 8],
|
||||
"end": [0, 8],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -177,14 +117,8 @@
|
||||
"children": [],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
0,
|
||||
8
|
||||
],
|
||||
"end": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"start": [0, 8],
|
||||
"end": [0, 0],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -198,14 +132,8 @@
|
||||
"children": [],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
0,
|
||||
5
|
||||
],
|
||||
"end": [
|
||||
3,
|
||||
5
|
||||
],
|
||||
"start": [0, 5],
|
||||
"end": [3, 5],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -216,19 +144,11 @@
|
||||
"parentId": "level_wyuoxj87czq3v0re",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"children": [
|
||||
"door_cjzja4lt8owg88wg"
|
||||
],
|
||||
"children": ["door_cjzja4lt8owg88wg"],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
3,
|
||||
5
|
||||
],
|
||||
"end": [
|
||||
3,
|
||||
8
|
||||
],
|
||||
"start": [3, 5],
|
||||
"end": [3, 8],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -242,14 +162,8 @@
|
||||
"children": [],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
7,
|
||||
5
|
||||
],
|
||||
"end": [
|
||||
10,
|
||||
5
|
||||
],
|
||||
"start": [7, 5],
|
||||
"end": [10, 5],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -260,19 +174,11 @@
|
||||
"parentId": "level_wyuoxj87czq3v0re",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"children": [
|
||||
"door_bs7bf0azevq9vd76"
|
||||
],
|
||||
"children": ["door_bs7bf0azevq9vd76"],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
7,
|
||||
5
|
||||
],
|
||||
"end": [
|
||||
7,
|
||||
8
|
||||
],
|
||||
"start": [7, 5],
|
||||
"end": [7, 8],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -283,19 +189,11 @@
|
||||
"parentId": "level_wyuoxj87czq3v0re",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"children": [
|
||||
"door_o8etwqsemfgj5mkj"
|
||||
],
|
||||
"children": ["door_o8etwqsemfgj5mkj"],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
4,
|
||||
6
|
||||
],
|
||||
"end": [
|
||||
6,
|
||||
6
|
||||
],
|
||||
"start": [4, 6],
|
||||
"end": [6, 6],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -309,14 +207,8 @@
|
||||
"children": [],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
4,
|
||||
6
|
||||
],
|
||||
"end": [
|
||||
4,
|
||||
8
|
||||
],
|
||||
"start": [4, 6],
|
||||
"end": [4, 8],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -330,14 +222,8 @@
|
||||
"children": [],
|
||||
"thickness": 0.2,
|
||||
"height": 2.7,
|
||||
"start": [
|
||||
6,
|
||||
6
|
||||
],
|
||||
"end": [
|
||||
6,
|
||||
8
|
||||
],
|
||||
"start": [6, 6],
|
||||
"end": [6, 8],
|
||||
"frontSide": "unknown",
|
||||
"backSide": "unknown"
|
||||
},
|
||||
@@ -349,24 +235,7 @@
|
||||
"parentId": "level_wyuoxj87czq3v0re",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"polygon": [
|
||||
[
|
||||
0,
|
||||
5
|
||||
],
|
||||
[
|
||||
3,
|
||||
5
|
||||
],
|
||||
[
|
||||
3,
|
||||
8
|
||||
],
|
||||
[
|
||||
0,
|
||||
8
|
||||
]
|
||||
],
|
||||
"polygon": [[0, 5], [3, 5], [3, 8], [0, 8]],
|
||||
"color": "#3b82f6"
|
||||
},
|
||||
"zone_u95l1bt35jci3gvu": {
|
||||
@@ -377,24 +246,7 @@
|
||||
"parentId": "level_wyuoxj87czq3v0re",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"polygon": [
|
||||
[
|
||||
7,
|
||||
5
|
||||
],
|
||||
[
|
||||
10,
|
||||
5
|
||||
],
|
||||
[
|
||||
10,
|
||||
8
|
||||
],
|
||||
[
|
||||
7,
|
||||
8
|
||||
]
|
||||
],
|
||||
"polygon": [[7, 5], [10, 5], [10, 8], [7, 8]],
|
||||
"color": "#3b82f6"
|
||||
},
|
||||
"zone_r9ma8tvsqt9w1zey": {
|
||||
@@ -405,24 +257,7 @@
|
||||
"parentId": "level_wyuoxj87czq3v0re",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"polygon": [
|
||||
[
|
||||
4,
|
||||
6
|
||||
],
|
||||
[
|
||||
6,
|
||||
6
|
||||
],
|
||||
[
|
||||
6,
|
||||
8
|
||||
],
|
||||
[
|
||||
4,
|
||||
8
|
||||
]
|
||||
],
|
||||
"polygon": [[4, 6], [6, 6], [6, 8], [4, 8]],
|
||||
"color": "#3b82f6"
|
||||
},
|
||||
"zone_l189q61kf9ra2m8t": {
|
||||
@@ -434,54 +269,18 @@
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"polygon": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
10,
|
||||
0
|
||||
],
|
||||
[
|
||||
10,
|
||||
5
|
||||
],
|
||||
[
|
||||
7,
|
||||
5
|
||||
],
|
||||
[
|
||||
7,
|
||||
8
|
||||
],
|
||||
[
|
||||
6,
|
||||
8
|
||||
],
|
||||
[
|
||||
6,
|
||||
6
|
||||
],
|
||||
[
|
||||
4,
|
||||
6
|
||||
],
|
||||
[
|
||||
4,
|
||||
8
|
||||
],
|
||||
[
|
||||
3,
|
||||
8
|
||||
],
|
||||
[
|
||||
3,
|
||||
5
|
||||
],
|
||||
[
|
||||
0,
|
||||
5
|
||||
]
|
||||
[0, 0],
|
||||
[10, 0],
|
||||
[10, 5],
|
||||
[7, 5],
|
||||
[7, 8],
|
||||
[6, 8],
|
||||
[6, 6],
|
||||
[4, 6],
|
||||
[4, 8],
|
||||
[3, 8],
|
||||
[3, 5],
|
||||
[0, 5]
|
||||
],
|
||||
"color": "#3b82f6"
|
||||
},
|
||||
@@ -492,16 +291,8 @@
|
||||
"parentId": "wall_qv53jm9kvl7k6slf",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"position": [
|
||||
0.5,
|
||||
1.05,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"position": [0.5, 1.05, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"wallId": "wall_qv53jm9kvl7k6slf",
|
||||
"width": 0.9,
|
||||
"height": 2.1,
|
||||
@@ -515,9 +306,7 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"heightRatio": 0.4,
|
||||
"columnRatios": [
|
||||
1
|
||||
],
|
||||
"columnRatios": [1],
|
||||
"dividerThickness": 0.03,
|
||||
"panelDepth": 0.01,
|
||||
"panelInset": 0.04
|
||||
@@ -525,9 +314,7 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"heightRatio": 0.6,
|
||||
"columnRatios": [
|
||||
1
|
||||
],
|
||||
"columnRatios": [1],
|
||||
"dividerThickness": 0.03,
|
||||
"panelDepth": 0.01,
|
||||
"panelInset": 0.04
|
||||
@@ -536,10 +323,7 @@
|
||||
"handle": true,
|
||||
"handleHeight": 1.05,
|
||||
"handleSide": "right",
|
||||
"contentPadding": [
|
||||
0.04,
|
||||
0.04
|
||||
],
|
||||
"contentPadding": [0.04, 0.04],
|
||||
"doorCloser": false,
|
||||
"panicBar": false,
|
||||
"panicBarHeight": 1
|
||||
@@ -551,16 +335,8 @@
|
||||
"parentId": "wall_hrfixeusz7zb7x63",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"position": [
|
||||
0.5,
|
||||
1.05,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"position": [0.5, 1.05, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"wallId": "wall_hrfixeusz7zb7x63",
|
||||
"width": 0.9,
|
||||
"height": 2.1,
|
||||
@@ -574,9 +350,7 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"heightRatio": 0.4,
|
||||
"columnRatios": [
|
||||
1
|
||||
],
|
||||
"columnRatios": [1],
|
||||
"dividerThickness": 0.03,
|
||||
"panelDepth": 0.01,
|
||||
"panelInset": 0.04
|
||||
@@ -584,9 +358,7 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"heightRatio": 0.6,
|
||||
"columnRatios": [
|
||||
1
|
||||
],
|
||||
"columnRatios": [1],
|
||||
"dividerThickness": 0.03,
|
||||
"panelDepth": 0.01,
|
||||
"panelInset": 0.04
|
||||
@@ -595,10 +367,7 @@
|
||||
"handle": true,
|
||||
"handleHeight": 1.05,
|
||||
"handleSide": "right",
|
||||
"contentPadding": [
|
||||
0.04,
|
||||
0.04
|
||||
],
|
||||
"contentPadding": [0.04, 0.04],
|
||||
"doorCloser": false,
|
||||
"panicBar": false,
|
||||
"panicBarHeight": 1
|
||||
@@ -610,16 +379,8 @@
|
||||
"parentId": "wall_1ullk9bm6dw15i9t",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"position": [
|
||||
0.5,
|
||||
1.05,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"position": [0.5, 1.05, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"wallId": "wall_1ullk9bm6dw15i9t",
|
||||
"width": 0.9,
|
||||
"height": 2.1,
|
||||
@@ -633,9 +394,7 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"heightRatio": 0.4,
|
||||
"columnRatios": [
|
||||
1
|
||||
],
|
||||
"columnRatios": [1],
|
||||
"dividerThickness": 0.03,
|
||||
"panelDepth": 0.01,
|
||||
"panelInset": 0.04
|
||||
@@ -643,9 +402,7 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"heightRatio": 0.6,
|
||||
"columnRatios": [
|
||||
1
|
||||
],
|
||||
"columnRatios": [1],
|
||||
"dividerThickness": 0.03,
|
||||
"panelDepth": 0.01,
|
||||
"panelInset": 0.04
|
||||
@@ -654,10 +411,7 @@
|
||||
"handle": true,
|
||||
"handleHeight": 1.05,
|
||||
"handleSide": "right",
|
||||
"contentPadding": [
|
||||
0.04,
|
||||
0.04
|
||||
],
|
||||
"contentPadding": [0.04, 0.04],
|
||||
"doorCloser": false,
|
||||
"panicBar": false,
|
||||
"panicBarHeight": 1
|
||||
@@ -669,27 +423,15 @@
|
||||
"parentId": "wall_y87bsrljd2245n51",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"position": [
|
||||
0.3,
|
||||
0.6,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"position": [0.3, 0.6, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"wallId": "wall_y87bsrljd2245n51",
|
||||
"width": 1.2,
|
||||
"height": 1.2,
|
||||
"frameThickness": 0.05,
|
||||
"frameDepth": 0.07,
|
||||
"columnRatios": [
|
||||
1
|
||||
],
|
||||
"rowRatios": [
|
||||
1
|
||||
],
|
||||
"columnRatios": [1],
|
||||
"rowRatios": [1],
|
||||
"columnDividerThickness": 0.03,
|
||||
"rowDividerThickness": 0.03,
|
||||
"sill": true,
|
||||
@@ -703,27 +445,15 @@
|
||||
"parentId": "wall_y87bsrljd2245n51",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"position": [
|
||||
0.7,
|
||||
0.6,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"position": [0.7, 0.6, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"wallId": "wall_y87bsrljd2245n51",
|
||||
"width": 1.2,
|
||||
"height": 1.2,
|
||||
"frameThickness": 0.05,
|
||||
"frameDepth": 0.07,
|
||||
"columnRatios": [
|
||||
1
|
||||
],
|
||||
"rowRatios": [
|
||||
1
|
||||
],
|
||||
"columnRatios": [1],
|
||||
"rowRatios": [1],
|
||||
"columnDividerThickness": 0.03,
|
||||
"rowDividerThickness": 0.03,
|
||||
"sill": true,
|
||||
@@ -737,27 +467,15 @@
|
||||
"parentId": "wall_aegff27krjwgmkmi",
|
||||
"visible": true,
|
||||
"metadata": {},
|
||||
"position": [
|
||||
0.5,
|
||||
0.6,
|
||||
0
|
||||
],
|
||||
"rotation": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"position": [0.5, 0.6, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"wallId": "wall_aegff27krjwgmkmi",
|
||||
"width": 1.2,
|
||||
"height": 1.2,
|
||||
"frameThickness": 0.05,
|
||||
"frameDepth": 0.07,
|
||||
"columnRatios": [
|
||||
1
|
||||
],
|
||||
"rowRatios": [
|
||||
1
|
||||
],
|
||||
"columnRatios": [1],
|
||||
"rowRatios": [1],
|
||||
"columnDividerThickness": 0.03,
|
||||
"rowDividerThickness": 0.03,
|
||||
"sill": true,
|
||||
@@ -765,8 +483,6 @@
|
||||
"sillThickness": 0.03
|
||||
}
|
||||
},
|
||||
"rootNodeIds": [
|
||||
"site_xs1r72ib2ymzpjus"
|
||||
],
|
||||
"rootNodeIds": ["site_xs1r72ib2ymzpjus"],
|
||||
"collections": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,7 @@
|
||||
"ok": true,
|
||||
"durationMs": 4,
|
||||
"summary": "building=building_bfqg91ai9ijps9ej, level=level_wyuoxj87czq3v0re (of 1 buildings, 1 levels)",
|
||||
"nodeIds": [
|
||||
"building_bfqg91ai9ijps9ej",
|
||||
"level_wyuoxj87czq3v0re"
|
||||
]
|
||||
"nodeIds": ["building_bfqg91ai9ijps9ej", "level_wyuoxj87czq3v0re"]
|
||||
},
|
||||
{
|
||||
"n": 2,
|
||||
@@ -85,10 +82,7 @@
|
||||
"ok": true,
|
||||
"durationMs": 3,
|
||||
"summary": "furthest: zone_3fyksm10tb0dhn1e <-> zone_u95l1bt35jci3gvu = 7.000m",
|
||||
"nodeIds": [
|
||||
"zone_3fyksm10tb0dhn1e",
|
||||
"zone_u95l1bt35jci3gvu"
|
||||
]
|
||||
"nodeIds": ["zone_3fyksm10tb0dhn1e", "zone_u95l1bt35jci3gvu"]
|
||||
},
|
||||
{
|
||||
"n": 8,
|
||||
@@ -117,9 +111,7 @@
|
||||
"ok": true,
|
||||
"durationMs": 2,
|
||||
"summary": "newLevelId=level_cxvltlqvgqcasiep, cloned=22, valid=true, errors=0",
|
||||
"nodeIds": [
|
||||
"level_cxvltlqvgqcasiep"
|
||||
]
|
||||
"nodeIds": ["level_cxvltlqvgqcasiep"]
|
||||
},
|
||||
{
|
||||
"n": 12,
|
||||
@@ -170,4 +162,4 @@
|
||||
"delta": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
* bun packages/mcp/test-reports/t3-scenario/run.ts
|
||||
*/
|
||||
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url))
|
||||
const SERVER_URL = 'http://localhost:3917/mcp'
|
||||
@@ -293,15 +293,8 @@ async function main(): Promise<void> {
|
||||
}
|
||||
})
|
||||
|
||||
const [
|
||||
bed1SouthId,
|
||||
bed1EastId,
|
||||
bed2SouthId,
|
||||
bed2WestId,
|
||||
bathSouthId,
|
||||
bathWestId,
|
||||
bathEastId,
|
||||
] = interior ?? []
|
||||
const [bed1SouthId, bed1EastId, bed2SouthId, bed2WestId, bathSouthId, bathWestId, bathEastId] =
|
||||
interior ?? []
|
||||
|
||||
// ----- Step 4: Set zones -----
|
||||
const zones = await timed(4, 'set zones', async () => {
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
/**
|
||||
* T5 — MCP resources + prompts test harness.
|
||||
*
|
||||
* Connects to the shared MCP HTTP server at http://localhost:3917 (path /mcp),
|
||||
* exercises the 4 documented resources and 3 prompts, and prints a structured
|
||||
* pass/fail summary that the REPORT.md can be authored from.
|
||||
*
|
||||
* Usage:
|
||||
* bun packages/mcp/test-reports/t5-resources-prompts/run.ts
|
||||
*/
|
||||
import { existsSync } from 'node:fs'
|
||||
import { dirname, resolve as pathResolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
|
||||
const HTTP_URL = new URL('http://localhost:3917/mcp')
|
||||
@@ -104,9 +92,7 @@ async function main(): Promise<void> {
|
||||
.join(', ')}`,
|
||||
)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[t5] listResources error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
)
|
||||
console.error(`[t5] listResources error: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
|
||||
// ---------------- Resource 1: pascal://scene/current ----------------
|
||||
@@ -116,9 +102,7 @@ async function main(): Promise<void> {
|
||||
if (!c) {
|
||||
resourceOutcomes.push(fail('scene/current', 'no contents returned'))
|
||||
} else if (c.mimeType !== 'application/json') {
|
||||
resourceOutcomes.push(
|
||||
fail('scene/current', `wrong mime type: ${String(c.mimeType)}`),
|
||||
)
|
||||
resourceOutcomes.push(fail('scene/current', `wrong mime type: ${String(c.mimeType)}`))
|
||||
} else {
|
||||
const text = typeof c.text === 'string' ? c.text : ''
|
||||
let parsed: unknown
|
||||
@@ -144,10 +128,7 @@ async function main(): Promise<void> {
|
||||
const nodeCount = Object.keys(obj.nodes as Record<string, unknown>).length
|
||||
const rootCount = (obj.rootNodeIds as unknown[]).length
|
||||
resourceOutcomes.push(
|
||||
ok(
|
||||
'scene/current',
|
||||
`application/json, nodes=${nodeCount}, rootNodeIds=${rootCount}`,
|
||||
),
|
||||
ok('scene/current', `application/json, nodes=${nodeCount}, rootNodeIds=${rootCount}`),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -172,13 +153,9 @@ async function main(): Promise<void> {
|
||||
const hasHeading = /^# /m.test(text)
|
||||
const hasZoneOrLevel = /level/i.test(text) || /zone/i.test(text)
|
||||
if (!hasHeading) {
|
||||
resourceOutcomes.push(
|
||||
fail('scene/current/summary', 'no markdown # heading found'),
|
||||
)
|
||||
resourceOutcomes.push(fail('scene/current/summary', 'no markdown # heading found'))
|
||||
} else if (!hasZoneOrLevel) {
|
||||
resourceOutcomes.push(
|
||||
fail('scene/current/summary', 'no level/zone references'),
|
||||
)
|
||||
resourceOutcomes.push(fail('scene/current/summary', 'no level/zone references'))
|
||||
} else {
|
||||
// Extract a few first lines as preview
|
||||
const preview = text.split('\n').slice(0, 4).join(' | ')
|
||||
@@ -192,10 +169,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
} catch (err) {
|
||||
resourceOutcomes.push(
|
||||
fail(
|
||||
'scene/current/summary',
|
||||
`threw: ${err instanceof Error ? err.message : String(err)}`,
|
||||
),
|
||||
fail('scene/current/summary', `threw: ${err instanceof Error ? err.message : String(err)}`),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -206,15 +180,16 @@ async function main(): Promise<void> {
|
||||
if (!c) {
|
||||
resourceOutcomes.push(fail('catalog/items', 'no contents returned'))
|
||||
} else if (c.mimeType !== 'application/json') {
|
||||
resourceOutcomes.push(
|
||||
fail('catalog/items', `wrong mime type: ${String(c.mimeType)}`),
|
||||
)
|
||||
resourceOutcomes.push(fail('catalog/items', `wrong mime type: ${String(c.mimeType)}`))
|
||||
} else {
|
||||
const text = typeof c.text === 'string' ? c.text : ''
|
||||
const parsed = JSON.parse(text) as { status?: unknown; items?: unknown }
|
||||
if (parsed.status !== 'catalog_unavailable') {
|
||||
resourceOutcomes.push(
|
||||
fail('catalog/items', `expected status='catalog_unavailable' got ${String(parsed.status)}`),
|
||||
fail(
|
||||
'catalog/items',
|
||||
`expected status='catalog_unavailable' got ${String(parsed.status)}`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
resourceOutcomes.push(
|
||||
@@ -241,8 +216,7 @@ async function main(): Promise<void> {
|
||||
name: 'find_nodes',
|
||||
arguments: { type: 'level' },
|
||||
})
|
||||
const sc = (findResult as { structuredContent?: { nodes?: unknown[] } })
|
||||
.structuredContent
|
||||
const sc = (findResult as { structuredContent?: { nodes?: unknown[] } }).structuredContent
|
||||
const nodes = Array.isArray(sc?.nodes) ? sc.nodes : []
|
||||
if (nodes.length > 0) {
|
||||
const first = nodes[0] as { id?: string }
|
||||
@@ -252,9 +226,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
console.log(`[t5] discovered levelId = ${String(discoveredLevelId)}`)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[t5] find_nodes threw: ${err instanceof Error ? err.message : String(err)}`,
|
||||
)
|
||||
console.error(`[t5] find_nodes threw: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
|
||||
if (!discoveredLevelId) {
|
||||
@@ -281,19 +253,12 @@ async function main(): Promise<void> {
|
||||
}
|
||||
if (parsed.error) {
|
||||
resourceOutcomes.push(
|
||||
fail(
|
||||
'constraints/{levelId}',
|
||||
`error in payload: ${safeStringify(parsed.error)}`,
|
||||
),
|
||||
fail('constraints/{levelId}', `error in payload: ${safeStringify(parsed.error)}`),
|
||||
)
|
||||
} else if (!Array.isArray(parsed.slabs)) {
|
||||
resourceOutcomes.push(
|
||||
fail('constraints/{levelId}', 'missing slabs array'),
|
||||
)
|
||||
resourceOutcomes.push(fail('constraints/{levelId}', 'missing slabs array'))
|
||||
} else if (!Array.isArray(parsed.wallPolygons)) {
|
||||
resourceOutcomes.push(
|
||||
fail('constraints/{levelId}', 'missing wallPolygons array'),
|
||||
)
|
||||
resourceOutcomes.push(fail('constraints/{levelId}', 'missing wallPolygons array'))
|
||||
} else {
|
||||
resourceOutcomes.push(
|
||||
ok(
|
||||
@@ -318,15 +283,9 @@ async function main(): Promise<void> {
|
||||
const list = await client.listPrompts()
|
||||
listPromptsCount = Array.isArray(list.prompts) ? list.prompts.length : 0
|
||||
console.log(`[t5] listPrompts count = ${listPromptsCount}`)
|
||||
console.log(
|
||||
`[t5] listPrompts names = ${(list.prompts ?? [])
|
||||
.map((p) => p.name)
|
||||
.join(', ')}`,
|
||||
)
|
||||
console.log(`[t5] listPrompts names = ${(list.prompts ?? []).map((p) => p.name).join(', ')}`)
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`[t5] listPrompts error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
)
|
||||
console.error(`[t5] listPrompts error: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
|
||||
// ---------------- Prompt 1: from_brief ----------------
|
||||
@@ -386,10 +345,7 @@ async function main(): Promise<void> {
|
||||
}
|
||||
} catch (err) {
|
||||
promptOutcomes.push(
|
||||
fail(
|
||||
'iterate_on_feedback',
|
||||
`threw: ${err instanceof Error ? err.message : String(err)}`,
|
||||
),
|
||||
fail('iterate_on_feedback', `threw: ${err instanceof Error ? err.message : String(err)}`),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -406,9 +362,7 @@ async function main(): Promise<void> {
|
||||
const messages = result.messages ?? []
|
||||
const userMsgs = messages.filter((m) => m.role === 'user')
|
||||
if (userMsgs.length === 0) {
|
||||
promptOutcomes.push(
|
||||
fail('renovation_from_photos', 'no user messages returned'),
|
||||
)
|
||||
promptOutcomes.push(fail('renovation_from_photos', 'no user messages returned'))
|
||||
} else {
|
||||
// Look across all message content for the URLs we passed.
|
||||
const allText = messages
|
||||
|
||||
Reference in New Issue
Block a user