#
tokens: 17053/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![smithery badge](https://smithery.ai/badge/@niklauslee/frame0-mcp-server)](https://smithery.ai/server/@niklauslee/frame0-mcp-server)
 2 | 
 3 | [![Frame0 MCP Video Example](https://github.com/niklauslee/frame0-mcp-server/raw/main/thumbnail.png)](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 | 
```