From f21198ca18f3e55a919bcc560088e7cf32fe6cca Mon Sep 17 00:00:00 2001 From: Abdur Rahman Date: Fri, 13 Feb 2026 19:27:22 +0530 Subject: [PATCH 1/6] chore: mcp server updates --- README.md | 65 +++- package-lock.json | 564 +++++++++++++++++++---------------- package.json | 12 +- src/index.ts | 6 +- src/parsers/xd-web-parser.ts | 380 +++++++++++++++++++++++ src/tools/xd-tools.ts | 27 +- 6 files changed, 768 insertions(+), 286 deletions(-) create mode 100644 src/parsers/xd-web-parser.ts diff --git a/README.md b/README.md index d7b32d2..684855a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ A Model Context Protocol (MCP) server that enables Claude to interact with Adobe ## Features -- **Document Analysis**: Extract comprehensive information from XD files including artboards, components, and assets +- **Document Analysis**: Extract comprehensive information from XD files (local `.xd` files or web share URLs) including artboards, components, and assets +- **Web URL Support**: Work directly with Adobe XD web share links (specs URLs) - no need to download files - **React Component Generation**: Convert XD designs to React components with multiple styling options - **Color Extraction**: Extract color palettes in various formats (CSS, JSON, Tailwind) - **Multiple Style Systems**: Support for styled-components, Tailwind CSS, and CSS modules @@ -36,12 +37,12 @@ npm run build ## Configuration -Add the server to your Claude Desktop configuration: +### Claude Desktop -### macOS +#### macOS Edit: `~/Library/Application Support/Claude/claude_desktop_config.json` -### Windows +#### Windows Edit: `%APPDATA%\Claude\claude_desktop_config.json` Add the following configuration: @@ -51,13 +52,39 @@ Add the following configuration: "mcpServers": { "adobe-xd": { "command": "node", - "args": ["/absolute/path/to/adobe-xd-mcp/dist/index.js"] + "args": ["C:\\Users\\abdur\\Documents\\IT Projects\\Lyzant\\Commerce\\Synergic Software\\adobe-xd-mcp\\dist\\index.js"] } } } ``` -**Note**: Replace `/absolute/path/to/adobe-xd-mcp` with the actual path to your installation directory. +### Cursor IDE + +Cursor IDE requires MCP servers to be configured through its settings: + +1. **Open Cursor Settings**: + - Press `Ctrl+,` (Windows/Linux) or `Cmd+,` (Mac) + - Or navigate to: File → Preferences → Settings + +2. **Search for "MCP"** in the settings search bar + +3. **Add MCP Server Configuration**: + - Look for "MCP Servers" or "Model Context Protocol" settings + - Click "Add" or "Edit" to add a new server + - Use the following configuration: + +```json +{ + "name": "adobe-xd", + "command": "node", + "args": ["C:\\Users\\abdur\\Documents\\IT Projects\\Lyzant\\Commerce\\Synergic Software\\adobe-xd-mcp\\dist\\index.js"] +} +``` + +**Alternative**: If Cursor uses a configuration file, it may be located at: +- Windows: `%APPDATA%\Cursor\User\settings.json` or `%APPDATA%\Cursor\User\globalStorage\cursor.mcp\settings.json` + +**Note**: Replace the path with your actual installation directory path. Use forward slashes `/` or escaped backslashes `\\` in JSON paths. ## Usage @@ -65,9 +92,10 @@ After configuring and restarting Claude Desktop, you can use the following comma ### Get XD Document Information -Ask Claude to analyze your XD file: +Ask Claude to analyze your XD file (local file or web URL): ``` "Get information about the XD file at /path/to/your/design.xd" +"Get information about the XD design at https://xd.adobe.com/view/82c81c10-0103-4445-b1f0-5db805a92e64-d79b/specs/" ``` This returns: @@ -75,25 +103,29 @@ This returns: - List of artboards with sizes - Components and their element counts +**Note**: You can use either local `.xd` file paths or Adobe XD web share URLs (specs links). + ### Generate React Components -Convert XD designs to React components: +Convert XD designs to React components (from local files or web URLs): ``` "Generate a React component from the LoginScreen artboard in /path/to/design.xd" +"Generate React components from https://xd.adobe.com/view/82c81c10-0103-4445-b1f0-5db805a92e64-d79b/specs/" ``` Options: - `artboardName`: Specific artboard to convert (optional) - `componentName`: Custom component name (optional) -- `outputDir`: Output directory (optional, defaults to ./generated) +- `outputDir`: Output directory (optional, defaults to ./generated for web URLs, or file directory for local files) - `styleSystem`: Choose from "styled-components", "tailwind", or "css-modules" - `typescript`: Generate TypeScript components (true/false) ### Extract Colors -Extract color palettes from your designs: +Extract color palettes from your designs (from local files or web URLs): ``` "Extract colors from /path/to/design.xd and save as CSS variables" +"Extract colors from https://xd.adobe.com/view/82c81c10-0103-4445-b1f0-5db805a92e64-d79b/specs/ in Tailwind format" ``` Options: @@ -102,19 +134,26 @@ Options: ## Examples -### Example 1: Generate a TypeScript React component with Tailwind +### Example 1: Generate a TypeScript React component with Tailwind (from local file) ``` "Generate a React component from the Header artboard in design.xd using TypeScript and Tailwind CSS" ``` -### Example 2: Extract colors as Tailwind config +### Example 2: Generate components from Adobe XD web specs +``` +"Generate React components from https://xd.adobe.com/view/82c81c10-0103-4445-b1f0-5db805a92e64-d79b/specs/ using Tailwind CSS" +``` + +### Example 3: Extract colors as Tailwind config ``` "Extract colors from design.xd in Tailwind format and save to colors.js" +"Extract colors from https://xd.adobe.com/view/82c81c10-0103-4445-b1f0-5db805a92e64-d79b/specs/ in Tailwind format" ``` -### Example 3: Get overview of all components +### Example 4: Get overview of all components ``` "Show me all components in my design system at /designs/system.xd" +"Get information about the XD design at https://xd.adobe.com/view/82c81c10-0103-4445-b1f0-5db805a92e64-d79b/specs/" ``` ## Development diff --git a/package-lock.json b/package-lock.json index 3025910..e0a09bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,22 +7,23 @@ "": { "name": "adobe-xd-mcp", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.15.1", + "@modelcontextprotocol/sdk": "^1.24.3", "jszip": "^3.10.1" }, "devDependencies": { "@types/jszip": "^3.4.1", - "@types/node": "^24.0.13", - "tsx": "^4.20.3", - "typescript": "^5.8.3" + "@types/minimatch": "^5.1.2", + "@types/node": "^20.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", "cpu": [ "ppc64" ], @@ -36,9 +37,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", "cpu": [ "arm" ], @@ -52,9 +53,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", "cpu": [ "arm64" ], @@ -68,9 +69,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", "cpu": [ "x64" ], @@ -84,9 +85,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", "cpu": [ "arm64" ], @@ -100,9 +101,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", "cpu": [ "x64" ], @@ -116,9 +117,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", "cpu": [ "arm64" ], @@ -132,9 +133,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", "cpu": [ "x64" ], @@ -148,9 +149,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", "cpu": [ "arm" ], @@ -164,9 +165,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", "cpu": [ "arm64" ], @@ -180,9 +181,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", "cpu": [ "ia32" ], @@ -196,9 +197,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", "cpu": [ "loong64" ], @@ -212,9 +213,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", "cpu": [ "mips64el" ], @@ -228,9 +229,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", "cpu": [ "ppc64" ], @@ -244,9 +245,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", "cpu": [ "riscv64" ], @@ -260,9 +261,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", "cpu": [ "s390x" ], @@ -276,9 +277,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", "cpu": [ "x64" ], @@ -292,9 +293,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", "cpu": [ "arm64" ], @@ -308,9 +309,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", "cpu": [ "x64" ], @@ -324,9 +325,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", "cpu": [ "arm64" ], @@ -340,9 +341,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", "cpu": [ "x64" ], @@ -356,9 +357,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", "cpu": [ "arm64" ], @@ -372,9 +373,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", "cpu": [ "x64" ], @@ -388,9 +389,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", "cpu": [ "arm64" ], @@ -404,9 +405,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", "cpu": [ "ia32" ], @@ -420,9 +421,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", "cpu": [ "x64" ], @@ -436,11 +437,12 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", - "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==", + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.3.tgz", + "integrity": "sha512-YgSHW29fuzKKAHTGe9zjNoo+yF8KaQPzDC2W9Pv41E7/57IfY+AMGJ/aDFlgTLcVVELoggKE4syABCE75u3NCw==", "dependencies": { - "ajv": "^6.12.6", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", @@ -448,13 +450,26 @@ "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, "node_modules/@types/jszip": { @@ -467,13 +482,19 @@ "jszip": "*" } }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, "node_modules/@types/node": { - "version": "24.0.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", - "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", + "version": "20.19.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", "dev": true, "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~6.21.0" } }, "node_modules/accepts": { @@ -489,37 +510,57 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bytes": { @@ -558,14 +599,15 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dependencies": { - "safe-buffer": "5.2.1" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -623,9 +665,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": { "ms": "^2.1.3" }, @@ -700,9 +742,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", "dev": true, "hasInstallScript": true, "bin": { @@ -712,32 +754,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" } }, "node_modules/escape-html": { @@ -765,25 +807,26 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", - "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -832,15 +875,25 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -850,7 +903,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/forwarded": { @@ -927,9 +984,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -972,37 +1029,37 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/immediate": { @@ -1038,10 +1095,18 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/jszip": { "version": "3.10.1", @@ -1098,14 +1163,18 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ms": { @@ -1181,17 +1250,18 @@ } }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "engines": { - "node": ">=16" + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "engines": { "node": ">=16.20.0" } @@ -1213,14 +1283,6 @@ "node": ">= 0.10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -1244,17 +1306,17 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/readable-stream": { @@ -1271,10 +1333,13 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", @@ -1301,23 +1366,9 @@ } }, "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -1472,11 +1523,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1486,12 +1532,12 @@ } }, "node_modules/tsx": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", - "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "dependencies": { - "esbuild": "~0.25.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -1518,9 +1564,9 @@ } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -1531,9 +1577,9 @@ } }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true }, "node_modules/unpipe": { @@ -1544,14 +1590,6 @@ "node": ">= 0.8" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -1593,11 +1631,11 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", "peerDependencies": { - "zod": "^3.24.1" + "zod": "^3.25 || ^4" } } } diff --git a/package.json b/package.json index 4c9b98c..670cfa4 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,21 @@ "dev": "tsx src/index.ts", "test": "tsx src/test.ts" }, - "keywords": ["mcp", "adobe-xd", "react", "code-generation"], + "keywords": [ + "mcp", + "adobe-xd", + "react", + "code-generation" + ], "author": "Your Name", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", + "@modelcontextprotocol/sdk": "^1.24.3", "jszip": "^3.10.1" }, "devDependencies": { - "@types/jszip": "^3.10.0", + "@types/jszip": "^3.4.1", + "@types/minimatch": "^5.1.2", "@types/node": "^20.0.0", "tsx": "^4.0.0", "typescript": "^5.0.0" diff --git a/src/index.ts b/src/index.ts index b1ef214..7709ef1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { properties: { path: { type: 'string', - description: 'Path to the XD document', + description: 'Path to the XD document (local file path or Adobe XD web specs URL)', }, }, required: ['path'], @@ -47,7 +47,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { properties: { path: { type: 'string', - description: 'Path to the XD document', + description: 'Path to the XD document (local file path or Adobe XD web specs URL)', }, artboardName: { type: 'string', @@ -82,7 +82,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { properties: { path: { type: 'string', - description: 'Path to the XD document', + description: 'Path to the XD document (local file path or Adobe XD web specs URL)', }, format: { type: 'string', diff --git a/src/parsers/xd-web-parser.ts b/src/parsers/xd-web-parser.ts new file mode 100644 index 0000000..e4d3ce6 --- /dev/null +++ b/src/parsers/xd-web-parser.ts @@ -0,0 +1,380 @@ +import { XDDocument, XDArtboard, XDElement, XDColor, XDComponent } from './xd-parser'; + +interface XDWebSpec { + artboards?: Array<{ + id: string; + name: string; + width: number; + height: number; + elements?: any[]; + }>; + colors?: Array<{ + value: string; + name?: string; + }>; + components?: Array<{ + id: string; + name: string; + elements?: any[]; + }>; +} + +export class XDWebParser { + async parseWebSpec(url: string): Promise { + const normalizedUrl = url.endsWith('/') ? url : `${url}/`; + + const response = await fetch(normalizedUrl, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + 'Referer': 'https://xd.adobe.com/' + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch XD spec: ${response.status} ${response.statusText}`); + } + + const html = await response.text(); + const specData = this.extractSpecData(html, normalizedUrl); + + let artboards = this.parseArtboards(specData); + let colors = this.extractColors(specData); + const components = this.parseComponents(specData); + + if (artboards.length === 0 && colors.length === 0) { + const apiUrl = this.extractApiUrl(html, normalizedUrl); + if (apiUrl) { + try { + const apiResponse = await fetch(apiUrl, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json', + 'Referer': normalizedUrl + } + }); + + if (apiResponse.ok) { + const apiData = await apiResponse.json() as any; + if (apiData?.artboards) artboards = this.parseArtboards({ artboards: apiData.artboards }); + if (apiData?.colors) colors = apiData.colors; + } + } catch (e) { + } + } + } + + return { + name: this.extractDocumentName(html) || 'XD Design', + artboards, + colors, + components + }; + } + + private extractApiUrl(html: string, baseUrl: string): string | null { + const patterns = [ + /"apiUrl":\s*"([^"]+)"/, + /api\.adobe\.com\/[^"'\s]+/, + /\/api\/[^"'\s]+/ + ]; + + for (const pattern of patterns) { + const match = html.match(pattern); + if (match) { + let url = match[1] || match[0]; + if (!url.startsWith('http')) { + url = new URL(url, baseUrl).href; + } + return url; + } + } + + return null; + } + + private extractSpecData(html: string, baseUrl: string): XDWebSpec { + const spec: XDWebSpec = { + artboards: [], + colors: [], + components: [] + }; + + const jsonPatterns = [ + /window\.__XD_DATA__\s*=\s*({.+?});/s, + /window\.__INITIAL_STATE__\s*=\s*({.+?});/s, + /window\.__PRELOADED_STATE__\s*=\s*({.+?});/s, + /window\.__APP_DATA__\s*=\s*({.+?});/s, + /]*type="application\/json"[^>]*id="[^"]*data[^"]*"[^>]*>(.+?)<\/script>/s, + /]*type="application\/json"[^>]*>(.+?)<\/script>/s + ]; + + for (const pattern of jsonPatterns) { + const jsonMatch = html.match(pattern); + if (jsonMatch) { + try { + const jsonStr = jsonMatch[1].trim(); + const data = JSON.parse(jsonStr); + + if (data.artboards || data.artboard || data.boards) { + spec.artboards = data.artboards || data.boards || (data.artboard ? [data.artboard] : []); + } + if (data.colors || data.colorPalette || data.palette) { + spec.colors = data.colors || data.colorPalette || data.palette || []; + } + if (data.components || data.component) { + spec.components = data.components || (data.component ? [data.component] : []); + } + + if (data.document) { + if (data.document.artboards) spec.artboards = data.document.artboards; + if (data.document.colors) spec.colors = data.document.colors; + if (data.document.components) spec.components = data.document.components; + } + + if (spec.artboards && spec.artboards.length > 0) { + break; + } + } catch (e) { + continue; + } + } + } + + const artboardPatterns = [ + /data-artboard-id="([^"]+)"[^>]*data-artboard-name="([^"]+)"[^>]*data-width="(\d+)"[^>]*data-height="(\d+)"/g, + /]*class="[^"]*artboard[^"]*"[^>]*data-width="(\d+)"[^>]*data-height="(\d+)"[^>]*data-name="([^"]+)"/g, + /]*width="(\d+)"[^>]*height="(\d+)"[^>]*data-artboard="([^"]+)"/g + ]; + + for (const pattern of artboardPatterns) { + const matches = html.matchAll(pattern); + for (const match of matches) { + if (!spec.artboards) spec.artboards = []; + const existing = spec.artboards.find(ab => + (match[1] && ab.id === match[1]) || + (match[3] && ab.name === match[3]) + ); + if (!existing) { + spec.artboards.push({ + id: match[1] || Math.random().toString(36), + name: match[2] || match[3] || `Artboard ${spec.artboards.length + 1}`, + width: parseInt(match[3] || match[1] || '1440', 10), + height: parseInt(match[4] || match[2] || '900', 10), + elements: [] + }); + } + } + } + + const cssVarPattern = /--([a-zA-Z0-9-]+):\s*([^;]+);/g; + const colorSet = new Set(); + let cssVarMatch; + while ((cssVarMatch = cssVarPattern.exec(html)) !== null) { + const color = this.normalizeColor(cssVarMatch[2].trim()); + if (color && !colorSet.has(color)) { + colorSet.add(color); + if (!spec.colors) spec.colors = []; + spec.colors.push({ + value: color, + name: cssVarMatch[1] + }); + } + } + + const stylePatterns = [ + /\.([a-zA-Z0-9_-]+)\s*\{[^}]*color:\s*([^;]+);/g, + /background-color:\s*([^;]+);/g, + /fill:\s*([^;]+);/g, + /stroke:\s*([^;]+);/g, + /rgb\((\d+),\s*(\d+),\s*(\d+)\)/g, + /rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)/g + ]; + + for (const pattern of stylePatterns) { + const matches = html.matchAll(pattern); + for (const match of matches) { + let color: string; + if (match.length === 4) { + const r = parseInt(match[1], 10).toString(16).padStart(2, '0'); + const g = parseInt(match[2], 10).toString(16).padStart(2, '0'); + const b = parseInt(match[3], 10).toString(16).padStart(2, '0'); + color = `#${r}${g}${b}`; + } else { + color = this.normalizeColor(match[1] || match[0]); + } + + if (color && !colorSet.has(color)) { + colorSet.add(color); + if (!spec.colors) spec.colors = []; + spec.colors.push({ value: color }); + } + } + } + + if (!spec.artboards || spec.artboards.length === 0) { + const viewportMatch = html.match(/viewport[^>]*width="(\d+)"[^>]*height="(\d+)"/i); + if (viewportMatch) { + if (!spec.artboards) spec.artboards = []; + spec.artboards.push({ + id: 'default', + name: 'Main Artboard', + width: parseInt(viewportMatch[1], 10) || 1440, + height: parseInt(viewportMatch[2], 10) || 900, + elements: [] + }); + } + } + + return spec; + } + + private parseArtboards(spec: XDWebSpec): XDArtboard[] { + if (!spec.artboards) return []; + + return spec.artboards.map(ab => ({ + id: ab.id, + name: ab.name, + x: 0, + y: 0, + width: ab.width, + height: ab.height, + elements: ab.elements ? this.parseElements(ab.elements) : [] + })); + } + + private parseElements(elements: any[]): XDElement[] { + return elements.map(el => ({ + id: el.id || Math.random().toString(36), + type: this.mapElementType(el.type || el.tagName), + name: el.name || el.className, + x: el.x || el.left || 0, + y: el.y || el.top || 0, + width: el.width || 0, + height: el.height || 0, + text: el.text || el.textContent, + fill: el.fill || el.backgroundColor, + stroke: el.stroke || el.borderColor, + children: el.children ? this.parseElements(el.children) : undefined + })); + } + + private parseComponents(spec: XDWebSpec): XDComponent[] { + if (!spec.components) return []; + + return spec.components.map(comp => ({ + id: comp.id, + name: comp.name, + elements: comp.elements ? this.parseElements(comp.elements) : [] + })); + } + + private extractColors(spec: XDWebSpec): XDColor[] { + if (spec.colors && spec.colors.length > 0) { + return spec.colors; + } + + const colors: XDColor[] = []; + const colorSet = new Set(); + + const findColors = (obj: any) => { + if (!obj || typeof obj !== 'object') return; + + if (obj.fill || obj.backgroundColor) { + const hex = this.normalizeColor(obj.fill || obj.backgroundColor); + if (hex && !colorSet.has(hex)) { + colorSet.add(hex); + colors.push({ value: hex }); + } + } + + if (obj.stroke || obj.borderColor) { + const hex = this.normalizeColor(obj.stroke || obj.borderColor); + if (hex && !colorSet.has(hex)) { + colorSet.add(hex); + colors.push({ value: hex }); + } + } + + if (Array.isArray(obj)) { + obj.forEach(findColors); + } else { + Object.values(obj).forEach(findColors); + } + }; + + if (spec.artboards) { + spec.artboards.forEach(ab => findColors(ab)); + } + + return colors; + } + + private mapElementType(type: string): XDElement['type'] { + const typeMap: { [key: string]: XDElement['type'] } = { + 'div': 'rectangle', + 'rect': 'rectangle', + 'ellipse': 'ellipse', + 'circle': 'ellipse', + 'text': 'text', + 'p': 'text', + 'h1': 'text', + 'h2': 'text', + 'h3': 'text', + 'span': 'text', + 'group': 'group', + 'component': 'component' + }; + + return typeMap[type?.toLowerCase()] || 'group'; + } + + private normalizeColor(color: string): string { + if (!color) return ''; + + color = color.trim(); + + if (color.startsWith('#')) { + if (color.length === 4) { + return `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`; + } + return color.length === 7 ? color : ''; + } + + if (color.startsWith('rgb')) { + const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (match) { + const r = parseInt(match[1], 10).toString(16).padStart(2, '0'); + const g = parseInt(match[2], 10).toString(16).padStart(2, '0'); + const b = parseInt(match[3], 10).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; + } + } + + const namedColors: { [key: string]: string } = { + 'black': '#000000', + 'white': '#ffffff', + 'red': '#ff0000', + 'green': '#00ff00', + 'blue': '#0000ff' + }; + + return namedColors[color.toLowerCase()] || ''; + } + + private extractDocumentName(html: string): string { + const titleMatch = html.match(/([^<]+)<\/title>/i); + if (titleMatch) { + return titleMatch[1].trim(); + } + + const metaMatch = html.match(/<meta\s+property="og:title"\s+content="([^"]+)"/i); + if (metaMatch) { + return metaMatch[1].trim(); + } + + return 'XD Design'; + } +} + diff --git a/src/tools/xd-tools.ts b/src/tools/xd-tools.ts index 88e56dc..b42aec4 100644 --- a/src/tools/xd-tools.ts +++ b/src/tools/xd-tools.ts @@ -2,18 +2,34 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { XDParser } from '../parsers/xd-parser'; +import { XDWebParser } from '../parsers/xd-web-parser'; import { ReactGenerator, GeneratorOptions } from '../generators/react-generator'; +import { XDDocument } from '../parsers/xd-parser'; export class XDTools { private parser: XDParser; + private webParser: XDWebParser; constructor() { this.parser = new XDParser(); + this.webParser = new XDWebParser(); + } + + private isWebUrl(pathOrUrl: string): boolean { + return pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://'); + } + + private async getDocument(pathOrUrl: string): Promise<XDDocument> { + if (this.isWebUrl(pathOrUrl)) { + return await this.webParser.parseWebSpec(pathOrUrl); + } else { + return await this.parser.parseDocument(pathOrUrl); + } } async getDocumentInfo(args: { path: string }) { try { - const doc = await this.parser.parseDocument(args.path); + const doc = await this.getDocument(args.path); return { success: true, @@ -54,7 +70,11 @@ export class XDTools { typescript?: boolean; }) { try { - const doc = await this.parser.parseDocument(args.path); + const doc = await this.getDocument(args.path); + + const outputDir = args.outputDir || (this.isWebUrl(args.path) ? './generated' : path.dirname(args.path)); + + await fs.mkdir(outputDir, { recursive: true }); const options: GeneratorOptions = { styleSystem: args.styleSystem || 'tailwind', @@ -62,7 +82,6 @@ export class XDTools { }; const generator = new ReactGenerator(options); - const outputDir = args.outputDir || path.dirname(args.path); let generated = 0; @@ -153,7 +172,7 @@ export class XDTools { outputFile?: string; }) { try { - const doc = await this.parser.parseDocument(args.path); + const doc = await this.getDocument(args.path); if (doc.colors.length === 0) { return { From 1b73da7dd0b6f8680ca5cd4c8c39f8c4bacb32f5 Mon Sep 17 00:00:00 2001 From: Abdur Rahman <mmarahman4847@gmail.com> Date: Fri, 13 Feb 2026 20:17:48 +0530 Subject: [PATCH 2/6] feat: add comprehensive type definitions for XD data structures --- src/types/design-hierarchy.ts | 79 +++++++++++ src/types/design-tokens.ts | 90 +++++++++++++ src/types/layout-system.ts | 51 +++++++ src/types/xd-data.ts | 245 ++++++++++++++++++++++++++++++++++ 4 files changed, 465 insertions(+) create mode 100644 src/types/design-hierarchy.ts create mode 100644 src/types/design-tokens.ts create mode 100644 src/types/layout-system.ts create mode 100644 src/types/xd-data.ts diff --git a/src/types/design-hierarchy.ts b/src/types/design-hierarchy.ts new file mode 100644 index 0000000..70a366f --- /dev/null +++ b/src/types/design-hierarchy.ts @@ -0,0 +1,79 @@ +import { AGCElement, AGCStyle } from './xd-data'; + +export interface DesignHierarchy { + artboardId: string; + artboardName: string; + root: HierarchyNode; + flattenedElements: FlattenedElement[]; + stats: HierarchyStats; +} + +export interface HierarchyNode { + id: string; + name: string; + type: string; + depth: number; + path: string; // e.g., "root/header/logo" + element: AGCElement; + children: HierarchyNode[]; + boundingBox: BoundingBox; + computedStyle: ComputedStyle; +} + +export interface FlattenedElement { + id: string; + name: string; + type: string; + depth: number; + path: string; + parentId?: string; + childIds: string[]; + boundingBox: BoundingBox; + styles: AGCStyle; +} + +export interface BoundingBox { + x: number; + y: number; + width: number; + height: number; + absoluteX: number; // Computed from transforms + absoluteY: number; +} + +export interface ComputedStyle { + position: { + x: number; + y: number; + absoluteX: number; + absoluteY: number; + }; + dimensions: { + width: number; + height: number; + }; + colors: { + fill?: string; // Hex color + stroke?: string; + }; + typography?: { + fontFamily: string; + fontSize: number; + fontWeight: number; + lineHeight?: number; + letterSpacing?: number; + }; + effects: { + shadows: string[]; // CSS shadow strings + blur?: number; + opacity?: number; + }; +} + +export interface HierarchyStats { + totalElements: number; + maxDepth: number; + elementsByType: Record<string, number>; + totalGroups: number; + totalComponents: number; +} diff --git a/src/types/design-tokens.ts b/src/types/design-tokens.ts new file mode 100644 index 0000000..34d3b01 --- /dev/null +++ b/src/types/design-tokens.ts @@ -0,0 +1,90 @@ +import { RGBA, AGCGradient } from './xd-data'; + +// Design tokens in Tailwind-compatible format +export interface DesignTokens { + colors: ColorTokens; + typography: TypographyTokens; + spacing: SpacingTokens; + shadows: ShadowTokens; + borderRadius: BorderRadiusTokens; + fontSize: FontSizeTokens; + fontWeight: FontWeightTokens; + lineHeight: LineHeightTokens; + letterSpacing: LetterSpacingTokens; +} + +export interface ColorTokens { + [key: string]: string | ColorScale; +} + +export interface ColorScale { + 50?: string; + 100?: string; + 200?: string; + 300?: string; + 400?: string; + 500?: string; + 600?: string; + 700?: string; + 800?: string; + 900?: string; + 950?: string; +} + +export interface TypographyTokens { + [key: string]: { + fontFamily: string; + fontSize: string; + fontWeight: string | number; + lineHeight: string; + letterSpacing?: string; + textTransform?: string; + }; +} + +export interface SpacingTokens { + [key: string]: string; +} + +export interface ShadowTokens { + [key: string]: string; +} + +export interface BorderRadiusTokens { + [key: string]: string; +} + +export interface FontSizeTokens { + [key: string]: [string, { lineHeight: string; letterSpacing?: string }]; +} + +export interface FontWeightTokens { + [key: string]: string | number; +} + +export interface LineHeightTokens { + [key: string]: string; +} + +export interface LetterSpacingTokens { + [key: string]: string; +} + +// Tailwind config type +export interface TailwindConfig { + theme: { + extend: DesignTokens; + }; +} + +// Token extraction metadata +export interface TokenExtractionResult { + tokens: DesignTokens; + tailwindConfig: TailwindConfig; + stats: { + totalColors: number; + totalTypographyStyles: number; + totalSpacingValues: number; + totalShadows: number; + }; +} diff --git a/src/types/layout-system.ts b/src/types/layout-system.ts new file mode 100644 index 0000000..692e483 --- /dev/null +++ b/src/types/layout-system.ts @@ -0,0 +1,51 @@ +export interface LayoutInformation { + artboardId: string; + artboardName: string; + deviceType: "desktop" | "mobile" | "tablet"; + dimensions: { + width: number; + height: number; + }; + grid?: GridSystem; + constraints: ConstraintSystem; + responsive: ResponsiveBreakpoints; +} + +export interface GridSystem { + type: "columns" | "rows" | "both"; + columns?: { + count: number; + width: number; + gutter: number; + }; + rows?: { + count: number; + height: number; + gutter: number; + }; +} + +export interface ConstraintSystem { + elements: Array<{ + id: string; + name: string; + constraints: { + left?: ConstraintType; + right?: ConstraintType; + top?: ConstraintType; + bottom?: ConstraintType; + width?: "fixed" | "stretch"; + height?: "fixed" | "stretch"; + }; + }>; +} + +export type ConstraintType = "fixed" | "min" | "max" | "center" | "scale"; + +export interface ResponsiveBreakpoints { + breakpoints: Array<{ + name: string; + width: number; + artboardId?: string; + }>; +} diff --git a/src/types/xd-data.ts b/src/types/xd-data.ts new file mode 100644 index 0000000..779d893 --- /dev/null +++ b/src/types/xd-data.ts @@ -0,0 +1,245 @@ +// Complete type definitions based on discovered Adobe XD structure +export interface PrototypeData { + manifest: PrototypeManifest; + linkTemplate: LinkTemplate; + ownerId: string; + modifiedDate: number; + version: number; + appVersion: string; + manifestURL: string; +} + +export interface PrototypeManifest { + id: string; + name: string; + thumbnail: ResourceRef; + artboards: ArtboardManifest[]; + globalResources: ResourceRef; + interactions: ResourceRef; + resources: Record<string, ResourceRef>; + platform: string; + docId: string; + homeArtboardType: string; + disableNavigation: boolean; + includeSpecs: boolean; + includeAssets: boolean; +} + +export interface ArtboardManifest { + id: string; + name: string; + bounds: { + width: number; + height: number; + x: number; + y: number; + }; + viewport: { + height: number; + }; + components: ComponentRef[]; + resources: string[]; +} + +export interface ComponentRef { + id: string; + path: string; + version: string; + revision: string; + rel: "primary" | "thumbnail"; + type?: "agc"; +} + +export interface ResourceRef { + id: string; + path: string; + version: string; + revision: string; + component_id?: string; +} + +export interface LinkTemplate { + href: string; + data: { + api_key: string; + access_token: string; + }; +} + +// AGC Data Types +export interface AGCData { + artboard: AGCArtboard; + version?: string; +} + +export interface AGCArtboard { + meta: AGCMeta; + children: AGCElement[]; + style?: AGCStyle; +} + +export interface AGCMeta { + uxdesign: { + width: number; + height: number; + }; +} + +export interface AGCElement { + id: string; + name?: string; + type: "shape" | "text" | "group" | "artboard" | "component" | "symbolInstance"; + transform?: AGCTransform; + style?: AGCStyle; + shape?: AGCShape; + text?: AGCText; + children?: AGCElement[]; + artboard?: AGCArtboard; +} + +export interface AGCTransform { + tx: number; // translateX + ty: number; // translateY + a: number; // scaleX + d: number; // scaleY + b?: number; // skewY + c?: number; // skewX +} + +export interface AGCStyle { + fill?: AGCFill; + stroke?: AGCStroke; + opacity?: number; + blendMode?: string; + filters?: AGCFilter[]; + font?: AGCFont; +} + +export interface AGCFill { + type: "solid" | "gradient" | "pattern"; + color?: RGBA; + gradient?: AGCGradient; + pattern?: { + width: number; + height: number; + href: string; + }; +} + +export interface RGBA { + r: number; + g: number; + b: number; + a: number; +} + +export interface AGCGradient { + type: "linear" | "radial"; + stops: Array<{ + offset: number; + color: RGBA; + }>; + x1?: number; + y1?: number; + x2?: number; + y2?: number; +} + +export interface AGCStroke { + color: RGBA; + width: number; + position?: "center" | "inside" | "outside"; + cap?: "butt" | "round" | "square"; + join?: "miter" | "round" | "bevel"; + dasharray?: number[]; +} + +export interface AGCShape { + type: "rect" | "ellipse" | "path" | "line"; + width?: number; + height?: number; + path?: string; // SVG path data + r?: number | { topLeft?: number; topRight?: number; bottomLeft?: number; bottomRight?: number }; // border radius + cx?: number; // circle/ellipse center x + cy?: number; // circle/ellipse center y + rx?: number; // ellipse radius x + ry?: number; // ellipse radius y +} + +export interface AGCText { + rawText: string; + paragraphs: Array<{ + lines: Array<{ + y: number; + x: number; + from: number; + to: number; + }>; + }>; + style?: AGCTextStyle; +} + +export interface AGCTextStyle { + font?: AGCFont; + fill?: AGCFill; + textAlign?: "left" | "center" | "right" | "justify"; + lineHeight?: number; + letterSpacing?: number; + textTransform?: "none" | "uppercase" | "lowercase" | "capitalize"; +} + +export interface AGCFont { + family: string; + postscriptName: string; + size: number; + style: "normal" | "italic" | "oblique"; + weight: number; +} + +export interface AGCFilter { + type: "dropShadow" | "blur" | "backgroundBlur" | "innerShadow"; + visible?: boolean; + params: { + dx?: number; + dy?: number; + r?: number; + color?: RGBA; + blurAmount?: number; + }; +} + +// Global Resources Types +export interface GlobalResources { + meta?: { + version: string; + }; + colors?: ColorToken[]; + characterStyles?: CharacterStyle[]; + gradients?: GradientToken[]; +} + +export interface ColorToken { + id: string; + name: string; + value: RGBA; +} + +export interface CharacterStyle { + id: string; + name: string; + fontFamily: string; + fontPostscriptName?: string; + fontSize: number; + fontWeight: number; + fontStyle?: string; + lineHeight?: number; + letterSpacing?: number; + textTransform?: string; + color?: RGBA; +} + +export interface GradientToken { + id: string; + name: string; + gradient: AGCGradient; +} From c15b0907cc1b9b25479f3c0b74ed557edf4a156a Mon Sep 17 00:00:00 2001 From: Abdur Rahman <mmarahman4847@gmail.com> Date: Fri, 13 Feb 2026 20:18:09 +0530 Subject: [PATCH 3/6] feat: add URL validator for Adobe XD specs URLs --- src/parsers/url-validator.ts | 75 ++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/parsers/url-validator.ts diff --git a/src/parsers/url-validator.ts b/src/parsers/url-validator.ts new file mode 100644 index 0000000..b52667b --- /dev/null +++ b/src/parsers/url-validator.ts @@ -0,0 +1,75 @@ +export interface ParsedXDUrl { + type: "project" | "screen"; + projectId: string; + screenId?: string; + isValid: boolean; + normalizedUrl: string; +} + +export class XDUrlValidator { + private static readonly XD_URL_PATTERN = /^https?:\/\/xd\.adobe\.com\/view\/([a-f0-9-]+)(?:\/screen\/([a-f0-9-]+))?/i; + + /** + * Parse and validate an Adobe XD specs URL + * + * Supported formats: + * - Project overview: https://xd.adobe.com/view/PROJECT_ID/specs/ + * - Specific screen: https://xd.adobe.com/view/PROJECT_ID/screen/SCREEN_ID/specs/ + */ + static parseUrl(url: string): ParsedXDUrl { + const trimmedUrl = url.trim(); + const match = trimmedUrl.match(this.XD_URL_PATTERN); + + if (!match) { + return { + type: "project", + projectId: "", + isValid: false, + normalizedUrl: trimmedUrl + }; + } + + const projectId = match[1]; + const screenId = match[2]; + + // Normalize URL to ensure it ends with /specs/ + let normalizedUrl = `https://xd.adobe.com/view/${projectId}`; + if (screenId) { + normalizedUrl += `/screen/${screenId}`; + } + if (!normalizedUrl.endsWith('/specs/')) { + normalizedUrl += '/specs/'; + } + + return { + type: screenId ? "screen" : "project", + projectId, + screenId, + isValid: true, + normalizedUrl + }; + } + + /** + * Validate if a URL is a valid Adobe XD specs URL + */ + static isValidUrl(url: string): boolean { + return this.parseUrl(url).isValid; + } + + /** + * Extract project ID from URL + */ + static getProjectId(url: string): string | null { + const parsed = this.parseUrl(url); + return parsed.isValid ? parsed.projectId : null; + } + + /** + * Extract screen ID from URL (if present) + */ + static getScreenId(url: string): string | null { + const parsed = this.parseUrl(url); + return parsed.isValid ? parsed.screenId || null : null; + } +} From f55d79f458325d667d029d8aca661a6dfc662d4e Mon Sep 17 00:00:00 2001 From: Abdur Rahman <mmarahman4847@gmail.com> Date: Fri, 13 Feb 2026 20:19:03 +0530 Subject: [PATCH 4/6] feat: add window.prototypeData extraction from HTML --- src/parsers/xd-web-parser.ts | 40 +++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/parsers/xd-web-parser.ts b/src/parsers/xd-web-parser.ts index e4d3ce6..2d787a5 100644 --- a/src/parsers/xd-web-parser.ts +++ b/src/parsers/xd-web-parser.ts @@ -1,4 +1,5 @@ import { XDDocument, XDArtboard, XDElement, XDColor, XDComponent } from './xd-parser'; +import { PrototypeData } from '../types/xd-data'; interface XDWebSpec { artboards?: Array<{ @@ -20,10 +21,33 @@ interface XDWebSpec { } export class XDWebParser { - async parseWebSpec(url: string): Promise<XDDocument> { - const normalizedUrl = url.endsWith('/') ? url : `${url}/`; + /** + * Extract window.prototypeData from HTML + */ + extractPrototypeData(html: string): PrototypeData | null { + // Pattern to match: window.prototypeData = {...}; + const pattern = /window\.prototypeData\s*=\s*({.+?});(?:\s*if\s*\(|$)/s; + const match = html.match(pattern); + + if (!match) { + return null; + } - const response = await fetch(normalizedUrl, { + try { + const jsonStr = match[1]; + const data = JSON.parse(jsonStr) as PrototypeData; + return data; + } catch (error) { + console.error('Failed to parse prototypeData:', error); + return null; + } + } + + /** + * Fetch HTML from Adobe XD specs URL + */ + async fetchSpecsPage(url: string): Promise<string> { + const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', @@ -31,12 +55,18 @@ export class XDWebParser { 'Referer': 'https://xd.adobe.com/' } }); - + if (!response.ok) { throw new Error(`Failed to fetch XD spec: ${response.status} ${response.statusText}`); } + + return await response.text(); + } - const html = await response.text(); + async parseWebSpec(url: string): Promise<XDDocument> { + const normalizedUrl = url.endsWith('/') ? url : `${url}/`; + + const html = await this.fetchSpecsPage(normalizedUrl); const specData = this.extractSpecData(html, normalizedUrl); let artboards = this.parseArtboards(specData); From c8c1277086b1f0e2c3af0ed9113e27b3ce3c605e Mon Sep 17 00:00:00 2001 From: Abdur Rahman <mmarahman4847@gmail.com> Date: Fri, 13 Feb 2026 20:19:22 +0530 Subject: [PATCH 5/6] first commit --- .../2026-02-13-design-understanding-first.md | 2323 +++++++++++++++++ 1 file changed, 2323 insertions(+) create mode 100644 docs/plans/2026-02-13-design-understanding-first.md diff --git a/docs/plans/2026-02-13-design-understanding-first.md b/docs/plans/2026-02-13-design-understanding-first.md new file mode 100644 index 0000000..5efdab0 --- /dev/null +++ b/docs/plans/2026-02-13-design-understanding-first.md @@ -0,0 +1,2323 @@ +# Adobe XD MCP Server: Design Understanding First + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Build a production-ready MCP server that deeply understands Adobe XD design structures, extracts comprehensive design data, and generates Tailwind-styled React components. + +**Architecture:** Extract data from Adobe XD specs URLs by parsing embedded `window.prototypeData`, fetching AGC files and global resources from Adobe CDN, then provide rich MCP tools for design analysis (80% effort) and React generation (20% effort). + +**Tech Stack:** TypeScript, MCP SDK, Zod, Node.js fetch API, Tailwind CSS code generation + +**Priority:** Design understanding and data extraction FIRST, then enhanced React component generation informed by that understanding. + +--- + +## Implementation Structure + +### Phase 1: Foundation - Data Extraction (Tasks 1-6) +**Goal:** Extract ALL design data from Adobe XD specs URLs +**Focus:** URL validation, prototype data parsing, AGC file fetching, global resources + +### Phase 2: Design Analysis (Tasks 7-12) +**Goal:** Analyze and expose design structures through MCP tools +**Focus:** Design tokens, hierarchy analysis, layout systems, element relationships + +### Phase 3: React Generation (Tasks 13-15) +**Goal:** Generate Tailwind-styled React components using design insights +**Focus:** Component structure, Tailwind class generation, configurable props + +### Phase 4: Testing & Documentation (Tasks 16-17) +**Goal:** Comprehensive testing and documentation +**Focus:** Integration tests, MCP Inspector validation, README updates + +--- + +## Data Structures Discovered + +### window.prototypeData Structure +```typescript +interface PrototypeData { + manifest: { + id: string; // e.g., "urn:aaid:sc:US:bbf16730-..." + name: string; // Project name + artboards: ArtboardManifest[]; // 20 artboards with metadata + globalResources: ResourceRef; // Design tokens reference + interactions: ResourceRef; // Interaction data reference + resources: Record<string, ResourceRef>; // Asset mapping + }; + linkTemplate: { + href: string; // URL pattern for Adobe CDN + data: { + api_key: string; + access_token: string; + }; + }; +} + +interface ArtboardManifest { + id: string; + name: string; + bounds: { width: number; height: number; x: number; y: number }; + viewport: { height: number }; + components: ComponentRef[]; // AGC file reference + thumbnail + resources: string[]; // Resource IDs used in this artboard +} + +interface ComponentRef { + id: string; + path: string; // e.g., "artwork/artboard-{id}/graphics/graphicContent.agc" + version: string; + revision: string; + rel: "primary" | "thumbnail"; + type?: "agc"; +} + +interface ResourceRef { + id: string; + path: string; + version: string; + revision: string; +} +``` + +### AGC File Structure (Assumption - to be validated during implementation) +```typescript +interface AGCData { + artboard: { + children: AGCElement[]; + style: AGCStyle; + meta: AGCMeta; + }; +} + +interface AGCElement { + id: string; + name: string; + type: "shape" | "text" | "group" | "artboard" | "component"; + transform: { + tx: number; // translateX + ty: number; // translateY + a: number; // scaleX + d: number; // scaleY + b?: number; // skewY + c?: number; // skewX + }; + style?: AGCStyle; + shape?: AGCShape; + text?: AGCText; + children?: AGCElement[]; +} + +interface AGCStyle { + fill?: AGCFill; + stroke?: AGCStroke; + opacity?: number; + blendMode?: string; + filters?: AGCFilter[]; +} + +interface AGCFill { + type: "solid" | "gradient" | "pattern"; + color?: { r: number; g: number; b: number; a: number }; + gradient?: AGCGradient; +} + +interface AGCGradient { + type: "linear" | "radial"; + stops: Array<{ offset: number; color: { r: number; g: number; b: number; a: number } }>; +} + +interface AGCStroke { + color: { r: number; g: number; b: number; a: number }; + width: number; + position: "center" | "inside" | "outside"; +} + +interface AGCShape { + type: "rect" | "ellipse" | "path"; + width?: number; + height?: number; + path?: string; // SVG path data + r?: number; // border radius for rects +} + +interface AGCText { + rawText: string; + paragraphs: Array<{ + lines: Array<{ + y: number; + x: number; + }>; + }>; + style: { + font: { + family: string; + postscriptName: string; + size: number; + style: string; + }; + fill: AGCFill; + }; +} + +interface AGCFilter { + type: "dropShadow" | "blur" | "backgroundBlur"; + params: Record<string, any>; +} +``` + +### Global Resources Structure (Design Tokens) +```typescript +interface GlobalResources { + colors: Array<{ + id: string; + name: string; + value: { r: number; g: number; b: number; a: number }; + }>; + characterStyles: Array<{ + id: string; + name: string; + fontFamily: string; + fontSize: number; + fontWeight: number; + lineHeight: number; + letterSpacing?: number; + textTransform?: string; + }>; + // Additional token types may exist +} +``` + +--- + +## Task Breakdown + +### Task 1: Create Type Definitions + +**Files:** +- Create: `src/types/xd-data.ts` +- Create: `src/types/design-tokens.ts` +- Create: `src/types/layout-system.ts` +- Create: `src/types/design-hierarchy.ts` + +**Step 1: Write comprehensive TypeScript types for XD data** + +Create `src/types/xd-data.ts`: + +```typescript +// Complete type definitions based on discovered structure above +export interface PrototypeData { + manifest: PrototypeManifest; + linkTemplate: LinkTemplate; + ownerId: string; + modifiedDate: number; + version: number; + appVersion: string; + manifestURL: string; +} + +export interface PrototypeManifest { + id: string; + name: string; + thumbnail: ResourceRef; + artboards: ArtboardManifest[]; + globalResources: ResourceRef; + interactions: ResourceRef; + resources: Record<string, ResourceRef>; + platform: string; + docId: string; + homeArtboardType: string; + disableNavigation: boolean; + includeSpecs: boolean; + includeAssets: boolean; +} + +export interface ArtboardManifest { + id: string; + name: string; + bounds: { + width: number; + height: number; + x: number; + y: number; + }; + viewport: { + height: number; + }; + components: ComponentRef[]; + resources: string[]; +} + +export interface ComponentRef { + id: string; + path: string; + version: string; + revision: string; + rel: "primary" | "thumbnail"; + type?: "agc"; +} + +export interface ResourceRef { + id: string; + path: string; + version: string; + revision: string; + component_id?: string; +} + +export interface LinkTemplate { + href: string; + data: { + api_key: string; + access_token: string; + }; +} + +// AGC Data Types +export interface AGCData { + artboard: AGCArtboard; + version?: string; +} + +export interface AGCArtboard { + meta: AGCMeta; + children: AGCElement[]; + style?: AGCStyle; +} + +export interface AGCMeta { + uxdesign: { + width: number; + height: number; + }; +} + +export interface AGCElement { + id: string; + name?: string; + type: "shape" | "text" | "group" | "artboard" | "component" | "symbolInstance"; + transform?: AGCTransform; + style?: AGCStyle; + shape?: AGCShape; + text?: AGCText; + children?: AGCElement[]; + artboard?: AGCArtboard; +} + +export interface AGCTransform { + tx: number; // translateX + ty: number; // translateY + a: number; // scaleX + d: number; // scaleY + b?: number; // skewY + c?: number; // skewX +} + +export interface AGCStyle { + fill?: AGCFill; + stroke?: AGCStroke; + opacity?: number; + blendMode?: string; + filters?: AGCFilter[]; + font?: AGCFont; +} + +export interface AGCFill { + type: "solid" | "gradient" | "pattern"; + color?: RGBA; + gradient?: AGCGradient; + pattern?: { + width: number; + height: number; + href: string; + }; +} + +export interface RGBA { + r: number; + g: number; + b: number; + a: number; +} + +export interface AGCGradient { + type: "linear" | "radial"; + stops: Array<{ + offset: number; + color: RGBA; + }>; + x1?: number; + y1?: number; + x2?: number; + y2?: number; +} + +export interface AGCStroke { + color: RGBA; + width: number; + position?: "center" | "inside" | "outside"; + cap?: "butt" | "round" | "square"; + join?: "miter" | "round" | "bevel"; + dasharray?: number[]; +} + +export interface AGCShape { + type: "rect" | "ellipse" | "path" | "line"; + width?: number; + height?: number; + path?: string; // SVG path data + r?: number | { topLeft?: number; topRight?: number; bottomLeft?: number; bottomRight?: number }; // border radius + cx?: number; // circle/ellipse center x + cy?: number; // circle/ellipse center y + rx?: number; // ellipse radius x + ry?: number; // ellipse radius y +} + +export interface AGCText { + rawText: string; + paragraphs: Array<{ + lines: Array<{ + y: number; + x: number; + from: number; + to: number; + }>; + }>; + style?: AGCTextStyle; +} + +export interface AGCTextStyle { + font?: AGCFont; + fill?: AGCFill; + textAlign?: "left" | "center" | "right" | "justify"; + lineHeight?: number; + letterSpacing?: number; + textTransform?: "none" | "uppercase" | "lowercase" | "capitalize"; +} + +export interface AGCFont { + family: string; + postscriptName: string; + size: number; + style: "normal" | "italic" | "oblique"; + weight: number; +} + +export interface AGCFilter { + type: "dropShadow" | "blur" | "backgroundBlur" | "innerShadow"; + visible?: boolean; + params: { + dx?: number; + dy?: number; + r?: number; + color?: RGBA; + blurAmount?: number; + }; +} + +// Global Resources Types +export interface GlobalResources { + meta?: { + version: string; + }; + colors?: ColorToken[]; + characterStyles?: CharacterStyle[]; + gradients?: GradientToken[]; +} + +export interface ColorToken { + id: string; + name: string; + value: RGBA; +} + +export interface CharacterStyle { + id: string; + name: string; + fontFamily: string; + fontPostscriptName?: string; + fontSize: number; + fontWeight: number; + fontStyle?: string; + lineHeight?: number; + letterSpacing?: number; + textTransform?: string; + color?: RGBA; +} + +export interface GradientToken { + id: string; + name: string; + gradient: AGCGradient; +} +``` + +**Step 2: Create design token types** + +Create `src/types/design-tokens.ts`: + +```typescript +import { RGBA, AGCGradient } from './xd-data'; + +// Design tokens in Tailwind-compatible format +export interface DesignTokens { + colors: ColorTokens; + typography: TypographyTokens; + spacing: SpacingTokens; + shadows: ShadowTokens; + borderRadius: BorderRadiusTokens; + fontSize: FontSizeTokens; + fontWeight: FontWeightTokens; + lineHeight: LineHeightTokens; + letterSpacing: LetterSpacingTokens; +} + +export interface ColorTokens { + [key: string]: string | ColorScale; +} + +export interface ColorScale { + 50?: string; + 100?: string; + 200?: string; + 300?: string; + 400?: string; + 500?: string; + 600?: string; + 700?: string; + 800?: string; + 900?: string; + 950?: string; +} + +export interface TypographyTokens { + [key: string]: { + fontFamily: string; + fontSize: string; + fontWeight: string | number; + lineHeight: string; + letterSpacing?: string; + textTransform?: string; + }; +} + +export interface SpacingTokens { + [key: string]: string; +} + +export interface ShadowTokens { + [key: string]: string; +} + +export interface BorderRadiusTokens { + [key: string]: string; +} + +export interface FontSizeTokens { + [key: string]: [string, { lineHeight: string; letterSpacing?: string }]; +} + +export interface FontWeightTokens { + [key: string]: string | number; +} + +export interface LineHeightTokens { + [key: string]: string; +} + +export interface LetterSpacingTokens { + [key: string]: string; +} + +// Tailwind config type +export interface TailwindConfig { + theme: { + extend: DesignTokens; + }; +} + +// Token extraction metadata +export interface TokenExtractionResult { + tokens: DesignTokens; + tailwindConfig: TailwindConfig; + stats: { + totalColors: number; + totalTypographyStyles: number; + totalSpacingValues: number; + totalShadows: number; + }; +} +``` + +**Step 3: Create layout system types** + +Create `src/types/layout-system.ts`: + +```typescript +export interface LayoutInformation { + artboardId: string; + artboardName: string; + deviceType: "desktop" | "mobile" | "tablet"; + dimensions: { + width: number; + height: number; + }; + grid?: GridSystem; + constraints: ConstraintSystem; + responsive: ResponsiveBreakpoints; +} + +export interface GridSystem { + type: "columns" | "rows" | "both"; + columns?: { + count: number; + width: number; + gutter: number; + }; + rows?: { + count: number; + height: number; + gutter: number; + }; +} + +export interface ConstraintSystem { + elements: Array<{ + id: string; + name: string; + constraints: { + left?: ConstraintType; + right?: ConstraintType; + top?: ConstraintType; + bottom?: ConstraintType; + width?: "fixed" | "stretch"; + height?: "fixed" | "stretch"; + }; + }>; +} + +export type ConstraintType = "fixed" | "min" | "max" | "center" | "scale"; + +export interface ResponsiveBreakpoints { + breakpoints: Array<{ + name: string; + width: number; + artboardId?: string; + }>; +} +``` + +**Step 4: Create design hierarchy types** + +Create `src/types/design-hierarchy.ts`: + +```typescript +import { AGCElement, AGCStyle } from './xd-data'; + +export interface DesignHierarchy { + artboardId: string; + artboardName: string; + root: HierarchyNode; + flattenedElements: FlattenedElement[]; + stats: HierarchyStats; +} + +export interface HierarchyNode { + id: string; + name: string; + type: string; + depth: number; + path: string; // e.g., "root/header/logo" + element: AGCElement; + children: HierarchyNode[]; + boundingBox: BoundingBox; + computedStyle: ComputedStyle; +} + +export interface FlattenedElement { + id: string; + name: string; + type: string; + depth: number; + path: string; + parentId?: string; + childIds: string[]; + boundingBox: BoundingBox; + styles: AGCStyle; +} + +export interface BoundingBox { + x: number; + y: number; + width: number; + height: number; + absoluteX: number; // Computed from transforms + absoluteY: number; +} + +export interface ComputedStyle { + position: { + x: number; + y: number; + absoluteX: number; + absoluteY: number; + }; + dimensions: { + width: number; + height: number; + }; + colors: { + fill?: string; // Hex color + stroke?: string; + }; + typography?: { + fontFamily: string; + fontSize: number; + fontWeight: number; + lineHeight?: number; + letterSpacing?: number; + }; + effects: { + shadows: string[]; // CSS shadow strings + blur?: number; + opacity?: number; + }; +} + +export interface HierarchyStats { + totalElements: number; + maxDepth: number; + elementsByType: Record<string, number>; + totalGroups: number; + totalComponents: number; +} +``` + +**Step 5: No test needed (types only)** + +Skip - type definitions don't need tests yet. + +**Step 6: Commit** + +```bash +git add src/types/ +git commit -m "feat: add comprehensive type definitions for XD data structures" +``` + +--- + +### Task 2: URL Validator + +**Files:** +- Create: `src/parsers/url-validator.ts` +- Test: Manual validation with example URLs + +**Step 1: Write URL validator** + +Create `src/parsers/url-validator.ts`: + +```typescript +export interface ParsedXDUrl { + type: "project" | "screen"; + projectId: string; + screenId?: string; + isValid: boolean; + normalizedUrl: string; +} + +export class XDUrlValidator { + private static readonly XD_URL_PATTERN = /^https?:\/\/xd\.adobe\.com\/view\/([a-f0-9-]+)(?:\/screen\/([a-f0-9-]+))?/i; + + /** + * Parse and validate an Adobe XD specs URL + * + * Supported formats: + * - Project overview: https://xd.adobe.com/view/PROJECT_ID/specs/ + * - Specific screen: https://xd.adobe.com/view/PROJECT_ID/screen/SCREEN_ID/specs/ + */ + static parseUrl(url: string): ParsedXDUrl { + const trimmedUrl = url.trim(); + const match = trimmedUrl.match(this.XD_URL_PATTERN); + + if (!match) { + return { + type: "project", + projectId: "", + isValid: false, + normalizedUrl: trimmedUrl + }; + } + + const projectId = match[1]; + const screenId = match[2]; + + // Normalize URL to ensure it ends with /specs/ + let normalizedUrl = `https://xd.adobe.com/view/${projectId}`; + if (screenId) { + normalizedUrl += `/screen/${screenId}`; + } + if (!normalizedUrl.endsWith('/specs/')) { + normalizedUrl += '/specs/'; + } + + return { + type: screenId ? "screen" : "project", + projectId, + screenId, + isValid: true, + normalizedUrl + }; + } + + /** + * Validate if a URL is a valid Adobe XD specs URL + */ + static isValidUrl(url: string): boolean { + return this.parseUrl(url).isValid; + } + + /** + * Extract project ID from URL + */ + static getProjectId(url: string): string | null { + const parsed = this.parseUrl(url); + return parsed.isValid ? parsed.projectId : null; + } + + /** + * Extract screen ID from URL (if present) + */ + static getScreenId(url: string): string | null { + const parsed = this.parseUrl(url); + return parsed.isValid ? parsed.screenId || null : null; + } +} +``` + +**Step 2: Test validator with example URLs** + +Run: `node -e "const { XDUrlValidator } = require('./dist/parsers/url-validator'); console.log(XDUrlValidator.parseUrl('https://xd.adobe.com/view/bbf16730-a767-4220-8569-80c09395e5b9-eea2/specs/'))"` + +Expected: Output shows `isValid: true`, `type: "project"`, `projectId: "bbf16730-a767-4220-8569-80c09395e5b9-eea2"` + +**Step 3: Commit** + +```bash +git add src/parsers/url-validator.ts +git commit -m "feat: add URL validator for Adobe XD specs URLs" +``` + +--- + +### Task 3: Enhanced XD Web Parser - Extract window.prototypeData + +**Files:** +- Modify: `src/parsers/xd-web-parser.ts` + +**Step 1: Add method to extract window.prototypeData from HTML** + +Add to `src/parsers/xd-web-parser.ts`: + +```typescript +import { PrototypeData } from '../types/xd-data'; + +export class XDWebParser { + // ... existing code ... + + /** + * Extract window.prototypeData from HTML + */ + extractPrototypeData(html: string): PrototypeData | null { + // Pattern to match: window.prototypeData = {...}; + const pattern = /window\.prototypeData\s*=\s*({.+?});(?:\s*if\s*\(|$)/s; + const match = html.match(pattern); + + if (!match) { + return null; + } + + try { + const jsonStr = match[1]; + const data = JSON.parse(jsonStr) as PrototypeData; + return data; + } catch (error) { + console.error('Failed to parse prototypeData:', error); + return null; + } + } + + /** + * Fetch HTML from Adobe XD specs URL + */ + async fetchSpecsPage(url: string): Promise<string> { + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + 'Referer': 'https://xd.adobe.com/' + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch XD spec: ${response.status} ${response.statusText}`); + } + + return await response.text(); + } +} +``` + +**Step 2: Test extraction** + +Run: Test with example URL in MCP Inspector after building + +Expected: Successfully extracts `window.prototypeData` with manifest, linkTemplate, etc. + +**Step 3: Commit** + +```bash +git add src/parsers/xd-web-parser.ts +git commit -m "feat: add window.prototypeData extraction from HTML" +``` + +--- + +### Task 4: AGC File Parser + +**Files:** +- Create: `src/parsers/agc-parser.ts` + +**Step 1: Create AGC parser** + +Create `src/parsers/agc-parser.ts`: + +```typescript +import { PrototypeData, AGCData, ComponentRef } from '../types/xd-data'; + +export class AGCParser { + /** + * Build AGC file URL from prototype data and component reference + */ + static buildAGCUrl(prototypeData: PrototypeData, componentRef: ComponentRef): string { + const { linkTemplate } = prototypeData; + const { href, data } = linkTemplate; + + // Replace template variables in href + let url = href + .replace('{;revision}', `;revision=${componentRef.revision}`) + .replace('{?component_id,component_path}', ''); + + // Add query parameters + const params = new URLSearchParams({ + component_id: componentRef.id, + component_path: componentRef.path, + api_key: data.api_key, + access_token: data.access_token + }); + + return `${url}?${params.toString()}`; + } + + /** + * Fetch and parse AGC file + */ + async fetchAGC(url: string): Promise<AGCData | null> { + try { + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json,*/*', + 'Referer': 'https://xd.adobe.com/' + } + }); + + if (!response.ok) { + console.error(`Failed to fetch AGC: ${response.status} ${response.statusText}`); + return null; + } + + const data = await response.json() as AGCData; + return data; + } catch (error) { + console.error('Error fetching AGC file:', error); + return null; + } + } + + /** + * Get primary AGC component for an artboard + */ + static getPrimaryAGCComponent(artboard: any): ComponentRef | null { + const primaryComponent = artboard.components?.find( + (c: ComponentRef) => c.rel === 'primary' && c.type === 'agc' + ); + return primaryComponent || null; + } +} +``` + +**Step 2: Test AGC fetching** + +Run: Test in MCP Inspector with example project + +Expected: Successfully fetches AGC JSON data + +**Step 3: Commit** + +```bash +git add src/parsers/agc-parser.ts +git commit -m "feat: add AGC file parser and fetcher" +``` + +--- + +### Task 5: Global Resources Parser + +**Files:** +- Create: `src/parsers/global-resources-parser.ts` + +**Step 1: Create global resources parser** + +Create `src/parsers/global-resources-parser.ts`: + +```typescript +import { PrototypeData, GlobalResources, ResourceRef } from '../types/xd-data'; + +export class GlobalResourcesParser { + /** + * Build global resources URL from prototype data + */ + static buildGlobalResourcesUrl(prototypeData: PrototypeData): string { + const { linkTemplate, manifest } = prototypeData; + const { globalResources } = manifest; + const { href, data } = linkTemplate; + + // Replace template variables + let url = href + .replace('{;revision}', `;revision=${globalResources.revision}`) + .replace('{?component_id,component_path}', ''); + + // Add query parameters + const params = new URLSearchParams({ + component_id: globalResources.id, + component_path: globalResources.path, + api_key: data.api_key, + access_token: data.access_token + }); + + return `${url}?${params.toString()}`; + } + + /** + * Fetch and parse global resources + */ + async fetchGlobalResources(url: string): Promise<GlobalResources | null> { + try { + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json,*/*', + 'Referer': 'https://xd.adobe.com/' + } + }); + + if (!response.ok) { + console.error(`Failed to fetch global resources: ${response.status} ${response.statusText}`); + return null; + } + + const data = await response.json() as GlobalResources; + return data; + } catch (error) { + console.error('Error fetching global resources:', error); + return null; + } + } +} +``` + +**Step 2: Test global resources fetching** + +Run: Test in MCP Inspector + +Expected: Successfully fetches design tokens JSON + +**Step 3: Commit** + +```bash +git add src/parsers/global-resources-parser.ts +git commit -m "feat: add global resources parser for design tokens" +``` + +--- + +### Task 6: Hierarchy Analyzer + +**Files:** +- Create: `src/analyzers/hierarchy-analyzer.ts` + +**Step 1: Create hierarchy analyzer** + +Create `src/analyzers/hierarchy-analyzer.ts`: + +```typescript +import { AGCData, AGCElement, AGCTransform } from '../types/xd-data'; +import { DesignHierarchy, HierarchyNode, FlattenedElement, BoundingBox, ComputedStyle, HierarchyStats } from '../types/design-hierarchy'; + +export class HierarchyAnalyzer { + /** + * Build design hierarchy from AGC data + */ + static buildHierarchy(artboardId: string, artboardName: string, agcData: AGCData): DesignHierarchy { + const root: HierarchyNode = { + id: 'root', + name: artboardName, + type: 'artboard', + depth: 0, + path: 'root', + element: agcData.artboard as any, + children: [], + boundingBox: { + x: 0, + y: 0, + width: agcData.artboard.meta.uxdesign.width, + height: agcData.artboard.meta.uxdesign.height, + absoluteX: 0, + absoluteY: 0 + }, + computedStyle: { + position: { x: 0, y: 0, absoluteX: 0, absoluteY: 0 }, + dimensions: { + width: agcData.artboard.meta.uxdesign.width, + height: agcData.artboard.meta.uxdesign.height + }, + colors: {}, + effects: { shadows: [] } + } + }; + + // Build tree recursively + if (agcData.artboard.children) { + root.children = agcData.artboard.children.map((child, index) => + this.buildNode(child, 1, `root/${index}`, { tx: 0, ty: 0, a: 1, d: 1 }) + ); + } + + // Flatten tree + const flattenedElements: FlattenedElement[] = []; + this.flattenTree(root, flattenedElements); + + // Calculate stats + const stats = this.calculateStats(flattenedElements); + + return { + artboardId, + artboardName, + root, + flattenedElements, + stats + }; + } + + private static buildNode( + element: AGCElement, + depth: number, + path: string, + parentTransform: AGCTransform + ): HierarchyNode { + // Compute absolute transform + const transform = element.transform || { tx: 0, ty: 0, a: 1, d: 1 }; + const absoluteX = parentTransform.tx + transform.tx; + const absoluteY = parentTransform.ty + transform.ty; + + // Compute bounding box + const boundingBox = this.computeBoundingBox(element, transform, parentTransform); + + // Compute styles + const computedStyle = this.computeStyle(element, boundingBox); + + // Build children + const children = element.children?.map((child, index) => + this.buildNode(child, depth + 1, `${path}/${child.name || index}`, { + tx: absoluteX, + ty: absoluteY, + a: transform.a, + d: transform.d + }) + ) || []; + + return { + id: element.id, + name: element.name || `${element.type}-${element.id.substring(0, 8)}`, + type: element.type, + depth, + path, + element, + children, + boundingBox, + computedStyle + }; + } + + private static computeBoundingBox( + element: AGCElement, + transform: AGCTransform, + parentTransform: AGCTransform + ): BoundingBox { + const x = transform.tx; + const y = transform.ty; + const width = element.shape?.width || 0; + const height = element.shape?.height || 0; + const absoluteX = parentTransform.tx + x; + const absoluteY = parentTransform.ty + y; + + return { x, y, width, height, absoluteX, absoluteY }; + } + + private static computeStyle(element: AGCElement, boundingBox: BoundingBox): ComputedStyle { + const style: ComputedStyle = { + position: { + x: boundingBox.x, + y: boundingBox.y, + absoluteX: boundingBox.absoluteX, + absoluteY: boundingBox.absoluteY + }, + dimensions: { + width: boundingBox.width, + height: boundingBox.height + }, + colors: {}, + effects: { + shadows: [] + } + }; + + // Extract colors + if (element.style?.fill?.color) { + const c = element.style.fill.color; + style.colors.fill = this.rgbaToHex(c); + } + + if (element.style?.stroke?.color) { + const c = element.style.stroke.color; + style.colors.stroke = this.rgbaToHex(c); + } + + // Extract typography + if (element.text?.style?.font) { + const font = element.text.style.font; + style.typography = { + fontFamily: font.family, + fontSize: font.size, + fontWeight: font.weight, + lineHeight: element.text.style.font.size * 1.5 // Default line height + }; + } + + // Extract effects + if (element.style?.filters) { + element.style.filters.forEach(filter => { + if (filter.type === 'dropShadow' && filter.params.color) { + const c = filter.params.color; + const colorStr = this.rgbaToRgbaString(c); + const shadow = `${filter.params.dx || 0}px ${filter.params.dy || 0}px ${filter.params.r || 0}px ${colorStr}`; + style.effects.shadows.push(shadow); + } + }); + } + + if (element.style?.opacity !== undefined) { + style.effects.opacity = element.style.opacity; + } + + return style; + } + + private static rgbaToHex(rgba: { r: number; g: number; b: number; a: number }): string { + const r = Math.round(rgba.r * 255).toString(16).padStart(2, '0'); + const g = Math.round(rgba.g * 255).toString(16).padStart(2, '0'); + const b = Math.round(rgba.b * 255).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; + } + + private static rgbaToRgbaString(rgba: { r: number; g: number; b: number; a: number }): string { + const r = Math.round(rgba.r * 255); + const g = Math.round(rgba.g * 255); + const b = Math.round(rgba.b * 255); + return `rgba(${r}, ${g}, ${b}, ${rgba.a})`; + } + + private static flattenTree(node: HierarchyNode, result: FlattenedElement[]): void { + const flattened: FlattenedElement = { + id: node.id, + name: node.name, + type: node.type, + depth: node.depth, + path: node.path, + childIds: node.children.map(c => c.id), + boundingBox: node.boundingBox, + styles: node.element.style || {} + }; + + result.push(flattened); + + node.children.forEach(child => this.flattenTree(child, result)); + } + + private static calculateStats(elements: FlattenedElement[]): HierarchyStats { + const stats: HierarchyStats = { + totalElements: elements.length, + maxDepth: 0, + elementsByType: {}, + totalGroups: 0, + totalComponents: 0 + }; + + elements.forEach(el => { + stats.maxDepth = Math.max(stats.maxDepth, el.depth); + stats.elementsByType[el.type] = (stats.elementsByType[el.type] || 0) + 1; + if (el.type === 'group') stats.totalGroups++; + if (el.type === 'component') stats.totalComponents++; + }); + + return stats; + } +} +``` + +**Step 2: Test hierarchy building** + +Run: Test in MCP Inspector + +Expected: Successfully builds hierarchy tree from AGC data + +**Step 3: Commit** + +```bash +git add src/analyzers/hierarchy-analyzer.ts +git commit -m "feat: add hierarchy analyzer for design tree structures" +``` + +--- + +### Task 7: Token Extractor + +**Files:** +- Create: `src/generators/token-extractor.ts` + +**Step 1: Create token extractor** + +Create `src/generators/token-extractor.ts`: + +```typescript +import { GlobalResources, RGBA, CharacterStyle } from '../types/xd-data'; +import { DesignTokens, ColorTokens, TypographyTokens } from '../types/design-tokens'; + +export class TokenExtractor { + /** + * Extract design tokens from global resources + */ + static extractTokens(globalResources: GlobalResources): DesignTokens { + const tokens: DesignTokens = { + colors: this.extractColors(globalResources), + typography: this.extractTypography(globalResources), + spacing: {}, + shadows: {}, + borderRadius: {}, + fontSize: {}, + fontWeight: {}, + lineHeight: {}, + letterSpacing: {} + }; + + return tokens; + } + + private static extractColors(globalResources: GlobalResources): ColorTokens { + const colors: ColorTokens = {}; + + if (!globalResources.colors) return colors; + + globalResources.colors.forEach(colorToken => { + const name = this.sanitizeTokenName(colorToken.name); + const hex = this.rgbaToHex(colorToken.value); + colors[name] = hex; + }); + + return colors; + } + + private static extractTypography(globalResources: GlobalResources): TypographyTokens { + const typography: TypographyTokens = {}; + + if (!globalResources.characterStyles) return typography; + + globalResources.characterStyles.forEach(style => { + const name = this.sanitizeTokenName(style.name); + typography[name] = { + fontFamily: style.fontFamily, + fontSize: `${style.fontSize}px`, + fontWeight: style.fontWeight, + lineHeight: style.lineHeight ? `${style.lineHeight}px` : 'normal', + letterSpacing: style.letterSpacing ? `${style.letterSpacing}px` : undefined, + textTransform: style.textTransform + }; + }); + + return typography; + } + + private static rgbaToHex(rgba: RGBA): string { + const r = Math.round(rgba.r * 255).toString(16).padStart(2, '0'); + const g = Math.round(rgba.g * 255).toString(16).padStart(2, '0'); + const b = Math.round(rgba.b * 255).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; + } + + private static sanitizeTokenName(name: string): string { + // Convert "Primary / 500" to "primary-500" + return name + .toLowerCase() + .replace(/\s*\/\s*/g, '-') + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, ''); + } +} +``` + +**Step 2: Test token extraction** + +Run: Test in MCP Inspector + +Expected: Successfully extracts color and typography tokens + +**Step 3: Commit** + +```bash +git add src/generators/token-extractor.ts +git commit -m "feat: add token extractor for design tokens" +``` + +--- + +### Task 8: Tailwind Config Generator + +**Files:** +- Create: `src/generators/tailwind-config-generator.ts` + +**Step 1: Create Tailwind config generator** + +Create `src/generators/tailwind-config-generator.ts`: + +```typescript +import { DesignTokens, TailwindConfig } from '../types/design-tokens'; + +export class TailwindConfigGenerator { + /** + * Generate Tailwind config from design tokens + */ + static generateConfig(tokens: DesignTokens): TailwindConfig { + return { + theme: { + extend: tokens + } + }; + } + + /** + * Generate Tailwind config as formatted string + */ + static generateConfigString(tokens: DesignTokens): string { + const config = this.generateConfig(tokens); + + return `/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [], + theme: { + extend: ${JSON.stringify(config.theme.extend, null, 2)} + }, + plugins: [], +}`; + } + + /** + * Generate TypeScript Tailwind config + */ + static generateConfigTypeScript(tokens: DesignTokens): string { + const config = this.generateConfig(tokens); + + return `import type { Config } from 'tailwindcss' + +const config: Config = { + content: [], + theme: { + extend: ${JSON.stringify(config.theme.extend, null, 2)} + }, + plugins: [], +} + +export default config`; + } +} +``` + +**Step 2: Test config generation** + +Run: Test in MCP Inspector + +Expected: Generates valid Tailwind config + +**Step 3: Commit** + +```bash +git add src/generators/tailwind-config-generator.ts +git commit -m "feat: add Tailwind config generator" +``` + +--- + +### Task 9: Implement MCP Tool - analyze_design_structure + +**Files:** +- Modify: `src/tools/xd-tools.ts` +- Modify: `src/index.ts` + +**Step 1: Add analyze_design_structure tool** + +Add to `src/tools/xd-tools.ts`: + +```typescript +import { z } from 'zod'; +import { XDUrlValidator } from '../parsers/url-validator'; +import { XDWebParser } from '../parsers/xd-web-parser'; +import { AGCParser } from '../parsers/agc-parser'; +import { HierarchyAnalyzer } from '../analyzers/hierarchy-analyzer'; + +export const analyzeDesignStructureSchema = z.object({ + url: z.string().describe('Adobe XD specs URL (project or screen)'), + artboardId: z.string().optional().describe('Specific artboard ID to analyze (optional)') +}); + +export async function analyzeDesignStructure(args: z.infer<typeof analyzeDesignStructureSchema>) { + const { url, artboardId } = args; + + // Validate URL + const parsedUrl = XDUrlValidator.parseUrl(url); + if (!parsedUrl.isValid) { + throw new Error('Invalid Adobe XD URL'); + } + + // Fetch specs page + const parser = new XDWebParser(); + const html = await parser.fetchSpecsPage(parsedUrl.normalizedUrl); + + // Extract prototype data + const prototypeData = parser.extractPrototypeData(html); + if (!prototypeData) { + throw new Error('Failed to extract prototype data from page'); + } + + // Determine which artboard to analyze + let targetArtboard; + if (artboardId) { + targetArtboard = prototypeData.manifest.artboards.find(ab => ab.id === artboardId); + } else if (parsedUrl.screenId) { + targetArtboard = prototypeData.manifest.artboards.find(ab => ab.id === parsedUrl.screenId); + } else { + targetArtboard = prototypeData.manifest.artboards[0]; // First artboard + } + + if (!targetArtboard) { + throw new Error('Artboard not found'); + } + + // Get AGC component + const agcComponent = AGCParser.getPrimaryAGCComponent(targetArtboard); + if (!agcComponent) { + throw new Error('No AGC component found for artboard'); + } + + // Fetch AGC data + const agcUrl = AGCParser.buildAGCUrl(prototypeData, agcComponent); + const agcParser = new AGCParser(); + const agcData = await agcParser.fetchAGC(agcUrl); + if (!agcData) { + throw new Error('Failed to fetch AGC data'); + } + + // Build hierarchy + const hierarchy = HierarchyAnalyzer.buildHierarchy( + targetArtboard.id, + targetArtboard.name, + agcData + ); + + return { + artboard: { + id: targetArtboard.id, + name: targetArtboard.name, + dimensions: targetArtboard.bounds + }, + hierarchy, + summary: `Analyzed ${hierarchy.stats.totalElements} elements across ${hierarchy.stats.maxDepth} levels` + }; +} +``` + +**Step 2: Register tool in index.ts** + +Add to `src/index.ts`: + +```typescript +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + // ... existing tools ... + { + name: 'analyze_design_structure', + description: 'Analyze complete design structure from Adobe XD specs URL. Returns full element hierarchy, bounding boxes, styles, and statistics. Use this to deeply understand the design before generating components.', + inputSchema: zodToJsonSchema(analyzeDesignStructureSchema) + } + ] +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + // ... existing tool handlers ... + + if (request.params.name === 'analyze_design_structure') { + const args = analyzeDesignStructureSchema.parse(request.params.arguments); + const result = await analyzeDesignStructure(args); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; + } +}); +``` + +**Step 3: Build and test** + +Run: `npm run build && npx @modelcontextprotocol/inspector node dist/index.js` + +Expected: Tool appears in MCP Inspector, successfully analyzes design structure + +**Step 4: Commit** + +```bash +git add src/tools/xd-tools.ts src/index.ts +git commit -m "feat: add analyze_design_structure MCP tool" +``` + +--- + +### Task 10: Implement MCP Tool - extract_design_tokens + +**Files:** +- Modify: `src/tools/xd-tools.ts` +- Modify: `src/index.ts` + +**Step 1: Add extract_design_tokens tool** + +Add to `src/tools/xd-tools.ts`: + +```typescript +import { GlobalResourcesParser } from '../parsers/global-resources-parser'; +import { TokenExtractor } from '../generators/token-extractor'; +import { TailwindConfigGenerator } from '../generators/tailwind-config-generator'; + +export const extractDesignTokensSchema = z.object({ + url: z.string().describe('Adobe XD specs URL'), + format: z.enum(['json', 'tailwind-js', 'tailwind-ts']).default('json').describe('Output format') +}); + +export async function extractDesignTokens(args: z.infer<typeof extractDesignTokensSchema>) { + const { url, format } = args; + + // Validate URL + const parsedUrl = XDUrlValidator.parseUrl(url); + if (!parsedUrl.isValid) { + throw new Error('Invalid Adobe XD URL'); + } + + // Fetch specs page + const parser = new XDWebParser(); + const html = await parser.fetchSpecsPage(parsedUrl.normalizedUrl); + + // Extract prototype data + const prototypeData = parser.extractPrototypeData(html); + if (!prototypeData) { + throw new Error('Failed to extract prototype data from page'); + } + + // Fetch global resources + const grUrl = GlobalResourcesParser.buildGlobalResourcesUrl(prototypeData); + const grParser = new GlobalResourcesParser(); + const globalResources = await grParser.fetchGlobalResources(grUrl); + if (!globalResources) { + throw new Error('Failed to fetch global resources'); + } + + // Extract tokens + const tokens = TokenExtractor.extractTokens(globalResources); + + // Format output + let output: string; + switch (format) { + case 'tailwind-js': + output = TailwindConfigGenerator.generateConfigString(tokens); + break; + case 'tailwind-ts': + output = TailwindConfigGenerator.generateConfigTypeScript(tokens); + break; + default: + output = JSON.stringify(tokens, null, 2); + } + + return { + tokens, + formatted: output, + stats: { + totalColors: Object.keys(tokens.colors).length, + totalTypographyStyles: Object.keys(tokens.typography).length + } + }; +} +``` + +**Step 2: Register tool** + +Add to `src/index.ts`: + +```typescript +{ + name: 'extract_design_tokens', + description: 'Extract complete design token system (colors, typography, spacing) from Adobe XD specs URL. Returns tokens in JSON or Tailwind config format.', + inputSchema: zodToJsonSchema(extractDesignTokensSchema) +} + +// In CallToolRequestSchema handler: +if (request.params.name === 'extract_design_tokens') { + const args = extractDesignTokensSchema.parse(request.params.arguments); + const result = await extractDesignTokens(args); + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] + }; +} +``` + +**Step 3: Build and test** + +Run: `npm run build && npx @modelcontextprotocol/inspector node dist/index.js` + +Expected: Successfully extracts design tokens + +**Step 4: Commit** + +```bash +git add src/tools/xd-tools.ts src/index.ts +git commit -m "feat: add extract_design_tokens MCP tool" +``` + +--- + +### Task 11: Implement MCP Tool - list_artboards + +**Files:** +- Modify: `src/tools/xd-tools.ts` +- Modify: `src/index.ts` + +**Step 1: Add list_artboards tool** + +Add to `src/tools/xd-tools.ts`: + +```typescript +export const listArtboardsSchema = z.object({ + url: z.string().describe('Adobe XD specs URL (project URL)') +}); + +export async function listArtboards(args: z.infer<typeof listArtboardsSchema>) { + const { url } = args; + + // Validate URL + const parsedUrl = XDUrlValidator.parseUrl(url); + if (!parsedUrl.isValid) { + throw new Error('Invalid Adobe XD URL'); + } + + // Fetch specs page + const parser = new XDWebParser(); + const html = await parser.fetchSpecsPage(parsedUrl.normalizedUrl); + + // Extract prototype data + const prototypeData = parser.extractPrototypeData(html); + if (!prototypeData) { + throw new Error('Failed to extract prototype data from page'); + } + + // Map artboards + const artboards = prototypeData.manifest.artboards.map(ab => ({ + id: ab.id, + name: ab.name, + dimensions: { + width: ab.bounds.width, + height: ab.bounds.height + }, + viewport: ab.viewport, + deviceType: ab.bounds.width >= 1024 ? 'desktop' : ab.bounds.width >= 768 ? 'tablet' : 'mobile', + resourceCount: ab.resources?.length || 0 + })); + + return { + projectId: prototypeData.manifest.id, + projectName: prototypeData.manifest.name, + totalArtboards: artboards.length, + artboards + }; +} +``` + +**Step 2: Register tool** + +Add to `src/index.ts` + +**Step 3: Build and test** + +Run: Test in MCP Inspector + +**Step 4: Commit** + +```bash +git add src/tools/xd-tools.ts src/index.ts +git commit -m "feat: add list_artboards MCP tool" +``` + +--- + +### Task 12: Implement MCP Tool - get_element_details + +**Files:** +- Modify: `src/tools/xd-tools.ts` +- Modify: `src/index.ts` + +**Step 1: Add get_element_details tool** + +Add to `src/tools/xd-tools.ts`: + +```typescript +export const getElementDetailsSchema = z.object({ + url: z.string().describe('Adobe XD specs URL'), + elementPath: z.string().describe('Element path (e.g., "root/header/logo")'), + artboardId: z.string().optional().describe('Artboard ID (optional)') +}); + +export async function getElementDetails(args: z.infer<typeof getElementDetailsSchema>) { + const { url, elementPath, artboardId } = args; + + // Use analyzeDesignStructure to get hierarchy + const analysis = await analyzeDesignStructure({ url, artboardId }); + + // Find element by path + const element = analysis.hierarchy.flattenedElements.find(el => el.path === elementPath); + if (!element) { + throw new Error(`Element not found at path: ${elementPath}`); + } + + return { + element, + artboard: analysis.artboard + }; +} +``` + +**Step 2: Register tool** + +**Step 3: Build and test** + +**Step 4: Commit** + +```bash +git add src/tools/xd-tools.ts src/index.ts +git commit -m "feat: add get_element_details MCP tool" +``` + +--- + +### Task 13: Tailwind Class Generator + +**Files:** +- Create: `src/generators/tailwind-class-generator.ts` + +**Step 1: Create Tailwind class generator** + +Create `src/generators/tailwind-class-generator.ts`: + +```typescript +import { AGCElement, AGCStyle, RGBA } from '../types/xd-data'; + +export class TailwindClassGenerator { + /** + * Generate Tailwind classes from AGC element + */ + static generateClasses(element: AGCElement): string[] { + const classes: string[] = []; + + // Position (absolute by default for XD designs) + classes.push('absolute'); + + // Dimensions + if (element.shape?.width) { + classes.push(`w-[${Math.round(element.shape.width)}px]`); + } + if (element.shape?.height) { + classes.push(`h-[${Math.round(element.shape.height)}px]`); + } + + // Positioning + if (element.transform) { + classes.push(`left-[${Math.round(element.transform.tx)}px]`); + classes.push(`top-[${Math.round(element.transform.ty)}px]`); + } + + // Background color + if (element.style?.fill?.color) { + const hex = this.rgbaToHex(element.style.fill.color); + classes.push(`bg-[${hex}]`); + } + + // Border + if (element.style?.stroke) { + const hex = this.rgbaToHex(element.style.stroke.color); + classes.push(`border-[${element.style.stroke.width}px]`); + classes.push(`border-[${hex}]`); + } + + // Border radius + if (element.shape?.r) { + const radius = typeof element.shape.r === 'number' ? element.shape.r : element.shape.r.topLeft || 0; + if (radius > 0) { + classes.push(`rounded-[${Math.round(radius)}px]`); + } + } + + // Opacity + if (element.style?.opacity !== undefined && element.style.opacity < 1) { + const opacityPercent = Math.round(element.style.opacity * 100); + classes.push(`opacity-${opacityPercent}`); + } + + // Typography + if (element.text?.style?.font) { + const font = element.text.style.font; + classes.push(`text-[${Math.round(font.size)}px]`); + classes.push(`font-[${font.weight}]`); + + if (font.style === 'italic') { + classes.push('italic'); + } + } + + // Text alignment + if (element.text?.style?.textAlign) { + classes.push(`text-${element.text.style.textAlign}`); + } + + // Shadows + if (element.style?.filters) { + element.style.filters.forEach(filter => { + if (filter.type === 'dropShadow' && filter.params.color) { + // Use arbitrary shadow value + const dx = filter.params.dx || 0; + const dy = filter.params.dy || 0; + const blur = filter.params.r || 0; + const color = this.rgbaToRgbaString(filter.params.color); + classes.push(`shadow-[${dx}px_${dy}px_${blur}px_${color}]`); + } + }); + } + + return classes; + } + + private static rgbaToHex(rgba: RGBA): string { + const r = Math.round(rgba.r * 255).toString(16).padStart(2, '0'); + const g = Math.round(rgba.g * 255).toString(16).padStart(2, '0'); + const b = Math.round(rgba.b * 255).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; + } + + private static rgbaToRgbaString(rgba: RGBA): string { + const r = Math.round(rgba.r * 255); + const g = Math.round(rgba.g * 255); + const b = Math.round(rgba.b * 255); + return `rgba(${r},${g},${b},${rgba.a})`; + } +} +``` + +**Step 2: Test class generation** + +Run: Test in MCP Inspector + +**Step 3: Commit** + +```bash +git add src/generators/tailwind-class-generator.ts +git commit -m "feat: add Tailwind class generator from AGC elements" +``` + +--- + +### Task 14: Component Structure Generator + +**Files:** +- Create: `src/generators/component-structure.ts` + +**Step 1: Create component structure generator** + +Create `src/generators/component-structure.ts`: + +```typescript +import { AGCElement } from '../types/xd-data'; +import { HierarchyNode } from '../types/design-hierarchy'; + +export interface ComponentNode { + id: string; + name: string; + type: 'div' | 'text' | 'image'; + className: string; + textContent?: string; + children: ComponentNode[]; + props: Record<string, any>; +} + +export class ComponentStructureGenerator { + /** + * Generate component structure from hierarchy + */ + static generateStructure(hierarchyNode: HierarchyNode): ComponentNode { + return this.buildComponentNode(hierarchyNode); + } + + private static buildComponentNode(node: HierarchyNode): ComponentNode { + const element = node.element; + + // Determine component type + let type: 'div' | 'text' | 'image' = 'div'; + if (element.type === 'text') { + type = 'text'; + } else if (element.shape?.type === 'rect' && element.style?.fill?.pattern) { + type = 'image'; + } + + // Build component node + const componentNode: ComponentNode = { + id: node.id, + name: node.name, + type, + className: '', // Will be filled with Tailwind classes + children: [], + props: {} + }; + + // Add text content + if (element.text?.rawText) { + componentNode.textContent = element.text.rawText; + } + + // Add props for configurability + if (element.text) { + componentNode.props.text = element.text.rawText; + } + + // Build children + componentNode.children = node.children.map(child => this.buildComponentNode(child)); + + return componentNode; + } +} +``` + +**Step 2: Test structure generation** + +**Step 3: Commit** + +```bash +git add src/generators/component-structure.ts +git commit -m "feat: add component structure generator" +``` + +--- + +### Task 15: Enhanced React Generator + +**Files:** +- Modify: `src/generators/react-generator.ts` + +**Step 1: Enhance React generator** + +Modify `src/generators/react-generator.ts`: + +```typescript +import { HierarchyNode } from '../types/design-hierarchy'; +import { ComponentStructureGenerator, ComponentNode } from './component-structure'; +import { TailwindClassGenerator } from './tailwind-class-generator'; + +export class ReactGenerator { + /** + * Generate React component from hierarchy + */ + static generateComponent( + componentName: string, + hierarchyNode: HierarchyNode, + options: { + typescript?: boolean; + includeComments?: boolean; + configurableProps?: boolean; + } = {} + ): string { + const { typescript = true, includeComments = true, configurableProps = true } = options; + + // Generate component structure + const structure = ComponentStructureGenerator.generateStructure(hierarchyNode); + + // Generate props interface + let propsInterface = ''; + if (typescript && configurableProps) { + propsInterface = this.generatePropsInterface(componentName, structure); + } + + // Generate JSX + const jsx = this.generateJSX(structure, hierarchyNode, 0, includeComments); + + // Build component + const propsType = typescript && configurableProps ? `: ${componentName}Props` : ''; + const propsParam = configurableProps ? 'props' : ''; + + return `${propsInterface} + +export default function ${componentName}(${propsParam}${propsType}) { + return ( +${jsx} + ); +}`; + } + + private static generatePropsInterface(componentName: string, structure: ComponentNode): string { + const props: string[] = []; + + // Collect props from structure + this.collectProps(structure, props); + + if (props.length === 0) { + return `interface ${componentName}Props {}`; + } + + return `interface ${componentName}Props { + ${props.join(';\n ')}; +}`; + } + + private static collectProps(node: ComponentNode, props: string[]): void { + Object.entries(node.props).forEach(([key, value]) => { + const type = typeof value === 'string' ? 'string' : 'any'; + props.push(`${key}?: ${type}`); + }); + + node.children.forEach(child => this.collectProps(child, props)); + } + + private static generateJSX( + structure: ComponentNode, + hierarchyNode: HierarchyNode, + indent: number, + includeComments: boolean + ): string { + const indentStr = ' '.repeat(indent + 2); + const classes = TailwindClassGenerator.generateClasses(hierarchyNode.element); + + let jsx = ''; + + // Add comment with design metadata + if (includeComments) { + jsx += `${indentStr}{/* ${structure.name} - ${structure.type} */}\n`; + } + + // Generate element + const classNames = classes.join(' '); + + if (structure.type === 'text') { + jsx += `${indentStr}<div className="${classNames}">\n`; + jsx += `${indentStr} {props.${structure.id}_text || "${structure.textContent || ''}"}\n`; + jsx += `${indentStr}</div>`; + } else if (structure.type === 'image') { + jsx += `${indentStr}<img className="${classNames}" alt="${structure.name}" />`; + } else { + jsx += `${indentStr}<div className="${classNames}">`; + + if (structure.children.length > 0) { + jsx += '\n'; + structure.children.forEach((child, index) => { + const childHierarchy = hierarchyNode.children[index]; + jsx += this.generateJSX(child, childHierarchy, indent + 1, includeComments); + jsx += '\n'; + }); + jsx += `${indentStr}`; + } + + jsx += `</div>`; + } + + return jsx; + } +} +``` + +**Step 2: Build and test** + +**Step 3: Commit** + +```bash +git add src/generators/react-generator.ts +git commit -m "feat: enhance React generator with Tailwind classes and configurable props" +``` + +--- + +### Task 16: Integration Testing + +**Files:** +- Test: All MCP tools with example URLs + +**Step 1: Test all tools in MCP Inspector** + +Run: `npx @modelcontextprotocol/inspector node dist/index.js` + +Test each tool: +1. `list_artboards` with project URL +2. `analyze_design_structure` with screen URL +3. `extract_design_tokens` with project URL +4. `get_element_details` with element path +5. `generate_react_component` (if exists) + +Expected: All tools work correctly with example Adobe XD URLs + +**Step 2: Document any issues found** + +Create test report in `docs/testing-report.md` + +**Step 3: Fix any issues** + +**Step 4: Commit** + +```bash +git add . +git commit -m "test: comprehensive integration testing of all MCP tools" +``` + +--- + +### Task 17: Documentation + +**Files:** +- Modify: `README.md` +- Create: `docs/ARCHITECTURE.md` +- Create: `docs/API.md` + +**Step 1: Update README** + +Update `README.md` with: +- New features +- Tool descriptions +- Usage examples +- Design-first approach explanation + +**Step 2: Create architecture documentation** + +Create `docs/ARCHITECTURE.md` explaining: +- Data flow (URL → HTML → prototypeData → AGC/GlobalResources → Tokens/Hierarchy) +- Component architecture +- Type system + +**Step 3: Create API documentation** + +Create `docs/API.md` with: +- All MCP tools +- Input schemas +- Output examples +- Error handling + +**Step 4: Commit** + +```bash +git add README.md docs/ +git commit -m "docs: comprehensive documentation for design-first MCP server" +``` + +--- + +## Success Criteria + +- [ ] All type definitions complete and accurate +- [ ] URL validator handles both project and screen URLs +- [ ] Successfully extracts `window.prototypeData` from Adobe XD specs pages +- [ ] AGC files fetch correctly from Adobe CDN +- [ ] Global resources (design tokens) fetch correctly +- [ ] Hierarchy analyzer builds complete element trees +- [ ] Design tokens extract in Tailwind-compatible format +- [ ] 6 MCP tools implemented and tested: + - `analyze_design_structure` + - `extract_design_tokens` + - `list_artboards` + - `get_element_details` + - `get_layout_information` (optional) + - `generate_react_component` (enhanced) +- [ ] All tools tested with example Adobe XD URLs +- [ ] Tailwind class generation from AGC styles +- [ ] React components generated with configurable props +- [ ] Comprehensive documentation +- [ ] Server builds and runs without errors + +--- + +## Testing Strategy + +**Unit Testing (Optional):** +- URL validator with various URL formats +- Color conversion utilities (RGBA → Hex) +- Token name sanitization + +**Integration Testing (Required):** +- Test with all 3 example URLs +- Verify data extraction completeness +- Test each MCP tool in MCP Inspector +- Validate Tailwind config output +- Verify React component output + +**Manual Testing:** +- Test with MCP Inspector: `npx @modelcontextprotocol/inspector node dist/index.js` +- Test in IDE with MCP client +- Verify outputs are correct and complete + +--- + +## Notes + +- AGC file structure may vary - be prepared to adjust types during implementation +- Adobe CDN URLs require proper authentication tokens from linkTemplate +- Design tokens may have different structures than assumed - validate with real data +- React generation is secondary - prioritize design understanding first +- Use MCP Inspector frequently to validate tools work correctly +- Keep error messages actionable (follow MCP best practices) + +--- + +## Execution Options + +**Plan complete and saved to `docs/plans/2026-02-13-design-understanding-first.md`.** + +**Two execution options:** + +1. **Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration + +2. **Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints + +**Which approach?** From 7c9636108955cf821a38e8b8a81132c84e51b99a Mon Sep 17 00:00:00 2001 From: Abdur Rahman <mmarahman4847@gmail.com> Date: Fri, 13 Feb 2026 20:19:36 +0530 Subject: [PATCH 6/6] feat: add AGC file parser and global resources parser --- src/parsers/agc-parser.ts | 62 ++++++++++++++++++++++++++ src/parsers/global-resources-parser.ts | 53 ++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 src/parsers/agc-parser.ts create mode 100644 src/parsers/global-resources-parser.ts diff --git a/src/parsers/agc-parser.ts b/src/parsers/agc-parser.ts new file mode 100644 index 0000000..f45b4e0 --- /dev/null +++ b/src/parsers/agc-parser.ts @@ -0,0 +1,62 @@ +import { PrototypeData, AGCData, ComponentRef, ArtboardManifest } from '../types/xd-data'; + +export class AGCParser { + /** + * Build AGC file URL from prototype data and component reference + */ + static buildAGCUrl(prototypeData: PrototypeData, componentRef: ComponentRef): string { + const { linkTemplate } = prototypeData; + const { href, data } = linkTemplate; + + // Replace template variables in href + let url = href + .replace('{;revision}', `;revision=${componentRef.revision}`) + .replace('{?component_id,component_path}', ''); + + // Add query parameters + const params = new URLSearchParams({ + component_id: componentRef.id, + component_path: componentRef.path, + api_key: data.api_key, + access_token: data.access_token + }); + + return `${url}?${params.toString()}`; + } + + /** + * Fetch and parse AGC file + */ + async fetchAGC(url: string): Promise<AGCData | null> { + try { + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json,*/*', + 'Referer': 'https://xd.adobe.com/' + } + }); + + if (!response.ok) { + console.error(`Failed to fetch AGC: ${response.status} ${response.statusText}`); + return null; + } + + const data = await response.json() as AGCData; + return data; + } catch (error) { + console.error('Error fetching AGC file:', error); + return null; + } + } + + /** + * Get primary AGC component for an artboard + */ + static getPrimaryAGCComponent(artboard: ArtboardManifest): ComponentRef | null { + const primaryComponent = artboard.components?.find( + (c: ComponentRef) => c.rel === 'primary' && c.type === 'agc' + ); + return primaryComponent || null; + } +} diff --git a/src/parsers/global-resources-parser.ts b/src/parsers/global-resources-parser.ts new file mode 100644 index 0000000..4908a4d --- /dev/null +++ b/src/parsers/global-resources-parser.ts @@ -0,0 +1,53 @@ +import { PrototypeData, GlobalResources, ResourceRef } from '../types/xd-data'; + +export class GlobalResourcesParser { + /** + * Build global resources URL from prototype data + */ + static buildGlobalResourcesUrl(prototypeData: PrototypeData): string { + const { linkTemplate, manifest } = prototypeData; + const { globalResources } = manifest; + const { href, data } = linkTemplate; + + // Replace template variables + let url = href + .replace('{;revision}', `;revision=${globalResources.revision}`) + .replace('{?component_id,component_path}', ''); + + // Add query parameters + const params = new URLSearchParams({ + component_id: globalResources.id, + component_path: globalResources.path, + api_key: data.api_key, + access_token: data.access_token + }); + + return `${url}?${params.toString()}`; + } + + /** + * Fetch and parse global resources + */ + async fetchGlobalResources(url: string): Promise<GlobalResources | null> { + try { + const response = await fetch(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json,*/*', + 'Referer': 'https://xd.adobe.com/' + } + }); + + if (!response.ok) { + console.error(`Failed to fetch global resources: ${response.status} ${response.statusText}`); + return null; + } + + const data = await response.json() as GlobalResources; + return data; + } catch (error) { + console.error('Error fetching global resources:', error); + return null; + } + } +}