Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Figma also provides official [remote and desktop MCP servers](https://developers
With the TemPad Dev panel open and MCP enabled, the MCP server exposes:

- `get_code`: High-fidelity JSX/Vue + TailwindCSS code output by default, plus attached assets and the codegen preset/config used.
- `get_structure`: A structural outline (ids, types, geometry) for the current selection.
- `get_structure`: A structural outline (ids, types, geometry) for one or more nodes in the current selection.
- Binary assets are returned as metadata + HTTP download URLs (`asset.url`) in tool responses. Asset MCP resources are not exposed.

### Setup guide
Expand Down
2 changes: 1 addition & 1 deletion README.zh-Hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ Figma 也提供官方的 [remote 与 desktop MCP server](https://developers.figm
打开 TemPad Dev 面板并启用 MCP 后,MCP 服务器会暴露以下能力:

- `get_code`:默认输出高保真的 JSX/Vue + TailwindCSS 代码,同时包含相关资源以及使用的 codegen 预设和配置。
- `get_structure`:当前选中节点的结构信息(id、类型、几何数据)。
- `get_structure`:当前选中的一个或多个节点的结构信息(id、类型、几何数据)。
- 二进制资源会通过工具响应中的元数据 + HTTP 下载地址(`asset.url`)提供;MCP 不再暴露 asset 资源模板。

### 配置指南
Expand Down
11 changes: 9 additions & 2 deletions agent-plugins/tempad-dev/skills/figma-design-to-code/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,19 @@ ask the user instead of inferring it.

### 2. Fetch the top-level design snapshot

Call `tempad-dev:get_code` first.
If multiple top-level designs are selected, call `tempad-dev:get_structure`
with `options.depth: 1` to enumerate the selected roots, then call
`tempad-dev:get_code` separately with each returned root `nodeId`. Use this
multi-root structure response only for selection discovery and hierarchy, not
as style evidence.

For each single design root, call `tempad-dev:get_code` first.

Use these defaults:

- `resolveTokens: false`
- pass `nodeId` only when the user provided one; otherwise use the current
- pass `nodeId` when the user provided one or when iterating roots returned by
a multi-selection `get_structure` call; otherwise use the current single
selection
- set `preferredLang` to match the project target, such as `jsx` or `vue`

Expand Down
1 change: 1 addition & 0 deletions docs/extension/mcp-context-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This document records the current context-control strategy for TemPad Dev MCP ou
rendering. Other overflow causes still reuse full-tree context for correctness.
- Only fail fast when a usable shell cannot be generated.
2. `get_structure` keeps existing API but output is compacted by default.
- When `nodeId` is omitted, accept one or more visible roots from the current selection.
- Limit total nodes.
- Normalize/trim long names.
- Round geometry values.
Expand Down
3 changes: 3 additions & 0 deletions docs/skill/rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ TemPad Dev `get_code` is treated as the primary design-evidence source.
`get_structure` is only a structural aid for hierarchy, geometry, overlap, and
scope recovery.

The only ordering exception is a current multi-selection: use `get_structure`
to enumerate its root node ids, then call `get_code` separately for each root.

This prevents the agent from reconstructing detailed UI from structural hints
alone.

Expand Down
19 changes: 17 additions & 2 deletions packages/extension/mcp/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ function resolveSingleNode(nodeId?: string): SceneNode {
return selection.value[0]
}

function resolveVisibleNodes(nodeId?: string): SceneNode[] {
if (nodeId) {
return [resolveSingleNode(nodeId)]
}

if (!selection.value.length || selection.value.some((node) => !node.visible)) {
throw createCodedError(
TEMPAD_MCP_ERROR_CODES.INVALID_SELECTION,
'Select one or more visible nodes (or provide nodeId) to proceed.'
)
}

return [...selection.value]
}

async function handleGetCode(args?: GetCodeParametersInput): Promise<GetCodeResult> {
return dispatchGetCode(args)
}
Expand Down Expand Up @@ -88,9 +103,9 @@ async function handleGetScreenshot(

async function handleGetStructure(args?: GetStructureParametersInput): Promise<GetStructureResult> {
const { nodeId, options } = args ?? {}
const root = resolveSingleNode(nodeId)
const roots = resolveVisibleNodes(nodeId)
const depth = options?.depth
return runGetStructure([root], depth)
return runGetStructure(roots, depth)
}

export type MCPHandlers = {
Expand Down
35 changes: 34 additions & 1 deletion packages/extension/tests/mcp/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ describe('mcp/runtime', () => {
})
})

it('throws coded error for invalid current selection (empty or invisible)', async () => {
it('throws coded error for invalid get_code selection (empty, invisible or multiple)', async () => {
setFigmaGetNodeById(null)
const runtime = await importRuntime()

Expand All @@ -183,6 +183,11 @@ describe('mcp/runtime', () => {
code: TEMPAD_MCP_ERROR_CODES.INVALID_SELECTION
})

mocks.selection.value = [createSceneNode('first'), createSceneNode('second')]
await expect(runtime.MCP_TOOL_HANDLERS.get_code()).rejects.toMatchObject({
code: TEMPAD_MCP_ERROR_CODES.INVALID_SELECTION
})

mocks.selection.value = [createSceneNode('hidden', false)]
await expect(runtime.MCP_TOOL_HANDLERS.get_code()).rejects.toMatchObject({
code: TEMPAD_MCP_ERROR_CODES.INVALID_SELECTION
Expand Down Expand Up @@ -249,4 +254,32 @@ describe('mcp/runtime', () => {
await runtime.MCP_TOOL_HANDLERS.get_structure()
expect(mocks.runGetStructure).toHaveBeenLastCalledWith([node], undefined)
})

it('routes all visible selected nodes to get_structure', async () => {
const first = createSceneNode('first')
const second = createSceneNode('second')
mocks.selection.value = [first, second]
setFigmaGetNodeById(null)
mocks.runGetStructure.mockResolvedValue({ roots: [] })

const runtime = await importRuntime()
await runtime.MCP_TOOL_HANDLERS.get_structure({ options: { depth: 1 } })

expect(mocks.runGetStructure).toHaveBeenCalledWith([first, second], 1)
})

it('throws coded error when get_structure selection is empty or contains an invisible node', async () => {
setFigmaGetNodeById(null)
const runtime = await importRuntime()

mocks.selection.value = []
await expect(runtime.MCP_TOOL_HANDLERS.get_structure()).rejects.toMatchObject({
code: TEMPAD_MCP_ERROR_CODES.INVALID_SELECTION
})

mocks.selection.value = [createSceneNode('visible'), createSceneNode('hidden', false)]
await expect(runtime.MCP_TOOL_HANDLERS.get_structure()).rejects.toMatchObject({
code: TEMPAD_MCP_ERROR_CODES.INVALID_SELECTION
})
})
})
19 changes: 19 additions & 0 deletions packages/extension/tests/mcp/tools/structure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ describe('mcp/tools/structure', () => {
expect(buildSemanticTree).toHaveBeenCalledWith([], { depthLimit: 3 })
})

it('preserves separate outlines for multiple selected roots', () => {
const roots = [
{ id: 'node-1', visible: true },
{ id: 'node-2', visible: true }
] as unknown as SceneNode[]
vi.mocked(buildSemanticTree).mockReturnValue({
roots: [{ id: 'semantic-1' }, { id: 'semantic-2' }]
} as unknown as ReturnType<typeof buildSemanticTree>)
vi.mocked(semanticTreeToOutline).mockReturnValue([
{ id: 'outline-1', name: 'First', type: 'FRAME', x: 0, y: 0, width: 100, height: 100 },
{ id: 'outline-2', name: 'Second', type: 'FRAME', x: 200, y: 0, width: 100, height: 100 }
])

const result = handleGetStructure(roots, 1)

expect(buildSemanticTree).toHaveBeenCalledWith(roots, { depthLimit: 1 })
expect(result.roots.map(({ id }) => id)).toEqual(['outline-1', 'outline-2'])
})

it('compacts large outlines to keep structure output small', () => {
vi.mocked(buildSemanticTree).mockReturnValue({ roots: [] } as unknown as ReturnType<
typeof buildSemanticTree
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ For agent-specific setup, open TemPad Dev's **Preferences → Agent integration
Supported tools/resources:

- `get_code`: Tailwind-first JSX/Vue markup plus assets and token references.
- `get_structure`: Hierarchy/geometry outline for the selection.
- `get_structure`: Hierarchy/geometry outline for one or more selected nodes.

Notes:

Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/README.zh-Hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
支持的工具和资源:

- `get_code`:以 Tailwind 优先的 JSX/Vue 标记输出,并附带资源和变量引用。
- `get_structure`:当前选中节点的层级/几何结构信息。
- `get_structure`:当前选中的一个或多个节点的层级/几何结构信息。

说明:

Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-server/src/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Rules:
- Never output any `data-hint-*` attributes from tool outputs (hints only).
- If `get_code` warns `depth-cap`, keep the returned parent code as composition evidence and use returned `data-hint-id` values to choose narrower `get_code` follow-ups.
- If `get_code` warns `shell`, read the inline code comment for omitted direct child ids, then call `get_code` for those ids in order and fill the results back into the returned shell.
- Use `get_structure` only to resolve layout/overlap uncertainty; do not derive numeric values from images.
- Use `get_structure` only to enumerate multi-selected roots or resolve layout/overlap uncertainty; do not derive numeric values from images.
- Tokens: `get_code.tokens` keys are canonical names (`--...`). Multi‑mode values use `${collectionName}:${modeName}`. Nodes may hint per-node overrides via `data-hint-variable-mode="Collection=Mode;..."`.
- Vectors: `vectorMode=smart` is the default. Treat the emitted markup as the source of truth for the current response; vector code is emitted as `<svg data-src="...">` placeholders, but if asset upload fails after export the tool may inline the SVG as a fallback to preserve source of truth.
- Themeable vectors: `themeable=true` means the SVG can safely adopt one contextual color channel. In `smart` mode, that color is typically already evidenced on the emitted `svg` root markup for the placeholder. It does not mean the SVG exposes multiple independent color parameters.
Expand Down
22 changes: 17 additions & 5 deletions packages/mcp-server/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ const CONNECTIVITY_TROUBLESHOOTING_LINES = [
'- Keep the Figma tab active/foreground while using the MCP server.'
]

const SELECTION_TROUBLESHOOTING_LINE = 'Tip: Select exactly one visible node, or pass nodeId.'
const SINGLE_SELECTION_TROUBLESHOOTING_LINE =
'Tip: Select exactly one visible node, or pass nodeId.'
const MULTI_SELECTION_TROUBLESHOOTING_LINE =
'Tip: Select one or more visible nodes, or pass nodeId.'

function getRecordProperty(record: unknown, key: string): unknown {
if (!record || typeof record !== 'object') {
Expand Down Expand Up @@ -138,7 +141,7 @@ export const TOOL_DEFS = [
extTool({
name: 'get_structure',
description:
'Get a compact structural + geometry outline for nodeId/current single selection to understand hierarchy and layout intent.',
'Get a compact structural + geometry outline for nodeId or all visible nodes in the current selection. Use multi-selection to enumerate design roots before calling get_code once per root nodeId.',
parameters: GetStructureParametersSchema,
target: 'extension',
format: createStructureToolResponse
Expand Down Expand Up @@ -181,7 +184,7 @@ function createToolErrorResponse(toolName: string, error: unknown): CallToolResu
const message = extractToolErrorMessage(error)
const code = extractToolErrorCode(error)
const codeLabel = code ? ` [${code}]` : ''
const troubleshooting = buildTroubleshootingText(code, message)
const troubleshooting = buildTroubleshootingText(toolName, code, message)

return {
isError: true,
Expand All @@ -194,15 +197,23 @@ function createToolErrorResponse(toolName: string, error: unknown): CallToolResu
}
}

function buildTroubleshootingText(code: TempadMcpErrorCode | undefined, message: string): string {
function buildTroubleshootingText(
toolName: string,
code: TempadMcpErrorCode | undefined,
message: string
): string {
const help: string[] = []

if (isConnectivityToolError(code, message)) {
help.push(...CONNECTIVITY_TROUBLESHOOTING_LINES)
}

if (isSelectionToolError(code, message)) {
help.push(SELECTION_TROUBLESHOOTING_LINE)
help.push(
toolName === 'get_structure'
? MULTI_SELECTION_TROUBLESHOOTING_LINE
: SINGLE_SELECTION_TROUBLESHOOTING_LINE
)
}

return help.length ? `\n\n${help.join('\n')}` : ''
Expand All @@ -221,6 +232,7 @@ function isSelectionToolError(code: TempadMcpErrorCode | undefined, message: str
return (
(code ? SELECTION_ERROR_CODES.has(code) : false) ||
/select exactly one visible node/i.test(message) ||
/select one or more visible nodes/i.test(message) ||
/no visible node found/i.test(message)
)
}
Expand Down
8 changes: 8 additions & 0 deletions packages/mcp-server/tests/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@ describe('tools response helpers', () => {
expect(textContent(selectionError.content[0])).toContain('[INVALID_SELECTION]')
expect(textContent(selectionError.content[0])).toContain('Tip: Select exactly one visible node')

const structureSelectionError = createToolErrorResponse('get_structure', {
code: TEMPAD_MCP_ERROR_CODES.INVALID_SELECTION,
message: 'Select one or more visible nodes.'
})
expect(textContent(structureSelectionError.content[0])).toContain(
'Tip: Select one or more visible nodes'
)

const unknownError = createToolErrorResponse('get_assets', 42)
expect(unknownError.isError).toBe(true)
expect(textContent(unknownError.content[0])).toBe(
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const GetStructureParametersSchema = z.object({
nodeId: z
.string()
.describe(
'Optional node id to outline; defaults to the current single selection. Useful when auto-layout hints are none/inferred or you need explicit geometry for refactors.'
'Optional node id to outline; defaults to all visible nodes in the current selection. Useful for enumerating multiple selected design roots or confirming hierarchy and geometry.'
)
.optional(),
options: z
Expand Down
11 changes: 9 additions & 2 deletions skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,19 @@ ask the user instead of inferring it.

### 2. Fetch the top-level design snapshot

Call `tempad-dev:get_code` first.
If multiple top-level designs are selected, call `tempad-dev:get_structure`
with `options.depth: 1` to enumerate the selected roots, then call
`tempad-dev:get_code` separately with each returned root `nodeId`. Use this
multi-root structure response only for selection discovery and hierarchy, not
as style evidence.

For each single design root, call `tempad-dev:get_code` first.

Use these defaults:

- `resolveTokens: false`
- pass `nodeId` only when the user provided one; otherwise use the current
- pass `nodeId` when the user provided one or when iterating roots returned by
a multi-selection `get_structure` call; otherwise use the current single
selection
- set `preferredLang` to match the project target, such as `jsx` or `vue`

Expand Down