Skip to content

Commit 7a02671

Browse files
committed
feat(m3): wire scan_components + component_map; demote analyze_project to internal helper
Two server-local MCP tools advertised (scan_components, component_map) via the save_screenshots precedent; SERVER_ONLY_TOOLS in the registry sync guard updated. analyze_project stays an internal helper (analyzeProject), not its own tool — honoring the pinned 'no analyze_project tool / feel like official Figma' decision (PLAN line 94/258); the profile is surfaced via both tools' output instead. PLAN.md M3 section records the component_map first cut + remaining token_map work. All 475 tests green, typecheck + build clean.
1 parent be0fb7c commit 7a02671

4 files changed

Lines changed: 81 additions & 97 deletions

File tree

packages/server/src/index.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@ import { Election } from './election/election.js';
1313
import { Follower } from './election/follower.js';
1414
import { attachLeaderEndpoints } from './election/leader-endpoints.js';
1515
import { Node, NodeRole } from './election/node.js';
16-
import { TOOL_DEFINITIONS, WRITE_TOOL_NAMES } from './tools/registry.js';
17-
import { ANALYZE_PROJECT_TOOL_NAME, handleAnalyzeProject } from './tools/analyze-project.js';
1816
import { COMPONENT_MAP_TOOL_NAME, handleComponentMap } from './tools/component-map.js';
19-
import { handleScanComponents, SCAN_COMPONENTS_TOOL_NAME } from './tools/scan-components.js';
2017
import { GET_SCREENSHOT_TOOL_NAME, screenshotContent } from './tools/get-screenshot.js';
2118
import { formatPingResult, handlePing } from './tools/ping.js';
19+
import { TOOL_DEFINITIONS, WRITE_TOOL_NAMES } from './tools/registry.js';
2220
import { handleSaveScreenshots, SAVE_SCREENSHOTS_TOOL_NAME } from './tools/save-screenshots.js';
21+
import { handleScanComponents, SCAN_COMPONENTS_TOOL_NAME } from './tools/scan-components.js';
2322

2423
const SERVER_NAME = '@figma-mcp-relay/server';
2524
const SERVER_VERSION = '0.0.0';
@@ -76,10 +75,6 @@ mcp.setRequestHandler(CallToolRequestSchema, async request => {
7675
const result = (await dispatchTool({ node, follower, log }, name, args)) as GetScreenshotResult;
7776
return { content: screenshotContent(result) };
7877
}
79-
if (name === ANALYZE_PROJECT_TOOL_NAME) {
80-
const result = await handleAnalyzeProject(args);
81-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] };
82-
}
8378
if (name === SCAN_COMPONENTS_TOOL_NAME) {
8479
const result = await handleScanComponents(args);
8580
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] };

packages/server/src/tools/analyze-project.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

packages/server/src/tools/registry.ts

Lines changed: 74 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,53 @@
11
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2-
32
// Single source of truth for what the MCP server advertises and which tools are writes. index.ts
43
// consumes these; a registry test asserts they stay in sync with the plugin's handler map (so a new
54
// tool can't be half-wired) and that every input schema property declares a type.
65

