# Directory Structure ``` ├── .gitignore ├── build │ ├── index.js │ ├── response.js │ └── utils.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── index.ts │ ├── response.ts │ └── utils.ts ├── thumbnail.png └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://smithery.ai/server/@niklauslee/frame0-mcp-server) 2 | 3 | [](https://frame0.app/videos/frame0-mcp-example.mp4) 4 | 5 | # Frame0 MCP Server 6 | 7 | [Frame0](https://frame0.app/) is a Balsamiq-alternative wireframe tool for modern apps. **Frame0 MCP Server** allows you for creating and modifying wireframes in Frame0 by prompting. 8 | 9 | ## Setup 10 | 11 | Prerequisite: 12 | - [Frame0](https://frame0.app/) `v1.0.0-beta.17` or higher. 13 | - [Node.js](https://nodejs.org/) `v22` or higher. 14 | 15 | Setup for Claude Desktop in `claude_desktop_config.json` as below: 16 | 17 | ```json 18 | { 19 | "mcpServers": { 20 | "frame0-mcp-server": { 21 | "command": "npx", 22 | "args": ["-y", "frame0-mcp-server"] 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | You can use `--api-port=<port>` optional parameter to use another port number for Frame0's API server. 29 | 30 | ## Example Prompts 31 | 32 | - _“Create a login screen for Phone in Frame0”_ 33 | - _“Create a Instagram home screen for Phone in Frame0”_ 34 | - _“Create a Netflix home screen for TV in Frame0”_ 35 | - _“Change the color of the Login button”_ 36 | - _“Remove the Twitter social login”_ 37 | - _“Replace the emojis by icons”_ 38 | - _“Set a link from the google login button to the Google website”_ 39 | 40 | ## Tools 41 | 42 | - `create_frame` 43 | - `create_rectangle` 44 | - `create_ellipse` 45 | - `create_text` 46 | - `create_line` 47 | - `create_polygon` 48 | - `create_connector` 49 | - `create_icon` 50 | - `create_image` 51 | - `update_shape` 52 | - `duplicate_shape` 53 | - `delete_shape` 54 | - `search_icons` 55 | - `move_shape` 56 | - `align_shapes` 57 | - `group` 58 | - `ungroup` 59 | - `set_link` 60 | - `export_shape_as_image` 61 | - `add_page` 62 | - `update_page` 63 | - `duplicate_page` 64 | - `delete_page` 65 | - `get_current_page_id` 66 | - `set_current_page_by_id` 67 | - `get_page` 68 | - `get_all_pages` 69 | - `export_page_as_image` 70 | 71 | ## Dev 72 | 73 | 1. Clone this repository. 74 | 2. Build with `npm run build`. 75 | 3. Update `claude_desktop_config.json` in Claude Desktop as below. 76 | 4. Restart Claude Desktop. 77 | 78 | ```json 79 | { 80 | "mcpServers": { 81 | "frame0-mcp-server": { 82 | "command": "node", 83 | "args": ["<full-path-to>/frame0-mcp-server/build/index.js"] 84 | } 85 | } 86 | } 87 | ``` 88 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "frame0-mcp-server", 3 | "version": "0.11.5", 4 | "type": "module", 5 | "description": "", 6 | "bin": { 7 | "frame0-mcp-server": "build/index.js" 8 | }, 9 | "scripts": { 10 | "start": "tsx src/index.ts", 11 | "build": "tsc", 12 | "dev": "tsx watch src/index.ts", 13 | "inspect": "npx @modelcontextprotocol/inspector node build/index.js", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "files": [ 17 | "build" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/niklauslee/frame0-mcp-server.git" 22 | }, 23 | "keywords": [], 24 | "author": "", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/niklauslee/frame0-mcp-server/issues" 28 | }, 29 | "homepage": "https://github.com/niklauslee/frame0-mcp-server#readme", 30 | "dependencies": { 31 | "@modelcontextprotocol/sdk": "^1.12.1", 32 | "node-fetch": "^3.3.2", 33 | "zod": "^3.22.4" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^22.14.0", 37 | "prettier": "^3.5.3", 38 | "tsx": "^4.19.3", 39 | "typescript": "^5.8.2" 40 | }, 41 | "packageManager": "[email protected]+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538" 42 | } 43 | ``` -------------------------------------------------------------------------------- /src/response.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; 2 | 3 | // Standard JSON-RPC Error Codes 4 | export enum JsonRpcErrorCode { 5 | ParseError = -32700, 6 | InvalidRequest = -32600, 7 | MethodNotFound = -32601, 8 | InvalidParams = -32602, 9 | InternalError = -32603, 10 | // -32000 to -32099 are reserved for implementation-defined server-errors. 11 | ServerError = -32000, 12 | } 13 | 14 | export interface JsonRpcError { 15 | code: number; // JsonRpcErrorCode or a custom server error code 16 | message: string; 17 | data?: unknown; 18 | } 19 | 20 | type MimeType = "image/png" | "image/jpeg" | "image/webp" | "image/svg+xml"; 21 | 22 | export function text(text: string): CallToolResult { 23 | return { 24 | content: [ 25 | { 26 | type: "text", 27 | text, 28 | }, 29 | ], 30 | }; 31 | } 32 | 33 | export function error(code: number, message: string, data?: unknown): CallToolResult { 34 | return { 35 | isError: true, 36 | error: { 37 | code, 38 | message, 39 | data, 40 | } as JsonRpcError, 41 | content: [ 42 | { 43 | type: "text", // Provide a textual representation of the error in content 44 | text: message, 45 | } 46 | ] 47 | }; 48 | } 49 | 50 | export function image(mimeType: MimeType, data: string): CallToolResult { 51 | return { 52 | content: [ 53 | { 54 | type: "image", 55 | data, 56 | mimeType, 57 | }, 58 | ], 59 | }; 60 | } 61 | ``` -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fetch from "node-fetch"; 2 | 3 | const URL = "http://localhost"; 4 | 5 | export const ARROWHEADS = [ 6 | "none", 7 | "arrow", 8 | "bar", 9 | "circle", 10 | "circle-filled", 11 | "circle-plus", 12 | "cross", 13 | "crowfoot-many", 14 | "crowfoot-one", 15 | "crowfoot-one-many", 16 | "crowfoot-only-one", 17 | "crowfoot-zero-many", 18 | "crowfoot-zero-one", 19 | "diamond", 20 | "diamond-filled", 21 | "dot", 22 | "plus", 23 | "solid-arrow", 24 | "square", 25 | "triangle", 26 | "triangle-filled", 27 | ] as const; 28 | 29 | type CommandResponse = { 30 | success: boolean; 31 | data?: any; 32 | error?: string; 33 | }; 34 | 35 | export async function command(port: number, command: string, args: any = {}) { 36 | const res = await fetch(`${URL}:${port}/execute_command`, { 37 | method: "POST", 38 | headers: { 39 | "Content-Type": "application/json", 40 | }, 41 | body: JSON.stringify({ 42 | command, 43 | args, 44 | }), 45 | }); 46 | if (!res.ok) { 47 | throw new Error( 48 | `Failed to execute command(${command}) with args: ${JSON.stringify(args)}` 49 | ); 50 | } 51 | const json = (await res.json()) as CommandResponse; 52 | if (!json.success) { 53 | throw new Error(`Command failed: ${json.error}`); 54 | } 55 | return json.data; 56 | } 57 | 58 | export function filterShape(shape: any, recursive: boolean = false): any { 59 | const json: any = { 60 | id: shape.id, 61 | parentId: shape.parentId, 62 | type: shape.type, 63 | name: shape.name, 64 | left: shape.left, 65 | top: shape.top, 66 | width: shape.width, 67 | height: shape.height, 68 | fillColor: shape.fillColor, 69 | strokeColor: shape.strokeColor, 70 | strokeWidth: shape.strokeWidth, 71 | fontColor: shape.fontColor, 72 | fontSize: shape.fontSize, 73 | }; 74 | if (typeof shape.text !== "undefined") json.text = shape.text; // TODO: convert node to text 75 | if (typeof shape.wordWrap !== "undefined") json.wordWrap = shape.wordWrap; 76 | if (typeof shape.corners !== "undefined") json.corners = shape.corners; 77 | if (typeof shape.horzAlign !== "undefined") json.horzAlign = shape.horzAlign; 78 | if (typeof shape.vertAlign !== "undefined") json.vertAlign = shape.vertAlign; 79 | if (typeof shape.path !== "undefined") json.path = shape.path; 80 | if (typeof shape.referenceId !== "undefined") 81 | json.linkToPage = shape.referenceId; 82 | if (recursive && Array.isArray(shape.children)) { 83 | json.children = shape.children.map((child: any) => { 84 | return filterShape(child, recursive); 85 | }); 86 | } 87 | return json; 88 | } 89 | 90 | export function filterPage(page: any): any { 91 | const json: any = { 92 | id: page.id, 93 | name: page.name, 94 | children: page.children?.map((shape: any) => { 95 | return filterShape(shape, true); 96 | }), 97 | }; 98 | return json; 99 | } 100 | 101 | export function convertArrowhead(arrowhead: string): string { 102 | switch (arrowhead) { 103 | case "none": 104 | return "flat"; // "flat" in dgmjs 105 | default: 106 | return arrowhead; 107 | } 108 | } 109 | 110 | /** 111 | * Trim object by removing undefined values. 112 | */ 113 | export function trimObject(obj: any) { 114 | const result: any = {}; 115 | Object.keys(obj).forEach((key) => { 116 | if (obj[key] !== undefined) { 117 | result[key] = obj[key]; 118 | } 119 | }); 120 | return result; 121 | } 122 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { z } from "zod"; 5 | import * as response from "./response.js"; 6 | import { JsonRpcErrorCode } from "./response.js"; 7 | import { 8 | ARROWHEADS, 9 | convertArrowhead, 10 | command, 11 | filterPage, 12 | filterShape, 13 | trimObject, 14 | } from "./utils.js"; 15 | import packageJson from "../package.json" with { type: "json" }; 16 | 17 | const NAME = "frame0-mcp-server"; 18 | const VERSION = packageJson.version; 19 | 20 | // port number for the Frame0's API server (default: 58320) 21 | let apiPort: number = 58320; 22 | 23 | // command line argument parsing 24 | const args = process.argv.slice(2); 25 | const apiPortArg = args.find((arg) => arg.startsWith("--api-port=")); 26 | if (apiPortArg) { 27 | const port = apiPortArg.split("=")[1]; 28 | try { 29 | apiPort = parseInt(port, 10); 30 | if (isNaN(apiPort) || apiPort < 0 || apiPort > 65535) { 31 | throw new Error(`Invalid port number: ${port}`); 32 | } 33 | } catch (error) { 34 | console.error(`Invalid port number: ${port}`); 35 | process.exit(1); 36 | } 37 | } 38 | 39 | // Create an MCP server 40 | const server = new McpServer({ 41 | name: NAME, 42 | version: VERSION, 43 | }); 44 | 45 | server.tool( 46 | "create_frame", 47 | "Create a frame shape in Frame0. Must add a new page before you create a new frame.", 48 | { 49 | frameType: z 50 | .enum(["phone", "tablet", "desktop", "browser", "watch", "tv"]) 51 | .describe("Type of the frame shape to create."), 52 | name: z.string().describe("Name of the frame shape."), 53 | fillColor: z 54 | .string() 55 | .optional() 56 | .default("#ffffff") 57 | .describe("Background color in hex code of the frame shape."), 58 | }, 59 | async ({ frameType, name, fillColor }) => { 60 | const FRAME_NAME = { 61 | phone: "Phone", 62 | tablet: "Tablet", 63 | desktop: "Desktop", 64 | browser: "Browser", 65 | watch: "Watch", 66 | tv: "TV", 67 | }; 68 | const FRAME_SIZE = { 69 | phone: { width: 320, height: 690 }, 70 | tablet: { width: 600, height: 800 }, 71 | desktop: { width: 800, height: 600 }, 72 | browser: { width: 800, height: 600 }, 73 | watch: { width: 198, height: 242 }, 74 | tv: { width: 960, height: 570 }, 75 | }; 76 | const FRAME_HEADER_HEIGHT = { 77 | phone: 0, 78 | tablet: 0, 79 | desktop: 32, 80 | browser: 76, 81 | watch: 0, 82 | tv: 0, 83 | }; 84 | try { 85 | // frame headers should be consider to calculate actual content area 86 | const frameHeaderHeight = FRAME_HEADER_HEIGHT[frameType]; 87 | const frameSize = FRAME_SIZE[frameType]; 88 | const frameName = FRAME_NAME[frameType]; 89 | const shapeId = await command( 90 | apiPort, 91 | "shape:create-shape-from-library-by-query", 92 | { 93 | query: `${frameName}&@Frame`, 94 | shapeProps: trimObject({ 95 | name, 96 | left: 0, 97 | top: -frameHeaderHeight, 98 | width: frameSize.width, 99 | height: frameSize.height + frameHeaderHeight, 100 | fillColor, 101 | }), 102 | convertColors: true, 103 | } 104 | ); 105 | await command(apiPort, "view:fit-to-screen"); 106 | const data = await command(apiPort, "shape:get-shape", { 107 | shapeId, 108 | }); 109 | return response.text( 110 | "Created frame: " + 111 | JSON.stringify({ 112 | ...filterShape(data), 113 | top: -frameHeaderHeight, 114 | height: frameSize.height + frameHeaderHeight, 115 | }) 116 | ); 117 | } catch (error) { 118 | console.error(error); 119 | return response.error( 120 | JsonRpcErrorCode.InternalError, 121 | `Failed to create frame: ${error instanceof Error ? error.message : String(error)}` 122 | ); 123 | } 124 | } 125 | ); 126 | 127 | server.tool( 128 | "create_rectangle", 129 | `Create a rectangle shape in Frame0.`, 130 | { 131 | name: z.string().describe("Name of the rectangle shape."), 132 | parentId: z 133 | .string() 134 | .optional() 135 | .describe("ID of the parent shape. Typically frame ID."), 136 | left: z 137 | .number() 138 | .describe( 139 | "Left position of the rectangle shape in the absolute coordinate system." 140 | ), 141 | top: z 142 | .number() 143 | .describe( 144 | "Top position of the rectangle shape in the absolute coordinate system." 145 | ), 146 | width: z.number().describe("Width of the rectangle shape."), 147 | height: z.number().describe("Height of the rectangle shape."), 148 | fillColor: z 149 | .string() 150 | .optional() 151 | .default("#ffffff") 152 | .describe("Fill color in hex code of the rectangle shape."), 153 | strokeColor: z 154 | .string() 155 | .optional() 156 | .default("#000000") 157 | .describe("Stroke color in hex code of the rectangle shape."), 158 | corners: z 159 | .array(z.number()) 160 | .length(4) 161 | .optional() 162 | .default([0, 0, 0, 0]) 163 | .describe( 164 | "Corner radius of the rectangle shape. Must be in the form of [left-top, right-top, right-bottom, left-bottom]." 165 | ), 166 | }, 167 | async ({ 168 | name, 169 | parentId, 170 | left, 171 | top, 172 | width, 173 | height, 174 | fillColor, 175 | strokeColor, 176 | corners, 177 | }) => { 178 | try { 179 | const shapeId = await command(apiPort, "shape:create-shape", { 180 | type: "Rectangle", 181 | shapeProps: trimObject({ 182 | name, 183 | left, 184 | top, 185 | width, 186 | height, 187 | fillColor, 188 | strokeColor, 189 | corners, 190 | }), 191 | parentId, 192 | convertColors: true, 193 | }); 194 | const data = await command(apiPort, "shape:get-shape", { 195 | shapeId, 196 | }); 197 | return response.text( 198 | "Created rectangle: " + JSON.stringify(filterShape(data)) 199 | ); 200 | } catch (error) { 201 | console.error(error); 202 | return response.error( 203 | JsonRpcErrorCode.InternalError, 204 | `Failed to create rectangle: ${error instanceof Error ? error.message : String(error)}` 205 | ); 206 | } 207 | } 208 | ); 209 | 210 | server.tool( 211 | "create_ellipse", 212 | `Create an ellipse shape in Frame0.`, 213 | { 214 | name: z.string().describe("Name of the ellipse shape."), 215 | parentId: z 216 | .string() 217 | .optional() 218 | .describe("ID of the parent shape. Typically frame ID."), 219 | left: z 220 | .number() 221 | .describe( 222 | "Left position of the ellipse shape in the absolute coordinate system." 223 | ), 224 | top: z 225 | .number() 226 | .describe( 227 | "Top position of the ellipse shape in the absolute coordinate system." 228 | ), 229 | width: z.number().describe("Width of the ellipse shape."), 230 | height: z.number().describe("Height of the ellipse shape."), 231 | fillColor: z 232 | .string() 233 | .optional() 234 | .default("#ffffff") 235 | .describe("Fill color in hex code of the ellipse shape."), 236 | strokeColor: z 237 | .string() 238 | .optional() 239 | .default("#000000") 240 | .describe("Stroke color in hex code of the ellipse shape."), 241 | }, 242 | async ({ 243 | name, 244 | parentId, 245 | left, 246 | top, 247 | width, 248 | height, 249 | fillColor, 250 | strokeColor, 251 | }) => { 252 | try { 253 | const shapeId = await command(apiPort, "shape:create-shape", { 254 | type: "Ellipse", 255 | shapeProps: trimObject({ 256 | name, 257 | left, 258 | top, 259 | width, 260 | height, 261 | fillColor, 262 | strokeColor, 263 | }), 264 | parentId, 265 | convertColors: true, 266 | }); 267 | const data = await command(apiPort, "shape:get-shape", { 268 | shapeId, 269 | }); 270 | return response.text( 271 | "Created ellipse: " + JSON.stringify(filterShape(data)) 272 | ); 273 | } catch (error) { 274 | console.error(error); 275 | return response.error( 276 | JsonRpcErrorCode.InternalError, 277 | `Failed to create ellipse: ${error instanceof Error ? error.message : String(error)}` 278 | ); 279 | } 280 | } 281 | ); 282 | 283 | server.tool( 284 | "create_text", 285 | "Create a text shape in Frame0.", 286 | { 287 | type: z 288 | .enum(["label", "paragraph", "heading", "link", "normal"]) 289 | .optional() 290 | .describe( 291 | "Type of the text shape to create. If type is 'paragraph', text width need to be updated using 'update_shape' tool." 292 | ), 293 | name: z.string().describe("Name of the text shape."), 294 | parentId: z 295 | .string() 296 | .optional() 297 | .describe("ID of the parent shape. Typically frame ID."), 298 | left: z 299 | .number() 300 | .describe( 301 | "Left position of the text shape in the absolute coordinate system. Position need to be adjusted using 'move_shape' tool based on the width and height of the created text." 302 | ), 303 | top: z 304 | .number() 305 | .describe( 306 | "Top position of the text shape in the absolute coordinate system. Position need to be adjusted using 'move_shape' tool based on the width and height of the created text." 307 | ), 308 | width: z 309 | .number() 310 | .optional() 311 | .describe( 312 | "Width of the text shape. if the type is 'paragraph' recommend to set width." 313 | ), 314 | text: z 315 | .string() 316 | .describe( 317 | "Plain text content to display of the text shape. Use newline character (0x0A) instead of '\\n' for new line. Dont's use HTML and CSS code in the text content." 318 | ), 319 | fontColor: z 320 | .string() 321 | .optional() 322 | .default("#000000") 323 | .describe("Font color in hex code of the text shape."), 324 | fontSize: z.number().optional().describe("Font size of the text shape."), 325 | }, 326 | async ({ 327 | type, 328 | name, 329 | parentId, 330 | left, 331 | top, 332 | width, 333 | text, 334 | fontColor, 335 | fontSize, 336 | }) => { 337 | try { 338 | const shapeId = await command(apiPort, "shape:create-shape", { 339 | type: "Text", 340 | shapeProps: trimObject({ 341 | name, 342 | left, 343 | width, 344 | top, 345 | text, 346 | fontColor, 347 | fontSize, 348 | wordWrap: type === "paragraph", 349 | }), 350 | parentId, 351 | convertColors: true, 352 | }); 353 | const data = await command(apiPort, "shape:get-shape", { 354 | shapeId, 355 | }); 356 | return response.text( 357 | "Created text: " + 358 | JSON.stringify({ ...filterShape(data), textType: type }) 359 | ); 360 | } catch (error) { 361 | console.error(error); 362 | return response.error( 363 | JsonRpcErrorCode.InternalError, 364 | `Failed to create text: ${error instanceof Error ? error.message : String(error)}` 365 | ); 366 | } 367 | } 368 | ); 369 | 370 | server.tool( 371 | "create_line", 372 | "Create a line shape in Frame0.", 373 | { 374 | name: z.string().describe("Name of the line shape."), 375 | parentId: z 376 | .string() 377 | .optional() 378 | .describe("ID of the parent shape. Typically frame ID."), 379 | x1: z.number().describe("X coordinate of the first point."), 380 | y1: z.number().describe("Y coordinate of the first point."), 381 | x2: z.number().describe("X coordinate of the second point."), 382 | y2: z.number().describe("Y coordinate of the second point."), 383 | strokeColor: z 384 | .string() 385 | .optional() 386 | .default("#000000") 387 | .describe( 388 | "Stroke color in hex code of the line shape. (e.g., black) - temp string type" 389 | ), 390 | }, 391 | async ({ name, parentId, x1, y1, x2, y2, strokeColor }) => { 392 | try { 393 | const shapeId = await command(apiPort, "shape:create-shape", { 394 | type: "Line", 395 | shapeProps: trimObject({ 396 | name, 397 | path: [ 398 | [x1, y1], 399 | [x2, y2], 400 | ], 401 | tailEndType: "flat", 402 | headEndType: "flat", 403 | strokeColor, 404 | lineType: "straight", 405 | }), 406 | parentId, 407 | convertColors: true, 408 | }); 409 | const data = await command(apiPort, "shape:get-shape", { 410 | shapeId, 411 | }); 412 | return response.text( 413 | "Created line: " + JSON.stringify(filterShape(data)) 414 | ); 415 | } catch (error) { 416 | console.error(error); 417 | return response.error( 418 | JsonRpcErrorCode.InternalError, 419 | `Failed to create line: ${error instanceof Error ? error.message : String(error)}` 420 | ); 421 | } 422 | } 423 | ); 424 | 425 | server.tool( 426 | "create_polygon", 427 | "Create a polygon or polyline shape in Frame0.", 428 | { 429 | name: z.string().describe("Name of the polygon shape."), 430 | parentId: z 431 | .string() 432 | .optional() 433 | .describe("ID of the parent shape. Typically frame ID."), 434 | points: z 435 | .array( 436 | z.object({ 437 | x: z.number().describe("X coordinate of the point."), 438 | y: z.number().describe("Y coordinate of the point."), 439 | }) 440 | ) 441 | .min(3) 442 | .describe("Array of points defining the polygon shape."), 443 | closed: z 444 | .boolean() 445 | .optional() 446 | .default(true) 447 | .describe("Whether the polygon shape is closed or not. Default is true."), 448 | fillColor: z 449 | .string() 450 | .optional() 451 | .default("#ffffff") 452 | .describe( 453 | "Fill color in hex code of the polygon shape. (e.g., white) - temp string type" 454 | ), 455 | strokeColor: z 456 | .string() 457 | .optional() 458 | .default("#000000") 459 | .describe( 460 | "Stroke color in hex code of the line shape. (e.g., black) - temp string type" 461 | ), 462 | }, 463 | async ({ name, parentId, points, closed, strokeColor }) => { 464 | try { 465 | const path = points.map((point) => [point.x, point.y]); 466 | const pathClosed = 467 | path[0][0] === path[path.length - 1][0] && 468 | path[0][1] === path[path.length - 1][1]; 469 | if (closed && !pathClosed) path.push(path[0]); 470 | const shapeId = await command(apiPort, "shape:create-shape", { 471 | type: "Line", 472 | shapeProps: trimObject({ 473 | name, 474 | path, 475 | tailEndType: "flat", 476 | headEndType: "flat", 477 | strokeColor, 478 | lineType: "straight", 479 | }), 480 | parentId, 481 | convertColors: true, 482 | }); 483 | const data = await command(apiPort, "shape:get-shape", { 484 | shapeId, 485 | }); 486 | return response.text( 487 | "Created line: " + JSON.stringify(filterShape(data)) 488 | ); 489 | } catch (error) { 490 | console.error(error); 491 | return response.error( 492 | JsonRpcErrorCode.InternalError, 493 | `Failed to create line: ${error instanceof Error ? error.message : String(error)}` 494 | ); 495 | } 496 | } 497 | ); 498 | 499 | server.tool( 500 | "create_connector", 501 | "Create a connector shape in Frame0.", 502 | { 503 | name: z.string().describe("Name of the line shape."), 504 | parentId: z 505 | .string() 506 | .optional() 507 | .describe("ID of the parent shape. Typically frame ID."), 508 | startId: z.string().describe("ID of the start shape."), 509 | endId: z.string().describe("ID of the end shape."), 510 | startArrowhead: z 511 | .enum(ARROWHEADS) 512 | .optional() 513 | .default("none") 514 | .describe("Start arrowhead of the line shape."), 515 | endArrowhead: z 516 | .enum(ARROWHEADS) 517 | .optional() 518 | .default("none") 519 | .describe("End arrowhead of the line shape."), 520 | strokeColor: z 521 | .string() 522 | .optional() 523 | .default("#000000") 524 | .describe("Stroke color in hex code of the line. shape"), 525 | }, 526 | async ({ 527 | name, 528 | parentId, 529 | startId, 530 | endId, 531 | startArrowhead, 532 | endArrowhead, 533 | strokeColor, 534 | }) => { 535 | try { 536 | const shapeId = await command(apiPort, "shape:create-connector", { 537 | tailId: startId, 538 | headId: endId, 539 | shapeProps: trimObject({ 540 | name, 541 | tailEndType: convertArrowhead(startArrowhead || "none"), 542 | headEndType: convertArrowhead(endArrowhead || "none"), 543 | strokeColor, 544 | }), 545 | parentId, 546 | convertColors: true, 547 | }); 548 | const data = await command(apiPort, "shape:get-shape", { 549 | shapeId, 550 | }); 551 | return response.text( 552 | "Created connector: " + JSON.stringify(filterShape(data)) 553 | ); 554 | } catch (error) { 555 | console.error(error); 556 | return response.error( 557 | JsonRpcErrorCode.InternalError, 558 | `Failed to create connector: ${error instanceof Error ? error.message : String(error)}` 559 | ); 560 | } 561 | } 562 | ); 563 | 564 | server.tool( 565 | "create_icon", 566 | "Create an icon shape in Frame0.", 567 | { 568 | name: z 569 | .string() 570 | .describe( 571 | "The name of the icon shape to create. The name should be one of the result of 'get_available_icons' tool." 572 | ), 573 | parentId: z 574 | .string() 575 | .optional() 576 | .describe("ID of the parent shape. Typically frame ID."), 577 | left: z 578 | .number() 579 | .describe( 580 | "Left position of the icon shape in the absolute coordinate system." 581 | ), 582 | top: z 583 | .number() 584 | .describe( 585 | "Top position of the icon shape in the absolute coordinate system." 586 | ), 587 | size: z 588 | .enum(["small", "medium", "large", "extra-large"]) 589 | .describe( 590 | "Size of the icon shape. 'small' is 16 x 16, 'medium' is 24 x 24, 'large' is 32 x 32, 'extra-large' is 48 x 48." 591 | ), 592 | strokeColor: z 593 | .string() 594 | .optional() 595 | .default("#000000") 596 | .describe(`Stroke color in hex code of the icon shape.`), 597 | }, 598 | async ({ name, parentId, left, top, size, strokeColor }) => { 599 | try { 600 | const sizeValue = { 601 | small: 16, 602 | medium: 24, 603 | large: 32, 604 | "extra-large": 48, 605 | }[size]; 606 | const shapeId = await command(apiPort, "shape:create-icon", { 607 | iconName: name, 608 | shapeProps: trimObject({ 609 | left, 610 | top, 611 | width: sizeValue ?? 24, 612 | height: sizeValue ?? 24, 613 | strokeColor, 614 | }), 615 | parentId, 616 | convertColors: true, 617 | }); 618 | const data = await command(apiPort, "shape:get-shape", { 619 | shapeId, 620 | }); 621 | return response.text( 622 | "Created icon: " + JSON.stringify(filterShape(data)) 623 | ); 624 | } catch (error) { 625 | console.error(error); 626 | return response.error( 627 | JsonRpcErrorCode.InternalError, 628 | `Failed to create icon: ${error instanceof Error ? error.message : String(error)}` 629 | ); 630 | } 631 | } 632 | ); 633 | 634 | server.tool( 635 | "create_image", 636 | "Create an image shape in Frame0.", 637 | { 638 | name: z.string().describe("The name of the image shape to create."), 639 | parentId: z 640 | .string() 641 | .optional() 642 | .describe("ID of the parent shape. Typically frame ID."), 643 | mimeType: z 644 | .enum(["image/png", "image/jpeg", "image/webp", "image/svg+xml"]) 645 | .describe("MIME type of the image."), 646 | imageData: z.string().describe("Base64 encoded image data."), 647 | left: z 648 | .number() 649 | .describe( 650 | "Left position of the image shape in the absolute coordinate system." 651 | ), 652 | top: z 653 | .number() 654 | .describe( 655 | "Top position of the image shape in the absolute coordinate system." 656 | ), 657 | }, 658 | async ({ name, parentId, mimeType, imageData, left, top }) => { 659 | try { 660 | const shapeId = await command(apiPort, "shape:create-image", { 661 | mimeType, 662 | imageData, 663 | shapeProps: trimObject({ 664 | name, 665 | left, 666 | top, 667 | }), 668 | parentId, 669 | }); 670 | const data = await command(apiPort, "shape:get-shape", { 671 | shapeId, 672 | }); 673 | return response.text( 674 | "Created image: " + JSON.stringify(filterShape(data)) 675 | ); 676 | } catch (error) { 677 | console.error(error); 678 | return response.error( 679 | JsonRpcErrorCode.InternalError, 680 | `Failed to create image: ${error instanceof Error ? error.message : String(error)}` 681 | ); 682 | } 683 | } 684 | ); 685 | 686 | server.tool( 687 | "update_shape", 688 | "Update properties of a shape in Frame0.", 689 | { 690 | shapeId: z.string().describe("ID of the shape to update"), 691 | name: z.string().optional().describe("Name of the shape."), 692 | width: z.number().optional().describe("Width of the shape."), 693 | height: z.number().optional().describe("Height of the shape."), 694 | fillColor: z 695 | .string() 696 | .optional() 697 | .describe("Fill color in hex code of the shape."), 698 | strokeColor: z 699 | .string() 700 | .optional() 701 | .describe("Stroke color in hex code of the shape."), 702 | fontColor: z 703 | .string() 704 | .optional() 705 | .describe("Font color in hex code of the text shape."), 706 | fontSize: z.number().optional().describe("Font size of the text shape."), 707 | corners: z 708 | .array(z.number()) 709 | .length(4) 710 | .optional() 711 | .describe( 712 | "Corner radius of the rectangle shape. Must be in the form of [left-top, right-top, right-bottom, left-bottom]." 713 | ), 714 | text: z 715 | .string() 716 | .optional() 717 | .describe( 718 | "Plain text content to display of the text shape. Don't include escape sequences and HTML and CSS code in the text content." 719 | ), 720 | }, 721 | async ({ 722 | shapeId, 723 | name, 724 | width, 725 | height, 726 | strokeColor, 727 | fillColor, 728 | fontColor, 729 | fontSize, 730 | corners, 731 | text, 732 | }) => { 733 | try { 734 | const updatedId = await command(apiPort, "shape:update-shape", { 735 | shapeId, 736 | shapeProps: trimObject({ 737 | name, 738 | width, 739 | height, 740 | fillColor, 741 | strokeColor, 742 | fontColor, 743 | fontSize, 744 | corners, 745 | text, 746 | }), 747 | convertColors: true, 748 | }); 749 | const data = await command(apiPort, "shape:get-shape", { 750 | shapeId: updatedId, 751 | }); 752 | return response.text( 753 | "Updated shape: " + JSON.stringify(filterShape(data)) 754 | ); 755 | } catch (error) { 756 | console.error(error); 757 | return response.error( 758 | JsonRpcErrorCode.InternalError, 759 | `Failed to update shape: ${error instanceof Error ? error.message : String(error)}` 760 | ); 761 | } 762 | } 763 | ); 764 | 765 | server.tool( 766 | "duplicate_shape", 767 | "Duplicate a shape in Frame0.", 768 | { 769 | shapeId: z.string().describe("ID of the shape to duplicate"), 770 | parentId: z 771 | .string() 772 | .optional() 773 | .describe( 774 | "ID of the parent shape where the duplicated shape will be added. If not provided, the duplicated shape will be added to the current page." 775 | ), 776 | dx: z 777 | .number() 778 | .optional() 779 | .describe("Delta X value by which the duplicated shape moves."), 780 | dy: z 781 | .number() 782 | .optional() 783 | .describe("Delta Y value by which the duplicated shape moves."), 784 | }, 785 | async ({ shapeId, parentId, dx, dy }) => { 786 | try { 787 | const duplicatedShapeIdArray = await command(apiPort, "edit:duplicate", { 788 | shapeIdArray: [shapeId], 789 | parentId, 790 | dx, 791 | dy, 792 | }); 793 | const duplicatedShapeId = duplicatedShapeIdArray[0]; 794 | const data = await command(apiPort, "shape:get-shape", { 795 | shapeId: duplicatedShapeId, 796 | }); 797 | return response.text( 798 | "Duplicated shape: " + JSON.stringify(filterShape(data)) 799 | ); 800 | } catch (error) { 801 | console.error(error); 802 | return response.error( 803 | JsonRpcErrorCode.InternalError, 804 | `Failed to duplicate shape: ${error instanceof Error ? error.message : String(error)}` 805 | ); 806 | } 807 | } 808 | ); 809 | 810 | server.tool( 811 | "delete_shape", 812 | "Delete a shape in Frame0.", 813 | { shapeId: z.string().describe("ID of the shape to delete") }, 814 | async ({ shapeId }) => { 815 | try { 816 | await command(apiPort, "edit:delete", { 817 | shapeIdArray: [shapeId], 818 | }); 819 | return response.text("Deleted shape of id: " + shapeId); 820 | } catch (error) { 821 | console.error(error); 822 | return response.error( 823 | JsonRpcErrorCode.InternalError, 824 | `Failed to delete shape: ${error instanceof Error ? error.message : String(error)}` 825 | ); 826 | } 827 | } 828 | ); 829 | 830 | server.tool( 831 | "search_icons", 832 | "Search icon shapes available in Frame0.", 833 | { 834 | keyword: z 835 | .string() 836 | .optional() 837 | .describe( 838 | "Search keyword to filter icon by name or tags (case-insensitive)" 839 | ), 840 | }, 841 | async ({ keyword }) => { 842 | try { 843 | const data = await command(apiPort, "shape:get-available-icons", {}); 844 | const icons = Array.isArray(data) ? data : []; 845 | const filtered = keyword 846 | ? icons.filter((icon: { name: string; tags: string[] }) => { 847 | if ( 848 | typeof icon !== "object" || 849 | !icon.name || 850 | !Array.isArray(icon.tags) 851 | ) { 852 | return false; 853 | } 854 | const searchLower = keyword.toLowerCase(); 855 | return ( 856 | icon.name.toLowerCase().includes(searchLower) || 857 | icon.tags.some((tag: string) => 858 | tag.toLowerCase().includes(searchLower) 859 | ) 860 | ); 861 | }) 862 | : icons; 863 | return response.text("Available icons: " + JSON.stringify(filtered)); 864 | } catch (error) { 865 | console.error(error); 866 | return response.error( 867 | JsonRpcErrorCode.InternalError, 868 | `Failed to search available icons: ${error instanceof Error ? error.message : String(error)}` 869 | ); 870 | } 871 | } 872 | ); 873 | 874 | server.tool( 875 | "move_shape", 876 | "Move a shape in Frame0.", 877 | { 878 | shapeId: z.string().describe("ID of the shape to move"), 879 | dx: z.number().describe("Delta X"), 880 | dy: z.number().describe("Delta Y"), 881 | }, 882 | async ({ shapeId, dx, dy }) => { 883 | try { 884 | await command(apiPort, "shape:move", { 885 | shapeId, 886 | dx, 887 | dy, 888 | }); 889 | return response.text(`Moved shape (id: ${shapeId}) as (${dx}, ${dy})`); 890 | } catch (error) { 891 | console.error(error); 892 | return response.error( 893 | JsonRpcErrorCode.InternalError, 894 | `Failed to move shape: ${error instanceof Error ? error.message : String(error)}` 895 | ); 896 | } 897 | } 898 | ); 899 | 900 | server.tool( 901 | "align_shapes", 902 | "Align shapes in Frame0.", 903 | { 904 | alignType: z 905 | .enum([ 906 | "bring-to-front", 907 | "send-to-back", 908 | "align-left", 909 | "align-right", 910 | "align-horizontal-center", 911 | "align-top", 912 | "align-bottom", 913 | "align-vertical-center", 914 | "distribute-horizontally", 915 | "distribute-vertically", 916 | ]) 917 | .describe("Type of the alignment to apply."), 918 | shapeIdArray: z.array(z.string()).describe("Array of shape IDs to align"), 919 | }, 920 | async ({ alignType, shapeIdArray }) => { 921 | const COMMAND = { 922 | "bring-to-front": "align:bring-to-front", 923 | "send-to-back": "align:send-to-back", 924 | "align-left": "align:align-left", 925 | "align-right": "align:align-right", 926 | "align-horizontal-center": "align:align-center", 927 | "align-top": "align:align-top", 928 | "align-bottom": "align:align-bottom", 929 | "align-vertical-center": "align:align-middle", 930 | "distribute-horizontally": "align:horizontal-distribute", 931 | "distribute-vertically": "align:vertical-distribute", 932 | }; 933 | try { 934 | await command(apiPort, COMMAND[alignType], { shapeIdArray }); 935 | return response.text("Shapes are aligned."); 936 | } catch (error) { 937 | console.error(error); 938 | return response.error( 939 | JsonRpcErrorCode.InternalError, 940 | `Failed to align shapes: ${error instanceof Error ? error.message : String(error)}` 941 | ); 942 | } 943 | } 944 | ); 945 | 946 | server.tool( 947 | "group", 948 | "Group shapes in Frame0.", 949 | { 950 | shapeIdArray: z.array(z.string()).describe("Array of shape IDs to group"), 951 | parentId: z 952 | .string() 953 | .optional() 954 | .describe( 955 | "ID of the parent shape where the group will be added. If not provided, the group will be added to the current page." 956 | ), 957 | }, 958 | async ({ shapeIdArray, parentId }) => { 959 | try { 960 | const groupId = await command(apiPort, "shape:group", { 961 | shapeIdArray, 962 | parentId, 963 | }); 964 | const data = await command(apiPort, "shape:get-shape", { 965 | shapeId: groupId, 966 | }); 967 | return response.text( 968 | "Created group: " + JSON.stringify(filterShape(data)) 969 | ); 970 | } catch (error) { 971 | console.error(error); 972 | return response.error( 973 | JsonRpcErrorCode.InternalError, 974 | `Failed to group shapes: ${error instanceof Error ? error.message : String(error)}` 975 | ); 976 | } 977 | } 978 | ); 979 | 980 | server.tool( 981 | "ungroup", 982 | "Ungroup a group in Frame0.", 983 | { 984 | groupId: z.string().describe("ID of the group to ungroup"), 985 | }, 986 | async ({ groupId }) => { 987 | try { 988 | await command(apiPort, "shape:ungroup", { 989 | shapeIdArray: [groupId], 990 | }); 991 | return response.text("Deleted group of id: " + groupId); 992 | } catch (error) { 993 | console.error(error); 994 | return response.error( 995 | JsonRpcErrorCode.InternalError, 996 | `Failed to ungroup shapes: ${error instanceof Error ? error.message : String(error)}` 997 | ); 998 | } 999 | } 1000 | ); 1001 | 1002 | server.tool( 1003 | "set_link", 1004 | "Set a link from a shape to a URL or a page in Frame0.", 1005 | { 1006 | shapeId: z.string().describe("ID of the shape to set link"), 1007 | linkType: z 1008 | .enum(["none", "web", "page", "action:backward"]) 1009 | .describe("Type of the link to set."), 1010 | url: z 1011 | .string() 1012 | .optional() 1013 | .describe("URL to set. Required if linkType is 'web'."), 1014 | pageId: z 1015 | .string() 1016 | .optional() 1017 | .describe("ID of the page to set. Required if linkType is 'page'."), 1018 | }, 1019 | async ({ shapeId, linkType, url, pageId }) => { 1020 | try { 1021 | await command(apiPort, "shape:set-link", { 1022 | shapeId, 1023 | linkProps: trimObject({ 1024 | linkType, 1025 | url, 1026 | pageId, 1027 | }), 1028 | }); 1029 | return response.text(`A link is assigned to shape (id: ${shapeId})`); 1030 | } catch (error) { 1031 | console.error(error); 1032 | return response.error( 1033 | JsonRpcErrorCode.InternalError, 1034 | `Failed to set link: ${error instanceof Error ? error.message : String(error)}` 1035 | ); 1036 | } 1037 | } 1038 | ); 1039 | 1040 | server.tool( 1041 | "export_shape_as_image", 1042 | "Export shape as image in Frame0.", 1043 | { 1044 | shapeId: z.string().describe("ID of the shape to export"), 1045 | format: z 1046 | .enum(["image/png", "image/jpeg", "image/webp"]) 1047 | .optional() 1048 | .default("image/png") 1049 | .describe("Image format to export."), 1050 | }, 1051 | async ({ shapeId, format }) => { 1052 | try { 1053 | const data = await command(apiPort, "shape:get-shape", { 1054 | shapeId, 1055 | }); 1056 | const image = await command(apiPort, "file:export-image", { 1057 | pageId: data.pageId, 1058 | shapeIdArray: [shapeId], 1059 | format, 1060 | fillBackground: true, 1061 | }); 1062 | return response.image(format, image); 1063 | } catch (error) { 1064 | console.error(error); 1065 | return response.error( 1066 | JsonRpcErrorCode.InternalError, 1067 | `Failed to export shape as image: ${error instanceof Error ? error.message : String(error)}` 1068 | ); 1069 | } 1070 | } 1071 | ); 1072 | 1073 | server.tool( 1074 | "add_page", 1075 | "Add a new page in Frame0. The added page becomes the current page.", 1076 | { 1077 | name: z.string().describe("Name of the page to add."), 1078 | }, 1079 | async ({ name }) => { 1080 | try { 1081 | const pageData = await command(apiPort, "page:add", { 1082 | pageProps: trimObject({ name }), 1083 | }); 1084 | return response.text(`Added page: ${JSON.stringify(pageData)}`); 1085 | } catch (error) { 1086 | console.error(error); 1087 | return response.error( 1088 | JsonRpcErrorCode.InternalError, 1089 | `Failed to add page: ${error instanceof Error ? error.message : String(error)}` 1090 | ); 1091 | } 1092 | } 1093 | ); 1094 | 1095 | server.tool( 1096 | "update_page", 1097 | "Update a page in Frame0.", 1098 | { 1099 | pageId: z.string().describe("ID of the page to update."), 1100 | name: z.string().describe("Name of the page."), 1101 | }, 1102 | async ({ pageId, name }) => { 1103 | try { 1104 | const updatedPageId = await command(apiPort, "page:update", { 1105 | pageId, 1106 | pageProps: trimObject({ name }), 1107 | }); 1108 | const pageData = await command(apiPort, "page:get", { 1109 | pageId: updatedPageId, 1110 | }); 1111 | return response.text(`Updated page: ${JSON.stringify(pageData)}`); 1112 | } catch (error) { 1113 | console.error(error); 1114 | return response.error( 1115 | JsonRpcErrorCode.InternalError, 1116 | `Failed to update page: ${error instanceof Error ? error.message : String(error)}` 1117 | ); 1118 | } 1119 | } 1120 | ); 1121 | 1122 | server.tool( 1123 | "duplicate_page", 1124 | "Duplicate a page in Frame0.", 1125 | { 1126 | pageId: z.string().describe("ID of the page to duplicate"), 1127 | name: z.string().optional().describe("Name of the duplicated page."), 1128 | }, 1129 | async ({ pageId, name }) => { 1130 | try { 1131 | const duplicatedPageId = await command(apiPort, "page:duplicate", { 1132 | pageId, 1133 | pageProps: trimObject({ name }), 1134 | }); 1135 | const pageData = await command(apiPort, "page:get", { 1136 | pageId: duplicatedPageId, 1137 | exportShapes: true, 1138 | }); 1139 | return response.text(`Duplicated page data: ${JSON.stringify(pageData)}`); 1140 | } catch (error) { 1141 | console.error(error); 1142 | return response.error( 1143 | JsonRpcErrorCode.InternalError, 1144 | `Failed to duplicate page: ${error instanceof Error ? error.message : String(error)}` 1145 | ); 1146 | } 1147 | } 1148 | ); 1149 | 1150 | server.tool( 1151 | "delete_page", 1152 | "Delete a page in Frame0.", 1153 | { 1154 | pageId: z.string().describe("ID of the page to delete"), 1155 | }, 1156 | async ({ pageId }) => { 1157 | try { 1158 | await command(apiPort, "page:delete", { 1159 | pageId, 1160 | }); 1161 | return response.text(`Deleted page ID is${pageId}`); 1162 | } catch (error) { 1163 | console.error(error); 1164 | return response.error( 1165 | JsonRpcErrorCode.InternalError, 1166 | `Failed to delete page: ${error instanceof Error ? error.message : String(error)}` 1167 | ); 1168 | } 1169 | } 1170 | ); 1171 | 1172 | server.tool( 1173 | "get_current_page_id", 1174 | "Get ID of the current page in Frame0.", 1175 | {}, 1176 | async () => { 1177 | try { 1178 | const pageId = await command(apiPort, "page:get-current-page"); 1179 | return response.text(`Current page ID is ${pageId},`); 1180 | } catch (error) { 1181 | console.error(error); 1182 | return response.error( 1183 | JsonRpcErrorCode.InternalError, 1184 | `Failed to get current page: ${error instanceof Error ? error.message : String(error)}` 1185 | ); 1186 | } 1187 | } 1188 | ); 1189 | 1190 | server.tool( 1191 | "set_current_page_by_id", 1192 | "Set current page by ID in Frame0.", 1193 | { 1194 | pageId: z.string().describe("ID of the page to set as current page."), 1195 | }, 1196 | async ({ pageId }) => { 1197 | try { 1198 | await command(apiPort, "page:set-current-page", { 1199 | pageId, 1200 | }); 1201 | return response.text(`Current page ID is ${pageId}`); 1202 | } catch (error) { 1203 | console.error(error); 1204 | return response.error( 1205 | JsonRpcErrorCode.InternalError, 1206 | `Failed to set current page: ${error instanceof Error ? error.message : String(error)}` 1207 | ); 1208 | } 1209 | } 1210 | ); 1211 | 1212 | server.tool( 1213 | "get_page", 1214 | "Get page data in Frame0.", 1215 | { 1216 | pageId: z 1217 | .string() 1218 | .optional() 1219 | .describe( 1220 | "ID of the page to get data. If not provided, the current page data is returned." 1221 | ), 1222 | exportShapes: z 1223 | .boolean() 1224 | .optional() 1225 | .default(true) 1226 | .describe("Export shapes data included in the page."), 1227 | }, 1228 | async ({ pageId, exportShapes }) => { 1229 | try { 1230 | const pageData = await command(apiPort, "page:get", { 1231 | pageId, 1232 | exportShapes, 1233 | }); 1234 | return response.text( 1235 | `The page data: ${JSON.stringify(filterPage(pageData))}` 1236 | ); 1237 | } catch (error) { 1238 | console.error(error); 1239 | return response.error( 1240 | JsonRpcErrorCode.InternalError, 1241 | `Failed to get page data: ${error instanceof Error ? error.message : String(error)}` 1242 | ); 1243 | } 1244 | } 1245 | ); 1246 | 1247 | server.tool( 1248 | "get_all_pages", 1249 | "Get all pages data in Frame0.", 1250 | { 1251 | exportShapes: z 1252 | .boolean() 1253 | .optional() 1254 | .default(false) 1255 | .describe("Export shapes data included in the page data."), 1256 | }, 1257 | async ({ exportShapes }) => { 1258 | try { 1259 | const docData = await command(apiPort, "doc:get", { 1260 | exportPages: true, 1261 | exportShapes, 1262 | }); 1263 | if (!Array.isArray(docData.children)) docData.children = []; 1264 | const pageArray = docData.children.map((page: any) => filterPage(page)); 1265 | return response.text(`The all pages data: ${JSON.stringify(pageArray)}`); 1266 | } catch (error) { 1267 | console.error(error); 1268 | return response.error( 1269 | JsonRpcErrorCode.InternalError, 1270 | `Failed to get page data: ${error instanceof Error ? error.message : String(error)}` 1271 | ); 1272 | } 1273 | } 1274 | ); 1275 | 1276 | server.tool( 1277 | "export_page_as_image", 1278 | "Export page as image in Frame0.", 1279 | { 1280 | pageId: z 1281 | .string() 1282 | .optional() 1283 | .describe( 1284 | "ID of the page to export. If not provided, the current page is used." 1285 | ), 1286 | format: z 1287 | .enum(["image/png", "image/jpeg", "image/webp"]) 1288 | .optional() 1289 | .default("image/png") 1290 | .describe("Image format to export."), 1291 | }, 1292 | async ({ pageId, format }) => { 1293 | try { 1294 | const image = await command(apiPort, "file:export-image", { 1295 | pageId, 1296 | format, 1297 | fillBackground: true, 1298 | }); 1299 | return response.image(format, image); 1300 | } catch (error) { 1301 | console.error(error); 1302 | return response.error( 1303 | JsonRpcErrorCode.InternalError, 1304 | `Failed to export page as image: ${error instanceof Error ? error.message : String(error)}` 1305 | ); 1306 | } 1307 | } 1308 | ); 1309 | 1310 | async function main() { 1311 | const transport = new StdioServerTransport(); 1312 | await server.connect(transport); 1313 | console.error("Frame0 MCP Server running on stdio"); 1314 | } 1315 | 1316 | main().catch((error) => { 1317 | console.error("Error starting server:", error); 1318 | process.exit(1); 1319 | }); 1320 | ```