From d807828270cd3773576c0f8a6f5a3f7a5e4a30df Mon Sep 17 00:00:00 2001 From: Victor Wads Date: Wed, 19 Aug 2026 22:08:13 -0300 Subject: [PATCH] feat(mcp): classify tool document access - Declare inspection or modification access on every canonical tool definition - Add bulk category controls while preserving individual disabled-tool storage - Keep runtime availability separate from document access semantics --- CHANGELOG.md | 2 +- packages/core/src/tools/analyze/clusters.ts | 1 + packages/core/src/tools/analyze/colors.ts | 1 + packages/core/src/tools/analyze/diff.ts | 2 ++ packages/core/src/tools/analyze/eval/index.ts | 1 + .../core/src/tools/analyze/overlaps/index.ts | 1 + packages/core/src/tools/analyze/spacing.ts | 1 + packages/core/src/tools/analyze/typography.ts | 1 + packages/core/src/tools/calc.ts | 1 + .../core/src/tools/codegen/component-map.ts | 1 + packages/core/src/tools/codegen/tokens.ts | 1 + packages/core/src/tools/create/basic.ts | 3 ++ packages/core/src/tools/create/components.ts | 2 ++ packages/core/src/tools/create/icons.ts | 3 ++ packages/core/src/tools/create/render.ts | 1 + packages/core/src/tools/create/svg.ts | 1 + packages/core/src/tools/create/vector.ts | 1 + packages/core/src/tools/describe/index.ts | 1 + packages/core/src/tools/index.ts | 2 +- packages/core/src/tools/modify/effects.ts | 1 + packages/core/src/tools/modify/geometry.ts | 4 +++ packages/core/src/tools/modify/layout.ts | 3 ++ packages/core/src/tools/modify/paint.ts | 3 ++ packages/core/src/tools/modify/state.ts | 4 +++ packages/core/src/tools/modify/text.ts | 5 +++ packages/core/src/tools/modify/update.ts | 1 + packages/core/src/tools/read/components.ts | 1 + packages/core/src/tools/read/fonts.ts | 3 ++ packages/core/src/tools/read/jsx.ts | 2 ++ packages/core/src/tools/read/libraries.ts | 2 ++ packages/core/src/tools/read/nodes.ts | 3 ++ packages/core/src/tools/read/pages.ts | 4 +++ packages/core/src/tools/read/query.ts | 1 + packages/core/src/tools/read/selection.ts | 2 ++ packages/core/src/tools/schema.ts | 3 ++ packages/core/src/tools/stock-photo.ts | 1 + packages/core/src/tools/structure/arrange.ts | 1 + packages/core/src/tools/structure/basic.ts | 6 ++++ packages/core/src/tools/structure/batch.ts | 1 + .../core/src/tools/structure/hierarchy.ts | 5 +++ packages/core/src/tools/structure/replace.ts | 1 + packages/core/src/tools/structure/tree.ts | 4 +++ packages/core/src/tools/variables/bindings.ts | 1 + .../core/src/tools/variables/collections.ts | 4 +++ packages/core/src/tools/variables/read.ts | 3 ++ packages/core/src/tools/variables/unbind.ts | 1 + packages/core/src/tools/variables/values.ts | 3 ++ packages/core/src/tools/vector/boolean.ts | 4 +++ packages/core/src/tools/vector/export.ts | 3 ++ packages/core/src/tools/vector/path.ts | 5 +++ packages/core/src/tools/vector/viewport.ts | 3 ++ packages/mcp/src/tool/catalog.ts | 24 ++++++++----- packages/vue/src/i18n/messages/dialogs.ts | 2 ++ src/app/automation/mcp/preferences.ts | 17 +++++++-- .../settings/mcp/MCPSettingsPanel.vue | 35 ++++++++++++++++++- tests/e2e/settings/credentials.spec.ts | 14 ++++++++ tests/engine/mcp/server/index.test.ts | 15 +++++++- 57 files changed, 208 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5f1f96d0..d3cbbf0b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ - Run Pi through AI SDK HarnessAgent as a configurable desktop provider with multiple saved model profiles, secure credentials, existing MCP design tools, and per-profile thinking and permission settings. - Open multiple selected design files in separate tabs. - Let Figma API scripts and automation combine components into variant sets. -- Monitor and restart the local MCP server and choose which tools it exposes from Settings. +- Monitor and restart the local MCP server and choose individual, inspection, or modification tools it exposes from Settings. - Add deterministic two-browser collaboration coverage for bidirectional edits, awareness, departure cleanup, partitioned-peer convergence, and reconnect synchronization without public network dependencies. (#530) - Import, render, edit, resize, select, and export Figma text-on-path layers while preserving their curved glyph layout. - Show Figma-style temporary distance measurements between selected and Option/Alt-hovered layers. (#491) diff --git a/packages/core/src/tools/analyze/clusters.ts b/packages/core/src/tools/analyze/clusters.ts index 0ed784de3..66bb7173f 100644 --- a/packages/core/src/tools/analyze/clusters.ts +++ b/packages/core/src/tools/analyze/clusters.ts @@ -26,6 +26,7 @@ export function calcClusterConfidence(nodes: SizedItem[]): number { export const analyzeClusters = defineTool({ name: 'analyze_clusters', + documentAccess: 'inspect', description: 'Find repeated design patterns (potential components). Groups nodes by structural signature — type, size, and child structure.', params: { diff --git a/packages/core/src/tools/analyze/colors.ts b/packages/core/src/tools/analyze/colors.ts index e00acc81e..11df0e832 100644 --- a/packages/core/src/tools/analyze/colors.ts +++ b/packages/core/src/tools/analyze/colors.ts @@ -24,6 +24,7 @@ function trackColor(colorMap: Map, color: Color, variableNam export const analyzeColors = defineTool({ name: 'analyze_colors', + documentAccess: 'inspect', description: 'Analyze color palette usage across the current page. Shows frequency, variable bindings, and optionally clusters similar colors.', params: { diff --git a/packages/core/src/tools/analyze/diff.ts b/packages/core/src/tools/analyze/diff.ts index f4a318fb5..95d038d0c 100644 --- a/packages/core/src/tools/analyze/diff.ts +++ b/packages/core/src/tools/analyze/diff.ts @@ -121,6 +121,7 @@ function createUnifiedDiff( export const diffCreate = defineTool({ name: 'diff_create', + documentAccess: 'inspect', description: 'Create a structural diff between two node trees. Compares properties (fills, strokes, effects, text, size, position) in unified diff format.', params: { @@ -175,6 +176,7 @@ export const diffCreate = defineTool({ export const diffShow = defineTool({ name: 'diff_show', + documentAccess: 'inspect', description: 'Preview what would change if properties were applied to a node. Shows a unified diff of current vs proposed state.', params: { diff --git a/packages/core/src/tools/analyze/eval/index.ts b/packages/core/src/tools/analyze/eval/index.ts index cb0fca20c..45730811b 100644 --- a/packages/core/src/tools/analyze/eval/index.ts +++ b/packages/core/src/tools/analyze/eval/index.ts @@ -4,6 +4,7 @@ import { wrapEvalCode } from './wrap' export const evalCode = defineTool({ name: 'eval', + documentAccess: 'modify', description: 'Execute JavaScript with full Figma Plugin API access. Use for operations not covered by other tools. The `figma` global is available.', params: { diff --git a/packages/core/src/tools/analyze/overlaps/index.ts b/packages/core/src/tools/analyze/overlaps/index.ts index 7e2780adc..9cc6b5695 100644 --- a/packages/core/src/tools/analyze/overlaps/index.ts +++ b/packages/core/src/tools/analyze/overlaps/index.ts @@ -330,6 +330,7 @@ export function computeOverlaps( export const analyzeOverlaps = defineTool({ name: 'analyze_overlaps', + documentAccess: 'inspect', description: 'Detect visual overlaps and layout overflows across the current page. Useful for finding content that covers footers, text that bleeds outside frames, and accidental sibling overlaps.', params: { diff --git a/packages/core/src/tools/analyze/spacing.ts b/packages/core/src/tools/analyze/spacing.ts index 332c65d56..37ce20714 100644 --- a/packages/core/src/tools/analyze/spacing.ts +++ b/packages/core/src/tools/analyze/spacing.ts @@ -4,6 +4,7 @@ import { defineTool } from '#core/tools/schema' export const analyzeSpacing = defineTool({ name: 'analyze_spacing', + documentAccess: 'inspect', description: 'Analyze spacing values (gap, padding) across the current page. Checks grid compliance.', params: { diff --git a/packages/core/src/tools/analyze/typography.ts b/packages/core/src/tools/analyze/typography.ts index 33604fbe0..175f51c71 100644 --- a/packages/core/src/tools/analyze/typography.ts +++ b/packages/core/src/tools/analyze/typography.ts @@ -4,6 +4,7 @@ import { defineTool } from '#core/tools/schema' export const analyzeTypography = defineTool({ name: 'analyze_typography', + documentAccess: 'inspect', description: 'Analyze typography usage across the current page. Shows font families, sizes, weights, and their frequencies.', params: { diff --git a/packages/core/src/tools/calc.ts b/packages/core/src/tools/calc.ts index 469892125..afea37362 100644 --- a/packages/core/src/tools/calc.ts +++ b/packages/core/src/tools/calc.ts @@ -20,6 +20,7 @@ function evalExpr( export const calc = defineTool({ name: 'calc', + documentAccess: 'inspect', description: 'Arithmetic calculator. ALWAYS use instead of mental math. ' + 'Pass one expression or a JSON array of expressions — all evaluated in one call. ' + diff --git a/packages/core/src/tools/codegen/component-map.ts b/packages/core/src/tools/codegen/component-map.ts index 2ec934578..e2a2d9533 100644 --- a/packages/core/src/tools/codegen/component-map.ts +++ b/packages/core/src/tools/codegen/component-map.ts @@ -110,6 +110,7 @@ function buildScreenInfo( export const designToComponentMap = defineTool({ name: 'design_to_component_map', + documentAccess: 'inspect', description: 'Analyze the document and return a structured component decomposition: components (with variants, props, instance counts), screens, and a dependency overview.', params: { diff --git a/packages/core/src/tools/codegen/tokens.ts b/packages/core/src/tools/codegen/tokens.ts index 0ff7641d6..b971b64cc 100644 --- a/packages/core/src/tools/codegen/tokens.ts +++ b/packages/core/src/tools/codegen/tokens.ts @@ -162,6 +162,7 @@ function renderJSON(tokens: TokenEntry[], modes: { id: string; name: string }[]) export const designToTokens = defineTool({ name: 'design_to_tokens', + documentAccess: 'inspect', description: 'Extract design tokens from Figma variables as CSS custom properties, Tailwind theme config, or JSON. Resolves aliases, handles multiple modes (light/dark).', params: { diff --git a/packages/core/src/tools/create/basic.ts b/packages/core/src/tools/create/basic.ts index fd756616b..d73385628 100644 --- a/packages/core/src/tools/create/basic.ts +++ b/packages/core/src/tools/create/basic.ts @@ -4,6 +4,7 @@ import { defineTool, nodeSummary } from '#core/tools/schema' export const createShape = defineTool({ name: 'create_shape', mutates: true, + documentAccess: 'modify', description: 'Create a shape on the canvas. Use FRAME for containers/cards, RECTANGLE for solid blocks, ELLIPSE for circles, TEXT for labels, LINE for rules and dividers, STAR for starbursts and badges, POLYGON for triangles and regular polygons, and SECTION for page sections. Use create_vector with an SVG path for arbitrary shapes.', params: { @@ -46,6 +47,7 @@ export const createShape = defineTool({ export const createPage = defineTool({ name: 'create_page', mutates: true, + documentAccess: 'modify', description: 'Create a new page.', params: { name: { type: 'string', description: 'Page name', required: true } @@ -60,6 +62,7 @@ export const createPage = defineTool({ export const createSlice = defineTool({ name: 'create_slice', mutates: true, + documentAccess: 'modify', description: 'Create a slice (export region) on the canvas.', params: { x: { type: 'number', description: 'X position', required: true }, diff --git a/packages/core/src/tools/create/components.ts b/packages/core/src/tools/create/components.ts index c91bfe898..b8d59a15a 100644 --- a/packages/core/src/tools/create/components.ts +++ b/packages/core/src/tools/create/components.ts @@ -4,6 +4,7 @@ import { defineTool, nodeSummary, requireNodes } from '#core/tools/schema' export const createComponent = defineTool({ name: 'create_component', mutates: true, + documentAccess: 'modify', description: 'Convert a frame/group into a component.', params: { id: { type: 'string', description: 'Node ID to convert', required: true } @@ -19,6 +20,7 @@ export const createComponent = defineTool({ export const createInstance = defineTool({ name: 'create_instance', mutates: true, + documentAccess: 'modify', description: 'Create an instance of a component.', params: { component_id: { type: 'string', description: 'Component node ID', required: true }, diff --git a/packages/core/src/tools/create/icons.ts b/packages/core/src/tools/create/icons.ts index 8f3c577d8..07438fcf4 100644 --- a/packages/core/src/tools/create/icons.ts +++ b/packages/core/src/tools/create/icons.ts @@ -5,6 +5,7 @@ import { defineTool } from '#core/tools/schema' export const fetchIconsTool = defineTool({ name: 'fetch_icons', + documentAccess: 'inspect', description: 'Pre-fetch icons from Iconify into cache. Batches by prefix (one HTTP request per set). Call this once with all needed icons, then use insert_icon to place them instantly. Popular sets: lucide (outline), mdi (filled), heroicons, tabler, solar, mingcute, ri (remix).', params: { @@ -33,6 +34,7 @@ export const fetchIconsTool = defineTool({ export const insertIcon = defineTool({ name: 'insert_icon', mutates: true, + documentAccess: 'modify', description: 'Insert one or more vector icons onto the canvas. Pass a single name or multiple names to batch-insert into the same parent. If already cached by fetch_icons — instant, no network request.', params: { @@ -93,6 +95,7 @@ export const insertIcon = defineTool({ export const searchIconsTool = defineTool({ name: 'search_icons', + documentAccess: 'inspect', description: 'Search Iconify for icons by keyword. Accepts multiple queries — all searched in parallel. Returns results keyed by query.', params: { diff --git a/packages/core/src/tools/create/render.ts b/packages/core/src/tools/create/render.ts index 11b3f3ce3..50f33c325 100644 --- a/packages/core/src/tools/create/render.ts +++ b/packages/core/src/tools/create/render.ts @@ -3,6 +3,7 @@ import { defineTool } from '#core/tools/schema' export const render = defineTool({ name: 'render', mutates: true, + documentAccess: 'modify', description: 'Render JSX to design nodes. Supports inline SVG paths, including open stroked paths: . Use replace_id to replace a placeholder while preserving its position.', params: { diff --git a/packages/core/src/tools/create/svg.ts b/packages/core/src/tools/create/svg.ts index 186024563..2800e4b9e 100644 --- a/packages/core/src/tools/create/svg.ts +++ b/packages/core/src/tools/create/svg.ts @@ -4,6 +4,7 @@ import { defineTool } from '#core/tools/schema' export const importSVG = defineTool({ name: 'import_svg', mutates: true, + documentAccess: 'modify', description: 'Import raw SVG markup onto the canvas as editable vector nodes. Supports common SVG shapes, inherited presentation attributes, transforms, gradients, and internal references.', params: { diff --git a/packages/core/src/tools/create/vector.ts b/packages/core/src/tools/create/vector.ts index b5614d5ea..89d5b5b66 100644 --- a/packages/core/src/tools/create/vector.ts +++ b/packages/core/src/tools/create/vector.ts @@ -55,6 +55,7 @@ function parseVectorPath(path: string): VectorPathResult { export const createVector = defineTool({ name: 'create_vector', mutates: true, + documentAccess: 'modify', description: 'Create a vector node from SVG path data or a VectorNetwork.', params: { x: { type: 'number', description: 'X position', required: true }, diff --git a/packages/core/src/tools/describe/index.ts b/packages/core/src/tools/describe/index.ts index aae7985f8..c6acf2e7d 100644 --- a/packages/core/src/tools/describe/index.ts +++ b/packages/core/src/tools/describe/index.ts @@ -4,6 +4,7 @@ import { autoDepth, describeOneNode } from './tree' export const describe = defineTool({ name: 'describe', + documentAccess: 'inspect', description: 'Semantic description of one or more nodes. Pass `id` for a single node, or `ids` for multiple nodes in one call. Omit depth for auto — adapts to subtree size (small block → deeper, large page → shallower).', params: { diff --git a/packages/core/src/tools/index.ts b/packages/core/src/tools/index.ts index 5daa785bf..f05edc661 100644 --- a/packages/core/src/tools/index.ts +++ b/packages/core/src/tools/index.ts @@ -10,7 +10,7 @@ export { ALL_TOOLS, CORE_TOOLS, EXTENDED_TOOLS } from './registry' export const CODEGEN_PROMPT: string = codegenPrompt export { exportImage } from './vector' export { defineTool, nodeToResult, nodeSummary, requireNode, NodeNotFoundError } from './schema' -export type { ToolDef, ParamDef, ParamType } from './schema' +export type { DocumentAccess, ToolDef, ParamDef, ParamType } from './schema' export { toolsToAI, buildDebugLog } from './ai-adapter' export type { ToolLogEntry, ToolDebugLog, AIAdapterOptions, StepBudget } from './ai-adapter' export { calcClusterConfidence, wrapEvalCode } from './analyze' diff --git a/packages/core/src/tools/modify/effects.ts b/packages/core/src/tools/modify/effects.ts index 090ad0028..a41cb7282 100644 --- a/packages/core/src/tools/modify/effects.ts +++ b/packages/core/src/tools/modify/effects.ts @@ -7,6 +7,7 @@ import { defineTool, nodeNotFound } from '#core/tools/schema' export const setEffects = defineTool({ name: 'set_effects', mutates: true, + documentAccess: 'modify', description: 'Set effects on a node (drop shadow, inner shadow, blur). Pass an array or a single effect.', params: { diff --git a/packages/core/src/tools/modify/geometry.ts b/packages/core/src/tools/modify/geometry.ts index f559b106f..ee79b6282 100644 --- a/packages/core/src/tools/modify/geometry.ts +++ b/packages/core/src/tools/modify/geometry.ts @@ -3,6 +3,7 @@ import { defineTool, nodeNotFound } from '#core/tools/schema' export const setRotation = defineTool({ name: 'set_rotation', mutates: true, + documentAccess: 'modify', description: 'Set rotation angle of a node in degrees.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -19,6 +20,7 @@ export const setRotation = defineTool({ export const setOpacity = defineTool({ name: 'set_opacity', mutates: true, + documentAccess: 'modify', description: 'Set opacity of a node (0-1).', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -35,6 +37,7 @@ export const setOpacity = defineTool({ export const setRadius = defineTool({ name: 'set_radius', mutates: true, + documentAccess: 'modify', description: 'Set corner radius. Use individual corners for independent values.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -71,6 +74,7 @@ export const setRadius = defineTool({ export const setMinMax = defineTool({ name: 'set_minmax', mutates: true, + documentAccess: 'modify', description: 'Set min/max width and height constraints on a node.', params: { id: { type: 'string', description: 'Node ID', required: true }, diff --git a/packages/core/src/tools/modify/layout.ts b/packages/core/src/tools/modify/layout.ts index cd73ea7bb..c227c0528 100644 --- a/packages/core/src/tools/modify/layout.ts +++ b/packages/core/src/tools/modify/layout.ts @@ -5,6 +5,7 @@ import { defineTool, nodeNotFound } from '#core/tools/schema' export const setLayout = defineTool({ name: 'set_layout', mutates: true, + documentAccess: 'modify', description: 'Set auto-layout (flexbox) on a frame. Direction, alignment, spacing, padding.', params: { id: { type: 'string', description: 'Frame node ID', required: true }, @@ -86,6 +87,7 @@ export const setLayout = defineTool({ export const setConstraints = defineTool({ name: 'set_constraints', mutates: true, + documentAccess: 'modify', description: 'Set resize constraints for a node within its parent.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -116,6 +118,7 @@ export const setConstraints = defineTool({ export const setLayoutChild = defineTool({ name: 'set_layout_child', mutates: true, + documentAccess: 'modify', description: 'Configure auto-layout child: sizing (FIXED/HUG/FILL), grow, alignment, absolute positioning.', params: { diff --git a/packages/core/src/tools/modify/paint.ts b/packages/core/src/tools/modify/paint.ts index 67be68e90..262d95af5 100644 --- a/packages/core/src/tools/modify/paint.ts +++ b/packages/core/src/tools/modify/paint.ts @@ -8,6 +8,7 @@ import { defineTool } from '#core/tools/schema' export const setFill = defineTool({ name: 'set_fill', mutates: true, + documentAccess: 'modify', description: 'Set fill on a node. Solid: color="#ff0000". Linear gradient: gradient="top-bottom" or "left-right" with color (start) and color_end (end).', params: { @@ -62,6 +63,7 @@ export const setFill = defineTool({ export const setStroke = defineTool({ name: 'set_stroke', mutates: true, + documentAccess: 'modify', description: 'Set the stroke (border) of a node.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -95,6 +97,7 @@ export const setStroke = defineTool({ export const setImageFill = defineTool({ name: 'set_image_fill', mutates: true, + documentAccess: 'modify', description: 'Set an image fill on a node from base64-encoded image data.', params: { id: { type: 'string', description: 'Node ID', required: true }, diff --git a/packages/core/src/tools/modify/state.ts b/packages/core/src/tools/modify/state.ts index e7722022b..f028223cc 100644 --- a/packages/core/src/tools/modify/state.ts +++ b/packages/core/src/tools/modify/state.ts @@ -3,6 +3,7 @@ import { defineTool } from '#core/tools/schema' export const setVisible = defineTool({ name: 'set_visible', mutates: true, + documentAccess: 'modify', description: 'Set visibility of a node.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -19,6 +20,7 @@ export const setVisible = defineTool({ export const setBlend = defineTool({ name: 'set_blend', mutates: true, + documentAccess: 'modify', description: 'Set blend mode of a node.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -57,6 +59,7 @@ export const setBlend = defineTool({ export const setLocked = defineTool({ name: 'set_locked', mutates: true, + documentAccess: 'modify', description: 'Set locked state of a node.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -73,6 +76,7 @@ export const setLocked = defineTool({ export const setStrokeAlign = defineTool({ name: 'set_stroke_align', mutates: true, + documentAccess: 'modify', description: 'Set stroke alignment of a node.', params: { id: { type: 'string', description: 'Node ID', required: true }, diff --git a/packages/core/src/tools/modify/text.ts b/packages/core/src/tools/modify/text.ts index e933312ce..faf193d92 100644 --- a/packages/core/src/tools/modify/text.ts +++ b/packages/core/src/tools/modify/text.ts @@ -8,6 +8,7 @@ import { defineTool, nodeNotFound } from '#core/tools/schema' export const setText = defineTool({ name: 'set_text', mutates: true, + documentAccess: 'modify', description: 'Set text content of a text node.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -24,6 +25,7 @@ export const setText = defineTool({ export const setFont = defineTool({ name: 'set_font', mutates: true, + documentAccess: 'modify', description: 'Set font properties of a text node.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -49,6 +51,7 @@ export const setFont = defineTool({ export const setFontRange = defineTool({ name: 'set_font_range', mutates: true, + documentAccess: 'modify', description: 'Set font properties for a text range.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -84,6 +87,7 @@ export const setFontRange = defineTool({ export const setTextResize = defineTool({ name: 'set_text_resize', mutates: true, + documentAccess: 'modify', description: 'Set text auto-resize mode.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -105,6 +109,7 @@ export const setTextResize = defineTool({ export const setTextProperties = defineTool({ name: 'set_text_properties', mutates: true, + documentAccess: 'modify', description: 'Set text layout properties: alignment, auto-resize, text case, decoration, truncation.', params: { diff --git a/packages/core/src/tools/modify/update.ts b/packages/core/src/tools/modify/update.ts index c4b0c028d..28e568a91 100644 --- a/packages/core/src/tools/modify/update.ts +++ b/packages/core/src/tools/modify/update.ts @@ -6,6 +6,7 @@ import { defineTool, nodeNotFound } from '#core/tools/schema' export const updateNode = defineTool({ name: 'update_node', mutates: true, + documentAccess: 'modify', description: 'Update properties of an existing node: position, size, opacity, corner radius, visibility, text, font.', params: { diff --git a/packages/core/src/tools/read/components.ts b/packages/core/src/tools/read/components.ts index 59e5e0254..b0325e9ca 100644 --- a/packages/core/src/tools/read/components.ts +++ b/packages/core/src/tools/read/components.ts @@ -24,6 +24,7 @@ interface LibraryComponentResult { export const getComponents = defineTool({ name: 'get_components', + documentAccess: 'inspect', description: 'List reusable components from the document and enabled component libraries, optionally filtered by name.', params: { diff --git a/packages/core/src/tools/read/fonts.ts b/packages/core/src/tools/read/fonts.ts index 5b466b21e..219a4a3cb 100644 --- a/packages/core/src/tools/read/fonts.ts +++ b/packages/core/src/tools/read/fonts.ts @@ -4,6 +4,7 @@ import { defineTool } from '#core/tools/schema' export const getFontStatus = defineTool({ name: 'get_font_status', + documentAccess: 'inspect', description: 'Report whether fonts used on the current page are faithfully available. Returns requested ' + 'faces, their loaded source, active substitutions, and affected nodes.', @@ -13,6 +14,7 @@ export const getFontStatus = defineTool({ export const listFonts = defineTool({ name: 'list_fonts', + documentAccess: 'inspect', description: 'List fonts used in the current page.', params: { family: { type: 'string', description: 'Filter by family name (substring)' } @@ -45,6 +47,7 @@ export const listFonts = defineTool({ export const listAvailableFonts = defineTool({ name: 'list_available_fonts', + documentAccess: 'inspect', description: 'List font families the host can render (system fonts on desktop plus any bundled fonts). ' + 'Use this to discover what fonts are available to set on a text node — distinct from list_fonts ' + diff --git a/packages/core/src/tools/read/jsx.ts b/packages/core/src/tools/read/jsx.ts index 5be756d8a..3129276c3 100644 --- a/packages/core/src/tools/read/jsx.ts +++ b/packages/core/src/tools/read/jsx.ts @@ -7,6 +7,7 @@ const MAX_JSX_LENGTH = 12_000 export const getJSX = defineTool({ name: 'get_jsx', + documentAccess: 'inspect', description: 'Get JSX representation of a node and its children. Compact round-trip format — same syntax as the render tool.', params: { @@ -35,6 +36,7 @@ export const getJSX = defineTool({ export const diffJSX = defineTool({ name: 'diff_jsx', + documentAccess: 'inspect', description: 'Structural diff between two nodes in JSX format. Shows added/removed children, changed props.', params: { diff --git a/packages/core/src/tools/read/libraries.ts b/packages/core/src/tools/read/libraries.ts index 346622fdb..aee171f49 100644 --- a/packages/core/src/tools/read/libraries.ts +++ b/packages/core/src/tools/read/libraries.ts @@ -3,6 +3,7 @@ import { defineTool } from '#core/tools/schema' export const listLibraries = defineTool({ name: 'list_libraries', + documentAccess: 'inspect', description: 'List available published component libraries.', params: {}, execute: async (figma) => { @@ -14,6 +15,7 @@ export const listLibraries = defineTool({ export const insertLibraryComponent = defineTool({ name: 'insert_library_component', + documentAccess: 'modify', description: 'Insert a reusable component from an enabled library by stable library and asset identity.', mutates: true, diff --git a/packages/core/src/tools/read/nodes.ts b/packages/core/src/tools/read/nodes.ts index 5fee785c6..0eece3235 100644 --- a/packages/core/src/tools/read/nodes.ts +++ b/packages/core/src/tools/read/nodes.ts @@ -40,6 +40,7 @@ function nodeToTreeEntry( export const getPageTree = defineTool({ name: 'get_page_tree', + documentAccess: 'inspect', description: 'Get the node tree of the current page. Returns lightweight hierarchy: id, type, name, size. Use depth, root_id, or node_types to keep large pages small. Use get_node for full properties of a specific node.', params: { @@ -78,6 +79,7 @@ export const getPageTree = defineTool({ export const getNode = defineTool({ name: 'get_node', + documentAccess: 'inspect', description: 'Get detailed properties of a node by ID. Use depth to limit child recursion (0 = node only, 1 = direct children, etc). Default: unlimited.', params: { @@ -96,6 +98,7 @@ export const getNode = defineTool({ export const findNodes = defineTool({ name: 'find_nodes', + documentAccess: 'inspect', description: 'Find nodes by name pattern and/or type.', params: { name: { type: 'string', description: 'Name substring to match (case-insensitive)' }, diff --git a/packages/core/src/tools/read/pages.ts b/packages/core/src/tools/read/pages.ts index 2c298a1b9..7d823c639 100644 --- a/packages/core/src/tools/read/pages.ts +++ b/packages/core/src/tools/read/pages.ts @@ -4,6 +4,7 @@ import { defineTool } from '#core/tools/schema' export const listPages = defineTool({ name: 'list_pages', + documentAccess: 'inspect', description: 'List all pages in the document.', params: {}, execute: (figma) => { @@ -18,6 +19,7 @@ export const listPages = defineTool({ export const switchPage = defineTool({ name: 'switch_page', mutates: true, + documentAccess: 'inspect', description: 'Switch to a different page by name or ID.', params: { page: { type: 'string', description: 'Page name or ID', required: true } @@ -33,6 +35,7 @@ export const switchPage = defineTool({ export const getCurrentPage = defineTool({ name: 'get_current_page', + documentAccess: 'inspect', description: 'Get the current page name and ID.', params: {}, execute: (figma) => { @@ -42,6 +45,7 @@ export const getCurrentPage = defineTool({ export const pageBounds = defineTool({ name: 'page_bounds', + documentAccess: 'inspect', description: 'Get bounding box of all objects on the current page.', params: {}, execute: (figma) => { diff --git a/packages/core/src/tools/read/query.ts b/packages/core/src/tools/read/query.ts index d01ffbe90..4ed23922d 100644 --- a/packages/core/src/tools/read/query.ts +++ b/packages/core/src/tools/read/query.ts @@ -3,6 +3,7 @@ import { queryByXPath } from '#core/xpath' export const queryNodes = defineTool({ name: 'query_nodes', + documentAccess: 'inspect', description: `Query nodes using XPath selectors. Node types are element names (FRAME, TEXT, RECTANGLE, ELLIPSE, etc.). Attributes: name, width, height, x, y, visible, opacity, cornerRadius, fontSize, fontFamily, fontWeight, layoutMode, itemSpacing, paddingTop/Right/Bottom/Left, strokeWeight, rotation, locked, blendMode, text, lineHeight, letterSpacing. Examples: diff --git a/packages/core/src/tools/read/selection.ts b/packages/core/src/tools/read/selection.ts index 07caab554..69d8de280 100644 --- a/packages/core/src/tools/read/selection.ts +++ b/packages/core/src/tools/read/selection.ts @@ -3,6 +3,7 @@ import { defineTool, nodeToResult } from '#core/tools/schema' export const getSelection = defineTool({ name: 'get_selection', + documentAccess: 'inspect', description: 'Get details about currently selected nodes.', params: {}, execute: (figma) => { @@ -14,6 +15,7 @@ export const getSelection = defineTool({ export const selectNodes = defineTool({ name: 'select_nodes', mutates: true, + documentAccess: 'inspect', description: 'Select one or more nodes by ID.', params: { ids: { type: 'string[]', description: 'Node IDs to select', required: true } diff --git a/packages/core/src/tools/schema.ts b/packages/core/src/tools/schema.ts index 46922a64f..5bd5d2331 100644 --- a/packages/core/src/tools/schema.ts +++ b/packages/core/src/tools/schema.ts @@ -11,6 +11,7 @@ import type { SceneNode } from '@open-pencil/scene-graph' import type { FigmaAPI, FigmaNodeProxy } from '#core/figma-api' export type ParamType = 'string' | 'number' | 'boolean' | 'color' | 'string[]' +export type DocumentAccess = 'inspect' | 'modify' export interface ParamDef { type: ParamType @@ -25,6 +26,7 @@ export interface ParamDef { export interface ToolDef { name: string description: string + documentAccess: DocumentAccess mutates?: boolean params: Record execute: (figma: FigmaAPI, args: Record) => unknown @@ -51,6 +53,7 @@ type ResolvedParams

> = { export function defineTool

>(def: { name: string description: string + documentAccess: DocumentAccess mutates?: boolean params: P execute: (figma: FigmaAPI, args: ResolvedParams

) => unknown diff --git a/packages/core/src/tools/stock-photo.ts b/packages/core/src/tools/stock-photo.ts index faa4bae87..ea1a23378 100644 --- a/packages/core/src/tools/stock-photo.ts +++ b/packages/core/src/tools/stock-photo.ts @@ -18,6 +18,7 @@ export { export const stockPhoto = defineTool({ name: 'stock_photo', mutates: true, + documentAccess: 'modify', description: 'Search stock photos and apply to nodes. Pass a JSON array — all fetched in parallel. ' + 'Each item: {id, query, index?, orientation?}. Only works on leaf shapes (Rectangle/Ellipse).', diff --git a/packages/core/src/tools/structure/arrange.ts b/packages/core/src/tools/structure/arrange.ts index 0405c45ad..2b68d5f85 100644 --- a/packages/core/src/tools/structure/arrange.ts +++ b/packages/core/src/tools/structure/arrange.ts @@ -4,6 +4,7 @@ import { defineTool } from '#core/tools/schema' export const arrangeNodes = defineTool({ name: 'arrange', mutates: true, + documentAccess: 'modify', description: 'Arrange top-level nodes on the canvas in a grid, row, or column layout. Useful after batch creation to tidy up overlapping frames.', params: { diff --git a/packages/core/src/tools/structure/basic.ts b/packages/core/src/tools/structure/basic.ts index c2a40bd84..945e1192b 100644 --- a/packages/core/src/tools/structure/basic.ts +++ b/packages/core/src/tools/structure/basic.ts @@ -3,6 +3,7 @@ import { defineTool, nodeNotFound, nodeSummary } from '#core/tools/schema' export const deleteNode = defineTool({ name: 'delete_node', mutates: true, + documentAccess: 'modify', description: 'Delete a node by ID.', params: { id: { type: 'string', description: 'Node ID to delete', required: true } @@ -18,6 +19,7 @@ export const deleteNode = defineTool({ export const cloneNode = defineTool({ name: 'clone_node', mutates: true, + documentAccess: 'modify', description: 'Clone (duplicate) a node.', params: { id: { type: 'string', description: 'Node ID to clone', required: true } @@ -33,6 +35,7 @@ export const cloneNode = defineTool({ export const renameNode = defineTool({ name: 'rename_node', mutates: true, + documentAccess: 'modify', description: 'Rename a node in the layers panel.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -48,6 +51,7 @@ export const renameNode = defineTool({ export const nodeBounds = defineTool({ name: 'node_bounds', + documentAccess: 'inspect', description: 'Get absolute bounding box of a node.', params: { id: { type: 'string', description: 'Node ID', required: true } @@ -62,6 +66,7 @@ export const nodeBounds = defineTool({ export const nodeMove = defineTool({ name: 'node_move', mutates: true, + documentAccess: 'modify', description: 'Move a node to new coordinates.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -80,6 +85,7 @@ export const nodeMove = defineTool({ export const nodeResize = defineTool({ name: 'node_resize', mutates: true, + documentAccess: 'modify', description: 'Resize a node.', params: { id: { type: 'string', description: 'Node ID', required: true }, diff --git a/packages/core/src/tools/structure/batch.ts b/packages/core/src/tools/structure/batch.ts index ee0069e4a..6656234a9 100644 --- a/packages/core/src/tools/structure/batch.ts +++ b/packages/core/src/tools/structure/batch.ts @@ -92,6 +92,7 @@ function applyBatchProps(node: FigmaNodeProxy, props: Record): export const batchUpdate = defineTool({ name: 'batch_update', mutates: true, + documentAccess: 'modify', description: 'Execute multiple modifications in one call. Each operation is {id, props} where props can include: spacing, padding, padding_horizontal, padding_vertical, counter_align, sizing_horizontal, sizing_vertical, grow, name, visible, corner_radius, auto_resize (for text), direction. Runs all updates with one layout recompute.', params: { diff --git a/packages/core/src/tools/structure/hierarchy.ts b/packages/core/src/tools/structure/hierarchy.ts index dbde91b16..cc6cde6d0 100644 --- a/packages/core/src/tools/structure/hierarchy.ts +++ b/packages/core/src/tools/structure/hierarchy.ts @@ -3,6 +3,7 @@ import { defineTool, nodeSummary, requireNodes } from '#core/tools/schema' export const reparentNode = defineTool({ name: 'reparent_node', mutates: true, + documentAccess: 'modify', description: 'Move a node into a different parent.', params: { id: { type: 'string', description: 'Node ID to move', required: true }, @@ -21,6 +22,7 @@ export const reparentNode = defineTool({ export const groupNodes = defineTool({ name: 'group_nodes', mutates: true, + documentAccess: 'modify', description: 'Group selected nodes.', params: { ids: { type: 'string[]', description: 'Node IDs to group', required: true } @@ -37,6 +39,7 @@ export const groupNodes = defineTool({ export const ungroupNode = defineTool({ name: 'ungroup_node', mutates: true, + documentAccess: 'modify', description: 'Ungroup a group node.', params: { id: { type: 'string', description: 'Group node ID', required: true } @@ -52,6 +55,7 @@ export const ungroupNode = defineTool({ export const flattenNodes = defineTool({ name: 'flatten_nodes', mutates: true, + documentAccess: 'modify', description: 'Flatten nodes into a single vector.', params: { ids: { type: 'string[]', description: 'Node IDs to flatten', required: true } @@ -65,6 +69,7 @@ export const flattenNodes = defineTool({ export const nodeToComponent = defineTool({ name: 'node_to_component', mutates: true, + documentAccess: 'modify', description: 'Convert one or more frames/groups into components.', params: { ids: { type: 'string[]', description: 'Node IDs to convert', required: true } diff --git a/packages/core/src/tools/structure/replace.ts b/packages/core/src/tools/structure/replace.ts index d649f7dd7..9be47be1a 100644 --- a/packages/core/src/tools/structure/replace.ts +++ b/packages/core/src/tools/structure/replace.ts @@ -3,6 +3,7 @@ import { defineTool } from '#core/tools/schema' export const nodeReplaceWith = defineTool({ name: 'node_replace_with', mutates: true, + documentAccess: 'modify', description: 'Replace a node with JSX content.', params: { id: { type: 'string', description: 'Node ID to replace', required: true }, diff --git a/packages/core/src/tools/structure/tree.ts b/packages/core/src/tools/structure/tree.ts index ce997799a..39e4691eb 100644 --- a/packages/core/src/tools/structure/tree.ts +++ b/packages/core/src/tools/structure/tree.ts @@ -3,6 +3,7 @@ import { defineTool, getRawNodeOrError, nodeNotFound, nodeSummary } from '#core/ export const nodeAncestors = defineTool({ name: 'node_ancestors', + documentAccess: 'inspect', description: 'Get the ancestor chain from a node to the page root.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -25,6 +26,7 @@ export const nodeAncestors = defineTool({ export const nodeChildren = defineTool({ name: 'node_children', + documentAccess: 'inspect', description: 'Get direct children of a node.', params: { id: { type: 'string', description: 'Node ID', required: true } @@ -38,6 +40,7 @@ export const nodeChildren = defineTool({ export const nodeTree = defineTool({ name: 'node_tree', + documentAccess: 'inspect', description: 'Get a node tree with types and hierarchy.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -65,6 +68,7 @@ export const nodeTree = defineTool({ export const nodeBindings = defineTool({ name: 'node_bindings', + documentAccess: 'inspect', description: 'Get variable bindings for a node.', params: { id: { type: 'string', description: 'Node ID', required: true } diff --git a/packages/core/src/tools/variables/bindings.ts b/packages/core/src/tools/variables/bindings.ts index 0b25c813d..648805ac3 100644 --- a/packages/core/src/tools/variables/bindings.ts +++ b/packages/core/src/tools/variables/bindings.ts @@ -3,6 +3,7 @@ import { defineTool } from '#core/tools/schema' export const bindVariable = defineTool({ name: 'bind_variable', mutates: true, + documentAccess: 'modify', description: 'Bind a variable to a node property. For fills/strokes color bindings use indexed format like "fills/0/color".', params: { diff --git a/packages/core/src/tools/variables/collections.ts b/packages/core/src/tools/variables/collections.ts index 30b206b29..0920d8595 100644 --- a/packages/core/src/tools/variables/collections.ts +++ b/packages/core/src/tools/variables/collections.ts @@ -2,6 +2,7 @@ import { defineTool } from '#core/tools/schema' export const listCollections = defineTool({ name: 'list_collections', + documentAccess: 'inspect', description: 'List all variable collections.', params: {}, execute: (figma) => { @@ -12,6 +13,7 @@ export const listCollections = defineTool({ export const getCollection = defineTool({ name: 'get_collection', + documentAccess: 'inspect', description: 'Get a variable collection by ID.', params: { id: { type: 'string', description: 'Collection ID', required: true } @@ -29,6 +31,7 @@ export const getCollection = defineTool({ export const createCollection = defineTool({ name: 'create_collection', mutates: true, + documentAccess: 'modify', description: 'Create a new variable collection.', params: { name: { type: 'string', description: 'Collection name', required: true } @@ -41,6 +44,7 @@ export const createCollection = defineTool({ export const deleteCollection = defineTool({ name: 'delete_collection', mutates: true, + documentAccess: 'modify', description: 'Delete a variable collection and all its variables.', params: { id: { type: 'string', description: 'Collection ID', required: true } diff --git a/packages/core/src/tools/variables/read.ts b/packages/core/src/tools/variables/read.ts index 3c52dba80..792a710ab 100644 --- a/packages/core/src/tools/variables/read.ts +++ b/packages/core/src/tools/variables/read.ts @@ -2,6 +2,7 @@ import { defineTool } from '#core/tools/schema' export const listVariables = defineTool({ name: 'list_variables', + documentAccess: 'inspect', description: 'List all design variables (colors, numbers, strings, booleans).', params: { type: { @@ -18,6 +19,7 @@ export const listVariables = defineTool({ export const getVariable = defineTool({ name: 'get_variable', + documentAccess: 'inspect', description: 'Get a variable by ID.', params: { id: { type: 'string', description: 'Variable ID', required: true } @@ -31,6 +33,7 @@ export const getVariable = defineTool({ export const findVariables = defineTool({ name: 'find_variables', + documentAccess: 'inspect', description: 'Find variables by name pattern.', params: { query: { type: 'string', description: 'Name substring (case-insensitive)', required: true }, diff --git a/packages/core/src/tools/variables/unbind.ts b/packages/core/src/tools/variables/unbind.ts index a22ba350c..070712d50 100644 --- a/packages/core/src/tools/variables/unbind.ts +++ b/packages/core/src/tools/variables/unbind.ts @@ -3,6 +3,7 @@ import { defineTool } from '#core/tools/schema' export const unbindVariable = defineTool({ name: 'unbind_variable', mutates: true, + documentAccess: 'modify', description: 'Remove a variable binding from a node property.', params: { node_id: { type: 'string', description: 'Node ID', required: true }, diff --git a/packages/core/src/tools/variables/values.ts b/packages/core/src/tools/variables/values.ts index 30451b03c..733020875 100644 --- a/packages/core/src/tools/variables/values.ts +++ b/packages/core/src/tools/variables/values.ts @@ -13,6 +13,7 @@ function parseVariableValue(type: VariableType, value: string): VariableValue { export const createVariable = defineTool({ name: 'create_variable', mutates: true, + documentAccess: 'modify', description: 'Create a new variable in a collection.', params: { name: { type: 'string', description: 'Variable name', required: true }, @@ -35,6 +36,7 @@ export const createVariable = defineTool({ export const setVariable = defineTool({ name: 'set_variable', mutates: true, + documentAccess: 'modify', description: 'Set the value of a variable for a specific mode.', params: { id: { type: 'string', description: 'Variable ID', required: true }, @@ -57,6 +59,7 @@ export const setVariable = defineTool({ export const deleteVariable = defineTool({ name: 'delete_variable', mutates: true, + documentAccess: 'modify', description: 'Delete a variable.', params: { id: { type: 'string', description: 'Variable ID', required: true } diff --git a/packages/core/src/tools/vector/boolean.ts b/packages/core/src/tools/vector/boolean.ts index 05d6f3520..51f8a64e0 100644 --- a/packages/core/src/tools/vector/boolean.ts +++ b/packages/core/src/tools/vector/boolean.ts @@ -3,6 +3,7 @@ import { defineTool, nodeSummary } from '#core/tools/schema' export const booleanUnion = defineTool({ name: 'boolean_union', mutates: true, + documentAccess: 'modify', description: 'Union (combine) multiple nodes.', params: { ids: { type: 'string[]', description: 'Node IDs to union', required: true } @@ -16,6 +17,7 @@ export const booleanUnion = defineTool({ export const booleanSubtract = defineTool({ name: 'boolean_subtract', mutates: true, + documentAccess: 'modify', description: 'Subtract the second node from the first.', params: { ids: { type: 'string[]', description: 'Node IDs (first minus rest)', required: true } @@ -29,6 +31,7 @@ export const booleanSubtract = defineTool({ export const booleanIntersect = defineTool({ name: 'boolean_intersect', mutates: true, + documentAccess: 'modify', description: 'Intersect multiple nodes.', params: { ids: { type: 'string[]', description: 'Node IDs to intersect', required: true } @@ -42,6 +45,7 @@ export const booleanIntersect = defineTool({ export const booleanExclude = defineTool({ name: 'boolean_exclude', mutates: true, + documentAccess: 'modify', description: 'Exclude (XOR) multiple nodes.', params: { ids: { type: 'string[]', description: 'Node IDs to exclude', required: true } diff --git a/packages/core/src/tools/vector/export.ts b/packages/core/src/tools/vector/export.ts index cb92a5195..de47675e1 100644 --- a/packages/core/src/tools/vector/export.ts +++ b/packages/core/src/tools/vector/export.ts @@ -4,6 +4,7 @@ import { defineTool } from '#core/tools/schema' export const exportSVG = defineTool({ name: 'export_svg', + documentAccess: 'inspect', description: 'Export nodes as SVG markup. Returns the SVG string.', params: { ids: { @@ -28,6 +29,7 @@ export const exportSVG = defineTool({ export const exportPDF = defineTool({ name: 'export_pdf', + documentAccess: 'inspect', description: 'Export nodes as a vector PDF document. Text remains selectable, paths stay sharp at any zoom. Returns base64-encoded PDF data.', params: { @@ -55,6 +57,7 @@ export const exportPDF = defineTool({ export const exportImage = defineTool({ name: 'export_image', + documentAccess: 'inspect', description: 'Export nodes as a raster image (PNG, JPG, or WEBP). Returns base64-encoded image data. Use to visually verify designs.', params: { diff --git a/packages/core/src/tools/vector/path.ts b/packages/core/src/tools/vector/path.ts index c5763db90..65a7d87bc 100644 --- a/packages/core/src/tools/vector/path.ts +++ b/packages/core/src/tools/vector/path.ts @@ -16,6 +16,7 @@ function getVectorNode( export const pathGet = defineTool({ name: 'path_get', + documentAccess: 'inspect', description: 'Get vector path data of a node.', params: { id: { type: 'string', description: 'Node ID', required: true } @@ -31,6 +32,7 @@ export const pathGet = defineTool({ export const pathSet = defineTool({ name: 'path_set', mutates: true, + documentAccess: 'modify', description: 'Set vector path data on a node. Provide a VectorNetwork JSON.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -48,6 +50,7 @@ export const pathSet = defineTool({ export const pathScale = defineTool({ name: 'path_scale', mutates: true, + documentAccess: 'modify', description: 'Scale vector path from center.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -78,6 +81,7 @@ export const pathScale = defineTool({ export const pathFlip = defineTool({ name: 'path_flip', mutates: true, + documentAccess: 'modify', description: 'Flip vector path horizontally or vertically.', params: { id: { type: 'string', description: 'Node ID', required: true }, @@ -113,6 +117,7 @@ export const pathFlip = defineTool({ export const pathMove = defineTool({ name: 'path_move', mutates: true, + documentAccess: 'modify', description: 'Move all path points by an offset.', params: { id: { type: 'string', description: 'Node ID', required: true }, diff --git a/packages/core/src/tools/vector/viewport.ts b/packages/core/src/tools/vector/viewport.ts index 6492ecedb..f8ef5eb1d 100644 --- a/packages/core/src/tools/vector/viewport.ts +++ b/packages/core/src/tools/vector/viewport.ts @@ -2,6 +2,7 @@ import { defineTool } from '#core/tools/schema' export const viewportGet = defineTool({ name: 'viewport_get', + documentAccess: 'inspect', description: 'Get current viewport position and zoom level.', params: {}, execute: (figma) => { @@ -12,6 +13,7 @@ export const viewportGet = defineTool({ export const viewportSet = defineTool({ name: 'viewport_set', mutates: true, + documentAccess: 'inspect', description: 'Set viewport position and zoom.', params: { x: { type: 'number', description: 'Center X', required: true }, @@ -27,6 +29,7 @@ export const viewportSet = defineTool({ export const viewportZoomToFit = defineTool({ name: 'viewport_zoom_to_fit', mutates: true, + documentAccess: 'inspect', description: 'Zoom viewport to fit specified nodes.', params: { ids: { type: 'string[]', description: 'Node IDs to fit in view', required: true } diff --git a/packages/mcp/src/tool/catalog.ts b/packages/mcp/src/tool/catalog.ts index 8f232df9e..166fb80e6 100644 --- a/packages/mcp/src/tool/catalog.ts +++ b/packages/mcp/src/tool/catalog.ts @@ -1,11 +1,13 @@ import { ALL_TOOLS } from '@open-pencil/core/tools' +import type { DocumentAccess } from '@open-pencil/core/tools' -export type MCPToolRequirement = 'always' | 'eval' | 'root' +export type MCPToolAvailability = 'default' | 'eval' | 'filesystem' export type MCPToolCatalogEntry = { name: string description: string - requirement: MCPToolRequirement + availability: MCPToolAvailability + documentAccess: DocumentAccess } export const MCP_ONLY_TOOLS = { @@ -13,27 +15,32 @@ export const MCP_ONLY_TOOLS = { name: 'list_documents', description: 'List open OpenPencil documents/tabs with their IDs, file paths, current pages, and pages.', - requirement: 'always' + availability: 'default', + documentAccess: 'inspect' }, saveFile: { name: 'save_file', description: 'Save the current document to disk.', - requirement: 'always' + availability: 'default', + documentAccess: 'inspect' }, openFile: { name: 'open_file', description: 'Open a .fig or .pen file from disk into a new tab.', - requirement: 'root' + availability: 'filesystem', + documentAccess: 'inspect' }, newDocument: { name: 'new_document', description: 'Create a new empty document.', - requirement: 'root' + availability: 'filesystem', + documentAccess: 'modify' }, getCodegenPrompt: { name: 'get_codegen_prompt', description: 'Get design-to-code generation guidelines. Call before generating frontend code.', - requirement: 'always' + availability: 'default', + documentAccess: 'inspect' } } as const satisfies Record @@ -41,7 +48,8 @@ export const MCP_TOOL_CATALOG: MCPToolCatalogEntry[] = [ ...ALL_TOOLS.map((tool) => ({ name: tool.name, description: tool.description, - requirement: tool.name === 'eval' ? ('eval' as const) : ('always' as const) + availability: tool.name === 'eval' ? ('eval' as const) : ('default' as const), + documentAccess: tool.documentAccess })), ...Object.values(MCP_ONLY_TOOLS) ] diff --git a/packages/vue/src/i18n/messages/dialogs.ts b/packages/vue/src/i18n/messages/dialogs.ts index f3b662afd..d9937ce1d 100644 --- a/packages/vue/src/i18n/messages/dialogs.ts +++ b/packages/vue/src/i18n/messages/dialogs.ts @@ -254,6 +254,8 @@ export const dialogMessageDefaults = { mcpAddress: 'Address', mcpVersion: 'Version', mcpTools: 'Available tools', + mcpInspectionTools: 'Inspection tools', + mcpModificationTools: 'Modification tools', mcpToolsEnabled: params('{enabled} of {total} enabled'), mcpEnableAllTools: 'Enable all', mcpSearchTools: 'Search MCP tools', diff --git a/src/app/automation/mcp/preferences.ts b/src/app/automation/mcp/preferences.ts index d997d16fa..eeabe62b9 100644 --- a/src/app/automation/mcp/preferences.ts +++ b/src/app/automation/mcp/preferences.ts @@ -1,10 +1,10 @@ import { useLocalStorage } from '@vueuse/core' -import { MCP_TOOL_CATALOG } from '@open-pencil/mcp/tools' +import { MCP_TOOL_CATALOG, type MCPToolCatalogEntry } from '@open-pencil/mcp/tools' const DISABLED_TOOLS_STORAGE_KEY = 'open-pencil:mcp:disabled-tools' -export const configurableMCPTools = MCP_TOOL_CATALOG.filter((tool) => tool.requirement !== 'eval') +export const configurableMCPTools = MCP_TOOL_CATALOG.filter((tool) => tool.availability !== 'eval') export const disabledMCPTools = useLocalStorage(DISABLED_TOOLS_STORAGE_KEY, []) @@ -15,6 +15,19 @@ export function setMCPToolEnabled(name: string, enabled: boolean): void { disabledMCPTools.value = [...disabled] } +export function setMCPToolCategoryEnabled( + documentAccess: MCPToolCatalogEntry['documentAccess'], + enabled: boolean +): void { + const disabled = new Set(disabledMCPTools.value) + for (const tool of configurableMCPTools) { + if (tool.documentAccess !== documentAccess) continue + if (enabled) disabled.delete(tool.name) + else disabled.add(tool.name) + } + disabledMCPTools.value = [...disabled] +} + export function disabledMCPToolsCSV(): string { return disabledMCPTools.value.join(',') } diff --git a/src/components/settings/mcp/MCPSettingsPanel.vue b/src/components/settings/mcp/MCPSettingsPanel.vue index aa8583538..3440c3350 100644 --- a/src/components/settings/mcp/MCPSettingsPanel.vue +++ b/src/components/settings/mcp/MCPSettingsPanel.vue @@ -5,6 +5,7 @@ import { useI18n } from '@open-pencil/vue' import { configurableMCPTools, disabledMCPTools, + setMCPToolCategoryEnabled, setMCPToolEnabled } from '@/app/automation/mcp/preferences' import { mcpRuntime, refreshMCPRuntime, restartMCPRuntime } from '@/app/automation/mcp/runtime' @@ -14,6 +15,16 @@ import AppSwitch from '@/components/ui/AppSwitch.vue' const { dialogs } = useI18n() const toolSearch = ref('') const disabledToolNames = computed(() => new Set(disabledMCPTools.value)) +function categoryStatus(documentAccess: 'inspect' | 'modify') { + const tools = configurableMCPTools.filter((tool) => tool.documentAccess === documentAccess) + const enabled = tools.filter((tool) => !disabledToolNames.value.has(tool.name)).length + return { + enabled: enabled > 0, + state: enabled > 0 && enabled < tools.length ? ('mixed' as const) : ('idle' as const) + } +} +const inspectionToolsStatus = computed(() => categoryStatus('inspect')) +const modificationToolsStatus = computed(() => categoryStatus('modify')) const enabledToolCount = computed( () => configurableMCPTools.filter((tool) => !disabledToolNames.value.has(tool.name)).length ) @@ -111,7 +122,6 @@ function enableAllTools(): void { +

+
+ {{ dialogs.mcpInspectionTools }} + +
+
+ {{ dialogs.mcpModificationTools }} + +
+
+
  • diff --git a/tests/e2e/settings/credentials.spec.ts b/tests/e2e/settings/credentials.spec.ts index b202210ae..b799041d1 100644 --- a/tests/e2e/settings/credentials.spec.ts +++ b/tests/e2e/settings/credentials.spec.ts @@ -88,6 +88,20 @@ test('MCP automation settings filter and persist tool availability', async ({ pa await expect(page.getByTestId('settings-mcp-tool-create_shape')).toBeVisible() await expect(page.getByTestId('settings-mcp-tool-get_page_tree')).toBeHidden() + await search.fill('') + const modificationTools = page.getByTestId('settings-mcp-modification-tools') + await modificationTools.click() + await expect(page.getByTestId('settings-mcp-tool-create_shape')).toHaveAttribute( + 'data-state', + 'unchecked' + ) + await expect(page.getByTestId('settings-mcp-tool-get_page_tree')).toHaveAttribute( + 'data-state', + 'checked' + ) + await modificationTools.click() + + await search.fill('create_shape') await page.getByTestId('settings-mcp-tool-create_shape').click() await page.reload() await canvas.waitForInit() diff --git a/tests/engine/mcp/server/index.test.ts b/tests/engine/mcp/server/index.test.ts index 4093437aa..9123c1932 100644 --- a/tests/engine/mcp/server/index.test.ts +++ b/tests/engine/mcp/server/index.test.ts @@ -136,12 +136,25 @@ describe('MCP server', () => { test('lists all registered tools', async () => { const { tools } = await client.listTools() - const expectedNames = MCP_TOOL_CATALOG.filter((tool) => tool.requirement === 'always') + const expectedNames = MCP_TOOL_CATALOG.filter((tool) => tool.availability === 'default') .map((tool) => tool.name) .sort() expect(tools.map((tool) => tool.name).sort()).toEqual(expectedNames) }) + test('classifies document access independently from runtime state mutation', () => { + const accessByName = new Map( + MCP_TOOL_CATALOG.map((tool) => [tool.name, tool.documentAccess] as const) + ) + expect(accessByName.get('get_page_tree')).toBe('inspect') + expect(accessByName.get('switch_page')).toBe('inspect') + expect(accessByName.get('viewport_set')).toBe('inspect') + expect(accessByName.get('export_image')).toBe('inspect') + expect(accessByName.get('update_node')).toBe('modify') + expect(accessByName.get('new_document')).toBe('modify') + expect(accessByName.get('eval')).toBe('modify') + }) + test('omits tools disabled in the generic registration filter', async () => { if (cleanup) await cleanup() cleanup = null