7-
import { analyzeProjectToolDefinition } from './analyze-project.js';
8-
import { scanComponentsToolDefinition } from './scan-components.js';
6+
import { ADD_PAGE_TOOL_NAME, addPageToolDefinition } from './add-page.js';
7+
import { ADD_VARIABLE_MODE_TOOL_NAME, addVariableModeToolDefinition } from './add-variable-mode.js';
8+
import {
9+
APPLY_STYLE_TO_NODE_TOOL_NAME,
10+
applyStyleToNodeToolDefinition,
11+
} from './apply-style-to-node.js';
12+
import {
13+
BATCH_RENAME_NODES_TOOL_NAME,
14+
batchRenameNodesToolDefinition,
15+
} from './batch-rename-nodes.js';
16+
import { BATCH_TOOL_NAME, batchToolDefinition } from './batch.js';
17+
import {
18+
BIND_VARIABLE_TO_NODE_TOOL_NAME,
19+
bindVariableToNodeToolDefinition,
20+
} from './bind-variable-to-node.js';
21+
import { CLONE_NODE_TOOL_NAME, cloneNodeToolDefinition } from './clone-node.js';
922
import { componentMapToolDefinition } from './component-map.js';
23+
import { CREATE_COMPONENT_TOOL_NAME, createComponentToolDefinition } from './create-component.js';
24+
import {
25+
CREATE_EFFECT_STYLE_TOOL_NAME,
26+
createEffectStyleToolDefinition,
27+
} from './create-effect-style.js';
28+
import { CREATE_ELLIPSE_TOOL_NAME, createEllipseToolDefinition } from './create-ellipse.js';
29+
import { CREATE_FRAME_TOOL_NAME, createFrameToolDefinition } from './create-frame.js';
30+
import { CREATE_GRID_STYLE_TOOL_NAME, createGridStyleToolDefinition } from './create-grid-style.js';
31+
import { CREATE_INSTANCE_TOOL_NAME, createInstanceToolDefinition } from './create-instance.js';
32+
import {
33+
CREATE_PAINT_STYLE_TOOL_NAME,
34+
createPaintStyleToolDefinition,
35+
} from './create-paint-style.js';
36+
import { CREATE_RECTANGLE_TOOL_NAME, createRectangleToolDefinition } from './create-rectangle.js';
37+
import { CREATE_SECTION_TOOL_NAME, createSectionToolDefinition } from './create-section.js';
38+
import { CREATE_TEXT_STYLE_TOOL_NAME, createTextStyleToolDefinition } from './create-text-style.js';
39+
import { CREATE_TEXT_TOOL_NAME, createTextToolDefinition } from './create-text.js';
40+
import {
41+
CREATE_VARIABLE_COLLECTION_TOOL_NAME,
42+
createVariableCollectionToolDefinition,
43+
} from './create-variable-collection.js';
44+
import { CREATE_VARIABLE_TOOL_NAME, createVariableToolDefinition } from './create-variable.js';
45+
import { DELETE_NODES_TOOL_NAME, deleteNodesToolDefinition } from './delete-nodes.js';
46+
import { DELETE_PAGE_TOOL_NAME, deletePageToolDefinition } from './delete-page.js';
47+
import { DELETE_STYLE_TOOL_NAME, deleteStyleToolDefinition } from './delete-style.js';
48+
import { DELETE_VARIABLE_TOOL_NAME, deleteVariableToolDefinition } from './delete-variable.js';
49+
import { DETACH_INSTANCE_TOOL_NAME, detachInstanceToolDefinition } from './detach-instance.js';
50+
import { FIND_REPLACE_TEXT_TOOL_NAME, findReplaceTextToolDefinition } from './find-replace-text.js';
1051
import { getAnnotationsToolDefinition } from './get-annotations.js';
1152
import { getDesignContextToolDefinition } from './get-design-context.js';
1253
import { getDocumentToolDefinition } from './get-document.js';
@@ -22,70 +63,51 @@ import { getSelectionToolDefinition } from './get-selection.js';
2263
import { getStylesToolDefinition } from './get-styles.js';
2364
import { getVariableDefsToolDefinition } from './get-variable-defs.js';
2465
import { getViewportToolDefinition } from './get-viewport.js';
66+
import { GROUP_NODES_TOOL_NAME, groupNodesToolDefinition } from './group-nodes.js';
67+
import { IMPORT_IMAGE_TOOL_NAME, importImageToolDefinition } from './import-image.js';
2568
import { listFilesToolDefinition } from './list-files.js';
26-
import { pingToolDefinition } from './ping.js';
27-
import { saveScreenshotsToolDefinition } from './save-screenshots.js';
28-
import { scanNodesByTypesToolDefinition } from './scan-nodes-by-types.js';
29-
import { scanTextNodesToolDefinition } from './scan-text-nodes.js';
30-
import { searchNodesToolDefinition } from './search-nodes.js';
31-
import { CLONE_NODE_TOOL_NAME, cloneNodeToolDefinition } from './clone-node.js';
32-
import { CREATE_FRAME_TOOL_NAME, createFrameToolDefinition } from './create-frame.js';
33-
import { CREATE_RECTANGLE_TOOL_NAME, createRectangleToolDefinition } from './create-rectangle.js';
34-
import { CREATE_TEXT_TOOL_NAME, createTextToolDefinition } from './create-text.js';
35-
import { DELETE_NODES_TOOL_NAME, deleteNodesToolDefinition } from './delete-nodes.js';
3669
import { LOCK_NODES_TOOL_NAME, lockNodesToolDefinition } from './lock-nodes.js';
3770
import { MOVE_NODES_TOOL_NAME, moveNodesToolDefinition } from './move-nodes.js';
71+
import { NAVIGATE_TO_PAGE_TOOL_NAME, navigateToPageToolDefinition } from './navigate-to-page.js';
72+
import { pingToolDefinition } from './ping.js';
73+
import { REMOVE_REACTIONS_TOOL_NAME, removeReactionsToolDefinition } from './remove-reactions.js';
3874
import { RENAME_NODE_TOOL_NAME, renameNodeToolDefinition } from './rename-node.js';
75+
import { RENAME_PAGE_TOOL_NAME, renamePageToolDefinition } from './rename-page.js';
76+
import { REORDER_NODES_TOOL_NAME, reorderNodesToolDefinition } from './reorder-nodes.js';
77+
import { REPARENT_NODES_TOOL_NAME, reparentNodesToolDefinition } from './reparent-nodes.js';
3978
import { RESIZE_NODES_TOOL_NAME, resizeNodesToolDefinition } from './resize-nodes.js';
4079
import { ROTATE_NODES_TOOL_NAME, rotateNodesToolDefinition } from './rotate-nodes.js';
80+
import { saveScreenshotsToolDefinition } from './save-screenshots.js';
81+
import { scanComponentsToolDefinition } from './scan-components.js';
82+
import { scanNodesByTypesToolDefinition } from './scan-nodes-by-types.js';
83+
import { scanTextNodesToolDefinition } from './scan-text-nodes.js';
84+
import { searchNodesToolDefinition } from './search-nodes.js';
4185
import { SET_AUTO_LAYOUT_TOOL_NAME, setAutoLayoutToolDefinition } from './set-auto-layout.js';
4286
import { SET_BLEND_MODE_TOOL_NAME, setBlendModeToolDefinition } from './set-blend-mode.js';
4387
import { SET_CONSTRAINTS_TOOL_NAME, setConstraintsToolDefinition } from './set-constraints.js';
4488
import { SET_CORNER_RADIUS_TOOL_NAME, setCornerRadiusToolDefinition } from './set-corner-radius.js';
89+
import { SET_EFFECTS_TOOL_NAME, setEffectsToolDefinition } from './set-effects.js';
4590
import { SET_FILLS_TOOL_NAME, setFillsToolDefinition } from './set-fills.js';
4691
import { SET_OPACITY_TOOL_NAME, setOpacityToolDefinition } from './set-opacity.js';
92+
import { SET_REACTIONS_TOOL_NAME, setReactionsToolDefinition } from './set-reactions.js';
4793
import { SET_STROKES_TOOL_NAME, setStrokesToolDefinition } from './set-strokes.js';
94+
import {
95+
SET_TEXT_PROPERTIES_TOOL_NAME,
96+
setTextPropertiesToolDefinition,
97+
} from './set-text-properties.js';
4898
import { SET_TEXT_TOOL_NAME, setTextToolDefinition } from './set-text.js';
99+
import {
100+
SET_VARIABLE_VALUE_TOOL_NAME,
101+
setVariableValueToolDefinition,
102+
} from './set-variable-value.js';
49103
import { SET_VISIBLE_TOOL_NAME, setVisibleToolDefinition } from './set-visible.js';
104+
import { SWAP_COMPONENT_TOOL_NAME, swapComponentToolDefinition } from './swap-component.js';
105+
import { UNGROUP_NODES_TOOL_NAME, ungroupNodesToolDefinition } from './ungroup-nodes.js';
50106
import { UNLOCK_NODES_TOOL_NAME, unlockNodesToolDefinition } from './unlock-nodes.js';
51-
import { SET_EFFECTS_TOOL_NAME, setEffectsToolDefinition } from './set-effects.js';
52-
import { CREATE_PAINT_STYLE_TOOL_NAME, createPaintStyleToolDefinition } from './create-paint-style.js';
53-
import { CREATE_TEXT_STYLE_TOOL_NAME, createTextStyleToolDefinition } from './create-text-style.js';
54-
import { CREATE_EFFECT_STYLE_TOOL_NAME, createEffectStyleToolDefinition } from './create-effect-style.js';
55-
import { CREATE_GRID_STYLE_TOOL_NAME, createGridStyleToolDefinition } from './create-grid-style.js';
56-
import { UPDATE_PAINT_STYLE_TOOL_NAME, updatePaintStyleToolDefinition } from './update-paint-style.js';
57-
import { APPLY_STYLE_TO_NODE_TOOL_NAME, applyStyleToNodeToolDefinition } from './apply-style-to-node.js';
58-
import { DELETE_STYLE_TOOL_NAME, deleteStyleToolDefinition } from './delete-style.js';
59107
import {
60-
CREATE_VARIABLE_COLLECTION_TOOL_NAME,
61-
createVariableCollectionToolDefinition,
62-
} from './create-variable-collection.js';
63-
import { ADD_VARIABLE_MODE_TOOL_NAME, addVariableModeToolDefinition } from './add-variable-mode.js';
64-
import { CREATE_VARIABLE_TOOL_NAME, createVariableToolDefinition } from './create-variable.js';
65-
import { SET_VARIABLE_VALUE_TOOL_NAME, setVariableValueToolDefinition } from './set-variable-value.js';
66-
import { SET_TEXT_PROPERTIES_TOOL_NAME, setTextPropertiesToolDefinition } from './set-text-properties.js';
67-
import { BIND_VARIABLE_TO_NODE_TOOL_NAME, bindVariableToNodeToolDefinition } from './bind-variable-to-node.js';
68-
import { DELETE_VARIABLE_TOOL_NAME, deleteVariableToolDefinition } from './delete-variable.js';
69-
import { GROUP_NODES_TOOL_NAME, groupNodesToolDefinition } from './group-nodes.js';
70-
import { UNGROUP_NODES_TOOL_NAME, ungroupNodesToolDefinition } from './ungroup-nodes.js';
71-
import { REPARENT_NODES_TOOL_NAME, reparentNodesToolDefinition } from './reparent-nodes.js';
72-
import { REORDER_NODES_TOOL_NAME, reorderNodesToolDefinition } from './reorder-nodes.js';
73-
import { FIND_REPLACE_TEXT_TOOL_NAME, findReplaceTextToolDefinition } from './find-replace-text.js';
74-
import { BATCH_RENAME_NODES_TOOL_NAME, batchRenameNodesToolDefinition } from './batch-rename-nodes.js';
75-
import { ADD_PAGE_TOOL_NAME, addPageToolDefinition } from './add-page.js';
76-
import { DELETE_PAGE_TOOL_NAME, deletePageToolDefinition } from './delete-page.js';
77-
import { RENAME_PAGE_TOOL_NAME, renamePageToolDefinition } from './rename-page.js';
78-
import { NAVIGATE_TO_PAGE_TOOL_NAME, navigateToPageToolDefinition } from './navigate-to-page.js';
79-
import { SET_REACTIONS_TOOL_NAME, setReactionsToolDefinition } from './set-reactions.js';
80-
import { REMOVE_REACTIONS_TOOL_NAME, removeReactionsToolDefinition } from './remove-reactions.js';
81-
import { SWAP_COMPONENT_TOOL_NAME, swapComponentToolDefinition } from './swap-component.js';
82-
import { DETACH_INSTANCE_TOOL_NAME, detachInstanceToolDefinition } from './detach-instance.js';
83-
import { IMPORT_IMAGE_TOOL_NAME, importImageToolDefinition } from './import-image.js';
84-
import { CREATE_ELLIPSE_TOOL_NAME, createEllipseToolDefinition } from './create-ellipse.js';
85-
import { CREATE_COMPONENT_TOOL_NAME, createComponentToolDefinition } from './create-component.js';
86-
import { CREATE_SECTION_TOOL_NAME, createSectionToolDefinition } from './create-section.js';
87-
import { CREATE_INSTANCE_TOOL_NAME, createInstanceToolDefinition } from './create-instance.js';
88-
import { BATCH_TOOL_NAME, batchToolDefinition } from './batch.js';
108+
UPDATE_PAINT_STYLE_TOOL_NAME,
109+
updatePaintStyleToolDefinition,
110+
} from './update-paint-style.js';
89111

