feat(mcp): add resources and prompts
Resources: pascal://scene/current (JSON), /scene/current/summary
(markdown with per-level counts, floor areas, bbox), /catalog/items
(returns catalog_unavailable in headless mode), and the templated
pascal://constraints/{levelId} which exposes slabs + wall footprints
via @pascal-app/core/wall helpers.
Prompts: from_brief (generate scene from a natural-language brief),
iterate_on_feedback (minimal-diff patch proposals), and
renovation_from_photos (orchestrates the vision tools).
17 tests, all passing.
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
58ad89e80b
commit
570c605446
@@ -0,0 +1,100 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import type { AnyNode, SlabNode, WallNode } from '@pascal-app/core/schema'
|
||||
import { getWallPlanFootprint } from '@pascal-app/core/wall'
|
||||
import type { SceneBridge } from '../bridge/scene-bridge'
|
||||
|
||||
type WallFootprint = {
|
||||
wallId: string
|
||||
footprint: Array<[number, number]>
|
||||
}
|
||||
|
||||
type ConstraintsPayload = {
|
||||
levelId: string
|
||||
slabs: SlabNode[]
|
||||
wallPolygons: WallFootprint[]
|
||||
}
|
||||
|
||||
type ConstraintsError = {
|
||||
error: 'level_not_found'
|
||||
levelId: string
|
||||
slabs: never[]
|
||||
wallPolygons: never[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty `WallMiterData` — we don't compute junctions here. The footprint
|
||||
* falls back to a simple rectangle based on start/end + thickness, which is
|
||||
* correct for non-intersecting walls and an acceptable approximation for
|
||||
* constraint hints.
|
||||
*
|
||||
* Typed via `Parameters<typeof getWallPlanFootprint>[1]` to avoid `any` and
|
||||
* to stay in sync with the core signature.
|
||||
*/
|
||||
const EMPTY_MITER_DATA: Parameters<typeof getWallPlanFootprint>[1] = {
|
||||
junctionData: new Map(),
|
||||
junctions: new Map(),
|
||||
}
|
||||
|
||||
function buildPayload(bridge: SceneBridge, levelId: string): ConstraintsPayload | ConstraintsError {
|
||||
const level = bridge.getNode(levelId as never)
|
||||
if (!level || level.type !== 'level') {
|
||||
return {
|
||||
error: 'level_not_found',
|
||||
levelId,
|
||||
slabs: [] as never[],
|
||||
wallPolygons: [] as never[],
|
||||
}
|
||||
}
|
||||
|
||||
const all = bridge.findNodes({ levelId: levelId as never })
|
||||
const slabs: SlabNode[] = []
|
||||
const walls: WallNode[] = []
|
||||
for (const n of all as AnyNode[]) {
|
||||
if (n.type === 'slab') slabs.push(n as SlabNode)
|
||||
else if (n.type === 'wall') walls.push(n as WallNode)
|
||||
}
|
||||
|
||||
const wallPolygons: WallFootprint[] = []
|
||||
for (const wall of walls) {
|
||||
const points = getWallPlanFootprint(wall, EMPTY_MITER_DATA)
|
||||
wallPolygons.push({
|
||||
wallId: wall.id,
|
||||
footprint: points.map((p) => [p.x, p.y] as [number, number]),
|
||||
})
|
||||
}
|
||||
|
||||
return { levelId, slabs, wallPolygons }
|
||||
}
|
||||
|
||||
/**
|
||||
* `pascal://constraints/{levelId}` — per-level geometric constraints used as
|
||||
* input hints for agents: slab nodes (with polygons/holes/elevation) + each
|
||||
* wall's plan-view footprint polygon.
|
||||
*/
|
||||
export function registerConstraints(server: McpServer, bridge: SceneBridge): void {
|
||||
server.registerResource(
|
||||
'constraints',
|
||||
new ResourceTemplate('pascal://constraints/{levelId}', { list: undefined }),
|
||||
{
|
||||
title: 'Level constraints',
|
||||
description:
|
||||
'Per-level constraints: slab nodes and wall plan footprints. Returns {error:"level_not_found"} if the level id is unknown.',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
async (uri, variables) => {
|
||||
const rawLevelId = variables.levelId
|
||||
const levelId = Array.isArray(rawLevelId) ? rawLevelId[0] : rawLevelId
|
||||
const payload = buildPayload(bridge, levelId ?? '')
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(payload),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user