├── .clinerules ├── .commitlintrc ├── .cursor └── rules │ └── git.mdc ├── .editorconfig ├── .github ├── dependabot.yml └── renovate.json ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc.mjs ├── .textlintrc.js ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── cspell.json ├── eslint.config.js ├── lint-staged.config.mjs ├── package-lock.json ├── package.json ├── src ├── bin.ts ├── coding-guideline.ts ├── figma │ ├── extract-file-id.spec.ts │ ├── extract-file-id.ts │ ├── extract-node-ids.spec.ts │ ├── extract-node-ids.ts │ ├── fetch-file.spec.ts │ ├── fetch-file.ts │ ├── fetch-image.spec.ts │ ├── fetch-image.ts │ ├── fetch-nodes.spec.ts │ ├── fetch-nodes.ts │ ├── get-data.spec.ts │ ├── get-data.ts │ ├── get-image.spec.ts │ ├── get-image.ts │ ├── serialize-error.spec.ts │ ├── serialize-error.ts │ └── types.ts ├── index.ts └── utils │ ├── get-task-step.ts │ ├── split-task-steps.spec.ts │ └── split-task-steps.ts ├── tsconfig.build.json └── tsconfig.json /.clinerules: -------------------------------------------------------------------------------- 1 | # AI Agent Rules 2 | 3 | ## Test Rules 4 | 5 | ### Execution Requirements 6 | 7 | - You must use vitest as the testing framework 8 | 9 | ### Test Code Requirements 10 | 11 | - You must write all tests in TypeScript 12 | - You must name test files with the pattern `*.spec.ts` 13 | - You must use `test` function, not `it` 14 | - You must use `expect` for all assertions 15 | - You must include both success and failure test cases 16 | - You must not write conditional logic or exception handling in test code 17 | - You must use `toThrow` for testing exceptions 18 | 19 | ## Git Rules 20 | 21 | - You must write in English 22 | - You must use the imperative mood 23 | - You must use conventional commits 24 | - You must use the following types: 25 | - `feat` 26 | - `fix` 27 | - `ui` 28 | - `style` 29 | - `docs` 30 | - `refactor` 31 | - `test` 32 | - `chore` 33 | - You must use the following scopes: 34 | - `repo` 35 | - `deps` 36 | - `github` 37 | -------------------------------------------------------------------------------- /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@d-zero/commitlint-config" 3 | } 4 | -------------------------------------------------------------------------------- /.cursor/rules/git.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Git manipulation rules 3 | globs: *,**/* 4 | alwaysApply: true 5 | --- 6 | # Commit creation 7 | 8 | - When asked to "commit": 9 | 1. Check staged files using `git --no-pager diff --staged` and create a commit message using *only* the staged files. 10 | - Once the message is ready, directly propose the commit command to the user. 11 | 2. If no files are staged, check the differences using `git status`, then stage files sequentially based on the following commit granularity before committing: 12 | - Separate commits by package. 13 | - Commit dependencies first (if dependency order is unclear, check using `npx lerna list --graph`). 14 | - If the OS, application settings, or context suggest a language other than English is being used, provide a translation and explanation of the commit message in that language immediately before proposing the commit command to the user. 15 | 16 | # Commit message format 17 | 18 | - You must write in English 19 | - You must use the imperative mood 20 | - You must use conventional commits 21 | - You must use the following types: 22 | - `feat` 23 | - `fix` 24 | - `docs` 25 | - `refactor` 26 | - `test` 27 | - `chore` 28 | - The message body's lines must not be longer than 100 characters 29 | - The subject must not be sentence-case, start-case, pascal-case, upper-case -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | indent_style = tab 4 | indent_size = 2 5 | 6 | [*.md] 7 | trim_trailing_whitespace = false 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'weekly' 12 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "configMigration": true, 4 | "extends": [ 5 | "config:recommended", 6 | "docker:pinDigests", 7 | "helpers:pinGitHubActionDigests", 8 | ":pinDevDependencies", 9 | ":semanticCommitTypeAll(chore)" 10 | ], 11 | "lockFileMaintenance": { 12 | "enabled": true, 13 | "automerge": true 14 | }, 15 | "autoApprove": true, 16 | "labels": ["Dependencies", "Renovate"], 17 | "packageRules": [ 18 | { 19 | "matchDepTypes": ["optionalDependencies"], 20 | "addLabels": ["Dependencies: Optional"] 21 | }, 22 | { 23 | "matchDepTypes": ["devDependencies"], 24 | "addLabels": ["Dependencies: Development"] 25 | }, 26 | { 27 | "matchDepTypes": ["dependencies"], 28 | "addLabels": ["Dependencies: Production"] 29 | }, 30 | { 31 | "description": "Automerge non-major updates", 32 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 33 | "matchCurrentVersion": "!/^0/", 34 | "automerge": true 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Dist *.js sources 5 | dist 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # next.js build output 67 | .next 68 | 69 | # TypeScript 70 | *.tsbuildinfo 71 | 72 | # Secret Test 73 | test/fixture/.__* 74 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit "$1" -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | export { default } from '@d-zero/prettier-config'; 2 | -------------------------------------------------------------------------------- /.textlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@d-zero/textlint-config'), 3 | }; 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "editorconfig.editorconfig", 5 | "esbenp.prettier-vscode", 6 | "streetsidesoftware.code-spell-checker" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "eslint.validate": ["javascript", "typescript"], 5 | "editor.formatOnSave": true, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": "explicit" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 D-ZERO Co., Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ディーゼロ開発環境用 MCPサーバー 2 | 3 | D-ZEROフロントエンド開発環境用のModel Context Protocol(MCP)サーバーです。 4 | 以下の機能を提供します: 5 | 6 | - **Figmaデータの取得**: FigmaファイルやノードのデータをAPIを通じて取得 7 | - **コーディングガイドラインの提供**: D-ZEROのフロントエンド開発規約の提供 8 | - **CLINEとの統合**: CLINEのMCPサーバーとして動作し、AIアシスタントとの対話をサポート 9 | 10 | このサーバーを使用することで、開発者はFigmaデザインの参照やコーディング規約の確認をAIアシスタントとの会話を通じて行うことができます。 11 | 12 | ## `cline_mcp_settings.json` 13 | 14 | CLINEの「MCP Servers」設定から、「Installed」タブを選択し、Configure MCP Serversで`cline_mcp_settings.json`ファイルを編集します。 15 | 16 | ```json 17 | { 18 | "mcpServers": { 19 | "coding_guidelines": { 20 | "command": "npx", 21 | "args": ["-y", "@d-zero/mcp-server"], 22 | "env": { 23 | "FIGMA_ACCESS_TOKEN": "abcd_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 24 | }, 25 | "disabled": false, 26 | "autoApprove": [] 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | `@d-zero/mcp-server`パッケージの実行に失敗する場合は、グローバルにインストールしてフルパスを指定してください([参考Issue](https://github.com/cline/cline/issues/1247))。 33 | 34 | Figmaのアクセストークンは https://www.figma.com/developers/api#access-tokens から取得してください。 35 | 36 | ## Contribution 37 | 38 | このMCPサーバー自体の開発には`.env`ファイルが必要です。 39 | 40 | ```env 41 | # Figma API設定 42 | FIGMA_ACCESS_TOKEN=abcd_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 43 | 44 | # テスト用のFigma URL 45 | FIGMA_TEST_URL=https://www.figma.com/file/abcdef123456/FileName 46 | ``` 47 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "import": ["@d-zero/cspell-config"], 3 | "ignorePaths": [".vscode/*"], 4 | "words": [] 5 | } 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import dz from '@d-zero/eslint-config'; 2 | 3 | /** 4 | * @type {import('eslint').ESLint.ConfigData[]} 5 | */ 6 | export default [ 7 | ...dz.configs.standard, 8 | { 9 | rules: { 10 | '@typescript-eslint/ban-ts-comment': 0, 11 | 'unicorn/prefer-add-event-listener': 0, 12 | 'unicorn/prefer-top-level-await': 0, 13 | }, 14 | }, 15 | { 16 | files: ['*.mjs', '**/*.spec.{js,mjs,ts}'], 17 | rules: { 18 | 'import/no-extraneous-dependencies': 0, 19 | }, 20 | }, 21 | { 22 | files: ['.textlintrc.js'], 23 | ...dz.configs.commonjs, 24 | }, 25 | { 26 | ignores: ['**/dist/**/*'], 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /lint-staged.config.mjs: -------------------------------------------------------------------------------- 1 | import lintStagedConfigGenerator from '@d-zero/lint-staged-config'; 2 | export default lintStagedConfigGenerator(); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@d-zero/mcp-server", 3 | "version": "0.5.0", 4 | "description": "D-Zero frontend coding MCP server", 5 | "repository": "https://github.com/d-zero-dev/mcp-server.git", 6 | "author": "D-ZERO Co., Ltd.", 7 | "license": "MIT", 8 | "private": false, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "type": "module", 13 | "exports": { 14 | ".": { 15 | "import": "./dist/index.js" 16 | } 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "bin": { 22 | "mcp-server": "./dist/bin.js" 23 | }, 24 | "scripts": { 25 | "build": "tsc --project tsconfig.build.json", 26 | "watch": "tsc --watch --project tsconfig.build.json", 27 | "dev": "tsc --watch --project tsconfig.build.json", 28 | "clean": "tsc --build --clean", 29 | "test": "vitest run", 30 | "lint": "run-s lint:eslint lint:prettier lint:textlint lint:cspell", 31 | "lint:cspell": "cspell --no-progress --show-suggestions \"{*,src/**/*}/\"", 32 | "lint:eslint": "eslint --fix \"{*,src/**/*}.{js,cjs,mjs,jsx,ts,cts,mts,tsx}\"", 33 | "lint:prettier": "prettier --write \"{*,src/**/*}.{md,json,js,cjs,mjs,jsx,ts,cts,mts,tsx}\"", 34 | "lint:textlint": "textlint --fix \"./{*,src/**/*}.{md}\"", 35 | "release:major": "npm version major; npm publish", 36 | "release:minor": "npm version minor; npm publish", 37 | "release:patch": "npm version patch; npm publish", 38 | "prepare": "husky", 39 | "commit": "cz", 40 | "co": "cz", 41 | "up": "npx npm-check-updates --interactive --format group" 42 | }, 43 | "config": { 44 | "commitizen": { 45 | "path": "./node_modules/cz-customizable" 46 | }, 47 | "cz-customizable": { 48 | "config": "./node_modules/@d-zero/cz-config" 49 | } 50 | }, 51 | "dependencies": { 52 | "@modelcontextprotocol/sdk": "1.9.0", 53 | "zod": "3.24.2" 54 | }, 55 | "devDependencies": { 56 | "@d-zero/commitlint-config": "5.0.0-alpha.62", 57 | "@d-zero/cspell-config": "5.0.0-alpha.62", 58 | "@d-zero/eslint-config": "5.0.0-alpha.62", 59 | "@d-zero/lint-staged-config": "5.0.0-alpha.62", 60 | "@d-zero/prettier-config": "5.0.0-alpha.62", 61 | "@d-zero/textlint-config": "5.0.0-alpha.62", 62 | "@d-zero/tsconfig": "0.4.1", 63 | "dotenv": "16.5.0", 64 | "husky": "9.1.7", 65 | "npm-run-all2": "7.0.2", 66 | "typescript": "5.8.3", 67 | "vitest": "3.1.1" 68 | }, 69 | "packageManager": "npm@11.3.0", 70 | "volta": { 71 | "node": "22.14.0", 72 | "npm": "11.3.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import './index.js'; 4 | -------------------------------------------------------------------------------- /src/coding-guideline.ts: -------------------------------------------------------------------------------- 1 | const guidelines = { 2 | general: [ 3 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/text-files.md', 4 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/naming/index.md', 5 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/naming/principles.md', 6 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/naming/structure.md', 7 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/naming/abbreviation.md', 8 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/naming/consistency.md', 9 | ], 10 | html: [ 11 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/html/style.md', 12 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/html/doctype.md', 13 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/html/meta.md', 14 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/html/links.md', 15 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/html/components.md', 16 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/html/elements.md', 17 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/html/ids.md', 18 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/html/accessibility.md', 19 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/html/interactions.md', 20 | ], 21 | css: [ 22 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/css/builder.md', 23 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/css/structure.md', 24 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/css/ids.md', 25 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/css/rules.md', 26 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/css/selectors.md', 27 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/css/order.md', 28 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/css/variables.md', 29 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/css/values.md', 30 | ], 31 | media: [ 32 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/media/index.md', 33 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/media/image.md', 34 | ], 35 | js: [ 36 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/js/structure.md', 37 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/js/loading.md', 38 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/js/development.md', 39 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/js/interactions.md', 40 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/js/libraries.md', 41 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/js/no-style-attr.md', 42 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/js/performance.md', 43 | ], 44 | 'web-components': [ 45 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/js/development.md', 46 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-guidelines/refs/heads/dev/src/js/libraries.md', 47 | 'https://raw.githubusercontent.com/d-zero-dev/frontend-env/refs/heads/dev/packages/%40d-zero/custom-components/src/hamburger-menu.mdx', 48 | ], 49 | } as const; 50 | 51 | /** 52 | * @param techTypes 53 | */ 54 | export async function getCodingGuidelines( 55 | techTypes: 'html' | 'css' | 'js' | 'media' | 'web-components', 56 | ) { 57 | const urls = [...guidelines.general, ...guidelines[techTypes]]; 58 | 59 | const contents = await Promise.all( 60 | urls.map(async (url) => { 61 | const res = await fetch(url); 62 | const text = await res.text(); 63 | return text; 64 | }), 65 | ); 66 | 67 | return contents.join('\n\n---\n\n'); 68 | } 69 | -------------------------------------------------------------------------------- /src/figma/extract-file-id.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { extractFigmaFileId } from './extract-file-id.js'; 4 | 5 | describe('extractFigmaFileId', () => { 6 | it('should extract file ID from file URL correctly', () => { 7 | const url = 'https://www.figma.com/file/abc123/FileName'; 8 | expect(extractFigmaFileId(url)).toBe('abc123'); 9 | }); 10 | 11 | it('should extract file ID from design URL correctly', () => { 12 | const url = 'https://www.figma.com/design/def456/FileName'; 13 | expect(extractFigmaFileId(url)).toBe('def456'); 14 | }); 15 | 16 | it('should return null for invalid URL', () => { 17 | const url = 'https://www.example.com'; 18 | expect(extractFigmaFileId(url)).toBeNull(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/figma/extract-file-id.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extract file ID from Figma URL 3 | * @param url Figma file URL 4 | * @returns File ID or null (if URL is invalid) 5 | */ 6 | export function extractFigmaFileId(url: string): string | null { 7 | // Regular expression to extract file ID from Figma URL 8 | // Example 1: https://www.figma.com/file/abcdef123456/FileName 9 | // Example 2: https://www.figma.com/design/abcdef123456/FileName 10 | const fileMatch = url.match(/figma\.com\/file\/([a-zA-Z0-9]+)/); 11 | if (fileMatch && fileMatch[1]) { 12 | return fileMatch[1]; 13 | } 14 | 15 | const designMatch = url.match(/figma\.com\/design\/([a-zA-Z0-9]+)/); 16 | return designMatch && designMatch[1] ? designMatch[1] : null; 17 | } 18 | -------------------------------------------------------------------------------- /src/figma/extract-node-ids.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { extractNodeIds } from './extract-node-ids.js'; 4 | 5 | describe('extractNodeIds', () => { 6 | it('should extract node ID from URL correctly', () => { 7 | const url = 'https://www.figma.com/file/abc123/FileName?node-id=123-456'; 8 | expect(extractNodeIds(url)).toEqual(['123-456']); 9 | }); 10 | 11 | it('should extract multiple node IDs correctly', () => { 12 | const url = 'https://www.figma.com/file/abc123/FileName?node-id=123-456,789-012'; 13 | expect(extractNodeIds(url)).toEqual(['123-456', '789-012']); 14 | }); 15 | 16 | it('should return empty array when no node IDs are present', () => { 17 | const url = 'https://www.figma.com/file/abc123/FileName'; 18 | expect(extractNodeIds(url)).toEqual([]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/figma/extract-node-ids.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extract node IDs from Figma URL 3 | * @param url Figma file URL 4 | * @returns Array of node IDs or empty array (if no node IDs specified) 5 | */ 6 | export function extractNodeIds(url: string): string[] { 7 | const nodeIdMatch = url.match(/node-id=([^&]+)/); 8 | if (nodeIdMatch && nodeIdMatch[1]) { 9 | return nodeIdMatch[1].split(','); 10 | } 11 | return []; 12 | } 13 | -------------------------------------------------------------------------------- /src/figma/fetch-file.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { fetchFigmaFile } from './fetch-file.js'; 4 | 5 | describe('fetchFigmaFile', () => { 6 | describe('Error cases', () => { 7 | it('should throw error when fetching file data with invalid API key', async () => { 8 | // Arrange 9 | const apiKey = 'invalid-api-key'; 10 | const fileId = 'test-file-id'; 11 | 12 | // Act & Assert 13 | await expect(fetchFigmaFile(apiKey, fileId)).rejects.toThrow(); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/figma/fetch-file.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | 3 | /** 4 | * Directly call Figma API to get file data 5 | * @param apiKey Figma API key 6 | * @param fileId File ID 7 | * @returns File data 8 | */ 9 | export async function fetchFigmaFile(apiKey: string, fileId: string): Promise { 10 | const response = await fetch(`https://api.figma.com/v1/files/${fileId}`, { 11 | headers: { 12 | 'X-FIGMA-TOKEN': apiKey, 13 | }, 14 | }); 15 | 16 | if (!response.ok) { 17 | const json = await response.json(); 18 | throw new McpError( 19 | ErrorCode.InternalError, 20 | `Figma API Error: ${response.status} ${response.statusText} ${json?.err ?? JSON.stringify(json)}`, 21 | ); 22 | } 23 | 24 | return response.json(); 25 | } 26 | -------------------------------------------------------------------------------- /src/figma/fetch-image.spec.ts: -------------------------------------------------------------------------------- 1 | import type { ImageFormat } from './types.js'; 2 | 3 | import dotenv from 'dotenv'; 4 | import { describe, it, expect, beforeAll } from 'vitest'; 5 | 6 | import { extractFigmaFileId } from './extract-file-id.js'; 7 | import { extractNodeIds } from './extract-node-ids.js'; 8 | import { fetchFigmaImage } from './fetch-image.js'; 9 | 10 | // Load .env file before running tests 11 | beforeAll(() => { 12 | // Load .env file 13 | dotenv.config(); 14 | }); 15 | 16 | describe('fetchFigmaImage', () => { 17 | describe('Error cases', () => { 18 | it('should throw error when fetching image URL for a node with invalid API key', async () => { 19 | // Arrange 20 | const apiKey = 'invalid-api-key'; 21 | const fileId = 'test-file-id'; 22 | const nodeIds = ['test-node-id']; 23 | const format: ImageFormat = 'png'; 24 | const scale = 1; 25 | 26 | // Act & Assert 27 | await expect( 28 | fetchFigmaImage(apiKey, fileId, nodeIds, format, scale), 29 | ).rejects.toThrow(); 30 | }); 31 | }); 32 | 33 | describe('Success cases', () => { 34 | it.skip('should successfully fetch image URL for a node', async () => { 35 | // Skip test if no API token 36 | if (!process.env.FIGMA_ACCESS_TOKEN) { 37 | throw new Error('Environment variable FIGMA_ACCESS_TOKEN is not set'); 38 | } 39 | 40 | // Get Figma URL from .env file 41 | const figmaUrl = process.env.FIGMA_TEST_URL; 42 | if (!figmaUrl) { 43 | throw new Error('Environment variable FIGMA_TEST_URL is not set'); 44 | } 45 | 46 | // Extract file ID and node IDs from URL 47 | const fileId = extractFigmaFileId(figmaUrl); 48 | const nodeIds = extractNodeIds(figmaUrl); 49 | 50 | // Check if file ID and node IDs are valid 51 | if (!fileId || nodeIds.length === 0) { 52 | throw new Error('Could not extract file ID or no node IDs in test URL'); 53 | } 54 | 55 | // Arrange 56 | const apiKey = process.env.FIGMA_ACCESS_TOKEN; 57 | const format: ImageFormat = 'png'; 58 | const scale = 1; 59 | 60 | // Act 61 | const response = await fetchFigmaImage(apiKey, fileId, nodeIds, format, scale); 62 | 63 | // Assert 64 | expect(response).toBeDefined(); 65 | expect(response.images).toBeDefined(); 66 | 67 | // Check if we got any image URLs 68 | const imageUrls = Object.values(response.images); 69 | expect(imageUrls.length).toBeGreaterThan(0); 70 | 71 | // Check if at least one URL is valid 72 | const hasValidUrl = imageUrls.some( 73 | (url) => typeof url === 'string' && url.startsWith('http'), 74 | ); 75 | expect(hasValidUrl).toBe(true); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/figma/fetch-image.ts: -------------------------------------------------------------------------------- 1 | import type { FigmaImageResponse, ImageFormat } from './types.js'; 2 | 3 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 4 | 5 | /** 6 | * Directly call Figma API to get image URLs 7 | * @param apiKey Figma API key 8 | * @param fileId File ID 9 | * @param nodeIds Array of node IDs 10 | * @param format Image format (png, jpg, svg) 11 | * @param scale Image scale (1-4) 12 | * @returns Image data with URLs 13 | */ 14 | export async function fetchFigmaImage( 15 | apiKey: string, 16 | fileId: string, 17 | nodeIds: string[], 18 | format: ImageFormat = 'png', 19 | scale: number = 1, 20 | ): Promise { 21 | const url = `https://api.figma.com/v1/images/${fileId}?ids=${nodeIds.join(',')}&format=${format}&scale=${scale}`; 22 | const response = await fetch(url, { 23 | headers: { 24 | 'X-FIGMA-TOKEN': apiKey, 25 | }, 26 | }); 27 | 28 | if (!response.ok) { 29 | const json = await response.json(); 30 | throw new McpError( 31 | ErrorCode.InternalError, 32 | `Figma API Error: ${response.status} ${response.statusText} ${json?.err ?? JSON.stringify(json)}`, 33 | ); 34 | } 35 | 36 | return response.json(); 37 | } 38 | -------------------------------------------------------------------------------- /src/figma/fetch-nodes.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { fetchFigmaNodes } from './fetch-nodes.js'; 4 | 5 | describe('fetchFigmaNodes', () => { 6 | describe('Error cases', () => { 7 | it('should throw error when fetching nodes data with invalid API key', async () => { 8 | // Arrange 9 | const apiKey = 'invalid-api-key'; 10 | const fileId = 'test-file-id'; 11 | const nodeIds = ['test-node-id']; 12 | 13 | // Act & Assert 14 | await expect(fetchFigmaNodes(apiKey, fileId, nodeIds)).rejects.toThrow(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/figma/fetch-nodes.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | 3 | /** 4 | * Directly call Figma API to get node data 5 | * @param apiKey Figma API key 6 | * @param fileId File ID 7 | * @param nodeIds Array of node IDs 8 | * @returns Node data 9 | */ 10 | export async function fetchFigmaNodes( 11 | apiKey: string, 12 | fileId: string, 13 | nodeIds: string[], 14 | ): Promise { 15 | const response = await fetch( 16 | `https://api.figma.com/v1/files/${fileId}/nodes?ids=${nodeIds.join(',')}`, 17 | { 18 | headers: { 19 | 'X-FIGMA-TOKEN': apiKey, 20 | }, 21 | }, 22 | ); 23 | 24 | if (!response.ok) { 25 | const json = await response.json(); 26 | throw new McpError( 27 | ErrorCode.InternalError, 28 | `Figma API Error: ${response.status} ${response.statusText} ${json?.err ?? JSON.stringify(json)}`, 29 | ); 30 | } 31 | 32 | return response.json(); 33 | } 34 | -------------------------------------------------------------------------------- /src/figma/get-data.spec.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { describe, it, expect, beforeAll } from 'vitest'; 3 | 4 | import { getFigmaData } from './get-data.js'; 5 | 6 | // Load .env file before running tests 7 | beforeAll(() => { 8 | // Load .env file 9 | dotenv.config(); 10 | }); 11 | 12 | describe('getFigmaData', () => { 13 | describe('Error cases', () => { 14 | it('should throw error when figma_url is not provided', async () => { 15 | // Act & Assert 16 | await expect(getFigmaData({ figma_url: '' })).rejects.toThrowError(); 17 | }); 18 | 19 | it('should return error when invalid Figma URL is provided', async () => { 20 | // Arrange 21 | const args = { figma_url: 'https://www.example.com' }; 22 | 23 | // Assert 24 | await expect(getFigmaData(args)).rejects.toThrow(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/figma/get-data.ts: -------------------------------------------------------------------------------- 1 | import type { GetFigmaDataParams } from './types.js'; 2 | 3 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 4 | 5 | import { extractFigmaFileId } from './extract-file-id.js'; 6 | import { extractNodeIds } from './extract-node-ids.js'; 7 | import { fetchFigmaFile } from './fetch-file.js'; 8 | import { fetchFigmaNodes } from './fetch-nodes.js'; 9 | 10 | /** 11 | * Get Figma data 12 | * @param args Parameters for getting Figma data 13 | * @param args.figma_url 14 | */ 15 | export async function getFigmaData({ figma_url }: GetFigmaDataParams) { 16 | // Get data using Figma API 17 | if (!process.env.FIGMA_ACCESS_TOKEN) { 18 | throw new McpError( 19 | ErrorCode.InvalidParams, 20 | 'Figma API access token is not set. Please set the FIGMA_ACCESS_TOKEN environment variable.', 21 | ); 22 | } 23 | 24 | const fileId = extractFigmaFileId(figma_url); 25 | 26 | if (!fileId) { 27 | throw new McpError( 28 | ErrorCode.InvalidParams, 29 | `Invalid Figma URL: ${figma_url} - Could not extract file ID. Please check the URL format.`, 30 | ); 31 | } 32 | 33 | // Extract node IDs 34 | const nodeIds = extractNodeIds(figma_url); 35 | 36 | try { 37 | const nodesData = 38 | nodeIds.length > 0 39 | ? await fetchFigmaNodes(process.env.FIGMA_ACCESS_TOKEN, fileId, nodeIds) 40 | : await fetchFigmaFile(process.env.FIGMA_ACCESS_TOKEN, fileId); 41 | 42 | const content = JSON.stringify(nodesData, null, 2); 43 | 44 | return { 45 | content, 46 | }; 47 | } catch (error) { 48 | throw new McpError( 49 | ErrorCode.InternalError, 50 | 'Error retrieving node information:', 51 | error, 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/figma/get-image.spec.ts: -------------------------------------------------------------------------------- 1 | import type { ImageFormat } from './types.js'; 2 | 3 | import dotenv from 'dotenv'; 4 | import { describe, it, expect, beforeAll } from 'vitest'; 5 | 6 | import { extractFigmaFileId } from './extract-file-id.js'; 7 | import { extractNodeIds } from './extract-node-ids.js'; 8 | import { getFigmaImage } from './get-image.js'; 9 | 10 | // Load .env file before running tests 11 | beforeAll(() => { 12 | // Load .env file 13 | dotenv.config(); 14 | }); 15 | 16 | describe('getFigmaImage', () => { 17 | describe('Error cases', () => { 18 | it('should throw error when using getFigmaImage with invalid params', async () => { 19 | // Arrange 20 | const fileId = 'invalid-file-id'; 21 | const nodeId = 'invalid-node-id'; 22 | const format: ImageFormat = 'png'; 23 | const scale = 1; 24 | const params = { fileId, nodeId, format, scale }; 25 | 26 | // Assert 27 | await expect(getFigmaImage(params)).rejects.toThrowError(); 28 | }); 29 | }); 30 | 31 | describe('Success cases', () => { 32 | it('should successfully get image URL using getFigmaImage with valid token', async () => { 33 | // Arrange 34 | const figmaUrl = 35 | process.env.FIGMA_TEST_URL ?? 36 | 'https://www.figma.com/file/abc123/test?node-id=123-456'; 37 | const fileId = extractFigmaFileId(figmaUrl) ?? 'abc123'; 38 | const nodeIds = extractNodeIds(figmaUrl); 39 | const nodeId = nodeIds[0] ?? '123-456'; 40 | const format: ImageFormat = 'png'; 41 | const scale = 1; 42 | const params = { fileId, nodeId, format, scale }; 43 | 44 | // Act 45 | const result = await getFigmaImage(params); 46 | 47 | // Assert 48 | expect(result).toBeDefined(); 49 | expect(result.isError).toBeUndefined(); 50 | expect(result.content).toBeDefined(); 51 | expect(result.content?.length).toBeGreaterThan(0); 52 | expect(result.content?.[0]?.text).toBeDefined(); 53 | expect(typeof result.content?.[0]?.text).toBe('string'); 54 | expect(result.content?.[0]?.text?.length).toBeGreaterThan(0); 55 | expect(result.content?.[0]?.text.startsWith('https://figma')).toBe(true); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/figma/get-image.ts: -------------------------------------------------------------------------------- 1 | import type { GetFigmaImageParams } from './types.js'; 2 | 3 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 4 | 5 | import { fetchFigmaImage } from './fetch-image.js'; 6 | 7 | /** 8 | * Get Figma image URL for a specific node 9 | * @param params Parameters for getting Figma image 10 | * @returns Image URL 11 | */ 12 | export async function getFigmaImage(params: GetFigmaImageParams) { 13 | // Check if API token is set 14 | if (!process.env.FIGMA_ACCESS_TOKEN) { 15 | throw new McpError(ErrorCode.InvalidParams, 'Figma API access token is not set'); 16 | } 17 | 18 | // Validate parameters 19 | if (!params.nodeId) { 20 | throw new McpError(ErrorCode.InvalidParams, 'nodeId parameter is required'); 21 | } 22 | 23 | // Ensure fileId is a string (parts[0] is guaranteed to exist after the length check) 24 | const fileId = params.fileId; 25 | const nodeId = params.nodeId; 26 | const format = params.format || 'png'; 27 | const scale = params.scale || 1; 28 | 29 | if (!fileId) { 30 | throw new McpError( 31 | ErrorCode.InvalidParams, 32 | `Invalid node ID: ${params.nodeId}. Could not extract file ID. Please check the node ID format.`, 33 | ); 34 | } 35 | 36 | // Call Figma API to get image URL 37 | const imageData = await fetchFigmaImage( 38 | process.env.FIGMA_ACCESS_TOKEN, 39 | fileId, 40 | [nodeId], 41 | format, 42 | scale, 43 | ); 44 | 45 | // Extract image URL from response 46 | const imageUrl = Object.values(imageData.images)?.[0]; 47 | 48 | // Type guard to ensure imageUrl is a string 49 | if (typeof imageUrl !== 'string') { 50 | throw new McpError(ErrorCode.InternalError, `No image URL found for node: ${nodeId}`); 51 | } 52 | 53 | // At this point, imageUrl is guaranteed to be a string 54 | const validImageUrl: string = imageUrl; 55 | 56 | return { 57 | content: [ 58 | { 59 | type: 'text', 60 | text: validImageUrl, 61 | }, 62 | ], 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/figma/serialize-error.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { serializeError } from './serialize-error.js'; 4 | 5 | describe('serializeError', () => { 6 | it('should serialize primitive values correctly', () => { 7 | expect(serializeError(null)).toBe('null'); 8 | expect(serializeError(123)).toBe('123'); 9 | expect(serializeError('test')).toBe('"test"'); 10 | expect(serializeError(true)).toBe('true'); 11 | }); 12 | 13 | it('should serialize Error objects with name, message and stack', () => { 14 | // Arrange 15 | const error = new Error('Test error'); 16 | 17 | // Act 18 | const result = serializeError(error); 19 | const parsed = JSON.parse(result); 20 | 21 | // Assert 22 | expect(parsed).toHaveProperty('name', 'Error'); 23 | expect(parsed).toHaveProperty('message', 'Test error'); 24 | expect(parsed).toHaveProperty('stack'); 25 | }); 26 | 27 | it('should serialize custom error properties', () => { 28 | // Arrange 29 | const error = new Error('Test error') as Error & Record; 30 | error.customProp = 'custom value'; 31 | error.code = 404; 32 | 33 | // Act 34 | const result = serializeError(error); 35 | const parsed = JSON.parse(result); 36 | 37 | // Assert 38 | expect(parsed).toHaveProperty('customProp', 'custom value'); 39 | expect(parsed).toHaveProperty('code', 404); 40 | }); 41 | 42 | it('should handle errors with response property', () => { 43 | // Arrange 44 | const error = new Error('API Error') as Error & Record; 45 | error.response = { 46 | status: 404, 47 | statusText: 'Not Found', 48 | headers: { 'content-type': 'application/json' }, 49 | data: { message: 'Resource not found' }, 50 | }; 51 | 52 | // Act 53 | const result = serializeError(error); 54 | const parsed = JSON.parse(result); 55 | 56 | // Assert 57 | expect(parsed).toHaveProperty('response'); 58 | expect(parsed.response).toHaveProperty('status', 404); 59 | expect(parsed.response).toHaveProperty('statusText', 'Not Found'); 60 | expect(parsed.response).toHaveProperty('headers'); 61 | expect(parsed.response).toHaveProperty('data'); 62 | expect(parsed.response.data).toHaveProperty('message', 'Resource not found'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/figma/serialize-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Serialize error object to a detailed string representation 3 | * This is needed because JSON.stringify(error) only returns "{}" for Error objects 4 | * @param error Any error object 5 | * @returns Detailed string representation of the error 6 | */ 7 | export function serializeError(error: unknown): string { 8 | // If it's not an object or null, just stringify it 9 | if (!error || typeof error !== 'object') { 10 | return JSON.stringify(error, null, 2); 11 | } 12 | 13 | // Create a detailed error representation 14 | const errorDetails: Record = {}; 15 | 16 | // Extract standard Error properties 17 | if (error instanceof Error) { 18 | errorDetails.name = error.name; 19 | errorDetails.message = error.message; 20 | errorDetails.stack = error.stack; 21 | 22 | // Extract cause if available 23 | if ('cause' in error && error.cause) { 24 | errorDetails.cause = error.cause; 25 | } 26 | } 27 | 28 | // Extract all enumerable properties 29 | for (const key in error) { 30 | if (Object.prototype.hasOwnProperty.call(error, key)) { 31 | try { 32 | const value = (error as Record)[key]; 33 | 34 | // Handle special case for response object 35 | if (key === 'response' && value && typeof value === 'object') { 36 | const response = value as Record; 37 | errorDetails.response = { 38 | status: response.status, 39 | statusText: response.statusText, 40 | headers: response.headers, 41 | data: response.data, 42 | }; 43 | } else { 44 | errorDetails[key] = value; 45 | } 46 | } catch (error_) { 47 | errorDetails[key] = 48 | `[Error extracting property: ${error_ instanceof Error ? error_.message : String(error_)}]`; 49 | } 50 | } 51 | } 52 | 53 | return JSON.stringify(errorDetails, null, 2); 54 | } 55 | -------------------------------------------------------------------------------- /src/figma/types.ts: -------------------------------------------------------------------------------- 1 | export type ImageFormat = 'jpg' | 'png' | 'svg'; 2 | 3 | export type GetFigmaImageParams = { 4 | fileId: string; 5 | nodeId: string; 6 | format?: ImageFormat; 7 | scale?: number; 8 | }; 9 | 10 | export type GetFigmaDataParams = { 11 | figma_url: string; 12 | }; 13 | 14 | // Figma API response type for image URLs 15 | export type FigmaImageResponse = { 16 | err: null | string; 17 | images: Record; 18 | }; 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 5 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 6 | import { z } from 'zod'; 7 | 8 | import { getCodingGuidelines } from './coding-guideline.js'; 9 | import { getFigmaData } from './figma/get-data.js'; 10 | import { getFigmaImage } from './figma/get-image.js'; 11 | import { getTaskStep } from './utils/get-task-step.js'; 12 | 13 | const packageJsonPath = path.resolve(import.meta.dirname, '../package.json'); 14 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); 15 | const version = packageJson.version || 'N/A'; 16 | 17 | const server = new McpServer({ 18 | name: 'frontend_env', 19 | version, 20 | }); 21 | 22 | server.tool( 23 | 'get_coding_guidelines', 24 | 'Get D-Zero frontend coding guidelines', 25 | { 26 | techTypes: z 27 | .enum(['html', 'css', 'js', 'media', 'web-components']) 28 | .describe( 29 | 'Will create a coding guideline for the following tech types: html, css, js, media (images, videos, audio, etc.), web-components', 30 | ), 31 | }, 32 | async ({ techTypes }) => { 33 | const content = await getCodingGuidelines(techTypes); 34 | return { 35 | content: [ 36 | { 37 | type: 'text', 38 | text: content, 39 | }, 40 | ], 41 | }; 42 | }, 43 | ); 44 | 45 | server.tool( 46 | 'get_task_step', 47 | 'Get task step', 48 | { 49 | cwd: z.string().describe('Current working directory (Absolute path)'), 50 | filePath: z.string().describe('Task file path (Relative to cwd)'), 51 | step: z.optional(z.number().min(1).int()).describe('Step number (default: 1)'), 52 | }, 53 | async ({ cwd, filePath, step }) => { 54 | const content = await getTaskStep( 55 | path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath), 56 | step, 57 | ); 58 | return { 59 | content: [ 60 | { 61 | type: 'text', 62 | text: content, 63 | }, 64 | ], 65 | }; 66 | }, 67 | ); 68 | 69 | server.tool( 70 | 'get_figma_data', 71 | 'Get data as a cached JSON file path and content from Figma URL', 72 | { 73 | figmaUrl: z 74 | .string() 75 | .describe('Figma URL (e.g.: https://www.figma.com/file/abcdef123456/FileName)'), 76 | }, 77 | async ({ figmaUrl }) => { 78 | const { content } = await getFigmaData({ figma_url: figmaUrl }); 79 | return { 80 | content: [ 81 | { 82 | type: 'text', 83 | text: content, 84 | }, 85 | ], 86 | }; 87 | }, 88 | ); 89 | 90 | server.tool( 91 | 'get_figma_image', 92 | 'Get image URL from Figma node ID', 93 | { 94 | fileId: z.string().describe('Figma file ID (e.g.: abcdef123456)'), 95 | nodeId: z.string().describe('Figma node ID (e.g.: 123:456)'), 96 | format: z 97 | .optional(z.enum(['png', 'jpg', 'svg'])) 98 | .describe('Image format (default: png)'), 99 | scale: z 100 | .optional(z.number().min(1).max(4)) 101 | .describe('Image scale factor (1-4, default: 1)'), 102 | }, 103 | async ({ fileId, nodeId, format, scale }) => { 104 | const res = await getFigmaImage({ fileId, nodeId, format, scale }); 105 | return { 106 | content: res.content.map((content) => { 107 | return { 108 | type: 'text', 109 | text: content.text, 110 | }; 111 | }), 112 | }; 113 | }, 114 | ); 115 | 116 | const transport = new StdioServerTransport(); 117 | await server.connect(transport); 118 | -------------------------------------------------------------------------------- /src/utils/get-task-step.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | 3 | import { splitTaskSteps } from './split-task-steps.js'; 4 | 5 | const cache = new Map(); 6 | 7 | /** 8 | * 9 | * @param filePath 10 | * @param step 11 | */ 12 | export async function getTaskStep(filePath: string, step = 1) { 13 | if (cache.has(filePath)) { 14 | const steps = cache.get(filePath); 15 | if (!steps) { 16 | throw new Error('No steps found'); 17 | } 18 | return getStep(steps, step); 19 | } 20 | const task = await fs.readFile(filePath, 'utf8'); 21 | const steps = splitTaskSteps(task); 22 | cache.set(filePath, steps); 23 | return getStep(steps, step); 24 | } 25 | 26 | /** 27 | * 28 | * @param steps 29 | * @param step 30 | */ 31 | function getStep(steps: string[], step: number) { 32 | if (steps.length < step) { 33 | return '全てのステップが完了しました。'; 34 | } 35 | return steps.at(step - 1) ?? 'ステップが見つかりません。次のステップに進んでください。'; 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/split-task-steps.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { splitTaskSteps } from './split-task-steps.js'; 4 | 5 | describe('splitTaskSteps', () => { 6 | it('should split the task into steps', () => { 7 | const task = ` 8 | # 1 9 | Step 1 10 | 11 | --- 12 | 13 | # 2 14 | Step 2 15 | 16 | --- 17 | 18 | # 3 19 | Step 3 20 | 21 | `; 22 | const steps = splitTaskSteps(task); 23 | expect(steps).toEqual(['Step 1\n\n---', 'Step 2\n\n---', 'Step 3']); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/utils/split-task-steps.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param task 4 | */ 5 | export function splitTaskSteps(task: string) { 6 | const steps = task.trim().split(/(?:^|\n\r?)(?=#\s)/); 7 | return steps.map((step) => { 8 | const lines = step 9 | .trim() 10 | .split('\n') 11 | .map((line) => line.trim()) 12 | .filter((line) => !/^#\s+\d+/.test(line)); 13 | return lines.join('\n'); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./tsconfig.json"], 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "rootDir": "./src" 6 | }, 7 | "include": ["./src/**/*"], 8 | "exclude": ["node_modules", "dist", "./src/**/*.spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@d-zero/tsconfig"], 3 | "exclude": ["node_modules", "dist", "**/*.js", "**/*.mjs"] 4 | } 5 | --------------------------------------------------------------------------------