90112
/** Every tool the MCP server advertises, in ListTools order. */
91113
export const TOOL_DEFINITIONS: readonly Tool[] = [
@@ -111,8 +133,8 @@ export const TOOL_DEFINITIONS: readonly Tool[] = [
111133
getDesignContextToolDefinition,
112134
getScreenshotToolDefinition,
113135
saveScreenshotsToolDefinition,
114-
// Server-local (filesystem; no plugin handler — like save_screenshots)
115-
analyzeProjectToolDefinition,
136+
// Server-local (filesystem; no plugin handler — like save_screenshots). Profile detection is an
137+
// internal helper (analyzeProject), surfaced via these tools' output rather than its own tool.
116138
scanComponentsToolDefinition,
117139
componentMapToolDefinition,
118140
// Writes

test/tool-registry.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { TOOL_DEFINITIONS, WRITE_TOOL_NAMES } from '../packages/server/src/tools/registry.js';
43
import { createSandboxHandlers } from '../packages/plugin/src/handlers/registry.js';
4+
import { TOOL_DEFINITIONS, WRITE_TOOL_NAMES } from '../packages/server/src/tools/registry.js';
55

66
// Cross-package guard: a tool is wired across ~6 places (server def + ListTools + WRITE set, plugin
77
// handler + idempotent wrap + batch inverse). Forgetting one fails silently at runtime. These tests
88
// lock the server's advertised tools and the plugin's handler map to each other, and assert every
99
// input schema property declares a type (the gap that let set_variable_value ship broken).
1010

1111
// Tools the server handles on its own and never dispatches to the plugin, so they have no sandbox
12-
// handler. save_screenshots is composed server-side from get_screenshot + filesystem writes.
13-
const SERVER_ONLY_TOOLS = new Set(['save_screenshots']);
12+
// handler. save_screenshots is composed server-side from get_screenshot + filesystem writes;
13+
// scan_components / component_map read the local project filesystem (component_map also reuses
14+
// get_design_context internally) and never touch the sandbox.
15+
const SERVER_ONLY_TOOLS = new Set(['save_screenshots', 'scan_components', 'component_map']);
1416

1517
const serverNames = TOOL_DEFINITIONS.map(d => d.name);
1618
const dispatchedNames = serverNames.filter(n => !SERVER_ONLY_TOOLS.has(n));

0 commit comments

Comments
 (0)