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
This commit is contained in:
parent
b43da1ecc3
commit
d807828270
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ function trackColor(colorMap: Map<string, ColorEntry>, 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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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. ' +
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: <svg viewBox="0 0 24 24" size={24}><path d="M2 12 L22 12" stroke="#000" fill="none" /></svg>. Use replace_id to replace a placeholder while preserving its position.',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -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 <use> references.',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 ' +
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)' },
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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<string, ParamDef>
|
||||
execute: (figma: FigmaAPI, args: Record<string, unknown>) => unknown
|
||||
|
|
@ -51,6 +53,7 @@ type ResolvedParams<P extends Record<string, ParamDef>> = {
|
|||
export function defineTool<P extends Record<string, ParamDef>>(def: {
|
||||
name: string
|
||||
description: string
|
||||
documentAccess: DocumentAccess
|
||||
mutates?: boolean
|
||||
params: P
|
||||
execute: (figma: FigmaAPI, args: ResolvedParams<P>) => unknown
|
||||
|
|
|
|||
|
|
@ -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).',
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ function applyBatchProps(node: FigmaNodeProxy, props: Record<string, unknown>):
|
|||
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: {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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<string, MCPToolCatalogEntry>
|
||||
|
||||
|
|
@ -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)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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<string[]>(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(',')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<AppInput
|
||||
v-model="toolSearch"
|
||||
type="search"
|
||||
tone="panel"
|
||||
size="sm"
|
||||
:placeholder="dialogs.search"
|
||||
:aria-label="dialogs.mcpSearchTools"
|
||||
|
|
@ -119,6 +129,29 @@ function enableAllTools(): void {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 border-b border-border p-2.5">
|
||||
<div class="flex items-center justify-between gap-2 rounded bg-input px-2.5 py-2">
|
||||
<span class="text-[10px] text-surface">{{ dialogs.mcpInspectionTools }}</span>
|
||||
<AppSwitch
|
||||
:model-value="inspectionToolsStatus.enabled"
|
||||
:state="inspectionToolsStatus.state"
|
||||
:label="dialogs.mcpInspectionTools"
|
||||
data-test-id="settings-mcp-inspection-tools"
|
||||
@update:model-value="setMCPToolCategoryEnabled('inspect', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 rounded bg-input px-2.5 py-2">
|
||||
<span class="text-[10px] text-surface">{{ dialogs.mcpModificationTools }}</span>
|
||||
<AppSwitch
|
||||
:model-value="modificationToolsStatus.enabled"
|
||||
:state="modificationToolsStatus.state"
|
||||
:label="dialogs.mcpModificationTools"
|
||||
data-test-id="settings-mcp-modification-tools"
|
||||
@update:model-value="setMCPToolCategoryEnabled('modify', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="max-h-72 divide-y divide-border overflow-y-auto">
|
||||
<li v-for="tool in visibleTools" :key="tool.name" class="flex items-start gap-3 p-2.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue